Models 44

DeepSeek V4 Pro 0813

by DeepSeekreasoning defaultClean
Open in new window

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

How it went

Total time
4 min 23 s
Cost
$0.107
Output tokens
26,742
Spent thinking
12,848 tokens
Speed
102 tok/s
Console errors
0
First token after
551 ms
First visible text after
2 min 29 s
Prompt tokens
504
Code size
49.3 KB
Finish reason
stop
Served by
Nebius
Model id
deepseek/deepseek-v4-pro-0813
Reasoning sent
model default
Max output
943,718 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 · 1206 lines

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Aquarium - Fish Flee From Mouse</title>
    <style>
        :root {
            --overlay-bg: rgba(5, 8, 20, 0.75);
            --text-color: #e8efff;
            --accent: #6bc8ff;
        }
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            overflow: hidden;
            background: #06080d;
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
            width: 100vw;
            cursor: none;
            user-select: none;
            -webkit-user-select: none;
            -webkit-tap-highlight-color: transparent;
        }
        canvas {
            display: block;
        }

        /* Custom cursor dot */
        #cursor-dot {
            position: fixed;
            pointer-events: none;
            z-index: 30;
            width: 22px;
            height: 22px;
            border-radius: 50%;
            border: 2px solid rgba(255, 255, 255, 0.65);
            background: rgba(120, 200, 255, 0.2);
            box-shadow: 0 0 12px rgba(100, 180, 255, 0.5), inset 0 0 6px rgba(255, 255, 255, 0.3);
            transform: translate(-50%, -50%);
            transition: width 0.15s ease, height 0.15s ease, border-color 0.15s ease, background 0.15s ease;
        }
        #cursor-dot.near-fish {
            width: 34px;
            height: 34px;
            border-color: rgba(255, 180, 120, 0.9);
            background: rgba(255, 160, 80, 0.3);
            box-shadow: 0 0 22px rgba(255, 140, 60, 0.7), inset 0 0 8px rgba(255, 200, 120, 0.5);
        }
        #cursor-dot.panic {
            border-color: #ff6b4a;
            background: rgba(255, 80, 50, 0.4);
            box-shadow: 0 0 32px rgba(255, 100, 50, 0.9), 0 0 60px rgba(255, 80, 40, 0.5);
        }

        /* Overlay */
        #overlay {
            position: fixed;
            inset: 0;
            z-index: 20;
            background: var(--overlay-bg);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            text-align: center;
            transition: opacity 0.5s ease, backdrop-filter 0.5s ease;
            pointer-events: auto;
            cursor: pointer;
        }
        #overlay.hidden {
            opacity: 0;
            pointer-events: none;
            backdrop-filter: blur(0px);
            -webkit-backdrop-filter: blur(0px);
        }
        #overlay h1 {
            color: var(--text-color);
            font-size: clamp(2rem, 5vw, 3.6rem);
            font-weight: 600;
            letter-spacing: 0.06em;
            margin-bottom: 0.4rem;
            text-shadow: 0 0 40px rgba(100, 180, 255, 0.6), 0 0 80px rgba(80, 140, 220, 0.3);
        }
        #overlay .subtitle {
            color: #bcc8e8;
            font-size: clamp(0.9rem, 2vw, 1.2rem);
            letter-spacing: 0.12em;
            margin-bottom: 0.5rem;
        }
        #overlay .instructions {
            color: #8899c0;
            font-size: clamp(0.75rem, 1.6vw, 1rem);
            letter-spacing: 0.05em;
            margin-top: 1.5rem;
            animation: pulse 2s ease-in-out infinite;
        }
        #overlay .fish-icon {
            font-size: 3rem;
            margin-bottom: 1rem;
            animation: swim 3s ease-in-out infinite;
        }
        #overlay .badge {
            display: flex;
            gap: 0.6rem;
            margin-top: 1rem;
            flex-wrap: wrap;
            justify-content: center;
        }
        #overlay .badge span {
            padding: 0.3rem 0.8rem;
            border-radius: 20px;
            font-size: 0.7rem;
            letter-spacing: 0.08em;
            color: #cfe0ff;
            border: 1px solid rgba(100, 160, 255, 0.4);
            background: rgba(40, 70, 130, 0.3);
        }
        @keyframes pulse {
            0%,
            100% {
                opacity: 0.6;
            }
            50% {
                opacity: 1;
            }
        }
        @keyframes swim {
            0%,
            100% {
                transform: translateX(-8px) rotate(-5deg);
            }
            50% {
                transform: translateX(8px) rotate(5deg);
            }
        }

        /* Info bar at bottom */
        #info-bar {
            position: fixed;
            bottom: 0;
            left: 0;
            right: 0;
            z-index: 15;
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 1rem;
            padding: 0.6rem 1rem;
            background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
            color: #c8d8f0;
            font-size: 0.75rem;
            letter-spacing: 0.04em;
            pointer-events: none;
            transition: opacity 0.4s;
        }
        #info-bar .dot {
            width: 6px;
            height: 6px;
            border-radius: 50%;
            background: #6bc8ff;
            animation: pulse 2.5s ease-in-out infinite;
            margin-right: 0.2rem;
        }
        #info-bar.dim {
            opacity: 0.3;
        }
    </style>
</head>
<body>

    <!-- Cursor dot -->
    <div id="cursor-dot"></div>

    <!-- Overlay -->
    <div id="overlay">
        <div class="fish-icon">🐟</div>
        <h1>3D Aquarium</h1>
        <p class="subtitle">Fish flee from your cursor</p>
        <div class="badge">
            <span>🖱️ Move mouse near fish</span>
            <span>🐠 They swim away</span>
            <span>💡 Auto-demo playing</span>
        </div>
        <p class="instructions">Click or press any key to take control</p>
    </div>

    <!-- Info bar -->
    <div id="info-bar">
        <span class="dot"></span>
        <span id="status-text">Demo mode — watch the fish flee from the virtual cursor</span>
    </div>

    <!-- 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>

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

        // ─── Constants ───
        const TANK_HALF_W = 10;
        const TANK_HALF_H = 6;
        const TANK_HALF_D = 7;
        const WATER_SURFACE_Y = 5.2;
        const SAND_Y = -5.75;
        const FISH_COUNT = 10;
        const BUBBLE_COUNT = 40;
        const FLEE_RADIUS = 4.5;
        const FLEE_STRENGTH = 14.0;
        const WANDER_STRENGTH = 5.0;
        const MAX_SPEED = 4.5;
        const MIN_SPEED = 1.2;
        const WALL_MARGIN = 1.0;

        // ─── Scene Setup ───
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x0a0e18);

        const camera = new THREE.PerspectiveCamera(42, window.innerWidth / window.innerHeight, 0.1, 120);
        camera.position.set(15, 3.5, 18);
        camera.lookAt(0, -0.8, 0);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
        renderer.outputColorSpace = THREE.SRGBColorSpace;
        renderer.toneMapping = THREE.ACESFilmicToneMapping;
        renderer.toneMappingExposure = 1.1;
        document.body.appendChild(renderer.domElement);

        // ─── Lighting ───
        const ambientLight = new THREE.AmbientLight(0x505a70, 0.7);
        scene.add(ambientLight);

        const backFillLight = new THREE.DirectionalLight(0x8899cc, 0.5);
        backFillLight.position.set(-5, 2, -8);
        scene.add(backFillLight);

        const mainLight = new THREE.DirectionalLight(0xfff2e0, 2.8);
        mainLight.position.set(4, 14, 3);
        mainLight.castShadow = true;
        mainLight.shadow.mapSize.width = 2048;
        mainLight.shadow.mapSize.height = 2048;
        mainLight.shadow.camera.near = 2;
        mainLight.shadow.camera.far = 40;
        mainLight.shadow.camera.left = -14;
        mainLight.shadow.camera.right = 14;
        mainLight.shadow.camera.top = 12;
        mainLight.shadow.camera.bottom = -12;
        mainLight.shadow.bias = -0.0003;
        mainLight.shadow.normalBias = 0.02;
        scene.add(mainLight);

        const waterGlowLight = new THREE.PointLight(0x88ccee, 1.8, 20, 2);
        waterGlowLight.position.set(0, 2, 0);
        scene.add(waterGlowLight);

        // Caustic spotlight that gently moves
        const causticSpotLight = new THREE.SpotLight(0xffffff, 1.2, 25, Math.PI / 6, 0.5, 1.5);
        causticSpotLight.position.set(2, 8, 1);
        causticSpotLight.target.position.set(0, -5, 0);
        causticSpotLight.castShadow = false;
        scene.add(causticSpotLight);
        scene.add(causticSpotLight.target);

        // ─── Tank Construction ───
        const tankGroup = new THREE.Group();
        scene.add(tankGroup);

        // Glass material
        const glassMaterial = new THREE.MeshStandardMaterial({
            color: 0xcceeff,
            roughness: 0.04,
            metalness: 0.1,
            transparent: true,
            opacity: 0.13,
            side: THREE.DoubleSide,
            depthWrite: false,
            envMapIntensity: 0.8,
        });

        const glassEdgeMaterial = new THREE.MeshStandardMaterial({
            color: 0xb8d8e8,
            roughness: 0.08,
            metalness: 0.3,
            transparent: true,
            opacity: 0.22,
            side: THREE.DoubleSide,
            depthWrite: false,
        });

        // Create glass panels (slightly larger than interior for edge overlap)
        function createGlassPanel(w, h, position, rotation) {
            const geo = new THREE.PlaneGeometry(w, h);
            const mesh = new THREE.Mesh(geo, glassMaterial);
            mesh.position.copy(position);
            if (rotation) mesh.rotation.set(rotation.x || 0, rotation.y || 0, rotation.z || 0);
            mesh.castShadow = false;
            mesh.receiveShadow = false;
            return mesh;
        }

        // Front and back (facing Z)
        tankGroup.add(createGlassPanel(TANK_HALF_W * 2, TANK_HALF_H * 2, new THREE.Vector3(0, 0, TANK_HALF_D)));
        tankGroup.add(createGlassPanel(TANK_HALF_W * 2, TANK_HALF_H * 2, new THREE.Vector3(0, 0, -TANK_HALF_D), {
            y: Math.PI
        }));
        // Left and right (facing X)
        tankGroup.add(createGlassPanel(TANK_HALF_D * 2, TANK_HALF_H * 2, new THREE.Vector3(TANK_HALF_W, 0, 0), {
            y: Math.PI / 2
        }));
        tankGroup.add(createGlassPanel(TANK_HALF_D * 2, TANK_HALF_H * 2, new THREE.Vector3(-TANK_HALF_W, 0, 0), {
            y: -Math.PI / 2
        }));
        // Top
        tankGroup.add(createGlassPanel(TANK_HALF_W * 2, TANK_HALF_D * 2, new THREE.Vector3(0, TANK_HALF_H, 0), {
            x: -Math.PI / 2
        }));

        // Metal frame edges
        const frameMaterial = new THREE.MeshStandardMaterial({
            color: 0x3a4048,
            roughness: 0.3,
            metalness: 0.85,
        });
        const frameThickness = 0.18;

        function createFrameBar(len, pos, rot) {
            const geo = new THREE.BoxGeometry(frameThickness, frameThickness, len);
            const mesh = new THREE.Mesh(geo, frameMaterial);
            mesh.position.copy(pos);
            if (rot) mesh.rotation.set(rot.x || 0, rot.y || 0, rot.z || 0);
            mesh.castShadow = true;
            mesh.receiveShadow = true;
            return mesh;
        }

        // Vertical corner bars
        const cornerPositions = [
            [-TANK_HALF_W, -TANK_HALF_D],
            [TANK_HALF_W, -TANK_HALF_D],
            [-TANK_HALF_W, TANK_HALF_D],
            [TANK_HALF_W, TANK_HALF_D],
        ];
        for (const [cx, cz] of cornerPositions) {
            const bar = createFrameBar(TANK_HALF_H * 2, new THREE.Vector3(cx, 0, cz));
            tankGroup.add(bar);
        }

        // Top horizontal bars
        const topEdgeLengths = [
            { len: TANK_HALF_W * 2, pos: new THREE.Vector3(0, TANK_HALF_H, -TANK_HALF_D), rot: { y: 0 } },
            { len: TANK_HALF_W * 2, pos: new THREE.Vector3(0, TANK_HALF_H, TANK_HALF_D), rot: { y: 0 } },
            { len: TANK_HALF_D * 2, pos: new THREE.Vector3(-TANK_HALF_W, TANK_HALF_H, 0), rot: { y: Math.PI / 2 } },
            { len: TANK_HALF_D * 2, pos: new THREE.Vector3(TANK_HALF_W, TANK_HALF_H, 0), rot: { y: Math.PI / 2 } },
        ];
        for (const edge of topEdgeLengths) {
            tankGroup.add(createFrameBar(edge.len, edge.pos, edge.rot));
        }

        // Bottom horizontal bars
        const bottomEdgeLengths = [
            { len: TANK_HALF_W * 2, pos: new THREE.Vector3(0, -TANK_HALF_H, -TANK_HALF_D), rot: { y: 0 } },
            { len: TANK_HALF_W * 2, pos: new THREE.Vector3(0, -TANK_HALF_H, TANK_HALF_D), rot: { y: 0 } },
            { len: TANK_HALF_D * 2, pos: new THREE.Vector3(-TANK_HALF_W, -TANK_HALF_H, 0), rot: { y: Math.PI / 2 } },
            { len: TANK_HALF_D * 2, pos: new THREE.Vector3(TANK_HALF_W, -TANK_HALF_H, 0), rot: { y: Math.PI / 2 } },
        ];
        for (const edge of bottomEdgeLengths) {
            tankGroup.add(createFrameBar(edge.len, edge.pos, edge.rot));
        }

        // Tank base / stand
        const baseMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2028, roughness: 0.6, metalness: 0.2 });
        const baseGeo = new THREE.BoxGeometry(TANK_HALF_W * 2 + 1.6, 1.2, TANK_HALF_D * 2 + 1.6);
        const baseMesh = new THREE.Mesh(baseGeo, baseMaterial);
        baseMesh.position.set(0, -TANK_HALF_H - 0.6, 0);
        baseMesh.castShadow = true;
        baseMesh.receiveShadow = true;
        tankGroup.add(baseMesh);

        // Tank hood (top lid)
        const hoodMaterial = new THREE.MeshStandardMaterial({ color: 0x1a1a20, roughness: 0.4, metalness: 0.6 });
        const hoodGeo = new THREE.BoxGeometry(TANK_HALF_W * 2 + 0.8, 1.0, TANK_HALF_D * 2 + 0.8);
        const hoodMesh = new THREE.Mesh(hoodGeo, hoodMaterial);
        hoodMesh.position.set(0, TANK_HALF_H + 0.5, 0);
        hoodMesh.castShadow = true;
        hoodMesh.receiveShadow = true;
        tankGroup.add(hoodMesh);

        // ─── Sand Bottom ───
        const sandMaterial = new THREE.MeshStandardMaterial({
            color: 0xcdb891,
            roughness: 0.92,
            metalness: 0.0,
        });
        const sandGeo = new THREE.PlaneGeometry(TANK_HALF_W * 2 - 0.4, TANK_HALF_D * 2 - 0.4, 40, 40);
        // Add subtle displacement to sand
        const sandPositions = sandGeo.attributes.position;
        for (let i = 0; i < sandPositions.count; i++) {
            const x = sandPositions.getX(i);
            const z = sandPositions.getY(i);
            const noise = Math.sin(x * 1.8) * Math.cos(z * 2.3) * 0.15 +
                Math.sin(x * 4.2 + z * 3.7) * 0.08 +
                Math.cos(x * 0.7 + z * 1.1) * 0.2;
            sandPositions.setZ(i, noise);
        }
        sandGeo.computeVertexNormals();
        const sandMesh = new THREE.Mesh(sandGeo, sandMaterial);
        sandMesh.rotation.x = -Math.PI / 2;
        sandMesh.position.set(0, SAND_Y, 0);
        sandMesh.receiveShadow = true;
        tankGroup.add(sandMesh);

        // ─── Water Volume ───
        const waterMaterial = new THREE.MeshStandardMaterial({
            color: 0x88bbdd,
            roughness: 0.1,
            metalness: 0.0,
            transparent: true,
            opacity: 0.07,
            side: THREE.BackSide,
            depthWrite: false,
        });
        const waterGeo = new THREE.BoxGeometry(
            TANK_HALF_W * 2 - 0.5,
            WATER_SURFACE_Y - SAND_Y,
            TANK_HALF_D * 2 - 0.5
        );
        const waterMesh = new THREE.Mesh(waterGeo, waterMaterial);
        waterMesh.position.set(0, (WATER_SURFACE_Y + SAND_Y) / 2, 0);
        tankGroup.add(waterMesh);

        // Water surface plane (slight shimmer)
        const surfaceMaterial = new THREE.MeshStandardMaterial({
            color: 0xbbddff,
            roughness: 0.05,
            metalness: 0.1,
            transparent: true,
            opacity: 0.2,
            side: THREE.DoubleSide,
            depthWrite: false,
        });
        const surfaceGeo = new THREE.PlaneGeometry(TANK_HALF_W * 2 - 0.4, TANK_HALF_D * 2 - 0.4, 30, 30);
        const surfaceMesh = new THREE.Mesh(surfaceGeo, surfaceMaterial);
        surfaceMesh.rotation.x = -Math.PI / 2;
        surfaceMesh.position.set(0, WATER_SURFACE_Y - 0.05, 0);
        surfaceMesh.name = 'waterSurface';
        tankGroup.add(surfaceMesh);

        // ─── Underwater Fog ───
        const waterFogColor = 0x88aacc;
        scene.fog = new THREE.FogExp2(waterFogColor, 0.013);

        // ─── Rocks ───
        function createRock(scale, position, colorVar) {
            const geo = new THREE.IcosahedronGeometry(1, 1);
            const pos = geo.attributes.position;
            for (let i = 0; i < pos.count; i++) {
                const x = pos.getX(i);
                const y = pos.getY(i);
                const z = pos.getZ(i);
                const noise = (Math.sin(x * 7 + y * 3 + z * 5) * 0.15 +
                    Math.cos(x * 2 + z * 8) * 0.1 +
                    Math.sin(y * 9 + x * 4) * 0.08);
                const len = Math.sqrt(x * x + y * y + z * z);
                const scaleFactor = 1 + noise;
                pos.setXYZ(i, x * scaleFactor, y * scaleFactor, z * scaleFactor);
            }
            geo.computeVertexNormals();
            const rockMat = new THREE.MeshStandardMaterial({
                color: new THREE.Color().setHSL(colorVar, 0.06, 0.35 + Math.random() * 0.2),
                roughness: 0.85,
                metalness: 0.05,
            });
            const mesh = new THREE.Mesh(geo, rockMat);
            mesh.scale.set(scale.x, scale.y * (0.6 + Math.random() * 0.4), scale.z);
            mesh.position.copy(position);
            mesh.rotation.y = Math.random() * Math.PI * 2;
            mesh.castShadow = true;
            mesh.receiveShadow = true;
            return mesh;
        }

        const rocks = [
            createRock({ x: 1.8, y: 1.4, z: 2.2 }, { x: -4.5, y: SAND_Y + 0.7, z: 3.5 }, 0.55),
            createRock({ x: 2.2, y: 1.8, z: 2.6 }, { x: 6.5, y: SAND_Y + 0.9, z: -2.5 }, 0.48),
            createRock({ x: 1.3, y: 1.0, z: 1.6 }, { x: -2.0, y: SAND_Y + 0.5, z: -4.5 }, 0.52),
            createRock({ x: 1.0, y: 0.8, z: 1.2 }, { x: 3.5, y: SAND_Y + 0.4, z: 5.0 }, 0.5),
            createRock({ x: 2.0, y: 1.5, z: 2.0 }, { x: -7.0, y: SAND_Y + 0.75, z: 1.0 }, 0.57),
        ];
        for (const rock of rocks) tankGroup.add(rock);

        // ─── Plants / Seaweed ───
        const plantMaterials = [
            new THREE.MeshStandardMaterial({ color: 0x2d8a4e, roughness: 0.65, metalness: 0.0 }),
            new THREE.MeshStandardMaterial({ color: 0x1a6b3c, roughness: 0.7, metalness: 0.0 }),
            new THREE.MeshStandardMaterial({ color: 0x3da55d, roughness: 0.6, metalness: 0.0 }),
            new THREE.MeshStandardMaterial({ color: 0x247a40, roughness: 0.68, metalness: 0.0 }),
            new THREE.MeshStandardMaterial({ color: 0x4ab870, roughness: 0.55, metalness: 0.0 }),
        ];

        function createSeaweed(basePos, height, leanDir, segments) {
            const group = new THREE.Group();
            const strands = 3 + Math.floor(Math.random() * 3);
            const baseY = SAND_Y + 0.15;

            for (let s = 0; s < strands; s++) {
                const strandHeight = height * (0.7 + Math.random() * 0.5);
                const strandLean = leanDir.clone().multiplyScalar(0.15 + Math.random() * 0.25);
                const curvePoints = [];
                const steps = segments || 8;
                for (let i = 0; i <= steps; i++) {
                    const t = i / steps;
                    const x = basePos.x + strandLean.x * t * strandHeight + Math.sin(t * 2.5 + s) * 0.3 * t;
                    const y = baseY + t * strandHeight;
                    const z = basePos.z + strandLean.z * t * strandHeight + Math.cos(t * 3 + s * 1.7) * 0.25 * t;
                    curvePoints.push(new THREE.Vector3(x, y, z));
                }
                const curve = new THREE.CatmullRomCurve3(curvePoints);
                const radius = 0.07 + Math.random() * 0.06;
                const tubeGeo = new THREE.TubeGeometry(curve, 12, radius, 5, false);
                const mat = plantMaterials[Math.floor(Math.random() * plantMaterials.length)];
                const tubeMesh = new THREE.Mesh(tubeGeo, mat);
                tubeMesh.castShadow = true;
                tubeMesh.receiveShadow = true;
                group.add(tubeMesh);
            }

            // Small leaves at base
            const leafGeo = new THREE.ConeGeometry(0.18, 0.5, 5);
            const leafMat = plantMaterials[Math.floor(Math.random() * plantMaterials.length)];
            const leafCount = 2 + Math.floor(Math.random() * 3);
            for (let l = 0; l < leafCount; l++) {
                const leaf = new THREE.Mesh(leafGeo, leafMat);
                leaf.scale.set(1, 0.7, 0.4);
                leaf.position.set(
                    basePos.x + (Math.random() - 0.5) * 0.6,
                    baseY + 0.15,
                    basePos.z + (Math.random() - 0.5) * 0.6
                );
                leaf.rotation.set(Math.random() * 0.5 - 0.2, Math.random() * Math.PI, Math.random() * 0.4 - 0.2);
                leaf.castShadow = true;
                group.add(leaf);
            }

            group.position.y = 0;
            group.userData = {
                baseY: baseY,
                swayPhase: Math.random() * Math.PI * 2,
                swaySpeed: 0.4 + Math.random() * 0.5,
                swayAmount: 0.04 + Math.random() * 0.06,
            };
            return group;
        }

        const seaweeds = [];
        const seaweedPositions = [
            { x: -8.0, z: 4.5, h: 3.5, lean: { x: 0.3, z: 0.2 } },
            { x: -5.5, z: -3.5, h: 4.0, lean: { x: -0.2, z: 0.4 } },
            { x: 7.8, z: 1.5, h: 3.2, lean: { x: -0.4, z: -0.1 } },
            { x: 2.5, z: -5.5, h: 3.8, lean: { x: 0.2, z: -0.3 } },
            { x: -3.0, z: 5.8, h: 3.0, lean: { x: 0.1, z: -0.2 } },
            { x: 6.0, z: 5.0, h: 2.8, lean: { x: -0.3, z: -0.25 } },
            { x: -1.5, z: -6.0, h: 3.6, lean: { x: 0.35, z: 0.15 } },
            { x: 9.0, z: -3.5, h: 3.3, lean: { x: -0.2, z: 0.3 } },
        ];

        for (const sp of seaweedPositions) {
            const plant = createSeaweed(
                { x: sp.x, y: SAND_Y, z: sp.z },
                sp.h,
                new THREE.Vector3(sp.lean.x, 0, sp.lean.z)
            );
            tankGroup.add(plant);
            seaweeds.push(plant);
        }

        // ─── Bubbles ───
        const bubbles = [];
        const bubbleMaterialShared = new THREE.MeshStandardMaterial({
            color: 0xffffff,
            roughness: 0.05,
            metalness: 0.0,
            transparent: true,
            opacity: 0.45,
            depthWrite: false,
        });

        for (let i = 0; i < BUBBLE_COUNT; i++) {
            const size = 0.05 + Math.random() * 0.15;
            const geo = new THREE.SphereGeometry(size, 8, 6);
            const mesh = new THREE.Mesh(geo, bubbleMaterialShared.clone());
            mesh.position.set(
                (Math.random() - 0.5) * (TANK_HALF_W * 2 - 3),
                SAND_Y + 0.2 + Math.random() * (WATER_SURFACE_Y - SAND_Y - 2),
                (Math.random() - 0.5) * (TANK_HALF_D * 2 - 3)
            );
            mesh.userData = {
                speed: 0.5 + Math.random() * 1.2,
                wobbleAmp: 0.08 + Math.random() * 0.2,
                wobbleFreq: 1.5 + Math.random() * 3.0,
                wobblePhase: Math.random() * Math.PI * 2,
                baseX: mesh.position.x,
                baseZ: mesh.position.z,
                size: size,
            };
            tankGroup.add(mesh);
            bubbles.push(mesh);
        }

        // ─── Fish ───
        const fishColors = [
            0xff7043, // orange
            0x42a5f5, // blue
            0xef5350, // red
            0xfdd835, // yellow
            0xab47bc, // purple
            0x66bb6a, // green
            0x26a69a, // teal
            0xec407a, // pink
            0xffa726, // amber
            0x5c6bc0, // indigo
        ];

        const fishFinColors = [
            0xe85a2a, 0x2a86d0, 0xc93530, 0xd4b020,
            0x8e2fa0, 0x489a4c, 0x1a8a7e, 0xc22e60,
            0xd88c18, 0x4450a5,
        ];

        class Fish {
            constructor(colorIdx, sizeScale, startPos) {
                this.group = new THREE.Group();
                this.sizeScale = sizeScale;
                this.colorIdx = colorIdx;
                this.bodyColor = fishColors[colorIdx];
                this.finColor = fishFinColors[colorIdx];

                this.buildModel();
                this.group.position.copy(startPos);
                this.group.castShadow = true;

                // Physics state
                this.velocity = new THREE.Vector3(
                    (Math.random() - 0.5) * 2,
                    (Math.random() - 0.5) * 0.8,
                    (Math.random() - 0.5) * 2
                );
                this.target = new THREE.Vector3();
                this.wanderTimer = 0;
                this.wanderInterval = 3 + Math.random() * 4;
                this.fleeForce = new THREE.Vector3();
                this.isFleeing = false;
                this.tailWagPhase = Math.random() * Math.PI * 2;
                this.tailWagSpeed = 4 + Math.random() * 4;
                this.tailWagAmp = 0.3;
                this.bobPhase = Math.random() * Math.PI * 2;
                this.bobSpeed = 2 + Math.random() * 2;
                this.bobAmp = 0.12;
                this.fleeRadius = FLEE_RADIUS * (0.7 + Math.random() * 0.6);
                this.maxSpeed = MAX_SPEED * (0.75 + Math.random() * 0.45);
                this.minSpeed = MIN_SPEED * (0.7 + Math.random() * 0.4);
                this.turnSpeed = 4 + Math.random() * 3;

                this.pickNewTarget();
                tankGroup.add(this.group);
            }

            buildModel() {
                const s = this.sizeScale;
                const bodyMat = new THREE.MeshStandardMaterial({
                    color: this.bodyColor,
                    roughness: 0.22,
                    metalness: 0.3,
                });
                const finMat = new THREE.MeshStandardMaterial({
                    color: this.finColor,
                    roughness: 0.35,
                    metalness: 0.1,
                    side: THREE.DoubleSide,
                });
                const eyeMat = new THREE.MeshStandardMaterial({
                    color: 0x111111,
                    roughness: 0.15,
                    metalness: 0.4,
                });
                const eyeHighlightMat = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    roughness: 0.0,
                    metalness: 0.1,
                });

                // Body (elongated along Z, nose at -Z, tail at +Z)
                const bodyGeo = new THREE.SphereGeometry(1, 18, 14);
                const bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
                bodyMesh.scale.set(0.38 * s, 0.52 * s, 1.25 * s);
                bodyMesh.position.set(0, 0, -0.1 * s);
                bodyMesh.castShadow = true;
                bodyMesh.receiveShadow = true;
                this.group.add(bodyMesh);

                // Nose bump
                const noseGeo = new THREE.SphereGeometry(1, 12, 10);
                const noseMesh = new THREE.Mesh(noseGeo, bodyMat);
                noseMesh.scale.set(0.25 * s, 0.35 * s, 0.45 * s);
                noseMesh.position.set(0, 0.05 * s, -1.1 * s);
                noseMesh.castShadow = true;
                this.group.add(noseMesh);

                // Tail group (at back, +Z)
                this.tailGroup = new THREE.Group();
                this.tailGroup.position.set(0, 0, 1.1 * s);
                this.group.add(this.tailGroup);

                // Tail fin (vertical triangle, extends backward in +Z)
                const tailGeo = new THREE.BufferGeometry();
                const tailVerts = new Float32Array([
                    0, 0, 0, // attachment point
                    0, 0.5 * s, -0.55 * s, // upper tip
                    0, -0.5 * s, -0.55 * s, // lower tip
                ]);
                tailGeo.setAttribute('position', new THREE.BufferAttribute(tailVerts, 3));
                tailGeo.setIndex([0, 1, 2]);
                tailGeo.computeVertexNormals();
                const tailMesh = new THREE.Mesh(tailGeo, finMat);
                tailMesh.castShadow = true;
                this.tailGroup.add(tailMesh);

                // Dorsal fin (on top, slightly back)
                const dorsalGeo = new THREE.BufferGeometry();
                const dorsalVerts = new Float32Array([
                    0, 0.35 * s, -0.2 * s, // base front
                    0, 0.35 * s, 0.7 * s, // base back
                    0, 0.85 * s, 0.25 * s, // tip
                ]);
                dorsalGeo.setAttribute('position', new THREE.BufferAttribute(dorsalVerts, 3));
                dorsalGeo.setIndex([0, 1, 2]);
                dorsalGeo.computeVertexNormals();
                const dorsalMesh = new THREE.Mesh(dorsalGeo, finMat);
                dorsalMesh.castShadow = true;
                this.group.add(dorsalMesh);

                // Pectoral fins (sides, lower)
                for (const side of [-1, 1]) {
                    const pectGeo = new THREE.BufferGeometry();
                    const pectVerts = new Float32Array([
                        0, 0, 0, // base
                        side * 0.6 * s, -0.25 * s, -0.3 * s, // tip
                        side * 0.45 * s, 0.1 * s, -0.15 * s, // mid
                    ]);
                    pectGeo.setAttribute('position', new THREE.BufferAttribute(pectVerts, 3));
                    pectGeo.setIndex([0, 1, 2]);
                    pectGeo.computeVertexNormals();
                    const pectMesh = new THREE.Mesh(pectGeo, finMat);
                    pectMesh.position.set(side * 0.32 * s, -0.1 * s, -0.2 * s);
                    pectMesh.rotation.z = side * 0.25;
                    pectMesh.castShadow = true;
                    pectMesh.userData = { side: side, baseRot: side * 0.25 };
                    this.group.add(pectMesh);
                    if (!this.pectoralFins) this.pectoralFins = [];
                    this.pectoralFins.push(pectMesh);
                }

                // Anal fin (bottom, back)
                const analGeo = new THREE.BufferGeometry();
                const analVerts = new Float32Array([
                    0, -0.3 * s, 0.3 * s,
                    0, -0.3 * s, 0.8 * s,
                    0, -0.65 * s, 0.55 * s,
                ]);
                analGeo.setAttribute('position', new THREE.BufferAttribute(analVerts, 3));
                analGeo.setIndex([0, 1, 2]);
                analGeo.computeVertexNormals();
                const analMesh = new THREE.Mesh(analGeo, finMat);
                analMesh.castShadow = true;
                this.group.add(analMesh);

                // Eyes
                for (const side of [-1, 1]) {
                    const eyeGeo = new THREE.SphereGeometry(0.11 * s, 8, 6);
                    const eyeMesh = new THREE.Mesh(eyeGeo, eyeMat);
                    eyeMesh.position.set(side * 0.22 * s, 0.08 * s, -1.05 * s);
                    this.group.add(eyeMesh);

                    const highlightGeo = new THREE.SphereGeometry(0.04 * s, 6, 5);
                    const highlightMesh = new THREE.Mesh(highlightGeo, eyeHighlightMat);
                    highlightMesh.position.set(
                        side * 0.26 * s,
                        0.12 * s,
                        -1.08 * s
                    );
                    this.group.add(highlightMesh);
                }
            }

            pickNewTarget() {
                const margin = WALL_MARGIN + 0.5;
                this.target.set(
                    (Math.random() - 0.5) * (TANK_HALF_W * 2 - margin * 2),
                    SAND_Y + 1.5 + Math.random() * (WATER_SURFACE_Y - SAND_Y - 3),
                    (Math.random() - 0.5) * (TANK_HALF_D * 2 - margin * 2)
                );
                this.wanderInterval = 3 + Math.random() * 5;
            }

            update(deltaTime, time, mouseRay, userActive) {
                // Compute wander force toward target
                const toTarget = new THREE.Vector3().subVectors(this.target, this.group.position);
                const distToTarget = toTarget.length();
                const wanderForce = toTarget.normalize().multiplyScalar(WANDER_STRENGTH);

                // Flee force from mouse ray
                const fleeForce = new THREE.Vector3();
                this.isFleeing = false;

                if (mouseRay && userActive) {
                    const fishPos = this.group.position;
                    const toPoint = new THREE.Vector3().subVectors(fishPos, mouseRay.origin);
                    const projection = toPoint.dot(mouseRay.direction);
                    const closestPoint = new THREE.Vector3().copy(mouseRay.origin)
                        .addScaledVector(mouseRay.direction, Math.max(0, projection));
                    const distToRay = fishPos.distanceTo(closestPoint);

                    if (distToRay < this.fleeRadius) {
                        this.isFleeing = true;
                        const fleeDir = new THREE.Vector3().subVectors(fishPos, closestPoint);
                        if (fleeDir.length() < 0.1) {
                            fleeDir.set(
                                (Math.random() - 0.5),
                                (Math.random() - 0.5) * 0.5,
                                (Math.random() - 0.5)
                            ).normalize();
                        } else {
                            fleeDir.normalize();
                        }
                        // Add slight random variation so fish don't all flee identically
                        fleeDir.x += (Math.random() - 0.5) * 0.6;
                        fleeDir.y += (Math.random() - 0.5) * 0.4;
                        fleeDir.z += (Math.random() - 0.5) * 0.6;
                        fleeDir.normalize();

                        const fleeStrength = FLEE_STRENGTH * (1 - distToRay / this.fleeRadius) * (0.7 + Math.random() *
                            0.6);
                        fleeForce.copy(fleeDir).multiplyScalar(fleeStrength);
                    }
                }

                // Wall avoidance
                const wallForce = new THREE.Vector3();
                const pos = this.group.position;
                const margin = WALL_MARGIN;
                const wallStrength = 8.0;
                const boundsX = TANK_HALF_W - margin;
                const boundsYTop = WATER_SURFACE_Y - margin * 0.6;
                const boundsYBottom = SAND_Y + margin * 0.8;
                const boundsZ = TANK_HALF_D - margin;

                if (pos.x > boundsX) wallForce.x -= wallStrength * (pos.x - boundsX) / margin;
                if (pos.x < -boundsX) wallForce.x += wallStrength * (-boundsX - pos.x) / margin;
                if (pos.y > boundsYTop) wallForce.y -= wallStrength * (pos.y - boundsYTop) / margin;
                if (pos.y < boundsYBottom) wallForce.y += wallStrength * (boundsYBottom - pos.y) / margin;
                if (pos.z > boundsZ) wallForce.z -= wallStrength * (pos.z - boundsZ) / margin;
                if (pos.z < -boundsZ) wallForce.z += wallStrength * (-boundsZ - pos.z) / margin;

                // Sum forces
                const totalForce = new THREE.Vector3()
                    .add(wanderForce)
                    .add(fleeForce)
                    .add(wallForce);

                // Update velocity with smooth damping
                const dampFactor = 1 - Math.exp(-this.turnSpeed * deltaTime);
                this.velocity.lerp(totalForce.normalize().multiplyScalar(this.maxSpeed), dampFactor);

                // Ensure minimum speed
                const currentSpeed = this.velocity.length();
                if (currentSpeed < this.minSpeed && !this.isFleeing) {
                    this.velocity.normalize().multiplyScalar(this.minSpeed);
                }

                // Update position
                this.group.position.x += this.velocity.x * deltaTime;
                this.group.position.y += this.velocity.y * deltaTime;
                this.group.position.z += this.velocity.z * deltaTime;

                // Hard clamp to bounds (safety)
                this.group.position.x = THREE.MathUtils.clamp(this.group.position.x, -TANK_HALF_W + 0.4, TANK_HALF_W -
                    0.4);
                this.group.position.y = THREE.MathUtils.clamp(this.group.position.y, SAND_Y + 0.6, WATER_SURFACE_Y -
                    0.8);
                this.group.position.z = THREE.MathUtils.clamp(this.group.position.z, -TANK_HALF_D + 0.4, TANK_HALF_D -
                    0.4);

                // Update orientation (smooth lookAt)
                if (this.velocity.length() > 0.05) {
                    const velDir = this.velocity.clone().normalize();
                    const targetPos = this.group.position.clone().add(velDir);
                    const lookMatrix = new THREE.Matrix4();
                    lookMatrix.lookAt(this.group.position, targetPos, new THREE.Vector3(0, 1, 0));
                    const targetQuat = new THREE.Quaternion().setFromRotationMatrix(lookMatrix);
                    const slerpFactor = 1 - Math.exp(-this.turnSpeed * 0.6 * deltaTime);
                    this.group.quaternion.slerp(targetQuat, slerpFactor);
                }

                // Tail wag animation based on speed
                const speedRatio = this.velocity.length() / this.maxSpeed;
                this.tailWagPhase += deltaTime * this.tailWagSpeed * (2 + speedRatio * 6);
                this.tailWagAmp = 0.15 + speedRatio * 0.35;
                this.tailGroup.rotation.y = Math.sin(this.tailWagPhase) * this.tailWagAmp;

                // Pectoral fin flutter
                if (this.pectoralFins) {
                    const finFlutter = Math.sin(this.tailWagPhase * 1.4) * 0.2 * (0.3 + speedRatio * 0.7);
                    for (const fin of this.pectoralFins) {
                        fin.rotation.z = fin.userData.baseRot + finFlutter * fin.userData.side;
                        fin.rotation.x = Math.sin(this.tailWagPhase * 0.9) * 0.15;
                    }
                }

                // Gentle bobbing
                this.bobPhase += deltaTime * this.bobSpeed;
                const bobOffset = Math.sin(this.bobPhase) * this.bobAmp * (1 - speedRatio * 0.5);
                this.group.position.y += bobOffset * deltaTime * 2;

                // Wander target update
                if (distToTarget < 1.2 || this.wanderTimer > this.wanderInterval) {
                    this.pickNewTarget();
                    this.wanderTimer = 0;
                }
                this.wanderTimer += deltaTime;
            }
        }

        // Create fish
        const fishes = [];
        for (let i = 0; i < FISH_COUNT; i++) {
            const startPos = new THREE.Vector3(
                (Math.random() - 0.5) * (TANK_HALF_W * 2 - 4),
                SAND_Y + 2 + Math.random() * (WATER_SURFACE_Y - SAND_Y - 4),
                (Math.random() - 0.5) * (TANK_HALF_D * 2 - 4)
            );
            const sizeScale = 0.55 + Math.random() * 0.5;
            const fish = new Fish(i, sizeScale, startPos);
            fishes.push(fish);
        }

        // ─── Background Decor ───
        // Dark wall behind the tank
        const bgWallMat = new THREE.MeshStandardMaterial({
            color: 0x111822,
            roughness: 0.9,
            metalness: 0.0,
        });
        const bgWallGeo = new THREE.PlaneGeometry(50, 35);
        const bgWall = new THREE.Mesh(bgWallGeo, bgWallMat);
        bgWall.position.set(0, 5, -20);
        bgWall.receiveShadow = true;
        scene.add(bgWall);

        // Floor
        const floorMat = new THREE.MeshStandardMaterial({
            color: 0x1e2228,
            roughness: 0.7,
            metalness: 0.1,
        });
        const floorGeo = new THREE.PlaneGeometry(60, 50);
        const floorMesh = new THREE.Mesh(floorGeo, floorMat);
        floorMesh.rotation.x = -Math.PI / 2;
        floorMesh.position.set(0, -TANK_HALF_H - 1.3, 0);
        floorMesh.receiveShadow = true;
        scene.add(floorMesh);

        // ─── Mouse Tracking ───
        const raycaster = new THREE.Raycaster();
        const mouseNDC = new THREE.Vector2(0, 0);
        let userActive = false;
        let virtualMouseNDC = new THREE.Vector2(0.3, 0.2);
        let demoTime = 0;

        const overlay = document.getElementById('overlay');
        const cursorDot = document.getElementById('cursor-dot');
        const statusText = document.getElementById('status-text');
        const infoBar = document.getElementById('info-bar');

        let hasInteracted = false;

        function activateUserMode() {
            if (hasInteracted) return;
            hasInteracted = true;
            userActive = true;
            overlay.classList.add('hidden');
            statusText.textContent = 'Move your mouse near fish to make them flee!';
            infoBar.classList.remove('dim');
            cursorDot.style.display = 'block';
        }

        overlay.addEventListener('click', activateUserMode);
        window.addEventListener('keydown', (e) => {
            if (e.key !== 'Escape') activateUserMode();
        });

        // Track real mouse
        window.addEventListener('pointermove', (e) => {
            mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
            mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
            cursorDot.style.left = e.clientX + 'px';
            cursorDot.style.top = e.clientY + 'px';
        });

        // Also update cursor dot on click area
        overlay.addEventListener('pointermove', (e) => {
            mouseNDC.x = (e.clientX / window.innerWidth) * 2 - 1;
            mouseNDC.y = -(e.clientY / window.innerHeight) * 2 + 1;
            cursorDot.style.left = e.clientX + 'px';
            cursorDot.style.top = e.clientY + 'px';
        });

        // Touch support
        window.addEventListener('touchmove', (e) => {
            if (e.touches.length > 0) {
                const touch = e.touches[0];
                mouseNDC.x = (touch.clientX / window.innerWidth) * 2 - 1;
                mouseNDC.y = -(touch.clientY / window.innerHeight) * 2 + 1;
                cursorDot.style.left = touch.clientX + 'px';
                cursorDot.style.top = touch.clientY + 'px';
                // Touch triggers user mode only after first tap
            }
        }, { passive: true });

        window.addEventListener('touchstart', (e) => {
            activateUserMode();
            if (e.touches.length > 0) {
                const touch = e.touches[0];
                mouseNDC.x = (touch.clientX / window.innerWidth) * 2 - 1;
                mouseNDC.y = -(touch.clientY / window.innerHeight) * 2 + 1;
                cursorDot.style.left = touch.clientX + 'px';
                cursorDot.style.top = touch.clientY + 'px';
            }
        }, { passive: true });

        // Hide cursor when mouse leaves
        document.addEventListener('mouseleave', () => {
            cursorDot.style.display = 'none';
        });
        document.addEventListener('mouseenter', () => {
            if (hasInteracted) cursorDot.style.display = 'block';
        });

        // ─── Demo Mode Virtual Mouse ───
        function computeVirtualMouse(time) {
            const t1 = time * 0.45;
            const t2 = time * 0.65;
            const t3 = time * 0.33;
            const x = Math.sin(t1) * 0.55 + Math.sin(t2 * 1.7) * 0.25;
            const y = Math.cos(t2 * 1.1) * 0.35 + Math.sin(t3 * 2.3) * 0.2;
            virtualMouseNDC.set(x, y * 0.8);
            return virtualMouseNDC;
        }

        // ─── Animation Loop ───
        const clock = new THREE.Clock();
        let frameCount = 0;
        let anyFishFleeing = false;

        function animate() {
            const deltaTime = Math.min(clock.getDelta(), 0.05);
            const elapsedTime = clock.elapsedTime;
            frameCount++;

            // Update virtual mouse in demo mode
            if (!userActive) {
                demoTime += deltaTime;
                computeVirtualMouse(demoTime);
                // Update cursor dot position for demo
                const screenX = (virtualMouseNDC.x * 0.5 + 0.5) * window.innerWidth;
                const screenY = (-virtualMouseNDC.y * 0.5 + 0.5) * window.innerHeight;
                cursorDot.style.left = screenX + 'px';
                cursorDot.style.top = screenY + 'px';
                cursorDot.style.display = 'block';

                // Update raycaster for demo
                raycaster.setFromCamera(virtualMouseNDC, camera);
            } else {
                raycaster.setFromCamera(mouseNDC, camera);
            }

            const mouseRay = {
                origin: raycaster.ray.origin.clone(),
                direction: raycaster.ray.direction.clone(),
            };

            // Update fish
            anyFishFleeing = false;
            for (const fish of fishes) {
                fish.update(deltaTime, elapsedTime, mouseRay, userActive || !hasInteracted);
                if (fish.isFleeing) anyFishFleeing = true;
            }

            // Update cursor dot state
            if (anyFishFleeing) {
                cursorDot.classList.add('near-fish');
                if (fishes.some(f => f.isFleeing && f.velocity.length() > f.maxSpeed * 0.8)) {
                    cursorDot.classList.add('panic');
                } else {
                    cursorDot.classList.remove('panic');
                }
            } else {
                cursorDot.classList.remove('near-fish', 'panic');
            }

            // Update bubbles
            for (const bubble of bubbles) {
                const ud = bubble.userData;
                bubble.position.y += ud.speed * deltaTime;
                ud.wobblePhase += deltaTime * ud.wobbleFreq;
                bubble.position.x = ud.baseX + Math.sin(ud.wobblePhase) * ud.wobbleAmp;
                bubble.position.z = ud.baseZ + Math.cos(ud.wobblePhase * 0.8) * ud.wobbleAmp;

                if (bubble.position.y > WATER_SURFACE_Y - 0.3) {
                    bubble.position.y = SAND_Y + 0.2 + Math.random() * 1.5;
                    bubble.position.x = (Math.random() - 0.5) * (TANK_HALF_W * 2 - 3);
                    bubble.position.z = (Math.random() - 0.5) * (TANK_HALF_D * 2 - 3);
                    ud.baseX = bubble.position.x;
                    ud.baseZ = bubble.position.z;
                }
            }

            // Update seaweed sway
            for (const plant of seaweeds) {
                const ud = plant.userData;
                const sway = Math.sin(elapsedTime * ud.swaySpeed + ud.swayPhase) * ud.swayAmount;
                plant.rotation.z = sway;
                plant.rotation.x = Math.cos(elapsedTime * ud.swaySpeed * 0.7 + ud.swayPhase) * ud.swayAmount * 0.6;
            }

            // Subtle water surface shimmer
            if (surfaceMesh.geometry.attributes.position) {
                const surfPos = surfaceMesh.geometry.attributes.position;
                for (let i = 0; i < surfPos.count; i++) {
                    const ox = surfPos.getX(i);
                    const oz = surfPos.getY(i);
                    const wave = Math.sin(ox * 1.5 + elapsedTime * 1.8) * Math.cos(oz * 2.2 + elapsedTime * 2.3) * 0.06 +
                        Math.sin(ox * 3.7 + oz * 4.1 + elapsedTime * 1.2) * 0.04;
                    surfPos.setZ(i, wave);
                }
                surfPos.needsUpdate = true;
                surfaceMesh.geometry.computeVertexNormals();
            }

            // Caustic light movement
            causticSpotLight.position.x = 3 + Math.sin(elapsedTime * 0.5) * 4;
            causticSpotLight.position.z = 2 + Math.cos(elapsedTime * 0.4) * 3;
            causticSpotLight.intensity = 0.8 + Math.sin(elapsedTime * 0.7) * 0.4;

            // Subtle camera sway (very slow)
            if (!userActive) {
                const camSwayX = Math.sin(elapsedTime * 0.12) * 0.8;
                const camSwayY = Math.sin(elapsedTime * 0.09) * 0.6;
                camera.position.x = 15 + camSwayX;
                camera.position.y = 3.5 + camSwayY;
                camera.lookAt(0, -0.8, 0);
            }

            // Update water glow
            waterGlowLight.intensity = 1.5 + Math.sin(elapsedTime * 1.2) * 0.5;

            renderer.render(scene, camera);
            requestAnimationFrame(animate);
        }

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

        // ─── Start ───
        // Initial cursor dot hidden until overlay interaction
        cursorDot.style.display = 'none';
        cursorDot.style.left = window.innerWidth / 2 + 'px';
        cursorDot.style.top = window.innerHeight / 2 + 'px';

        console.log('🐟 3D Aquarium loaded — fish flee from your cursor!');
        animate();
    </script>
</body>
</html>
Reasoning trace (12,848 tokens)
The user wants a 3D aquarium with Three.js where fish swim away when the mouse moves near them. Let me build a complete, self-contained HTML file with:

1. Three.js loaded via the import map
2. A 3D aquarium scene with:
   - A tank (glass walls, water)
   - Fish (procedurally generated with Three.js geometries)
   - Plants/seaweed
   - Bubbles
   - Sand/gravel bottom
   - Lighting
3. Fish behavior:
   - Fish swim around randomly
   - When mouse gets near (in 3D space projected to their position), fish swim away
4. Self-playing demo mode with click/key to start

Let me design this carefully:

**Aquarium setup:**
- A glass tank with visible edges
- Water with slight blue tint
- Sandy bottom
- Some rocks and plants
- Bubbles rising
- Several fish of different colors

**Fish AI:**
- Each fish has a target position (wander behavior)
- Fish move toward targets with smooth turning
- When mouse is near (raycast to find fish near mouse position), fish flee away from mouse
- Fish have slight bobbing/waving animation

**Mouse interaction:**
- Track mouse position in 3D space (raycast onto a plane or use projected coordinates)
- Calculate distance from mouse to each fish
- If within threshold, fish swims away from mouse direction

Let me write the code:

```html
<!DOCTYPE html>
<html>
<head>
<style>
  /* styles */
</style>
</head>
<body>
<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';
// ... everything else
</script>
</body>
</html>
```

Let me think about the details:

**Tank geometry:**
- A box with glass material (transparent)
- Inner dimensions, say 20 x 12 x 14 units
- Glass walls with slight transparency and reflection
- Metal frame edges

**Water:**
- Slightly blue-tinted transparent volume
- Maybe a subtle fog inside

**Sand bottom:**
- A plane with a sandy color, maybe with some bumps
- Could use a simple plane with vertex coloring or a texture generated procedurally

**Plants:**
- Green curved cylinders or custom shapes
- Animate them swaying

**Fish:**
- Made from simple geometries (body ellipsoid, tail fin triangle, etc.)
- Different colors
- Animate tail wagging
- Each fish has: position, velocity, target, wander timer, color, size

**Bubbles:**
- Rising spheres with transparency
- Reset when they reach top

**Lighting:**
- Ambient light
- Directional light from above (like aquarium light)
- Maybe a point light

**Camera:**
- Fixed or slightly orbiting view
- OrbitControls might be nice but could interfere with mouse interaction... Let me skip OrbitControls and keep a fixed camera with slight sway, or allow orbit with right-click drag or something. Actually the requirement says fish swim away when mouse moves near them, so left-click/mouse move should just move the mouse. Let me use a fixed camera that slowly orbits or a mostly fixed view.

Actually, let me think again about the mouse interaction. The mouse should move freely over the scene. To determine "near" the fish, I need to raycast from the camera through the mouse position and find the closest point to each fish. Or I can project fish positions to screen space and check 2D distance to mouse.

The 2D screen space approach might be simpler and more reliable: project each fish's position to screen coordinates, calculate distance to mouse, if close enough, fish flees in the direction away from the mouse ray.

Actually, a better approach for 3D: raycast from mouse into the scene. Find the ray. For each fish, find the closest point on the ray to the fish's position. If that distance is below a threshold, the fish flees away from the mouse direction.

Let me implement this:
- Use THREE.Raycaster to get the ray from mouse position
- For each fish, compute distance from fish position to the ray
- If distance < threshold, add a flee force away from the ray

This is actually the "mouse near them" in 3D sense.

Let me also add some subtle water caustics or light rays through the water for visual effect.

For the self-playing demo, fish just swim around naturally. The demo message says "Click or press a key to start" - clicking will show the mouse cursor approaching fish and they'll flee.

Actually, let me re-read: "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." This is for games. This is more of an interactive demo, but I'll still include the message and have it work both ways - the fish will swim around regardless, and clicking allows mouse interaction.

Let me also add a nice UI overlay with title and instructions.

**Fish creation:**
Each fish can be made from:
- Body: A scaled sphere (ellipsoid)
- Tail: A triangle or cone shape that wags
- Eyes: Small dark spheres
- Dorsal fin: A triangle
- Color variations

I'll create a Fish class/function that builds a THREE.Group with all these parts.

**Fish behavior:**
- Wander: Pick random target points within tank bounds, swim toward them
- When target reached, pick new target
- Flee: When mouse is near, swim away from the mouse ray
- Smooth turning using quaternion slerp or lerping direction
- Tail wagging based on speed

**Tank bounds:** Keep fish within the tank walls with soft boundaries (steer back in when near edges).

Let me write all of this:

For the fish body, I'll use:
- Main body: SphereGeometry scaled (1, 0.5, 0.4) - ellipsoid
- Tail: Custom triangle geometry or a flattened cone
- Dorsal fin: Triangle or flattened shape
- Pectoral fins: Small flattened shapes
- Eyes: Two small spheres

For tail wagging, I'll rotate the tail group back and forth based on speed.

For fish materials, I'll use MeshStandardMaterial or MeshPhongMaterial with different colors.

Let me design the plants:
- Several strands of seaweed: cylinders that curve upward, maybe using TubeGeometry or just a series of connected segments
- Color: various greens

For bubbles:
- SphereGeometry with transparent material
- Rise from bottom, wobble slightly
- Reset at top

Let me create the full implementation now.

Actually, for the fish to look nice, let me think about the shape more carefully. A simple fish can be:
- Body: scaled sphere - `new THREE.Mesh(new THREE.SphereGeometry(1, 16, 12), material)` then scale to (1.2, 0.6, 0.45)
- Tail fin: A flattened triangle at the back - could use a custom BufferGeometry or a ConeGeometry scaled flat
- Actually, let me use custom shapes

For the tail, I can use a simple approach: create a triangle geometry using BufferGeometry with 3 vertices.
Or use a flattened cone.

Let me use a simpler approach - create tail from a custom geometry:
- 3 vertices forming a triangle
- Double-sided material

For plants, I'll use curved tubes. THREE.TubeGeometry with a CatmullRomCurve3.

For the tank:
- Outer glass box (transparent material)
- Inner water volume (slightly blue transparent)
- Bottom sand
- Some rocks (icosahedron geometries)

Let me also add a subtle grid or pattern on the back wall, or maybe a gradient background.

I also want to add some caustic-like effect - maybe moving light spots on the sand from a spotlight above with a slight movement.

Actually let me focus on making it look great and the fish interaction smooth. I'll add:

1. A simple environment with warm lighting
2. Good fish models
3. Smooth fleeing behavior
4. Bubbles
5. Plants
6. Nice UI

Let me code this up now.

For the fish fleeing: when the mouse ray comes within a certain distance of a fish, the fish should swim away from the mouse cursor. The direction of fleeing should be away from the closest point on the ray to the fish.

The closest point on a ray to a point can be computed as:
- Ray: origin O, direction D (normalized)
- Point P
- t = dot(P - O, D)
- Closest point C = O + t * D
- Distance = |P - C|

If distance < threshold, flee direction = normalize(P - C) * flee_strength

The fish should also avoid walls, so I'll add wall avoidance.

For smooth fish movement, I'll use a simple steering behavior:
- Current velocity (direction + speed)
- Desired velocity from wander/flee
- Smoothly blend between them
- Use quaternion to rotate fish to face direction of travel

Let me also add some slight bobbing (up/down oscillation) to make the fish look alive.

I'll create maybe 8-10 fish of various colors and sizes.

Now about the camera: I want the user to be able to see the whole aquarium nicely. Let me position the camera at a slight angle looking at the tank. Maybe a slow auto-orbit that stops when the user interacts, or just keep it fixed.

I'll do a slow orbit camera that can be paused. Actually for simplicity, I'll have a gentle camera sway/movement and let the user orbit with mouse drag (holding a button). But the mouse movement is used for fish interaction... Hmm.

Let me think: the user moves the mouse over the aquarium to scare fish. The mouse position in 3D (raycast) determines proximity. So mouse position is used for fish interaction, not camera control. I'll keep the camera mostly fixed with maybe a very slow auto-orbit or slight movement.

Actually, I could add OrbitControls for right-click drag and mouse wheel zoom, while left mouse movement (no click) drives the fish interaction. That would be nice. But it adds complexity. Let me keep the camera fixed with a very slight sway for now.

Let me also think about what the insect should look like. I want colorful fish - maybe some orange/red, some blue, some yellow.

Here's my plan for the code structure:

```javascript
// Scene setup
// Create tank (glass, water, sand, back panel)
// Create plants
// Create rocks
// Create bubbles
// Create fish
// Mouse tracking + raycaster
// Animation loop
// Fish behavior
```

Let me write the complete code now.

For the tank size, I'll use:
- Width: 20
- Height: 12  
- Depth: 14

Tank positioned so the sand bottom is at y = -6 and water surface at y = 6 (center at y=0).

Fish swim between -9 and 9 in x, -4 to 4 in y (keeping some distance from surface and bottom), -6 to 6 in z.

Actually, let me make it simpler. Tank centered at origin:
- Half-width X: 10
- Half-height Y: 6
- Half-depth Z: 7

Glass walls at x=±10, y=±6, z=±7.
Sand at y=-6 (bottom), water surface at y=6 (top).
Fish constraints: x ∈ [-9, 9], y ∈ [-4.5, 4.5], z ∈ [-6, 6].

Bubbles rise from near the bottom (y=-5) up to y=5.5.

Camera at maybe (0, 2, 20) looking at (0, 0, 0). Actually, at an angle so we see the front and side.

Let me put camera at (16, 4, 18) looking at (0, -1, 0).

Alright, now let me be more careful about the glass visualization. I want to see through the glass to the fish. The glass walls should be mostly transparent with some reflection. I'll use MeshPhysicalMaterial with transmission or just MeshStandardMaterial with transparency and low opacity.

Actually, MeshPhysicalMaterial with transmission is great for glass in Three.js 0.170. Let me use that. Or simpler: MeshStandardMaterial with transparent: true, opacity: 0.15, and maybe some metalness/roughness for reflection.

For the water inside, I'll also have a slightly blue transparent box inside the tank bounds.

I should also add a subtle edge/frame to the tank so the glass is visible. I'll add metal-colored edges along the corners.

Let me also create the back panel with a gradient or slightly darker blue to suggest deeper water.

For the sand, I could create a PlaneGeometry with some procedural texture (using a canvas). Or just use a MeshStandardMaterial with a sandy color and some noise via vertex displacement. Let me create a procedural sand texture using a canvas.

Actually, let me keep things simpler and use colored materials with some roughness variation. I'll add a few displaced vertices for the sand to make it bumpy.

Now I need to think about the fish tail animation. Each fish has a tail that wags. I'll store the tail as a child of the fish group and rotate its y-axis back and forth.

For fish swimming animation, I'll also add slight body undulation - but that's complex with a single mesh. The tail wagging plus slight bobbing should be enough.

Let me define the Fish class:

```javascript
class Fish {
    constructor(scene, color, size, position) {
        this.group = new THREE.Group();
        // Body
        // Tail
        // Fins
        // Eyes
        // Position, velocity, target, etc.
        scene.add(this.group);
    }
    
    update(deltaTime, mouseRay, mousePos3D, time) {
        // Wander behavior
        // Flee behavior
        // Movement
        // Tail animation
    }
}
```

For the body of the fish, I'll use a slightly pointed shape. I could use a SphereGeometry and scale it, or create a custom shape. Let me use a sphere scaled to look like a fish body:
- Main body: SphereGeometry(1, 16, 12), scale (1.3, 0.55, 0.45) - elongate
- The nose (front) should be slightly more pointed

Actually, for a nicer fish shape, I could use a LatheGeometry or extrude a spline profile. But to keep it simple and performant, I'll use scaled spheres and add fins.

For the tail: I'll create a triangle shape using BufferGeometry:
- Vertices: (0,0,0), (0, 0.5, -0.7), (0, -0.5, -0.7)
- This creates a vertical triangle tail fin at the back
- I'll place this at the back of the fish

Actually, the tail fin should be in the vertical plane (spread out left-right from the fish's perspective). Wait no - for a typical fish, the tail fin is vertical (spread in the Y direction) and it beats side to side (rotating around the Y axis).

So the tail fin vertices would be:
- (0, 0, 0) - attachment point at body rear
- (0, 0.45, -0.6) - upper tip
- (0, -0.45, -0.6) - lower tip
- And maybe make it more triangular: (0, 0, -0.3) as a mid-point

Actually, let me make it easier: create the tail as a flattened triangle in the Y-Z plane (vertical) at the back of the fish.

Let me define the fish geometry more carefully:

Fish faces +X direction (forward).
- Body: at origin, sphere scaled (1.2, 0.5, 0.4)
- Nose: at +0.8, slightly narrower
- Tail: at -1.2, two triangular fins (upper and lower)
- Dorsal fin: on top (+Y)
- Pectoral fins: on sides

For the tail group, I'll put it at x = -1.0 (back of body) and rotate it around Y axis for wagging.

Tail fin mesh: a flat triangle shape. I'll use ShapeGeometry or BufferGeometry.

Let me use a simple approach - create a flattened cone or a custom BufferGeometry triangle:

```javascript
const tailGeometry = new THREE.BufferGeometry();
const tailVertices = new Float32Array([
    0, 0, 0,      // attachment
    0, 0.5, -0.7, // top tip
    0, -0.5, -0.7 // bottom tip
]);
tailGeometry.setAttribute('position', new THREE.BufferAttribute(tailVertices, 3));
tailGeometry.setIndex([0, 1, 2]);
tailGeometry.computeVertexNormals();
```

And make it double-sided with a material.

Actually, I want the tail fin to look like a fin - maybe with a slight curve. Three points should be fine for a stylized look. I'll also add a slightly wider base.

Let me also add dorsal fin (top fin) and pectoral fins (side fins) for more visual interest.

OK, I think I've planned enough. Let me write the complete HTML now.

One more thing - the bubbles. They should be small transparent spheres rising from various points near the bottom of the tank. I'll have about 30-40 bubbles at various positions, all cycling. Each bubble: sphere geometry at a starting position, rises at a certain rate, wobbles side to side slightly, resets at top.

And I want to add some underwater light effects - maybe a few light rays or caustic spots. I'll add a spotlight from above with a subtle moving pattern if possible, or just keep it simple with a nice directional light.

Let me also add subtle fog inside the water for depth.

Alright, I also want to handle the "Click or press a key to start" overlay. The fish will be swimming around regardless (demo mode). When the user clicks, the overlay disappears and mouse interaction becomes active.

Actually, re-reading the instructions: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or press a key to start' message". This is more of a demo than a game, but I'll still include it for good UX. The fish swim naturally in demo mode. The overlay says "Click or press a key to start" - clicking removes the overlay and activates mouse tracking.

In demo mode, I could also simulate a mouse moving around to show the fish fleeing behavior. That would make the self-playing demo more interesting! Let me add a simulated mouse that moves around in a pattern during demo mode, scaring the fish periodically. This way the judge can see the fish fleeing behavior without clicking.

Let me implement this: in demo mode, there's a virtual mouse that moves in a smooth Lissajous pattern across the viewport. The fish flee from this virtual mouse. When the user clicks, the real mouse takes over.

This is a great idea. The virtual mouse will demonstrate the flee behavior, and the user can then interact with their real mouse.

Let me code all of this up now. I'll be thorough about the implementation.

For the import map, I need to use exactly the format specified by the user.

Let me write the complete code:

Actually, one thing to note - I need to make sure the fish look good. I'll add slight color variations, some iridescence or metallic sheen to make them look pretty. Maybe use MeshStandardMaterial with some roughness/metalness adjustments.

For the water surface, I could add a subtle plane at the top with some transparency and movement. But that adds complexity. Let me instead add a semi-transparent plane at the water surface with slight animation to suggest ripples.

Actually, let me keep it simpler and just use the tank's transparent top and a subtle color change. The tank frame will show where the glass is.

For the tank construction:
- 5 glass faces (bottom is sand) - actually I'll make all faces but the bottom might be sand
- Metal frame edges
- Glass is transparent

Hmm, actually the bottom will be sand on top of the glass bottom. Let me put sand right at y = -6 (bottom of tank), and the glass bottom at y = -6.1 (just below). Or just have sand at the bottom without a separate glass bottom panel visible.

I'll create:
- Glass walls: front (z=7), back (z=-7), left (x=-10), right (x=10), top (y=6), bottom (y=-6)
- Sand: a plane at y=-5.8 with some bumps
- Metal frame at edges

For the glass, I'll use MeshPhysicalMaterial with transmission: 0.9, roughness: 0.05, thickness: 0.1. This gives realistic glass in Three.js 0.170. But it can be expensive to render. Let me use a simpler transparent material with low opacity instead.

Actually, MeshPhysicalMaterial with transmission requires the transmission renderer setting which might be complex. For simplicity, I'll use MeshStandardMaterial with transparent: true, opacity: 0.12, with some envMap-like reflection simulated by setting metalness and roughness.

Actually, for glass to work well, I might want to use MeshPhongMaterial with transparency and specular highlights. Let me use:
- Glass: MeshStandardMaterial with transparent: true, opacity: 0.15, roughness: 0.05, metalness: 0.1
- This should give a subtle glassy reflection effect

Or I could use a simple MeshBasicMaterial with very low opacity for the glass faces. Let me go with a slightly more sophisticated approach.

For the back panel of the aquarium, I'll add a darker blue/green gradient panel inside the tank to suggest depth.

OK, here's my comprehensive plan. Let me write the code.

One issue: with transparent materials in Three.js, rendering order matters. I need to handle transparency carefully. The fish inside the tank need to render after the glass walls. Three.js handles this with the transparency sorting, but for better results I might need to set renderOrder.

Actually, Three.js generally handles transparent objects okay if they're all in the same scene. The glass will be rendered after opaque objects. Fish are opaque. The water inside (if transparent) should render after glass.

Let me also add some decorations:
- A few rocks on the sand (icosahedron geometries with grey/brown colors)
- Sea plants (curved tubes)
- Maybe a small treasure chest? That's fun but complex. Let me skip it and focus on plants and rocks.

Let me think about the plants more carefully. I'll create seaweed using TubeGeometry along a curved path. Each plant strand will be a TubeGeometry following a CatmullRomCurve3 that curves upward and slightly outward. The plants will gently sway.

For swaying plants, I can either:
1. Rotate the entire plant group slightly
2. Recompute the tube geometry each frame (expensive)
3. Use vertex animation in a shader

I'll go with option 1 - rotating the plant group slightly around its base for a swaying effect.

For simplicity, each plant is a group of 3-5 tube strands. The group rotates gently around the Y axis at the base.

For rocks, I'll use IcosahedronGeometry with some displacement, colored gray/brown.

Now about the sand texture - I'll create it procedurally using a canvas. Draw noise dots in sandy colors. Or use vertex displacement on a plane geometry. Let me create a simple canvas texture for the sand.

Actually, let me just use a solid sandy color with some roughness. The sand at the bottom isn't the main focus. I could add a few rocks and it'll look natural.

OK, now let me also plan the lighting:
- Ambient light (warm, low intensity) - mood lighting
- Directional light from above (simulating aquarium light from the hood)
- Maybe a subtle blue point light for underwater ambiance
- A spotlight from above with slight movement for caustic effect

The aquarium light would be from the top of the tank (like an LED light strip in the hood). I'll add a directional light pointing down from above the tank.

For the caustic effect, I could add a point light or spotlight that moves slowly over the tank bottom. Or I can use a simple animated texture on the sand. Let me try a simple approach: a spotlight from above that slightly oscillates, creating moving light patterns on the sand.

Actually, a simpler eye-catching effect: I'll have a few light rays visible in the water (using transparent cone or plane geometries). These are cone-shaped light beams from the top of the tank.

OK, I think I've over-planned this. Let me start writing the code now. I'll aim for something comprehensive and visually appealing.

Here's my implementation strategy:
1. Set up the scene, camera, renderer
2. Build the tank (glass walls, frame, sand, back panel)
3. Create plants (multiple groups of seaweed tubes)
4. Create rocks
5. Create bubbles
6. Create fish (8-10 fish with different colors)
7. Set up mouse tracking (for user interaction)
8. Set up demo mode with virtual mouse
9. Animation loop with fish behavior, bubbles, plant sway
10. UI overlay

Let me code this carefully. I'll be detailed about the fish creation since that's the centerpiece.

For the fish body, I realize I should think about what direction the fish faces. In Three.js, the default forward direction for a mesh is +Z. But for the fish, I'll make them face +X direction (right) by default. When the fish moves in a direction, I'll use lookAt or set the quaternion to orient the fish.

Actually, let me make the fish geometry face +X. The fish body is an ellipsoid elongated along X:
- Scale X: 1.3 (forwards/backwards)
- Scale Y: 0.5 (up/down)
- Scale Z: 0.35 (left/right)

Nose at +X, tail at -X.

For orientation, when the fish moves in direction D, I need to rotate it so that its +X axis aligns with D. I can use `group.quaternion.setFromUnitVectors(new THREE.Vector3(1, 0, 0), D.normalized())`.

Or I could use lookAt, but lookAt makes the -Z axis face the target. I'd need the fish to face +X, so I'd need to adjust. The setFromUnitVectors approach is cleaner.

For smoothing the orientation, I can slerp between current and target quaternions.

Alright, one more consideration: the fish should also slightly bank/tilt when turning. This is a nice touch. When the fish turns, I can add a slight roll. This is complex, so I'll skip it for now or add a tiny bit of tilt based on turning direction.

Let me finalize and write the code. I'll aim for about 500-700 lines of JavaScript.

Let me also think about performance. With 10 fish, ~40 bubbles, ~15 plants, and the tank, this should run fine at 60fps. I'll use reasonable geometry sizes and avoid excessive subdivisions.

Here's the final plan for the fish flee behavior:

```javascript
// For each fish:
// 1. Compute wander force (towards random target)
// 2. Compute flee force from mouse
// 3. Compute wall avoidance force
// 4. Sum forces to get acceleration
// 5. Update velocity (smooth damping)
// 6. Update position
// 7. Clamp position to tank bounds
// 8. Update orientation
// 9. Animate tail
```

For the mouse ray, I'll compute it as:
- Raycaster from camera through mouse screen position
- For each fish, compute distance from fish position to ray
- If distance < fleeRadius, add flee force away from ray

Distance from point to ray:
```javascript
function pointToRayDistance(point, rayOrigin, rayDirection) {
    const toPoint = new THREE.Vector3().subVectors(point, rayOrigin);
    const projection = toPoint.dot(rayDirection);
    const closestPoint = new THREE.Vector3().copy(rayOrigin).addScaledVector(rayDirection, projection);
    return point.distanceTo(closestPoint);
}
```

Flee direction: from the closest point on the ray to the fish (away from mouse).

Now let me write the code. I'll start with the HTML structure.

For the demo mode virtual mouse, I'll create a smooth moving point on the screen (in NDC coordinates) that follows a Lissajous curve. The virtual mouse position is converted to a ray and fish flee from it.

When the user clicks or presses a key:
- The overlay disappears
- The virtual mouse stops
- The real mouse position is used

Actually, I need to handle the pointer move event even before the user clicks, so that when they click, the mouse is already where they expect it.

Let me also add an event for mouse move (pointermove) that updates the mouse position even before clicking, but the fish only respond to it after the user clicks.

Alright, I think I've planned enough. Let me write the complete code now.

One thing I want to get right: the appearance of the fish. Since I'm building them from primitives, they should look decent. Let me make:
- Body: scaled sphere (or multiple spheres for a more fish-like shape)
- Tail fin: triangle shape, wagging
- Dorsal fin: triangle on top
- Pectoral fins: small shapes on sides
- Eyes: small dark spheres
- Colors: various bright colors (orange, blue, red, yellow, purple, green)

Each fish will have a slightly different size too.

For the body, to make it more fish-like, I could use two spheres - a larger one for the main body and a smaller one at the head, slightly overlapping. Or just one elongated sphere. A single elongated sphere looks decent.

Actually, I want to make the nose slightly pointy. I could use a ConeGeometry with low radial segments at the front, or just accept the rounded sphere look. For simplicity, I'll use a scaled sphere.

Let me also add stripes or patterns on some fish. I could use a simple stripe texture generated on a canvas. For each fish, generate a unique stripe/spot pattern.

To keep things simpler, I'll give each fish a solid body color with a slightly lighter belly (using a second smaller sphere underneath). Some fish will have a stripe pattern using a canvas texture.

I think the simplest approach that looks good: solid colored fish with slightly different shades on body vs fins. The tail and fins use a slightly more translucent material.

OK, enough planning. Let me write the code now.

Actually, one more important thing. I need to make sure the fish don't go through glass walls. I'll add soft boundary constraints.

It's also worth noting that the user specified the import map format exactly, so I need to use it as given.

Let me also note that I should not have any console errors. I need to use Three.js 0.170.0 APIs correctly. Let me be careful about:
- Using `THREE.MeshStandardMaterial` (should use `meshStandardMaterial` for better quality in newer Three.js)
- Using `THREE.sRGBEncoding` vs `outputColorSpace` - in Three.js 0.170, it's `renderer.outputColorSpace = THREE.SRGBColorSpace`
- Using `physicallyCorrectLights` - this was replaced by the use of `renderer.useLegacyLights = false` (default in newer versions)

Actually, let me check what Three.js 0.170.0 uses:
- `THREE.SRGBColorSpace` is available
- `renderer.outputColorSpace = THREE.SRGBColorSpace` is the default in newer versions
- MeshStandardMaterial works fine
- Lights use physical units by default in newer versions

I'll use the standard approach and set `renderer.outputColorSpace = THREE.SRGBColorSpace` for correct color rendering with the newer version.

OK, I'm going to write the complete HTML file now. I'll structure it carefully.

I'll also need to handle the case where `document.body` isn't ready - but since the script is a module and loads after parsing, it should be fine.

Let me also consider adding a subtle background outside the aquarium - maybe a dark room with warm lighting. I'll set the scene background to a dark color, and perhaps add a simple floor outside the tank.

Or actually, the aquarium could be the main focus with a dark background. A nice dark blue/black background with the aquarium illuminated from within would look great.

Let me set the background to a dark navy/black color. The aquarium will be the main light source in the scene.

For extra depth, I'll add a plane behind the aquarium (like a wall) with a dark color. Or just use a solid background color.

Alright, now for real - I'll write the code.

One concern: creating a nice-looking aquarium with transparent glass in Three.js can be tricky due to rendering order. The key points:
1. Opaque objects render first (fish, sand, rocks, plants)
2. Transparent objects render after, sorted by distance from camera
3. Glass faces should be transparent
4. The water volume inside the tank should also be transparent with a slight blue tint

I'll set the glass to use a low opacity and the water to have a slight blue tint. The water should render after the glass for correct appearance... actually, Three.js handles this automatically through the transparent sorting.

One issue: the back glass face and the water inside the tank might have z-fighting or rendering order issues. I'll handle this by making the glass slightly larger than the water volume (glass is the outer boundary, water is slightly inside).

For the actual implementation:

Tank dimensions: half-extents (W=10, H=6, D=7)
Glass walls at x=±10, y=±6, z=±7 (using plane geometry)
Water volume: slightly inside the glass (half-extents 9.95, 5.95, 6.95)
Sand: at y=-5.9, spanning the bottom of the tank

The glass walls should be double-sided (since we can see through from one side to the other). I'll use DoubleSide for the glass material.

Actually, for a truly nice aquarium, I need the front glass (facing the camera) to be transparent enough to see inside, and the back glass to be slightly tinted. But for simplicity, all glass walls are the same material.

For the metal frame, I'll add BoxGeometry bars along the edges of the tank. These provide the visual frame that makes the glass boundaries visible.

I'll also add some caustic light spots on the sand. A simple way: add a large plane with a transparent texture that has light patch patterns, and animate it slightly. Or use a spotlight with a textured shadow. For simplicity, I'll skip caustics and use a nice plain sand color with some vertex displacement for texture.

Alright, let me finally write the code. I'll make it comprehensive.

One more thing: for the demo mode virtual mouse, I need to simulate mouse movement in screen coordinates. In Three.js, I need to convert screen coordinates to NDC (-1 to 1) for the raycaster. My virtual mouse will move in NDC space using Lissajous curves.

Lissajous: x = sin(t * a + phase), y = sin(t * b)
With a and b being different frequencies for interesting patterns.

For example, x = sin(t * 0.7), y = sin(t * 1.3) gives a nice pattern.

I'll also need to handle the pointer events. For `pointermove`, I update the mouse NDC coordinates. For `click` or `keydown`, I enable user interaction mode.

Let me also make the overlay nice - dark semi-transparent with a title and instructions.

I think it's time to actually write the code. I've been thinking too long.

One last detail: for the fish tail animation. I'll create the tail as a separate mesh inside a tail group. The tail group is positioned at the back of the fish. Each frame, I rotate the tail group around the Y axis (fish's "up" direction) to simulate the side-to-side tail beat.

The tail beat frequency should be related to the fish's swimming speed. Faster swimming = faster tail beats.

I'll also animate the pectoral fins slightly.

Here's the code I'm about to write - I'm going to focus on creating something visually appealing and functional.

One thing I want to note about the structure: the user asked for "a 3D aquarium with Three.js where the fish swim away when I move the mouse near them". So the key interaction is the mouse-near-fish flee behavior. This should be the star of the demo. I need to make this feel good - the fish should flee smoothly and naturally, with some randomness so they don't all behave identically.

For the flee behavior:
- Each fish has a personal space radius (slightly randomized, 3-6 units in 3D space)
- When the mouse ray comes within this radius, the fish accelerates away
- The flee force decreases smoothly with distance (within the personal space)
- The fish also maintain a minimum speed (they never fully stop)
- After the mouse moves away, the fish gradually return to normal swimming

I'll implement this with a simple force-based system:
- Wander force: always present, points toward random targets
- Flee force: strong when mouse is close, zero when far
- The net force is normalized and scaled by the fish's speed

For smooth movement, I'll use a velocity blend:
- Current velocity is smoothly interpolated toward the target velocity
- Orientation follows velocity direction with slerp

I'll also add some randomness to the flee direction (a slight angle offset) so fish don't all flee in exactly the same direction.

OK, writing code now. I'll aim for clean, well-commented code.

Let me structure the JS code:
1. Imports and constants
2. Scene, camera, renderer setup
3. Tank construction (glass, frame, sand)
4. Water volume
5. Back wall decoration (plants, rocks)
6. Bubbles
7. Fish class and creation
8. Mouse tracking
9. Animation loop
10. UI

I estimate this will be ~600-800 lines. Let me write it efficiently.

One concern: getting the fish looking nice. Let me design a more detailed fish:

The fish body: I'll use a slightly teardrop-shaped body. For simplicity, I can use:
- Main body: SphereGeometry scaled to (1.4, 0.55, 0.4) - elongated
- Nose section: ConeGeometry or additional sphere at the front to taper
- Or just accept the ellipsoid shape

I think an ellipsoid with fins looks sufficiently fish-like. Let me add:
- Tail fin (vertical, at back) - triangle shape, wags
- Dorsal fin (top, slightly back from center) - triangle
- Pectoral fins (sides, below center) - small flippers
- Anal fin (bottom, near tail) - small triangle
- Eyes (front, sides) - dark spheres
- Mouth: skip, too small to see

For a nicer body, I could use a slightly squashed and tapered ellipsoid. I could achieve tapering by using multiple overlapping spheres of decreasing size toward the nose. But this complicates the mesh. Let me keep it simple with a single ellipsoid body and rely on the fins and colors to make it look like a fish.

Actually, for a nicer shape, I'll use a custom BufferGeometry for the body. I can start with a sphere and stretch/taper it. But this requires more complex vertex manipulation. For the purposes of this demo, a scaled sphere body with good materials and fins will look fine.

One improvement: I'll use a second smaller sphere for the head/forehead to create a slight bump that's characteristic of many fish species.

OK, I'll make the body as a group containing:
- BodyMain: SphereGeometry, scaled (1.3, 0.5, 0.38), centered at origin
- BodyNose: SphereGeometry, scaled (0.5, 0.4, 0.3), positioned at x=0.6, slightly above center
- The nose creates a slight forehead bump

This will look better than a simple ellipsoid.

Alright, NOW I'm writing code.

Also, for plants, I'll make several types:
1. Tall seaweed: 4-6 tube strands per group, curving upward
2. Short grass: small cone shapes or curved strands
3. Maybe some broad-leaf plants: flat shapes

Let me use TubeGeometry for seaweed and simple shapes for other plants.

The seaweed will be created using CatmullRomCurve3 with control points going from the base upward, with slight curve. Each strand has a different curve for variety. The tube radius is small (0.08-0.15).

For broad-leaf plants (like Amazon sword), I can use flattened shapes. These are harder to make from primitives. Let me stick with tube-based seaweed and grass, plus rocks.

I think for visual interest, I'll add:
- 5-7 seaweed groups with 3-5 strands each
- 3-4 rocks
- Maybe some small gravel patches (small spheres scattered on the sand)

OK, one more consideration before I code: the user experience. When the user moves their mouse near a fish in 3D (which corresponds to the 3D cursor position being near a fish), the fish flees. The 3D cursor position is defined by the ray from the camera through the mouse position. So the "mouse being near a fish" in 3D means the ray passes close to the fish.

This is intuitive: move your mouse over a fish (the ray hits near the fish) and the fish swims away.

The flee radius in 3D should be reasonable - maybe 2.5-4 units (the tank is 20 units wide, so fish are ~1.5-2.5 units long). A flee radius of 3-5 units feels right.

For the demo mode, the virtual mouse follows a Lissajous pattern over the tank, periodically coming close to fish and making them flee. After the user clicks, the real mouse takes over.

For the Lissajous pattern, I need the virtual mouse to sometimes come close to fish (to demonstrate the flee behavior). The fish are scattered throughout the tank. A Lissajous pattern with decent coverage should work. I'll tune the frequencies to get good coverage.

Now I'll write the code. Here it goes.

Oh wait, one more thing. The tank needs a lid/hood with the light. I'll add a box on top of the tank (y > 6) that represents the hood, with light coming from it. This adds realism.

Also, the tank needs stand/supports at the bottom. I'll add simple legs or a base.

I'll add a wooden or dark-colored base under the tank (y < -6) and a hood at the top (y > 6).

For the tank stand, a simple dark box under the tank. For the hood, a box on top with a light source inside.

The directional light can be positioned above the tank pointing down, simulating the aquarium light.

OK, I'm going to write the code now. For real this time.

I should also think about the exact visual style. I want:
- Dark background (room environment)
- Well-lit aquarium (bright inside compared to surroundings)
- Colorful fish that stand out
- Nice water tint
- Plants that sway gently

Color palette:
- Background: dark navy (#0a0a14)
- Tank glass: very transparent with slight blue tint
- Water: light blue tint (rgba(100, 180, 220, 0.08))
- Sand: light tan (#d4b896)
- Rocks: gray-brown
- Seaweed: various greens (#2d8a4e, #1a6b3c, #3da55d)
- Fish colors: orange (#ff7043), blue (#42a5f5), red (#ef5350), yellow (#fdd835), purple (#ab47bc), green (#66bb6a), teal (#26a69a), pink (#ec407a)

These will look vibrant against the blue water and sand.

For the lighting:
- Ambient light: warm white (low intensity)
- Directional light: warm white from above (higher intensity) - simulating aquarium hood light
- Optional: subtle blue fill light from below/sides for underwater ambiance

I'll use:
- AmbientLight: color 0x404060, intensity 0.4
- DirectionalLight: color 0xffeedd, intensity 1.5, position (5, 15, 3)
- Maybe a PointLight inside the water for extra glow

Actually, with the newer Three.js lighting (physical units), I need to be careful with intensities. In Three.js 0.170, lights use physical units by default, so DirectionalLight intensity of 1.5-3 is reasonable, AmbientLight intensity of 0.3-0.8 is reasonable.

For the tone mapping, I'll use ACESFilmicToneMapping (default in newer versions) or ReinhardToneMapping for a softer look.

I think ACESFilmicToneMapping with a proper exposure setting looks good.

Alright, here's the code:

Wait, one more thought about the import. The user said to use the import map with "three" and "three/addons/". I'll use the OrbitControls for camera rotation (maybe?) or just use the basic Three.js. I think I'll skip OrbitControls and keep the camera mostly fixed to simplify the interaction. The user's mouse movement should scare fish, not rotate the camera.

But I could add: right-click to orbit, scroll to zoom. This would use OrbitControls. However, OrbitControls adds complexity and might have issues with the interaction. Let me skip it and keep the camera fixed with maybe a very subtle auto-sway.

Actually, just to keep the camera from being totally static (which could look boring), I'll add a very slow camera orbit/sway. The camera orbits slightly around the tank, very slowly. This makes the scene feel alive without interfering with mouse interaction.

OK final code writing. Here goes.

I realize I also need to add a small animated water surface at the top of the tank. A plane with a slight wave animation would be nice. But this might be overkill given everything else. Let me use a simple transparent plane with a very subtle normal map... no, that requires textures. I'll just leave the top open (you can see the water surface from above through the glass top).

Actually, for the water inside the tank, I'll add a subtle blue-tinted transparent box that fills the tank interior. The top of this box is at the water surface level (y = 5.5 or so, a bit below the glass top).

One final thing: I want to add light rays that shine from the top through the water. These are cone-shaped transparent shapes extending from the top of the water down into the tank. They create beautiful "god rays" effects. I'll create 3-5 of these with varying widths and intensities, slowly moving.

For god rays, I can use:
- ConeGeometry with very low opacity
- CylinderGeometry with expanding radius
- Custom triangle shapes

I'll use a simple approach: flat triangular planes (3-sided) with additive blending and very low opacity, positioned from the top surface downward. These will look like light beams in the water.

Actually, additive blending with transparent materials in Three.js can be tricky. Let me use a simpler approach - cone geometries with transparent material and low opacity.

OK, I'll skip the god rays for now and focus on making everything else polished. I can add them if there's time at the end. The fish fleeing interaction is the star.

Let me write the code now. I'll aim for a comprehensive, polished result.

For the numbers: 
- Tank: half-width 10, half-height 6, half-depth 7
- Center at origin
- Sand at y = -6 (bottom of tank)
- Water surface at y = 5.5 (slightly below glass top at y = 6)
- Fish roam between y = -4.5 and y = 4.5

Camera position: I'll place it at (14, 3, 16) looking at (0, -1, 0). This gives a nice 3/4 view of the aquarium showing both front and side.

Let me also think about the aspect ratio - the viewport is 1280x800. The tank is 20 wide, 12 tall, 14 deep. At a distance of ~22 from the center, the tank should fit nicely.

Alright, HERE is the code:

(A note: I'll handle the fish orientation carefully. The fish model's forward direction is +X. When the fish has velocity vector V, I need to rotate so that +X aligns with V. I'll use Matrix4.lookAt or quaternion.setFromUnitVectors.)

Actually, a cleaner way: I'll build the fish model with its forward direction along -Z (the default Three.js forward direction for things like lookAt). Then I can use lookAt to orient the fish toward its movement direction. Wait, but for the fish, the tail is at the back and nose at the front. If forward is -Z, then the nose points toward -Z.

Hmm, actually Three.js convention: for cameras and objects, -Z is the default "forward" direction (into the screen). For a fish built with nose pointing toward -Z, I can use `fish.lookAt(targetPosition)` to make the nose face the direction of movement.

But lookAt only works for a point, not a direction. For direction-based rotation, I'd use:
```javascript
const targetPos = fish.position.clone().add(fish.velocity);
fish.lookAt(targetPos);
```

This works well. I'll build the fish with nose at -Z direction.

Actually, wait. I want to be careful with the fish orientation. Let me define:
- Fish nose at +X (forward = +X)
- Fish tail at -X
- Fish up = +Y
- Fish left/right = ±Z (body is wider horizontally than vertically)

For orientation: when fish moves in direction D, rotate so +X aligns with D.

To do this: 
```javascript
const quaternion = new THREE.Quaternion();
quaternion.setFromUnitVectors(new THREE.Vector3(1, 0, 0), direction);
fish.quaternion.slerp(quaternion, 0.1);
```

This is clean. But I need to handle the edge case where direction is exactly opposite to +X (which causes setFromUnitVectors to fail). I'll add a small check for that.

Alternatively, I can build the fish with -Z as forward and use lookAt. That's simpler and more standard for Three.js. Let me do that.

Fish model:
- Nose points toward -Z
- Tail points toward +Z
- Up is +Y
- Body is wider in X direction

For orientation with lookAt:
```javascript
const lookTarget = fish.position.clone().add(fish.velocity.clone().normalize());
fish.lookAt(lookTarget);
```

This makes the -Z axis point toward the look target. Since the nose is at -Z, the fish faces its direction of movement.

For smoothing orientation, I can either:
1. Use lookAt directly (snaps to direction) - but this might be too abrupt
2. Use slerp between current quaternion and target quaternion - smoother

I'll use slerp for smooth turning:
```javascript
const targetQuat = new THREE.Quaternion();
const lookMatrix = new THREE.Matrix4();
lookMatrix.lookAt(fish.position.clone().add(velDir), fish.position, new THREE.Vector3(0, 1, 0));
targetQuat.setFromRotationMatrix(lookMatrix);
fish.quaternion.slerp(targetQuat, 0.08);
```

This will give smooth turning. Good.

Actually wait, lookAt's signature is `lookAt(target, up)` - the target is a position, not a direction. So I need `fish.lookAt(fish.position.clone().add(velDir))` which makes the fish's -Z face that point.

For the quaternion approach:
```javascript
const targetPosition = fish.position.clone().add(velDir);
const m = new THREE.Matrix4();
m.lookAt(fish.position, targetPosition, new THREE.Vector3(0, 1, 0));
const targetQuat = new THREE.Quaternion().setFromRotationMatrix(m);
fish.quaternion.slerp(targetQuat, 0.08);
```

This is the proper way. Let me use this.

OK, I also need to handle the fish bouncing off walls and the turning being smooth.

For wall avoidance, I'll use soft boundaries: when a fish gets close to a wall, it gets a force pushing it back toward the center. This force increases as the fish gets closer to the wall.

Alright, writing code. I'm going to be comprehensive. Here's the complete implementation.

Let me also figure out how the fish tail animation works. The fish model has:
- Fish group (contains everything)
  - Body (scaled sphere at origin)
  - Nose bump (small sphere at front)
  - Eyes (two small spheres)
  - Tail group (at back of body, x = +tail_offset in local -Z direction... wait I need to be consistent)

If fish faces -Z (nose at -Z), then:
- Body: SphereGeometry scaled (0.5, 0.5, 0.35) in X/Y/Z... hmm, I need to think about this more carefully.

The fish is elongated along its length (the Z axis in local space if forward is -Z, or X axis if forward is +X). Let me use the convention: the fish's long axis is Z (body extends from -Z nose to +Z tail). The body sphere is scaled to be longer in Z.

Body: SphereGeometry(1, 16, 12), scale: (0.4, 0.55, 1.3) - wide in X, medium in Y, long in Z. Nose at -Z end (z = -1.3), tail at +Z end (z = +1.3).

Wait, I need the forward direction to be -Z for the lookAt to work. Let me set:
- Nose at Z = -1.3
- Tail at Z = +1.3
- Body sphere scaled to stretch along Z

Body: SphereGeometry(1, 16, 12), scale: (0.4, 0.55, 1.3)
- This makes it: 0.8 wide in X, 1.1 tall in Y, 2.6 long in Z

Fish size: total length about 2.6 * scale_factor. With scale_factor of about 0.8, total fish length is about 2.1 units. For a 20-wide tank, that's reasonable. Let me adjust per fish - some bigger, some smaller (scale 0.6-1.0).

Tail: at Z = +1.2 (back of body). The tail fin is a triangle in the X-Z plane... wait no. For a real fish, the tail fin is in the Y-Z plane (vertical) and beats side to side (rotating around Y axis).

For the tail fin:
- The tail fin extends backward (+Z) and up/down (Y)
- It's flat in the X direction (thin from side to side)
- The fin vertices: (0, 0, +1.2) attachment, (0, 0.45, +1.8) upper tip, (0, -0.45, +1.8) lower tip

This creates a vertical triangular fin at the back.

The tail group (containing the fin) rotates around the Y axis for wagging. Since the tail is at Z = +1.2, I need to set the tail group's position and rotate it.

Tail group:
- Position: (0, 0, 1.2)
- Contains tail fin mesh with geometry centered near origin
- Rotation around Y axis for wagging

The tail fin geometry:
- Vertices in local space: (0, 0, 0) attachment at body, (0, 0.5, -0.6) upper tip, (0, -0.5, -0.6) lower tip
- Note: negative Z in the tail group's local space extends backward from the fish
- Wait, I need to be careful. The tail group is at Z = +1.2 (back of fish). The tail fin extends further backward (+Z direction from the fish's perspective, which is +Z in fish local space).

Actually, in fish local space, forward is -Z (nose direction). The tail extends in +Z direction. So the tail group is at Z = +1.2, and the tail fin extends further in +Z.

Tail fin geometry vertices (in tail group local space):
- (0, 0, 0) - attachment at tail group origin (which is at fish Z=+1.2)
- (0, 0.45, 0.5) - upper back tip
- (0, -0.45, 0.5) - lower back tip

This creates a triangular fin extending backward from the fish. The tail group rotates around Y for wagging.

Dorsal fin (on top of fish):
- Position: (0, 0.55, back portion) on top of body
- Triangle shape pointing up and slightly back

Pectoral fins (on sides):
- Position: (±0.3, -0.1, slightly in front of center) on sides
- Small flipper shapes

Eyes:
- Position: (±0.18, 0.1, -1.0) near the front sides
- Small dark spheres

This is getting quite detailed. Let me simplify slightly and just use:
- Body (scaled sphere)
- Tail fin (triangle, wags)
- Dorsal fin (triangle on top)
- Two eyes
- Optional: pectoral fins

The eyes make a big difference in making it look "fish-like". Small dark spheres with a tiny white glint (specular highlight) will look great.

For colors, I'll use bright, saturated colors for the fish body and slightly darker/warmer colors for the fins.

OK I've planned this extensively. Time to write the actual complete HTML file. I'll make it excellent.

For the sand, I realize I should create a simple texture. I'll use a canvas to generate a sandy texture with subtle variation.

Actually, for simplicity and since this is already quite complex, I'll just use a MeshStandardMaterial with an appropriate color and roughness. The visual quality won't suffer much.

For the back wall of the aquarium, I'll add a panel with a gradient or darker blue to suggest deep water. I'll create a canvas texture for this.

Actually, let me keep it minimal: just a dark blue background behind the tank. The tank glass allows you to see through to the background color.

Alright, I need to stop overthinking and just write the code.