> Idea: A 3D n-body gravity sandbox in WebGL: glowing planets and a sun orbit in real 3D space under Newtonian gravity, each leaving a fading 3D motion trail; orbit the camera with the mouse and zoom with the wheel; drag-release on the scene to fling a new body, add a sun, adjust gravity, and watch slingshots and merge-on-collision. Cinematic dark space with a starfield.

Build a single-page, static WebGL application called **Gravity3D** — a real-time, fully three-dimensional n-body gravity sandbox. Glowing planets and stars orbit one another in true 3D space under Newtonian gravity, each dragging a fading volumetric motion trail behind it. The user orbits a cinematic camera around the scene with the mouse, zooms with the wheel, and drags-and-releases directly on the 3D scene to fling new bodies into existence — then watches them swing through slingshot encounters, capture into orbits, and merge on collision. The whole thing lives in deep, cinematic space against a procedurally generated starfield. It must look jaw-dropping, run at a smooth 60fps, and produce zero console errors on load.

This is a public showcase of one-shot output quality. There are no placeholders, no "TODO", no broken affordances, and no half-built panels. Every control works. Every visual is intentional. Ship it as if it were a portfolio centerpiece.

## Goal

Deliver a self-contained, offline-capable, static single-page WebGL toy that makes Newtonian gravity *feel* tangible and beautiful in three dimensions. The emotional target: a person opens the page and within five seconds is dragging glowing comets across a starfield, watching them whip around a sun and smear light-trails through the dark. Within thirty seconds they have built a chaotic little solar system of their own and cannot stop poking at it.

Concretely, the finished app must:

- Render a true 3D scene (perspective camera, depth, real orbital planes that are *not* coplanar) using WebGL via a **vendored, same-origin Three.js module**. It is a static single-page app — open `index.html` and it runs, no build step, no server logic, no network.
- Import Three.js from a vendored ES module at `../vendor/three.module.js` (relative to the app directory). Do **not** re-vendor, fetch, bundle, or rewrite Three.js. Do **not** import from a CDN or a bare specifier such as `'three'`. Do **not** use any Three.js addon/example modules (no `OrbitControls`, no `EffectComposer`, no loaders) — hand-roll every interaction and effect.
- Simulate gravity between every pair of bodies (n-body, Newtonian, softened) with a numerically stable integrator, running independently of frame rate, and remaining stable indefinitely — no NaN, no infinities, no bodies launched to the edge of the universe by a single bad timestep.
- Let the user **orbit** the camera by dragging on empty space (or with a modifier), **zoom** with the mouse wheel / trackpad, and **fling** a new body by press-drag-releasing on the scene, with a live aiming guide that previews launch direction and speed.
- Provide a compact, elegant HUD with real controls: add a sun, pause/resume, reset to a curated demo system, clear everything, adjust the gravitational constant, adjust trail length, and toggle the starfield and velocity vectors.
- Show each body trailing a fading 3D ribbon/point-trail that follows its actual path through space, glowing additively against the dark.
- Merge bodies on collision with conservation of momentum and mass (radius grows from combined volume), with a brief flash on impact.
- Be robust: DPR-aware rendering, correct resize handling, a clamped fixed-timestep physics loop, defensive guards against numerical explosions, and disciplined disposal of GPU resources so nothing leaks over a long session.

### Hard platform constraints (must hold exactly)

The app is served under a strict Content-Security-Policy:

```
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'
```

Therefore:

- **All JavaScript lives in external, same-origin ES module files.** `index.html` loads exactly one script: `<script type="module" src="./main.js"></script>`. You may split logic into additional local `./*.js` modules that `main.js` imports with relative paths. No inline `<script>` blocks. No `on*=` inline event handlers — wire everything with `addEventListener`. No `eval`, no `new Function`.
- **No import maps** (an inline `<script type="importmap">` is CSP-blocked). Import Three.js directly: `import * as THREE from '../vendor/three.module.js';`.
- **No external resources of any kind**: no CDNs, no web fonts (use a system font stack only), no remote images or textures (everything procedural or canvas-generated), no analytics, and no network calls whatsoever (no `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, or beacons).
- **Styling**: an external same-origin `./styles.css` plus inline `<style>` / `style` attributes are permitted. All paths are relative (`./main.js`, `./styles.css`, `../vendor/three.module.js`).
- Glow/bloom must be achieved **without** post-processing addons: use emissive `MeshStandardMaterial` for body surfaces and additive-blended `Sprite`s whose texture is a canvas-generated radial gradient. No `EffectComposer`, no `UnrealBloomPass`.

## Tech stack

- **Rendering**: Three.js (the vendored r160-era module at `../vendor/three.module.js`), `WebGLRenderer`, `PerspectiveCamera`, `Scene`. Hand-rolled orbit/zoom camera controller (~25 lines: pointer drag → spherical azimuth/polar deltas, wheel → radius). No addons.
- **Language**: modern vanilla JavaScript as native ES modules (`import`/`export`). No TypeScript build, no bundler, no transpile step — the files run as-authored in an evergreen browser.
- **State**: a single plain-object app/store module holding simulation parameters and UI state, mutated through small named functions. No framework, no reactivity library — direct DOM updates from a thin controller. (Were this a React/Vite/Tailwind app the natural choice would be Zustand or Context; here the strict CSP and single-file-showcase goal make a tiny hand-written store the right call, and we make that assumption explicitly.)
- **Physics**: hand-written n-body integrator (semi-implicit/symplectic Euler with sub-stepping; gravitational softening; optional velocity-Verlet quality bump) in its own module, deterministic given a seed, decoupled from render cadence via a fixed-timestep accumulator.
- **Geometry/visuals**: `SphereGeometry` (shared, low-poly, instanced where helpful) for bodies; per-body additive glow `Sprite`; `Points`/`BufferGeometry` for the starfield and motion trails; `MeshStandardMaterial` with `emissive` for surfaces; a `PointLight` co-located with each star/sun plus a faint ambient/hemisphere fill so non-emissive planets read in 3D.
- **Styling**: external `./styles.css` for the dark cinematic HUD, with a system font stack (`ui-sans-serif, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif`) and a small monospace stack for numeric readouts.

### File / module layout

```
gravity3d/
  index.html        — shell: canvas, HUD markup, footer; loads ONLY <script type="module" src="./main.js">
  styles.css        — dark cinematic theme, HUD/panel/slider/button styling, responsive layout, reduced-motion
  main.js           — entry ES module; imports THREE from ../vendor/three.module.js; boots everything
  vendor/three.module.js   — (already exists; do NOT modify) imported as ../vendor/three.module.js
```

`main.js` is the single entry the HTML loads. You **may** split into additional sibling modules imported relatively from `main.js` (for example `./sim.js`, `./scene.js`, `./controls.js`, `./store.js`, `./ui.js`, `./util.js`) if it improves clarity — each must be a same-origin relative import. If you keep it as one file, it must still be cleanly organized into the same logical sections. Whichever you choose, the only thing `index.html` references is `./main.js` (plus `./styles.css` and the inline favicon). Document the chosen split with a short comment block at the top of `main.js`.

Logical responsibilities to cover, however you split files:

- **Bootstrap / lifecycle**: create renderer, scene, camera, lights; size to viewport with DPR cap; start the RAF loop; tear down on `pagehide`/`visibilitychange` if needed.
- **Store / parameters**: gravitational constant scale, paused flag, trail length, starfield on/off, vectors on/off, body count, fps, demo-system definition, RNG seed.
- **Physics**: body list, force accumulation, integration, softening, collision detection + merge, energy/center-of-mass bookkeeping, NaN guards.
- **Scene sync**: keep Three.js meshes/sprites/trails in lockstep with the physics body list; create/dispose on add/remove/merge.
- **Camera controller**: spherical orbit from pointer drag on empty space; wheel zoom with clamped radius; smoothed (damped) motion; auto-frame on reset.
- **Interaction**: distinguish "orbit the camera" from "fling a body"; project a drag on the scene into a 3D launch ray and velocity; live aiming overlay; keyboard shortcuts.
- **HUD/UI**: buttons, sliders, toggles, live stats, help/shortcuts, collapse/expand, ARIA labels.
- **Util**: seeded RNG, clamp, vector helpers not already in THREE, color ramps, canvas radial-gradient texture factory, fps meter.

## Data model

The simulation is a flat array of **Body** records plus a small **SimState** and **UIState**. Define them precisely.

### Body

| Field | Type | Meaning / rules |
|---|---|---|
| `id` | number (monotonic) | Stable identity across frames; used to map physics → Three.js objects. |
| `kind` | `'planet' \| 'sun'` | Suns emit light and bloom strongly; planets are lit. A merge may promote a planet to a sun past a mass threshold. |
| `mass` | number (> 0) | Drives gravity and (via cube-root) radius. Clamp to `[MASS_MIN, MASS_MAX]`. |
| `radius` | number (> 0) | Visual + collision radius, derived from mass: `radius = k * cbrt(mass)`, so merged volume sums. |
| `position` | `THREE.Vector3` | World-space position in simulation units. |
| `velocity` | `THREE.Vector3` | World-space velocity, sim units/second. |
| `acceleration` | `THREE.Vector3` | Scratch accumulator, recomputed each step. |
| `color` | `THREE.Color` | Base/emissive tint; assigned from a curated palette by kind and mass. |
| `trail` | ring buffer of `Vector3` | Last N world positions for the fading motion trail; length tracks the trail slider. |
| `alive` | boolean | False once merged into another body; pruned after scene cleanup. |
| `mesh` | `THREE.Mesh` | Sphere surface (emissive standard material). |
| `glow` | `THREE.Sprite` | Additive radial-gradient sprite scaled with radius for bloom-like halo. |
| `trailLine` | `THREE.Line`/`Points` | Geometry rendering the ring-buffer trail with per-vertex fading alpha. |
| `light` | `THREE.PointLight \| null` | Present for suns; intensity scales with mass; null for planets. |
| `spin` | number | Slow self-rotation rate for surface life. |

### SimState

| Field | Type | Meaning |
|---|---|---|
| `G` | number | Effective gravitational constant = `BASE_G * gravityScale`. |
| `gravityScale` | number `[0, 2.5]` | From the gravity slider (1.0 = default). 0 = bodies coast inertially. |
| `softening` | number | Plummer softening length `ε`; force uses `1 / (r² + ε²)^{3/2}` to prevent singular blow-ups at close range. |
| `dtFixed` | number | Fixed physics timestep (e.g. `1/120` s). |
| `substeps` | number | Integration sub-steps per fixed step for stability under strong gravity. |
| `accumulator` | number | Leftover real time awaiting fixed steps. |
| `paused` | boolean | Pauses integration (rendering and camera continue). |
| `bounds` | number | Soft world radius; bodies far beyond it are gently culled to keep the sim sane. |
| `maxBodies` | number | Hard cap (e.g. 300) to protect frame rate; flinging past it prunes the oldest planet. |
| `seed` | number | RNG seed for the reproducible demo system. |
| `merges` | number | Running count, for the stats readout. |

### UIState

| Field | Type | Meaning |
|---|---|---|
| `trailLength` | int `[0, 200]` | Ring-buffer length per body; 0 disables trails. |
| `showStars` | boolean | Starfield visibility. |
| `showVectors` | boolean | Velocity vector arrows on each body. |
| `hudCollapsed` | boolean | Panel collapsed state (persists in-memory for the session). |
| `helpOpen` | boolean | Shortcuts list expanded. |
| `aiming` | null \| `{ originNDC, currentNDC, ray }` | Active fling drag, for the preview overlay. |
| `dragMode` | `'idle' \| 'orbit' \| 'fling'` | Resolved on pointer-down to disambiguate gestures. |
| `fps` | number | Smoothed frames-per-second readout. |

### Constants (tune to taste, but define them all in one place)

`BASE_G`, `MASS_MIN`, `MASS_MAX`, `RADIUS_K`, `SUN_MASS_THRESHOLD`, `SOFTENING`, `DT_FIXED`, `SUBSTEPS`, `MAX_BODIES`, `WORLD_BOUNDS`, `CAMERA_MIN_RADIUS`, `CAMERA_MAX_RADIUS`, `DPR_CAP` (e.g. 2), `STAR_COUNT`, `TRAIL_DEFAULT`, palette arrays for planets and suns.

## Behavior & features

### Core simulation

- **Newtonian n-body gravity** between every pair of live bodies. For bodies *i, j*: direction `d = pⱼ − pᵢ`, `r² = d·d`, force magnitude `F = G · mᵢ · mⱼ / (r² + ε²)`, acceleration on *i* `+= F/mᵢ · d̂` using softened denominator `(r² + ε²)^{3/2}`. Accumulate accelerations for all bodies, then integrate. Suns participate exactly like planets (they are just heavy, bright bodies); there is no special-cased fixed central mass — it is genuinely n-body.
- **Integrator**: semi-implicit (symplectic) Euler at minimum — update velocity from acceleration, then position from the new velocity — which conserves energy far better than naive Euler and keeps orbits from spiraling out. Run `SUBSTEPS` integration sub-steps per fixed timestep so close encounters stay stable. A velocity-Verlet upgrade is welcome but not required.
- **Fixed-timestep loop with accumulator**: each animation frame, add the (clamped) real elapsed time to an accumulator and consume it in `dtFixed` chunks; render with the leftover as interpolation if you like. **Clamp `dt`** so a tab returning from background (a huge delta) cannot inject a giant step — cap accumulated time to e.g. 0.1 s per frame and drop the remainder.
- **Collision + merge**: two live bodies collide when `|pᵢ − pⱼ| < radiusᵢ + radiusⱼ`. Merge them into one body conserving momentum (`v = (mᵢvᵢ + mⱼvⱼ)/(mᵢ+mⱼ)`) and mass (`m = mᵢ + mⱼ`); position at the mass-weighted centroid; new radius from summed volume; color blended by mass; if the merged mass crosses `SUN_MASS_THRESHOLD`, promote to a sun (add light + stronger glow). Mark the lighter body `alive=false`. Emit a brief additive **impact flash** sprite that fades out. Increment `merges`.
- **Stability guards**: after each step, if any body's position or velocity component is non-finite (`!Number.isFinite`), neutralize it (reset that body to a safe state or remove it) rather than letting NaN poison the whole array. Cap maximum speed to a sane ceiling. Cull bodies that drift past `WORLD_BOUNDS · someFactor` (fade their glow, then dispose). Never divide by an unsoftened zero distance.

### Camera (hand-rolled, no OrbitControls)

- **Orbit**: pointer-drag on empty scene space rotates the camera around the system's center of mass (or origin). Maintain spherical coordinates (radius, azimuth θ, polar φ). Drag dx → Δazimuth, dy → Δpolar; clamp polar to `(0.05, π − 0.05)` so it never flips through the poles. Apply light **damping** so motion glides to a stop instead of snapping.
- **Zoom**: wheel / trackpad pinch changes spherical radius, clamped to `[CAMERA_MIN_RADIUS, CAMERA_MAX_RADIUS]`, with exponential (multiplicative) steps so zoom feels consistent at all scales. Prevent the default page scroll.
- **Auto-frame**: on reset/clear, smoothly ease the camera to a flattering default angle and a radius that frames the demo system.
- **Touch**: one finger orbits; two-finger pinch zooms; works on mobile without breaking desktop.

### Interaction: flinging a body

- On pointer-down, resolve `dragMode`: if the press begins on/near an existing body **or** with no modifier on empty space you must pick one consistent scheme — specify it and make it discoverable. Recommended: **plain drag on empty space orbits the camera; drag that *starts on a body* or a press-drag with a held modifier/secondary button flings a new body.** Simpler, equally acceptable, and what the instructions overlay should teach: **a dedicated "Fling" affordance** — left-drag always aims+flings a new body from a point on a focal plane through the scene; orbiting uses right-drag or a one-finger drag on the labeled background ring / a modifier. Choose one, implement it cleanly, and tell the user in the overlay. The chosen gesture must never feel ambiguous.
- **Aim preview**: while dragging to fling, draw a live overlay (a 2D canvas/SVG line or a 3D line in the scene) from the spawn point to the cursor, with length/thickness encoding launch speed and an arrowhead showing direction. Show the projected spawn point as a faint ghost sphere.
- **Spawn math**: project the pointer onto a plane through the scene center facing the camera (or onto the camera-facing plane at a chosen depth) to get a 3D spawn position; the drag vector (origin → release), scaled, becomes the initial velocity. Mass can be a default or scale subtly with drag distance — pick one and keep it predictable.
- **Release** creates a `planet` with palette color, glow sprite, and an empty trail, and it immediately participates in gravity. **Esc** cancels an in-progress aim.

### HUD controls (every one functional)

- **Add Sun** (`S`): spawn a heavy, bright sun near the center (or at a sensible offset) with a `PointLight` and strong glow; it immediately dominates nearby orbits.
- **Pause / Resume** (`Space`): toggle integration; camera and rendering keep running; button reflects state with icon + label + `aria-pressed`.
- **Reset** (`R`): rebuild the curated, reproducible demo system (a sun plus several planets on **non-coplanar** orbits with sensible circular-ish velocities, seeded by `seed`) and auto-frame the camera.
- **Clear** (`C`): remove all bodies, dispose their GPU resources, zero the stats.
- **Gravity slider**: maps to `gravityScale` ∈ `[0, 2.5]`; live numeric readout (e.g. `1.00×`); 0 lets bodies coast in straight lines (great for showing inertia).
- **Trail length slider**: `0…200`; updates every body's ring-buffer capacity live; 0 hides trails.
- **Toggle Starfield**: show/hide the background `Points` starfield.
- **Toggle Vectors**: show/hide per-body velocity arrows (scaled `ArrowHelper` or custom lines).
- **Live stats**: body count, FPS (smoothed), merge count. Update without thrashing layout.
- **Collapse/expand** the HUD to get an unobstructed view; **Help/shortcuts** disclosure listing every key.

### Visual systems

- **Starfield**: a few thousand `Points` on a large sphere shell, slight size/brightness variance, a touch of color temperature variety (cool whites, faint blue/amber), optionally a barely-perceptible parallax drift. Built once, toggle visibility, dispose on teardown.
- **Body surfaces**: low-poly spheres with `MeshStandardMaterial`, `emissive` tinted to the body color (planets faintly self-lit so they read in the dark; suns strongly emissive). Slow self-rotation (`spin`).
- **Glow**: each body carries an additive-blended `Sprite` using a **canvas-generated radial-gradient texture** (white→transparent), tinted to the body color, scaled to ~2–4× the radius. Suns get a larger, brighter halo. This is the bloom substitute — no post-processing.
- **Trails**: per-body fading ribbon following the real path. Implement as a `Line`/`Points` fed by the body's ring buffer, with per-vertex alpha ramping from 0 (oldest) to full (newest) via vertex colors + additive blending, so trails glow and dissolve into the dark. Update the buffer in place each frame; resize on trail-length change.
- **Impact flash**: on merge, a quick expanding, fading additive sprite at the collision point.
- **Lighting**: each sun is a `PointLight`; a low ambient/hemisphere light keeps non-emissive planet hemispheres from going pure black. Tone mapping (ACES Filmic) + sane exposure for a filmic look; `outputColorSpace = SRGB`.

### Aesthetic direction (cinematic dark space)

- **Palette**: background near-black with a hint of blue-violet — base `#05060d` to `#090b16`. Planet palette: cyan `#54e0ff`, violet `#a98bff`, mint `#7CF5C6`, rose `#ff8fb0`, amber `#ffce6b`. Suns: hot white-gold core `#fff4d6` with `#ffb74d` glow. HUD accent: cyan `#54e0ff`. Text: `#e8ecf8` primary, `#8b93ad` muted.
- **HUD**: a single translucent glass panel (subtle backdrop blur, 1px hairline border `rgba(255,255,255,0.08)`, soft shadow), top-left or top-right, ~300px wide, rounded `14px`. Sliders are custom-styled (thin track, glowing thumb). Buttons have a quiet idle state and a luminous hover/active. Generous but tight spacing on a 4px scale (4/8/12/16/24). Numeric readouts in monospace.
- **Typography**: system sans for UI; monospace stack for numbers/stats. Clear hierarchy: app title, lede line, section controls, footnote.
- **Motion**: camera damping, slider feedback, glow pulsing subtly on suns, trails dissolving — everything eased, nothing janky. Respect `prefers-reduced-motion` by toning down idle drift/pulse (but never breaking the sim).
- **Footer**: a small link reading **“Built with a one-shot prompt from 1ShotGen”** pointing to `https://1shotgen.com` (`target="_blank"`, `rel="noopener noreferrer"`).
- **Instructions overlay**: a brief, dismissible starter card (or always-visible lede) telling the user exactly how to orbit, zoom, and fling — written in plain language, gone or minimized after first interaction.

## Edge cases & error handling

- **WebGL unavailable / context lost**: if `WebGLRenderer` can’t be created, replace the canvas with a graceful dark message card (“This showcase needs WebGL”) instead of throwing. Listen for `webglcontextlost`/`webglcontextrestored` and pause/resume cleanly.
- **Resize / DPR**: on `resize` (and `devicePixelRatio` changes), update renderer size, `camera.aspect`, and `projectionMatrix`; cap pixel ratio at `DPR_CAP` to protect fill-rate on retina displays. Debounce if needed. Resizing must never distort the scene or blur the HUD.
- **Background tab / huge dt**: clamp accumulated time per frame; never integrate a multi-second jump. On `visibilitychange` to hidden, optionally pause the accumulator so returning doesn’t fast-forward.
- **Numerical explosion**: softened gravity prevents close-encounter singularities; still, after each step scan for non-finite components and a max-speed ceiling, and sanitize offending bodies. One bad body must never NaN the entire array or freeze the loop.
- **Too many bodies**: enforce `MAX_BODIES`; when flinging past it, prune the oldest planet (never a sun the user explicitly added, if avoidable) and dispose its resources. Keep FPS smooth.
- **Empty scene**: with zero bodies, the sim idles quietly; Reset/Add Sun bring it back to life; stats read 0.
- **Gravity = 0**: bodies coast in straight lines (pure inertia) — verify this visibly works (great teaching moment).
- **Trail length = 0**: trails fully hidden and their geometry not updated (cheap). Increasing it re-grows buffers without artifacts.
- **Degenerate fling**: a zero-length drag (click without movement) should *not* spawn a stray zero-velocity body in your face — require a minimum drag distance, or spawn with a tiny default speed; decide and be consistent.
- **Gesture ambiguity**: orbit vs. fling must be unambiguous on both mouse and touch; a fling-in-progress must not also orbit the camera, and vice versa. Esc cancels an aim.
- **Pointer capture**: use `setPointerCapture` so a drag that leaves the canvas still completes correctly; handle `pointercancel`.
- **Memory/leaks**: dispose geometries, materials, and textures for every body removed (merge, cull, clear) and on teardown; reuse shared geometries/materials where possible; never accumulate orphaned objects in the scene graph over a long session.
- **Accessibility**: canvas has an `aria-label`; interactive controls are real `<button>`/`<input>` with labels and visible focus rings; keyboard shortcuts documented; honor `prefers-reduced-motion`. The app must be operable enough that a keyboard user can at least reset, pause, add a sun, and adjust sliders.
- **Console hygiene**: **zero** errors or warnings on load and during normal use. No 404s (no external assets). No unhandled rejections.

## Definition of done

- [ ] `index.html` opens as a static file and runs with **no build step and no network**; it loads exactly one script: `<script type="module" src="./main.js"></script>` plus `./styles.css` and an inline data-URI favicon. No inline `<script>` blocks, no `on*=` handlers.
- [ ] Three.js is imported from **`../vendor/three.module.js`** (vendored, same-origin) — never a CDN, never a bare `'three'` specifier, never re-vendored or fetched. No Three.js addons are used; orbit, zoom, glow, and trails are all hand-rolled.
- [ ] No external resources whatsoever: no web fonts, no remote images/textures (all procedural or canvas-generated), no analytics, no `fetch`/XHR/WebSocket/EventSource. Fully CSP-compliant under `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'`.
- [ ] The scene is genuinely 3D: perspective camera, depth, and demo orbits on **non-coplanar** planes; orbiting the camera reveals real three-dimensional structure.
- [ ] **Newtonian n-body gravity** runs between all bodies with Plummer softening, on a **fixed-timestep accumulator** with sub-stepping; `dt` is clamped; the sim is stable indefinitely with **no NaN/Inf** (verified by leaving it running and by stress-flinging bodies into a sun).
- [ ] **Merge-on-collision** conserves momentum and mass, grows radius from summed volume, blends color, optionally promotes to a sun, and shows an impact flash; merge count updates.
- [ ] **Camera**: mouse-drag orbits (damped, polar-clamped), wheel zooms (clamped, multiplicative); touch supports one-finger orbit and two-finger pinch-zoom; auto-frame on reset.
- [ ] **Fling**: press-drag-release spawns a body with a live aim preview (direction + speed); Esc cancels; a click without drag doesn’t spawn a junk body; the orbit-vs-fling gesture is unambiguous and explained in the overlay.
- [ ] Each body has an **emissive surface**, an **additive radial-gradient glow sprite** (canvas-generated), and a **fading 3D motion trail** that follows its real path; suns carry a `PointLight` and a larger halo.
- [ ] A procedural **starfield** of thousands of points renders behind everything and toggles on/off.
- [ ] HUD controls all work: **Add Sun, Pause/Resume, Reset, Clear**, **Gravity** slider (0 = inertial coast), **Trail length** slider (0 = off), **Starfield** toggle, **Vectors** toggle, live **body/FPS/merge** stats, collapse/expand, and a **shortcuts** disclosure. Keyboard shortcuts (`Space`, `S`, `R`, `C`, `Esc`, `?`) work.
- [ ] **DPR-aware** renderer (ratio capped), correct **resize** (size + aspect + projection), ACES Filmic tone mapping with sRGB output for a filmic look.
- [ ] **Robustness**: WebGL-unavailable fallback card; context-loss handling; clamped dt on tab-return; per-step finite/max-speed sanitization; `MAX_BODIES` cap with oldest-prune; full GPU **disposal** on remove/clear/teardown (no leaks over a long session).
- [ ] **Aesthetic**: deep cinematic dark-space palette, translucent glass HUD with custom sliders/buttons, system font stack with monospace numerics, eased motion, `prefers-reduced-motion` respected.
- [ ] **Footer** link “Built with a one-shot prompt from 1ShotGen” → `https://1shotgen.com` (`rel="noopener noreferrer"`).
- [ ] **Smooth ~60fps** on a typical laptop with the demo system running; **zero console errors/warnings** on load and during normal use; no placeholders, no dead controls, no broken layout at any common viewport (including mobile).
