> Idea: An interactive 3D cloth simulation in WebGL using verlet integration: a draped fabric of point masses with structural and shear constraints, pinned along the top edge, falling under gravity and rippling under an adjustable wind; grab and drag any point with the mouse to pull the cloth around; orbit the camera and zoom. Shaded double-sided cloth with lighting, optional wireframe and tear toggles.

Build a single-page, no-build, static WebGL application that renders an interactive 3D cloth simulation in real time and runs flawlessly offline. The artifact is a polished, self-contained showcase: a draped rectangular fabric made of a grid of point masses, integrated with Verlet integration and held together by structural and shear distance constraints, pinned along its top edge, sagging under gravity, and rippling under an adjustable wind. The user can grab and drag any point of the cloth with the mouse to pull the fabric around, orbit a camera around the scene, and zoom in and out with the wheel. The cloth is rendered as a shaded, double-sided surface with smooth per-frame-recomputed normals and cinematic lighting, with optional wireframe and tear (cut constraints) toggles. Deliver something that feels alive, tactile, and beautiful on first load — a piece anyone would want to keep playing with and would screenshot. This is a public showcase of one-shot output quality; the bar is "I'd pin this and show my friends," not "it technically simulates cloth."

You own the environment. Do not include shell commands, install steps, or first-run setup. Produce the files described below, complete and final, with zero placeholders.

## Goal
Create a real-time, GPU-rendered 3D cloth toy that is physically plausible, visually striking, and directly manipulable, built on a vendored copy of Three.js (revision 160) that already exists at `../vendor/three.module.js` relative to the app directory. The defining experience is the combination of three things working together at a smooth 60fps:

1. **Believable cloth physics.** A grid of point masses (default 40×40 ≈ 1,600 particles) integrated with Verlet integration. Each particle is connected to its neighbors by distance constraints that are relaxed over several solver iterations per frame: **structural** constraints (to the 4 orthogonally adjacent particles — left/right/up/down), **shear** constraints (to the 4 diagonal neighbors), and optional **bend** constraints (to the particles two cells away) for stiffness. The top row of particles is pinned, so the sheet hangs and drapes; gravity pulls it down, and an adjustable, gusting wind pushes against its surface based on each triangle's normal, making it billow and ripple.

2. **Direct manipulation.** The user can press the pointer on any visible part of the cloth, "grab" the nearest particle (via raycasting against the live mesh, then snapping to the closest vertex), and drag it through space — the grabbed particle follows a plane facing the camera at the grab depth, dragging the surrounding fabric with it through the constraint solver. Releasing lets the cloth fall back naturally. The user can also orbit the camera (drag on empty space) and zoom (mouse wheel / pinch).

3. **A cinematic, shaded presentation.** The cloth is a single `THREE.Mesh` with a double-sided `MeshStandardMaterial`, lit by a key directional light (with soft contact shadows on a subtle ground plane), a cool fill light, and a warm rim light, on a dark graded background with a faint radial vignette glow. Normals are recomputed every frame from the deformed geometry so lighting reacts to every wrinkle. A floating control panel exposes wind, gravity, stiffness, and toggles for wireframe overlay and tearing.

The simulation is the product. It must stay **rock-solid stable**: no exploding particles, no NaN propagation, no runaway energy. Favor stability and beauty over numerical purity. The page must run with **zero console errors**, **zero external network requests**, and **zero build step** — opening it via a static server (it is served under a strict CSP, see below) must just work.

## Tech stack
- **Static, no-build, pure ES modules.** Author modern ES2020+ JavaScript that runs natively in evergreen browsers (Chrome, Firefox, Safari, Edge). No bundler, no transpile, no framework (no React/Vue/Svelte), no state library.
- **WebGL via Three.js, already vendored at `../vendor/three.module.js`.** This is the single hard dependency and it is same-origin. Import it exactly as `import * as THREE from '../vendor/three.module.js';`. **Do NOT** re-vendor, rewrite, fetch, or re-download Three.js; **do NOT** import from a CDN, from `unpkg`/`jsdelivr`, or from a bare specifier like `'three'`. The revision is **r160** — use only APIs present in r160.
- **No Three.js addons / examples.** Do **not** import `OrbitControls`, `EffectComposer`, `RGBELoader`, `GLTFLoader`, or anything from `three/examples/...`. **Hand-roll** everything you need:
  - **Camera controls** (orbit-on-drag + wheel-to-zoom) in ~25 lines of plain pointer/wheel event handling that updates spherical coordinates (azimuth, polar, radius) and repositions the camera with `lookAt`.
  - **"Glow"** via emissive `MeshStandardMaterial` plus additive-blended `THREE.Sprite`s that use a **canvas-generated radial-gradient texture** for soft bloom-like halos behind the lights. **No postprocessing, no bloom passes, no `EffectComposer`.**
  - **Shadows** via the renderer's built-in `shadowMap` (PCFSoft) on the directional light, not a postprocessing trick.
- **No importmap.** An inline `<script>` is blocked by CSP, and an importmap requires inline script, so do not use one. Reference the vendored module by its relative path directly in your own `import` statements.
- **All JavaScript in external, same-origin ES module files.** `index.html` must load **only** `<script type="module" src="./main.js"></script>`. You may split logic into additional local modules (e.g. `./cloth.js`, `./camera.js`, `./scene.js`, `./ui.js`) that `main.js` imports with **relative** paths. **No inline `<script>` blocks. No inline event-handler attributes** (`onclick=`, `onload=`, `onpointerdown=`, etc.) — wire every event with `addEventListener`. **No `eval`, no `new Function`, no string `setTimeout`.**
- **Strict CSP compliance.** The page is served under: `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'`. This means: external/inline JS is forbidden; inline `<style>` and `style=` attributes are allowed; images may be `data:` URIs or same-origin; **no network** beyond same-origin (no `fetch`/XHR/WebSocket to other origins — and in practice, no network at all). Author the app so it is fully clean under this policy.
- **No external resources of any kind.** No CDNs, no web fonts (use the system font stack `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`), no external images or textures (every texture is procedural or canvas-generated), no analytics, no service worker, no telemetry.
- **CSS:** an external same-origin `./styles.css` (linked with `<link rel="stylesheet" href="./styles.css">`) is the primary stylesheet; inline `<style>` and `style=` attributes are also permitted by the CSP if convenient. Use CSS custom properties for the palette, modern fl/grid layout, and `backdrop-filter` for the glassy panel where supported (with a solid fallback).
- **Rendering:** `THREE.WebGLRenderer` with `antialias: true`, `powerPreference: 'high-performance'`, DPR clamped to `Math.min(devicePixelRatio, 2)`, ACES filmic tone mapping, `outputColorSpace = SRGBColorSpace`, and `shadowMap.enabled = true` with `PCFSoftShadowMap`.
- **Loop & timing:** a single `requestAnimationFrame` loop using a fixed-timestep accumulator for the physics (e.g. 60Hz substeps) with `dt` clamped (cap accumulated time / max substeps) so a backgrounded tab or a long frame never detonates the simulation. Use `performance.now()` for timing.
- **State:** a small set of module-scoped objects (a `Cloth` class/instance, a `cameraRig`, a `settings` object). No global leakage beyond what is necessary. Persist user settings to `localStorage` under one namespaced key and restore on load, wrapped in try/catch so private-mode browsers degrade to defaults.

## File / module layout
Produce these files, using **relative** paths throughout so the app works from a domain root, a sub-path, or a subdomain. The vendored Three.js is at `../vendor/three.module.js` (one directory up from the app), which already exists — just import it.

- `index.html` — semantic markup only. Contains: a full-bleed `<canvas>` mount (or a container the renderer attaches to), a glassy control panel (`<aside>`/`<section>`) with sliders and toggles, a compact "how to interact" instructions overlay, a small live stats readout (particle count, FPS), and the footer link. Sets `<meta name="viewport">`, `<meta name="color-scheme" content="dark">`, an inline SVG `data:` favicon, and links `./styles.css`. Loads **only** `<script type="module" src="./main.js"></script>` at the end of `<body>`. No inline JS, no `on*=` attributes.
- `./main.js` — the ES module entry point. `import * as THREE from '../vendor/three.module.js';` Owns bootstrap: create renderer/scene/camera/lights, instantiate the cloth, wire the camera rig and pointer interaction, build the render+physics loop, handle resize, wire the UI, and start. May import the modules below; if you prefer a single file, keep all logic in `main.js` — either is acceptable, but keep it readable and sectioned.
- `./cloth.js` *(optional split)* — the `Cloth` class: particle arrays (current + previous positions as flat `Float32Array`s for Verlet), constraint list (structural/shear/bend), `update(dt, forces)` (Verlet integrate → apply wind/gravity → relax constraints N iterations → re-pin top row → handle grab), `syncGeometry()` (write positions into the `BufferGeometry`, recompute normals), grab/release, tear (constraint removal) logic, and rebuild on resolution change.
- `./camera.js` *(optional split)* — the hand-rolled orbit/zoom rig: spherical state (azimuth, polar, radius) with damping, pointer-drag to orbit, wheel/pinch to zoom, clamped polar angle and radius, and a method to apply the state to the `THREE.PerspectiveCamera`.
- `./scene.js` *(optional split)* — scene construction: lights (key/fill/rim), ground/shadow-catcher plane, background gradient, the procedural radial-gradient sprite texture and additive glow sprites, the vignette.
- `./ui.js` *(optional split)* — control-panel wiring: sliders (wind, gravity, stiffness/iterations, damping), toggles (wireframe, tearing, pin/unpin), buttons (reset, pause), live stat updates, persistence to `localStorage`, instructions overlay toggle. Wire everything with `addEventListener`.
- `./styles.css` — the stylesheet: palette via custom properties, layout, the glassy panel, slider/checkbox styling, the instructions overlay, the footer, responsive behavior. All paths relative; no `@import` of remote resources, no webfonts.

Recommended internal module boundaries (sections, not necessarily separate files):
1. **Config & palette constants** — grid resolution, rest spacing, gravity default, wind defaults, constraint iteration count, damping, drag/grab radius, tear stretch threshold, slider ranges, colors.
2. **Math / Verlet core** — flat typed-array particle storage (x/y/z current and previous), constraint records (indices + rest length), the integrate + relax steps, pin handling, grab handling, tear handling, NaN/explosion guards.
3. **Geometry sync** — indexed `BufferGeometry` with a position attribute backed by (or copied from) the particle array, an index buffer of two triangles per cell, per-frame `computeVertexNormals()` (or a hand-written normal accumulation for speed), and a normals/positions `needsUpdate` flag.
4. **Scene & lighting** — renderer config, camera, key/fill/rim lights, shadow setup, ground catcher, procedural glow sprites, background + vignette.
5. **Camera rig** — spherical orbit + zoom with damping and clamps.
6. **Interaction** — raycast to grab the nearest particle, drag on a camera-facing plane, orbit when not grabbing, wheel zoom, touch support.
7. **UI / controls** — sliders, toggles, buttons, stats, persistence, instructions overlay.
8. **Bootstrap & loop** — DPR-aware sizing, resize handling, fixed-timestep accumulator with clamped dt, the `requestAnimationFrame` loop, dispose/teardown helpers.

## Data model
There is no backend and no persisted documents beyond user settings. The "data model" is the in-memory simulation state plus a tiny settings record. Define it precisely:

**Particle storage (Structure-of-Arrays, for cache-friendliness and zero per-particle object churn):**
- `cols: number`, `rows: number` — grid dimensions (default 40 × 40). `count = cols * rows`.
- `pos: Float32Array(count * 3)` — current positions (x,y,z interleaved).
- `prev: Float32Array(count * 3)` — previous positions (Verlet history; velocity is implicit as `pos - prev`).
- `acc: Float32Array(count * 3)` *(optional)* — accumulated acceleration per step, or apply forces inline.
- `pinned: Uint8Array(count)` — 1 if the particle is fixed (top row by default; togglable corners).
- `invMass: Float32Array(count)` — 0 for pinned particles (so constraints never move them), 1 otherwise. Using inverse mass makes the constraint solver branch-free and correct at pins.
- Index helper: `idx(c, r) = r * cols + c`; component access at `i*3`, `i*3+1`, `i*3+2`.

**Constraints (one flat list, each a record):**
- `a: number`, `b: number` — particle indices.
- `rest: number` — rest length (initial distance between a and b).
- `type: 'structural' | 'shear' | 'bend'` — for optional per-type stiffness and for which to draw/tear.
- `active: boolean` (or a parallel `Uint8Array`) — set false when torn.
- Built once on (re)initialization: structural to (c±1,r) and (c,r±1); shear to (c±1,r±1); bend to (c±2,r) and (c,r±2). De-duplicate so each pair appears once.

**Mesh / geometry:**
- An indexed `BufferGeometry`: `position` attribute (count vertices, kept in sync with `pos`), `normal` attribute (recomputed per frame), `uv` attribute (regular grid UVs for any procedural surface detail), and an index buffer with `2 * (cols-1) * (rows-1)` triangles (two per cell). When tearing, you may either keep triangles but visually separate via stretched constraints, or rebuild the index buffer to drop triangles whose constraints are torn (preferred for a real visible tear) — choose one and make it look intentional.

**Grab state:**
- `grabbed: number | -1` — index of the currently held particle.
- `grabPlane: THREE.Plane` — a plane through the grab point, facing the camera, that the pointer ray intersects to compute the drag target.
- `grabTarget: THREE.Vector3` — where the grabbed particle is being pulled (its `invMass` is temporarily forced so it follows exactly, then restored on release unless it was pinned).

**Camera rig state:**
- `azimuth: number`, `polar: number` (clamped, e.g. 0.15–1.5 rad from vertical), `radius: number` (clamped, e.g. 3–14), plus damped targets `azimuthT`, `polarT`, `radiusT`, and a `target: THREE.Vector3` look-at point (the cloth center).

**Settings (persisted to `localStorage` under e.g. `cloth3d.settings.v1`):**
- `wind: number` (0–3, default ~0.8), `windDir: number` (degrees, optional), `gravity: number` (0–20, default ~9.8 scaled), `stiffness: number` (solver iterations 1–12, default ~6), `damping: number` (0.0–0.06, default ~0.012), `wireframe: boolean`, `tearEnabled: boolean`, `paused: boolean`, `resolution: number` (e.g. 24/40/56), `pinMode: 'top' | 'corners' | 'topcorners'`. Validate/clamp every value on load; fall back to defaults on parse failure.

## Behavior & features
**Cloth initialization & draping.**
- On load, build the grid laid out as a vertical sheet (the top edge horizontal), pin the entire top row, set `prev = pos` (zero initial velocity), and let it settle under gravity into a natural drape with subtle catenary sag between pins. Seed a faint initial wind so it is already gently moving — never a dead-still rectangle on first paint.

**Verlet integration (per substep, fixed 1/60s).**
- For each non-pinned particle: compute `velocity = (pos - prev) * (1 - damping)`, then `next = pos + velocity + accel * dt²`, where `accel` includes gravity (down) and the wind force. Set `prev = pos`, `pos = next`. Pinned particles (invMass 0) are skipped / unaffected.
- **Wind** is applied per triangle as a function of the triangle's normal: `force = windStrength * dot(normal, windDir) * normal` (so faces perpendicular to the wind catch the most), distributed to the triangle's three vertices, with a low-frequency time-varying gust (sum of a couple of sines, plus a little noise) so it breathes rather than blowing constantly. Make the wind direction subtly wander.

**Constraint relaxation (Gauss–Seidel, N iterations/frame from the stiffness slider).**
- For each active constraint: compute the current delta between a and b, the difference from `rest`, and move a and b toward/away from each other proportionally to their `invMass` so the distance returns toward rest. Pinned endpoints don't move. Run N iterations (default 6) for stiffness; more iterations = stiffer cloth.
- **Over-stretch / tear:** if tearing is enabled and a constraint's current length exceeds `rest * tearFactor` (e.g. 4×), mark it inactive (the cloth rips). When grabbing with tearing on, yanking hard should visibly rip a slit. Rebuild or hide the affected triangles so the tear is visible, double-sided, and lit on both faces.
- **Re-pin** the pinned row each iteration (snap pinned particles back to their fixed positions) so dragging/wind never drifts them.

**Stability guards (critical — the showcase must never visibly blow up).**
- Clamp per-substep displacement to a sane maximum (e.g. no particle moves more than ~0.5 rest-length per substep) to absorb a violent mouse yank.
- After integration, scan for non-finite components; if any `pos` value is NaN/Infinity, reset that particle to its `prev` (or to its rest position) — never let NaN propagate across the grid.
- Cap the physics accumulator (e.g. process at most ~5 substeps per frame); if the tab was backgrounded, drop the backlog rather than spiraling.
- Keep total kinetic energy bounded via the damping term; ensure increasing wind/stiffness to max never produces visible jitter or explosion.

**Geometry & shading sync (per render frame).**
- Copy `pos` into the geometry's position attribute (`needsUpdate = true`), recompute vertex normals from the deformed surface (use `computeVertexNormals()` or a hand-written accumulation), and mark normals dirty. The `MeshStandardMaterial` is `side: THREE.DoubleSide` so the back of the cloth is correctly lit; give the two sides a subtle color/sheen difference if desired (e.g. via vertex colors or a slightly different emissive) so curls read clearly.
- Material: a tasteful base color, moderate roughness (~0.6), low metalness, gentle emissive for lift, optional subtle procedural surface variation (canvas-generated normal-ish or AO-ish texture, or vertex-color gradient) — all procedural, no external image. A faint Fresnel-like rim from the rim light should make silhouettes glow against the dark background.

**Direct manipulation (pointer).**
- **Grab:** on pointer-down over the cloth, raycast against the live mesh; if it hits, find the nearest particle to the hit point and grab it. While held, build a plane through the grab point parallel to the camera's view plane; each pointer-move intersects the pointer ray with that plane to get `grabTarget`, and the grabbed particle is pulled there (its motion drags neighbors via the solver). On pointer-up, release. A held particle should feel like it has the cloth's weight hanging from it.
- **Orbit:** pointer-down on empty space (ray misses the cloth) starts a camera orbit; horizontal drag changes azimuth, vertical drag changes polar (clamped). Damped so it glides to a stop.
- **Zoom:** mouse wheel (and two-finger pinch on touch) changes `radius` (clamped), with damping.
- **Touch:** support one-finger grab/orbit and two-finger pinch-zoom. Use Pointer Events so mouse and touch share code paths. Disable page scroll/gesture interference on the canvas (`touch-action: none`).

**Controls (panel).**
- Sliders: **Wind** (0–3), **Gravity** (0–20), **Stiffness** (iterations 1–12), **Damping** (0–0.06). Each updates live and shows its value.
- Toggles: **Wireframe** (overlay the constraint grid or set `material.wireframe`), **Tearing** (enable/disable rips), **Pause**, and a **Pin mode** (top edge / corners) selector.
- Buttons: **Reset** (re-drape from scratch), **Resolution** selector (24/40/56) that rebuilds the cloth, optional **Drop pins** (unpin the top row so the whole sheet falls — a delightful party trick).
- Live stats: particle count, active-constraint count, and a smoothed FPS readout.
- A compact **instructions overlay** ("Drag the cloth to pull it · Drag the background to orbit · Scroll to zoom · Toggle wind & tearing in the panel") that is visible on load and dismissible, re-openable via a small "?" button.

**Aesthetic & motion.**
- Dark, cinematic backdrop (deep blue-black gradient ~`#05070d` → `#0b1020`), a soft radial glow/vignette, the cloth catching warm key light and cool fill, a faint rim. Smooth, weighty motion; the cloth should look like real fabric, not a bouncy net. Everything at a steady 60fps on a typical laptop.

## UX & visual design
- **Palette (hex):** background graded `#05070d` → `#0b1020`; panel surface `rgba(14,18,30,0.66)` with `backdrop-filter: blur(14px)` and a 1px hairline border `rgba(255,255,255,0.08)`; primary text `#e7ecf5`, muted `#8a93a8`; accent / focus `#7cc0ff` (cool) with a warm secondary `#ffb86b`; cloth base around `#9fb6ff`/`#c0c8ff` reading silvery-blue under the lights; key light warm `#fff1dc`, fill cool `#3a5cff` (dim), rim `#ff9d5c` (dim). Slider tracks `rgba(255,255,255,0.12)` with an accent fill.
- **Typography:** system font stack only. Title ~18–20px 600; labels ~12px 600 uppercase with slight letter-spacing; values tabular/monospaced-ish via `font-variant-numeric: tabular-nums`. Never load a webfont.
- **Spacing scale:** 4 / 8 / 12 / 16 / 24px. Panel padding 16px, control rows 12px gap, 12px radius on the panel, 8px on controls.
- **Panel:** top-left or top-right glassy `<aside>`, collapsible, never covering the center of the cloth. Sliders are custom-styled (accent thumb, glowing focus ring), checkboxes are custom switches, the value chips sit at the right of each label. Buttons have a subtle hover lift and an accent press state.
- **Instructions overlay:** centered or lower-center, low-opacity glass, fades out after first interaction or on dismiss; a tiny floating "?" button brings it back. The footer link "Built with a one-shot prompt from 1ShotGen" sits unobtrusively in a corner, linking to `https://1shotgen.com` (open in a new tab, `rel="noopener noreferrer"`).
- **Motion behavior:** camera orbit/zoom are damped (ease toward target each frame), not snappy. Panel open/close and overlay fade use ~180–240ms CSS transitions with an ease-out curve. Slider changes apply instantly to the sim. No motion-sickness-inducing auto-spin (an optional gentle idle drift is fine when untouched).
- **Cursor affordances:** `grab` cursor over the cloth, `grabbing` while held, default elsewhere; `ns-resize`/`ew-resize` hints are unnecessary but the canvas uses `touch-action: none` and a crosshair-free, clean default.
- **Responsiveness:** full-viewport canvas; the panel collapses to an icon on narrow screens; controls remain reachable. Honor `prefers-reduced-motion` by disabling the idle drift and reducing wind animation amplitude.
- **Accessibility:** the canvas has an `aria-label` describing the interaction; controls have real `<label>`s and visible focus rings; the panel is keyboard-reachable; color choices keep text contrast ≥ 4.5:1 against the panel surface.

## Edge cases & error handling
- **No WebGL / context creation failure:** detect a failed `WebGLRenderer` creation (try/catch) and render a graceful, styled fallback message ("This showcase needs WebGL — try a current browser") instead of a blank page or a thrown error. Also handle `webglcontextlost` by pausing the loop and `webglcontextrestored` by rebuilding GPU resources.
- **Resize / DPR changes:** on `resize` (and orientation change), update the renderer size, camera aspect, and DPR (clamped to 2). Debounce if needed. Never let the canvas blur or stretch; never leak by re-creating the renderer.
- **Backgrounded tab / huge dt:** clamp `dt` and cap substeps so returning to the tab doesn't fling the cloth across the screen. Pause the RAF loop on `visibilitychange` to hidden and resume cleanly.
- **Physics explosion / NaN:** the stability guards above must make an explosion effectively impossible; additionally, expose a **Reset** that fully re-drapes. If a non-finite value is ever detected globally, auto-reset the cloth rather than freezing or spamming the console.
- **Violent drag:** clamp grab displacement per substep and let constraints catch up so a fast yank stretches and (if tearing) rips, but never NaNs.
- **Raycast misses / lost pointer:** if pointer-up happens off-canvas or the pointer is canceled (`pointercancel`, `lostpointercapture`), release the grab cleanly. Use `setPointerCapture` so a drag continues even if the pointer leaves the canvas.
- **Resolution change:** rebuilding the cloth must dispose the old geometry/material/mesh (`geometry.dispose()`, `material.dispose()`, remove from scene) before creating the new one — no GPU memory leaks.
- **localStorage unavailable / corrupt:** wrap all reads/writes in try/catch; on parse failure, clear the key and use defaults. Never let storage break the app.
- **Tear edge cases:** tearing must not crash the index rebuild (handle a cell whose constraints are partially torn), and the cloth must remain double-sided and lit on freshly exposed edges. Prevent tearing the pinned row from detaching the whole sheet unexpectedly unless that's the intended "drop pins" action.
- **Touch quirks:** prevent default on touch to stop page scroll/zoom hijacking the gesture; handle a second finger arriving mid-drag (switch to pinch-zoom) and leaving (resume single-finger) without a jump.
- **Teardown:** provide a dispose path (used on resolution change and context loss) that removes event listeners it owns, cancels the RAF, and disposes Three.js resources, so repeated rebuilds don't accumulate leaks.
- **CSP self-check:** the running page must produce **zero** CSP violations and **zero** console errors — no inline script, no remote fetch, no bare-specifier import, no addon import. Verify the only network request is for the same-origin `../vendor/three.module.js` and your own files.

## Definition of done
- [ ] `index.html`, `./main.js`, and `./styles.css` exist (plus any optional local `./*.js` modules `main.js` imports relatively); `index.html` loads **only** `<script type="module" src="./main.js"></script>` and links `./styles.css`.
- [ ] `main.js` imports Three.js as `import * as THREE from '../vendor/three.module.js';` — **not** a CDN, **not** a bare specifier, **not** re-vendored. The existing vendored r160 file is used as-is.
- [ ] **No inline `<script>`**, **no `on*=` handlers**, **no `eval`/`new Function`**, **no importmap**, **no Three.js addons** (camera control and "glow" are hand-rolled).
- [ ] **No external resources**: no CDNs, web fonts, external images/textures, analytics, or non-same-origin network calls. Textures are procedural/canvas-generated. System font stack only.
- [ ] A 40×40 (default) Verlet cloth drapes from a pinned top edge, sags under gravity, and ripples under adjustable, gusting wind with believable, weighty motion.
- [ ] Structural **and** shear constraints (plus optional bend) are present and relaxed over a slider-controlled number of iterations; increasing stiffness visibly stiffens the cloth.
- [ ] The user can **grab any point** by clicking the cloth and drag it through space, pulling the fabric around; releasing lets it fall back naturally; a held point feels weighted.
- [ ] **Orbit** (drag background) and **zoom** (wheel / pinch) work, are damped, and are clamped to sane ranges; touch works on mobile.
- [ ] The cloth is a **double-sided, shaded** mesh with per-frame normals, lit by key/fill/rim lights with soft shadows on a subtle ground; the scene is dark and cinematic with a procedural glow/vignette.
- [ ] **Wireframe** and **tear** toggles work; tearing produces a visible, lit, double-sided rip and never crashes.
- [ ] Sliders (wind, gravity, stiffness, damping), toggles, Reset, Pause, resolution selector, and live stats (particles, constraints, FPS) all function; settings persist via `localStorage` and restore on reload.
- [ ] **Stability:** maxing every slider, yanking violently, backgrounding/refocusing the tab, and resizing never produce NaN, explosion, jitter, or a frozen sim. A global non-finite check auto-resets if ever triggered.
- [ ] **Performance:** steady ~60fps at default resolution on a typical laptop; DPR clamped; geometry/normal updates are efficient.
- [ ] **Robustness:** DPR-aware renderer; resize, `visibilitychange`, and `webglcontextlost/restored` handled; resolution rebuild disposes old GPU resources with no leaks; graceful no-WebGL fallback.
- [ ] An instructions overlay explains grab / orbit / zoom and is dismissible/re-openable; cursor affordances (`grab`/`grabbing`) are correct.
- [ ] Footer link "Built with a one-shot prompt from 1ShotGen" → `https://1shotgen.com` is present and opens in a new tab.
- [ ] **Zero console errors and zero CSP violations on load and during interaction.** No placeholders, no TODOs, no broken UI. The page works opened from a static server under the stated CSP, with the only external file being the same-origin vendored Three.js.
