# Main Route

`/` is the 2D CSS verification of all animation modes, plus a ThreeJS Demos section that runs the engine's own character groups. It consumes the same `SPRITE_CELLS` and `SPRITE_MAP` as the ThreeJS groups and draws everything from `SPRITE_SHEET_FILE` (`a1.png`). Paths below start from `three-isometric-engine/` unless they start with `pages/`.

See `skills/add-sprite-assembly-from-source` for the frame placement formula, overlay conventions, and the full 2D verification procedure.

## Intro

Four serif paragraphs above the sections, 16px in `--theme-text`, with no heading. The first is written as YAML frontmatter, says what the skill gives a visitor and points agents at `SKILL.md` (its newlines are in the markup, so the HTML reads the same); its `name` ends with `version` from `package.json`, as the page title does, so both follow the manifest. The others say who the skill is for and what it builds on, linking the Shishi Sprite Editor's source, what the engine adds to those foundations so sprites share space with other meshes in dynamic combat, and where to point agents, and name the page's two fonts, Geist Serif for reading and VileR's Nix8810 for everything below the intro. Geist Serif links to https://vercel.com/blog/introducing-geist-serif and VileR to https://int10h.org/oldschool-pc-fonts/. `REPOSITORY_URL` stays the GitHub repository; the page's own address, https://3d.internet.dev, is `SITE_URL`, its canonical link. `SOCIAL_IMAGE_URL` is the Open Graph and Twitter image, `public/static/social.jpg`: marble.place's 400x400 logo with its hue turned from blue to green, so the card is `summary`, which is the square one. `pages/_document.tsx` links the favicons, marble.place's set with the same hue turn, and the markdown and `llms.txt` versions of the page for agents (`three-isometric-engine/web/AGENTS.md`). The explaining stops there: sections carry labels, legends and data in Nix8810, and no serif text. The first paragraph lines up with the first sidebar row because `.page` and `.sidebar` both start 24px down and a sidebar row is one serif line tall.

## Sections

`NumberedSections` numbers the top-level sections in the order they are written, 001 to 099, and gives `MainSidebar` the same list. Each section's number is its anchor (`#024`). The sidebar marks the section crossing the top of the viewport and scrolls with it. After a jump it holds the target at the top while the lazy sections around it mount, until the reader scrolls or `JUMP_SETTLE_MS` passes. The window is the scroller: the sidebar is sticky, because the demo canvas and the lazy sections measure against the viewport.

| Section | Details |
|---------|---------|
| SpriteSheet | 256x488 at SCALE, labeled boxes for all 200 cells: black borders, blue for the SR sub-regions. Each label is white text on its box's border color, on the box's top edge (SR labels on the bottom edge). The sheet has no pixels outside the boxes |
| ThreeJS Demos | Engine groups for every 3D mode, 100 cards: Idle, G, FL, Walk (A1 / W / I), A1 Jump, A1 Stab, A1 Cast, C, T, H, K, D, J, TS twice (a small stone and the heavy sphere), P1-P3, S1-S3, FI, ID and one card per `SEQUENCE_MODES` row — see below |
| Sequence Library | Every TYPE1.SEQ pair by selector, 4 patterns, dry or in water (`SHP_FRAMES`, `SEQ_LIBRARY`) |
| Dead (D) | 4 patterns, KO1/KO2 cells + anchored stage (SEQ 40/41) |
| Taking Damage (H) | 4 patterns, top+bottom stacked, animated 1px shiver (SEQ 38/39, `HURT_SHIVER_MOVES`) |
| Frozen Walk 2 (G) | 4 patterns, static 3-tile, walk frame 5 (SEQ 76/77) |
| Idle | 8 rotation steps, P1-P5 + reversal, cells + anchored stage (3/4 views 1px beside the origin) |
| Jump (J) | 4 patterns, 9 frames once, take-off, Mid Jump pose, landing (SEQ 204 + 42 + 52 / 205 + 43 + 53) |
| Critical (K) | 4 patterns, KO3/KO4 cells + anchored stage (SEQ 32/33) |
| Singing (C) | 4 patterns, 2-frame loop, legs overlay behind body (SEQ 64/65) |
| Bow High, Mid, Low (P1-P3) | 4 patterns, 9, 8 and 8-frame loops, 4-tile full-body, wind-up + lunge (SEQ 140-145) |
| Quick Throw (T) | 4 patterns, 6-frame non-looping, variable body heights, 2px rise overhead (SEQ 48/49) |
| Bounced to Death (FI) | 4 patterns, 13 draws of 3 compositions, 12px hop (SEQ 82/83) |
| Revived (ID) | 4 patterns, 3 frames, last one held (SEQ 84/85) |
| Float (FL) | 4 patterns, 3-tile walk frame 3; the whole unit hovers up to 2px above the ground line (SEQ 12/13 = animation 0x12/0x13, `FLOAT_MOVES`) |
| Unarmed High, Mid, Low (S1-S3) | 4 patterns, 9-frame loops, 4-tile full-body, wind-up + lunge or thrust (SEQ 96-101) |
| Throw Stone (TS) | 4 patterns, 12-frame non-looping, 5 overlay types (SEQ 208/209) |
| Walk (I) | 4 patterns, 8-frame animated, variable delays (SEQ 6/7) |
| Every `SEQUENCE_MODES` row, in SEQ order | `SequenceModeSection`: titled like the others ("Swing High (102, 103)"), pipeline table, then per pattern the tiles of each SHP frame, every drawn frame and the animation |

Each section but SpriteSheet, ThreeJS Demos and Sequence Library renders a `SpriteSheetTable*` pipeline trace component from `SpriteSheetTables.tsx`.

Every animated or composite frame renders inside `AnchoredStage` (single tiles via `AnchoredTile`). Composition, depth, flips, anchors, timing and SEQ unit motion are verified against the decoded TYPE1.SHP / TYPE1.SEQ in `tests/fixtures/type1.json` by `tests/sprite-shp-parity.test.js`; the page is the visual check of the same data. Bounced to Death plays `FATAL_ITEM_IMPACT_TIMELINE` (13 SEQ draws); a SEQ delay of 0 (held end frame) is previewed for `SEQ_HOLD_MS` before the loop restarts.

Titles use the SEQ name; a card that plays an engine behavior of another name carries that name in its SEQ line (`documentation/seq_animation_to_frame/AGENTS.md` "Names"). The page renders sections in source order (`pages/index.tsx`). A `SequenceModeSection` mounts its body when it first comes within 200px of the screen, keeps a placeholder height until then, and pauses its animation while it is away, because those sections outnumber all the others.

## ThreeJS Demos

`components/SpriteAnimationDemos.tsx` (section UI) and `debug/sprite-animation-demos.js` (stage). The cards render from `debug/sprite-animation-demo-catalog.js`, which imports no three.js; three, the engine groups, the sprite sheet, the black marble and Set's item models load when the section first scrolls into view. One `WebGLRenderer` on one canvas draws the cards as scissor viewports of their `.stageSlot`. The canvas is a window on the grid, because the grid (about 9500px tall at 1600px wide) would need a drawing buffer past WebGL's limits: it is two screens tall, scrolls with the page, and `placeCanvasWindow` recenters it in the frame that redraws it once the visible part of the grid leaves it. Every station updates every frame, only the cards in the window draw, and only they refresh their status lines. The `requestAnimationFrame` loop runs while the section intersects the viewport, and while paused it redraws only when the camera orbits, a control changed or the window moved. `prefers-reduced-motion` starts paused.

- Every character is built by `createCharacter` (the registered `group-*.js` factory; a target's flinch by `createFlinchGroup`) and updated by its own visual driver with `camera.userData.now` / `azimuthStep` primed once per frame. Each demo scene installs `syncCharacterVisuals` as `scene.onBeforeRender`, so every layer is drawn from the same frame (`meshes/character-groups/AGENTS.md` "Draw Order and Visual Drivers").
- Every unit starts settled in its mode (IDLE, `walkStart` = clock start), on the floor or, on Jump Down, on its ledge — no spawn drop. Each demo replays its action from `actions/action-timelines.js`: Guard holds; Float hovers 0-2px above a shadow that stays on the floor (SEQ 12/13 = animation 0x12/0x13); Mode J stands through the five take-off frames of SEQ 204/205 (22 ticks), flies the `jumpLift` arc (2 cubes) on the Mid Jump pose (533ms), lands on the first frame of High Jump Land and plays it on the ground, then the 400ms dwell; Mode TS has two cards, one throw with two stones (`STONE_VARIANTS` in the catalog). Both pick the stone up at the kneel (frame 2), carry it on the throwing glove (`throwStoneHandPoint`, overhead at frame 5, past the chest at frame 6) and throw it as frame 7 starts, right after the SEQ's PlaySound: it leaves the frame 6 hand on one arc from the camera-independent release point (`throwStoneFlightPoint`) while the arm follows through low with an empty hand. The stations resolve frames from `THROW_STONE_DELAY_MAP`, as the TS visual driver does, and read the hand from the grips (`THROW_STONE_HANDS['a1.png']`; the status line names the grip tile and pixel). One arm throws both (`throwSpeed`), so the stone's mass sets its speed. Both are black marble orbs. Throw Stone is the hand-sized one (radius 2.4) that leaves at 162 u/s on a flat `solveArc` to a target 2.5 cubes away and hits on tick 52.6. Throw Heavy Stone is the big one, keeping the engine marble's radius, mass and 45° launch (tests hold it to `THROW_LAUNCH_VH/VV`) with the target where that lob comes back to its release height, tick 133. Each stone ends on the target's collision box at the same world point at every camera, `collide` shares the momentum with the target's mass of 3, so the small stone bounces off and barely moves it and the heavy one knocks it back about 12 units and carries on, and the stone drops, bounces low, rolls out and fades; the next throw waits for that and puts the target back after the cooldown; Mode T catches a marble resting in front of the unit: its body rises on the hold point (half a cube forward, `quickThrowHoldHeight`) and the T visual driver draws it on the hand (`ch._heldMarble`, `quickThrowMarblePoint`: catch point to kneel hand over frames 0-1, on the hand from the kneel to the reach, leaving the overhead hand on frame 5), the body leaves on the `deflectLaunchVH` / `deflectLaunchVV` arc at tick 22 and rests on the floor, then a new marble appears and the next pass starts after the 400ms dwell; the status line names the grip tile and pixel; the Stab Lv1-3 targets flinch (engine Mode H group) at frame 5. The Bow cards shoot a bow with no sprite (`weapons/bow.js`, `weapons/AGENTS.md`): BOW_SPEC generates the stave, string and arrow, the grip rides the frame's bow hand (`frame.bowHand`, with the SEQ lunge) and the bow is a 3D object aimed along the launch. The arrow nocks on tick 12, the archer draws to full draw by tick 20, the hand opens on PlaySound (tick 26), the simulated string drives the arrow off the nock at 294 u/s about 2 ticks later and rings down, and the arrow flies a `solveArc` low arc to the target, settling onto it from the drawn bow over 8 ticks. It lands on the tick in the card's strip (`bowPlan`: 39-42 by aim), the target flinches and the arrow stays in it until the next nock. High, Mid and Low are the target BOW_LEDGE above the archer, level with it and BOW_LEDGE below it. The status line gives the draw, the live nock force and how far the nock sits from the drawn hand (`frame.drawHand`): 1-7 art pixels at the game elevation, 7-11 on steps 3 and 7, which look along the arrow, where the sprite cannot follow it. The Gun cards stand at the same three heights with Set's rifle, its barrel held on the target: two ticks before PostGenericAttack the muzzle flashes and a light streak with a red tip leaves the opening along the barrel's line, and it lands where the target flinches, on that tick. Swing, Poke, Read Book, Play Instrument, Shield Block, Katana Use and the Swing and Thrust Recoils hold Set's sword, spear, tome, harp, shield and katana on the hand of each frame (`weapons/AGENTS.md` "Set Items"), and every Item Throw card holds a black marble: the small one in one hand on High, and the big one on Mid and Low, between the hands once both come down. Weapon Throw throws its sword at a target two cubes off, turning about its middle, and the target flinches where the point arrives, with the sword staying in it until the next pass. Item Throw Release throws its marble on the SEQ's PlaySound at a target two cubes off, half a cube above the thrower, level with it or half a cube below it, and it comes off the target, drops, rests and fades while the target slides. Poke High and Low stage heights as Gun does, on a ledge half a cube tall, and the two Thrust Recoils play on that stage against a unit in Shield Block, from the moment the spear meets its shield, where a spark shows. The Cloth Bag cards swing a simulated bag on a cord that meets the target at the SEQ's PostGenericAttack, within two ticks (`weapons/AGENTS.md` "The Cloth Bag"). Every weapon wears mythril, the bow included, and a shield and a book wear a worked outside over a different inside, darker on the shield (`weapons/AGENTS.md` "Faces"). Bounced to Death, Revived and the one-shot sequence modes play once, hold their last frame for 800ms and start over; a single held frame is the same cycle with nothing to play. Some rows are staged instead, because the game plays them while something else moves the unit (`SEQUENCE_STAGES` in the catalog). Mid Jump, High Jump Hold and High Jump Land play the chain each belongs to (`leap`): a take-off on the ground, the held air pose up the 2-cube column and back down (`leapHeight`), High Jump Land, then the 400ms dwell. Jump Down plays once from a ledge 2 cubes up, crossing one cube to the floor over the SEQ's air frames on one arc (`jumpDownHeight`), then rests 400ms, dithers away on the engine's despawn fade and comes back on the ledge on its spawn fade, because nothing walks it back up. Teleport plays its sequence through that despawn fade, draws nothing for `TELEPORT_GAP_TICKS`, plays again on the other side through the spawn fade, `TELEPORT_OFFSET` either side of the card's center, and rests 800ms. The other attack sequence modes loop, and a target standing in reach flinches on the SEQ's PostGenericAttack: nothing acts on them in the timelines, so their strip has no engine event. Looping sequence modes loop from the settle frame. A1 runs its own actions: the walk loop, Stab and Cast (one action, then the 400ms dwell) and the jump, which replays the player arc — the scripted rise, then gravity + `playerFallExtraAccel` (no apex hang) — with the last take-off frames, the Mid Jump pose (`playerJumpFrame`), and landing clearing the action and playing High Jump Land (`playerLandFrame`). Singing loops; Taking Damage shivers; Critical and Dead hold their pose on their shorter bodies. There is no physics engine: marbles stop on the floor, the only knockback is the stone collision above, and the body the water measures comes from the sprite (see In water). Contact shadows, the plain floor with its grid, the white marble ledges, the black marble orbs (`debug/marble-material.js`) and the 2-cube column in the Jump and leap cards (on the unit's own cell, drawn without depth writes so the sprite covers it; the feet reach its top at the peak) are demo aids.
- Framing (`fitStationView`): each station declares its action envelope — group origins with a screen-space sprite extent, the held prop's hand points (screen-space offsets) and the stone's or marble's arc, the Jump column's corners — and the view is fitted to it for the current elevation and the 6:5 slot. The half-height never goes below `MIN_VIEW_HALF_HEIGHT` (hold 20; jump, leap, jump-down and throw 38.7; every other kind 22.7 world units), widens only as far as the envelope needs over a full orbit (so the scale does not change with the camera step or while orbiting), and the focus centers the envelope for the current azimuth. The numbers are computed, so `fitStationView` is their owner.
- Damage counters (`debug/damage-counter.js`): every hit shows a number over the struck unit, always `DAMAGE_COUNTER` (10, in the catalog), because the page has no stats. Strike, bow and throw cards show it on the frame the target flinches, and Taking Damage (`repeatsDamage`) repeats it over the shiver. The digits are 5x7 bitmaps in that file, white with a 1px black outline, drawn in art pixels at the sprites' scale, so there is no asset to load. They hop one at a time and last `COUNTER_TICKS` on the demo clock, so they pause and step with the sprites, and the status line of a strike, bow, gun or weapon-throw card says `target hit 10`. `COUNTER_ANCHOR_PX` keeps the hop inside `SPRITE_EXTENT`, so a counter never changes a card's framing.
- In water (`debug/water-simulation.js`, `debug/water-surface.js`): every card stands in its own low polygon sheet of clear shallows, with the swell of the ocean under the metal platform in `www-prototypes`, off until the control is pressed. One 64x64 Tessendorf swell (about 1.2ms of main thread per frame, measured under Node) runs per stage, and each scene reads it from its own offset, so no two show the same waves. It runs on the demo clock. Each scene has a 96x96 wave-equation ripple field, stepped at 60Hz only while that card is drawn. Swell and ripples are the prototype's math at the scale of a sprite; the sheet is 48x48 segments crowded around the origin (the prototype's is 256x256). The shading is this project's: each pixel refracts through the surface to a pale seabed `SEABED_DEPTH` below it, rolling in long low dunes, and is tinted by how far the ray went through the water (`ABSORPTION`), with Fresnel sky, a clean sun glint and smooth sine shimmer in place of noise. The seabed's light is real caustics: once per frame, while the swell moves, `createCausticPass` refracts a 180x180 grid through the swell's slopes onto a 1024px tile and lights each triangle by how much the move squeezed it, so light gathers under the crests into filaments that move with the waves, with no pattern behind them. The slopes go through a cubic filter so the 64x64 field does not show its cells, and the sheet reads the tile three times, slightly apart, for the color fringe of real caustics. What stands in the water shades the bed under it, along the sun. All foam is one sheet of lace (`lace`, the creases of warped smooth noise, so strands run and join and never close into cells), shown as far as a pixel holds foam: the waterline from the ripples, the wake a fast wave leaves there (`WAKE_SPEED`, gone in about a second), whitecaps on the swell's sharpest crests and a contour of the distance to a ledge. Below a low coverage it fades out, so open water stays clean. The sheet is opaque, so it draws before every sprite layer and writes depth: anything below the surface fails the depth test, overlay planes included, no sprite material changes, and the waterline follows the waves across a sprite to the pixel. The sprite plane leans with the camera, so `waterHeight` moves the surface with the elevation to cut a standing unit on one row at every elevation: 10px below the stance point, the bottom of the idle water frame (F183). Sequence cards also draw their F(n + 182) frames (`applyWater`, `ch.seqWater`), which assemblies do not have, only while the unit stands at the water: those frames are cut for it, so a unit on a ledge or in the air keeps its dry frames and the sheet takes it row by row as it comes down. Nulled Walk draws them dry or wet, because its dry frames are placeholders. Three cards float over the surface instead, on their dry frames: Float, Float Fast and Float Slow, whose water frames stop at the waist (`FLOAT_WATER_ROWS`). Dead lies in the shallows (`waterRows`). The surface moves for one reason only: a body in it (`physics/AGENTS.md`). Each drawn frame `createStationWater` measures every unit's body against the surface, with the swell under it: the sprite row it crosses, the share of the volume under it, and, pixel by pixel, what is displaced and where the waterline runs. The change since the last frame makes the waves, so a unit standing still rocks the water only as the swell rises against it, a stride or lunge sends rings out, and a body or thrown prop crossing the surface splashes. Waves reflect off the waterline, and the foam follows it as the fourth channel of the ripple field, the greater of `hug` and the wake, so it takes the body's real shape, two lobes for a wide stride, and a floating unit touches nothing until a crest reaches its soles. The field is 96x96 and does not wrap: it covers the card's action envelope (`rippleWindow`, at least 192 units), its outer 12 cells absorb a wave, and past them the sheet shows no ripples at all. A field with nothing in it stops stepping. The body is measured against a surface no camera moves, 10px below a stance point at every elevation, so the bodies and their ripples are the same at any camera, and only the drawing moves: the ripple field slides to where the leaning sprite plane crosses the sheet. A thrown prop is a real object, so it meets the sheet where it is drawn, and it stamps back by that slide to splash under itself. A ledge is a box in the shader (`posts`, up to `MAX_WATER_POSTS` per scene), so its foam is a contour of the distance to that box and takes its corners, thick at the wall with a second line lapping out with the swell. It goes down to the seabed: each pixel's refracted ray stops at the box if it reaches it first (`postHit`), so the wall carries on below the surface, or the top where a post stands under it, lit from the blocks' fixed direction and tinted by the water it went through, and the bed behind it lies in a soft shadow, the footprint swept along the sun (`postShadow`). The ripples treat it as a wall that reflects them (`ripples.wall`). Water is created the first time a card is drawn in it, and the floor, grid and contact shadows it covers come back when it is turned off. Repeat stills the surface.
- Controls: play/pause, **Repeat** (starts every card's action over), **Step 1 tick** (pauses and advances the demo clock by 1/60 s, the unit SEQ delays are written in), speed 1X / 0.5X / 0.25X / 0.1X, camera step 0-7 or orbit, elevation (game lowest / initial / highest), **Tint layers** (colors each mesh by renderOrder: far behind, behind, body, head/over, front, face), **In water** (see above; the water meaning of F(n + 182) is inferred from the data, `documentation/shp_frame_to_spritesheet_tile/AGENTS.md`) and, beside it, **Physics body** (`debug/physics-body-mesh.js`), on or off the water: every body and thrown prop the scene measures, over the sprite with its own depth so the part under the sheet shows, orange above the water, violet under it (`BODY_TINTS`, which share no color with the layer tints), with a white line where the surface crosses it. The body is clear in the middle and solid at the edge, so the sprite reads through it, and the status line starts with its frame, its volume and how much of it is under water. Because the sprite is drawn on a leaning plane, the body's height axis leans the same way, which keeps it on the sprite at every elevation.
- Each card is its title + SEQ header, the viewport, one live status line (state, frame, lunge / hover / height, pattern, step) and the timeline strip: the action's frames sized by tick (`timeline.ticks` in the catalog), `seq` events on the top edge (blue: opcodes read from TYPE1.SEQ, checked by `tests/sprite-animation-demos.test.js`), `engine` events on the bottom edge (magenta: the moment the behavior acts) and a black playhead on the demo clock. Where the two edges disagree the strip shows it: the Unarmed hit sits on PostGenericAttack (tick 18), the engine's punch check (tick 12) runs before Bow's PlaySound (tick 26), the marble leaves on the PlaySound of SEQ 206/207 (tick 22), and the stone leaves on PlaySound (tick 26) while the SEQ's PostGenericAttack (tick 32) has no engine counterpart because the stone damages on contact (`documentation/AGENTS.md` "Engine Differences From the Original Tactical RPG"). Header, status and strip are fixed-height (ellipsis when too long), because the shared canvas measures every `.stageSlot` against the grid and a card that changed height would move the viewports.

## Components

| Component | Purpose |
|-----------|---------|
| `SpriteSlice` | Single cell via CSS `background-position` |
| `AnimationFrames` | Static filmstrip of every composited frame of a sequence, above its `AnimatedSequence` |
| `AnimatedSequence` | Frame cycler with flat or variable delays, timeline scrubber, filmstrip |
| `PositionedSpriteSlice` | Absolute-positioned overlay in composite container |
| `AnchoredStage` | Fixed 50x54 px stage on light grey, so transparent pixels show (plus `headroom` rows for Jump Up, the one sequence that rises past it) centered on the unit origin (canvas x 53, stance row 103) with a ground line; places each composite at its body `anchor` (mirrored about the origin for reversed patterns) plus an optional SEQ `move` (`du` screen-down, `fb` screen-horizontal toward the sprite's facing), so frames move exactly as the game draws them (walk bob, lunges, kneels, hop) |
| `AnchoredTile` | Single-tile pose (idle, kneel, dead) at its anchor on the stage |
| `SpriteSheetSection` | Card wrapper with header |
| `SpriteSheetSectionRow` | Horizontal flex for side-by-side patterns |
| `SpriteAnimationDemos` | ThreeJS Demos section (`components/SpriteAnimationDemos.tsx`) |
| `SequenceLibrary` / `SequenceFrame` | Sequence Library section: one SHP frame on the stage, tile by tile, with the head, hand and waist-to-feet highlights, for the selected pair |
| `SequenceModeSection` | One `SEQUENCE_MODES` row: `SpriteSheetTableSequence`, then `sequencePatterns` per pattern through `SequenceFrame` |
| `SpriteSequencePicker` | Sequence selector of the Sequence Library section (`components/SpriteSequencePicker.tsx`). A select and the buttons beside it share `--control-height`, because a native select ignores the inherited line height, and they center their text with `--control-select-text-inset` and `--control-text-inset`, because Nix8810's capitals sit above the middle of its line |

## Key Files

| File | Purpose |
|------|---------|
| `pages/index.tsx` | Route entry, all section rendering |
| `pages/SpriteTest.module.css` | Layout and styling |
| `pages/api/[...route].ts` | The HTTP adapter for the web service, not part of this route (`three-isometric-engine/web/AGENTS.md`) |
| `components/MainLayout.jsx`, `components/MainSidebar.jsx` | Sticky numbered sidebar beside the page |
| `components/SpriteSheetTables.tsx` | Pipeline trace tables |
| `components/SpriteAnimationDemos.tsx` / `.module.css` | ThreeJS Demos section |
| `debug/sprite-animation-demos.js` / `debug/sprite-animation-demo-catalog.js` | Demo stage / catalog |
| `debug/water-simulation.js` / `debug/water-surface.js` | The swell and ripples, with no three.js / the sheet, its shader and what each body does to it |
| `debug/physics-body-mesh.js` | What the Physics body control draws, one closed mesh per SHP frame, built when first drawn |
| `physics/sprite-body.js` | The body a sprite gives a unit (`physics/AGENTS.md`) |
| `debug/damage-counter.js` | The digits and the number a hit shows |
| `debug/marble-material.js` | The two marbles, neither with a `map`, because a mesh with a map counts as a sprite plane in the demos and their tests. White marble of the ledges: a 2048px tileable texture baked once per stage from seeded noise in a fragment shader (about 40ms, where the same noise in JavaScript took seconds on the main thread), so no image file, read along one slanted axis so veins run across every edge. It bakes again when a lost WebGL context comes back. Black marble of the thrown orbs: `marble-black.gif`, read from the orb's own position along its three axes, so it has no pole or seam, and it fades with `uniforms.opacity` |
| `weapons/` | The generated bow and cloth bag, the shared projectile math, the models Set lends, the cards that hold a marble and the mythril they wear (`weapons/AGENTS.md`) |
| `public/static/models/set-items.glb` | Set's held items, baked by `scripts/bake-set-items` |
| `public/static/social.jpg` | The page's share image |
| `public/static/favicon-*.png`, `favicon.ico` | The green marble icon, which `pages/_document.tsx` links |
| `constants/sprites/` | Cell + assembly source data, every SHP frame and SEQ sequence (`shp-frames.js`, `seq-library.js`) |
| `public/static/textures/a1.png` | The sheet (4-bit indexed PNG, 15 colors and one transparent entry, transparent outside `SPRITE_CELLS`) |
| `public/static/textures/marble-black.gif` | 128x128 black marble of the thrown orbs |

## Design

`global.css` owns the tokens. White ground, grey borders (`--theme-border`), black text; a pressed control or the active frame inverts to white text on black. Sprites sit on light grey (`--theme-background-subdued`) wherever they are drawn in CSS, mirrored or not, so transparency shows. Geist Serif carries reading text at one size, 16px, and Nix8810 M16 carries labels, data and controls, always uppercase, at 14px or at 12px where a label sits inside a tile or a narrow column. The controls of a group touch, with one border between options: each is pulled 1px over the one before it and the row above it, and a pressed or focused control sits on top so its border shows. The sidebar has a 2px scrollbar: `::-webkit-scrollbar` in Chrome and Safari, `scrollbar-width: thin` only where that pseudo-element is missing, because Chrome drops it once the standard properties are set. The stage in `debug/sprite-animation-demos.js` follows the same palette: a light grey floor with no texture under a plain grid, white marble ledges and black marble orbs. In water the floor gives way to clear aqua shallows, the one place the stage leaves the palette, with the weapons' mythril, the arrow's tan shaft and red vanes and the gun streak's red tip. `BODY_TINTS` in the catalog colors the physics body and its legend, under the layer tint legend.
