# LANE HANDOVER LOG

Coordination file between lanes (Opus 4.8 execution sessions) and the reviewer
(Fable). **Append-only** — never edit or delete previous blocks. Newest block at the
BOTTOM. Push after every session so the reviewer can see state remotely.

## Protocol

1. Before starting work: read this whole file + your lane file in `lanes/` +
   `docs/CONTRACTS.md`. Check for a REVIEW block addressed to your lane — it
   overrides your lane file where they conflict.
2. Work only inside your lane's owned directories (see BUILD_PLAN.md §2).
3. End of session: append a SESSION block (template below), commit, push your
   `lane/<name>` branch.
4. Never merge to main — the reviewer does that.
5. Blocked on a contract ambiguity? Don't stall and don't silently change the
   contract: stub locally with `// TODO(contract):`, log a CONTRACT CHANGE REQUEST
   below, keep moving.

## SESSION block template

```markdown
---
### SESSION — <LANE-NAME> — <date> <time>
**Branch/commits:** lane/<name> @ <short-sha>..<short-sha>
**Done:** bullet list of what actually works now (be honest — "renders but untested" ≠ "done")
**How to see it:** exact command + what to click (e.g. `npm run dev` → press D → deny 3 patrons)
**Tests:** X passing / Y failing (name the failures)
**Broke / known-wonky:** anything you left imperfect
**Contract change requests:** none | numbered list with justification
**Questions for reviewer:** none | numbered list
**Next session should:** bullet list
```

## REVIEW block template (reviewer only)

```markdown
---
### REVIEW — Fable — <date>
**Reviewed:** <lanes/commits>
**Merged to main:** <yes/no + sha>
**Contract decisions:** rulings on any open change requests
**Instructions for <LANE>:** what to do next / what to fix first
```

---

*(log starts below)*

---
### SESSION — LANE-0 (executed by Fable, reviewer) — 2026-07-19 16:30
**Branch/commits:** main (reviewer executed Phase 0 directly; no lane branch)
**Done:**
- Vite + strict-TS + Phaser 3.90 + Vitest + ESLint scaffold; `Math.random` banned by lint rule.
- All CONTRACTS.md types verbatim in `src/data/types.ts`.
- `core/`: SeededRNG (mulberry32, named streams), typed EventBus (snapshot-emit
  semantics), GameClock (real-ms driven, `ageOn` birthday math incl. leap-day),
  Meters (single-writer, hype multiplies positive vibe only, clicker drift),
  StubBeatClock (128 BPM), versioned corrupt-safe localStorage save.
- `data/`: outfit vocabulary (+PALETTE), 9 archetypes, 10 contraband items,
  17 Dazza texts (8 rule-bearing with ruleIds).
- `patrons/`: generator (fake-ID tell system, DOBs clustered near the 18
  boundary, late-arrival drunk drift), pure `dollPlan` + Phaser `renderDoll`
  (3 poses, drunk tells baked), regulars' memory store.
- `rules/drunk.ts`: drunkStage thresholds.
- ParadeScene demo: rainy street, kebab-shop + venue dressing, 24-walker parade
  with ID cards rendered from the same data, SPACE reseed / D toggle cards.
- 49 tests green; lint/build/test gate clean.
**How to see it:** `npm run dev` → http://localhost:5199 (in the Claude preview
pane, drive frames per CLAUDE.md "Dev notes"). Watch for an `almost18` label
walking under an ID card claiming 18+ — that's the whole game in one image.
**Tests:** 49 passing / 0 failing.
**Broke / known-wonky:**
- ID cards overlap when the footpath is crowded — cosmetic, parade-only.
- `window.game` debug handle exposed in main.ts (deliberate, for pane-driving).
- Spec deviations: doll renderer split into `dollPlan.ts` (pure) + `doll.ts`
  (Phaser) — better than spec'd, plans are pixel-testable without a GPU; added
  a 5th fake-ID tell (expired real card, no boolean tells); no MainMenu scene
  (Boot → Parade direct) — menu is Phase 2 shell work.
**Contract change requests:** none (contracts implemented as written).
**Questions for reviewer:** n/a (self-reviewed).
**Next session should:** Phase 1 lanes are OPEN — see REVIEW block below.

---
### REVIEW — Fable — 2026-07-19
**Reviewed:** Phase 0 (self-executed; verified via test gate + in-browser parade).
**Merged to main:** yes (direct).
**Contract decisions:** CONTRACTS.md now frozen. Extension requests via this file.
**Instructions for Phase 1 (LANE-DOOR, LANE-FLOOR, LANE-JUICE):**
1. Branch from current main into `lane/door` / `lane/floor` / `lane/juice`
   (worktrees if sharing a machine). Read your lane file's new
   "Phase-0 reality" section FIRST — it corrects the original spec where the
   landed code differs.
2. LANE-DOOR is the critical path; if only one Opus session is available, run
   DOOR first, JUICE second, FLOOR third.
3. Every session ends with the gate + a SESSION block here + a push. Fable
   reviews on request ("review the lanes").

---
### SESSION — LANE-DOOR — 2026-07-19 18:50
**Branch/commits:** lane/door @ 9805ff8..(this commit) — branched from main after Phase 0,
worked in worktree `../not-tonight-door`.

**Done** (all verified by playing it, not just by tests):
- **Full Door-only night is playable start→summary.** `npm run dev` boots straight into
  9:00 PM, patrons arrive on a curve, you work the rope, rule on them, Dazza texts land,
  meters move, 3:00 AM → summary. Both fail states reachable.
- **Rules engine** (`src/rules/`, pure, no Phaser):
  - `dressCode.ts` — 8 rules. `text`/`activeFrom` are READ OUT of `data/strings/dazza.ts`
    by ruleId rather than retyped, and the module throws at load if a rule and its
    announcing text ever disagree, so the card can't drift from the message.
  - `idCheck.ts` — all the ID date maths (`idVerdict`, `idDisplayLines`). Builds on
    `ageOn`, does not reimplement it. Catches the 5th tell (expired-but-otherwise-real).
  - `judge.ts` — the single scoring function + `JUDGE_TUNING` (every magic number named).
  - `sobriety.ts` — challenge content + the drunk-performance model.
  - `doorTypes.ts` — this lane's contract EXTENSIONS (see change requests below).
- **Scene layer** (`src/scenes/door/`): `NightScene` (thin night shell — clock, meters,
  deferred consequences, fail states, marked `TODO(integration)`), `DoorScene` (tableau +
  desk + all input), `QueueManager` + `arrivalCurve` (pure, no Phaser), `PatronUpView`
  (×3 doll, 5 hover zones), `IdCardView` (tray thumbnail + half-screen zoom),
  `PhoneWidget` (buzz, preview, scrollback thread), `DressCodeCard` (NEW badges),
  `SobrietyModal`, `StampFx` (marked `TODO(juice)`), `NightSummaryScene`, `inspect.ts`,
  `dazzaSchedule.ts`, `ui.ts`.
- **Tests: 49 → 229** (+180). Every judge branch, every ID boundary (18th birthday
  tonight, leap-day DOB, expiry today vs tomorrow), every dress-code predicate, the queue
  economy, the Dazza schedule, and the inspection vocabulary.

**How to see it:** `npm run dev` → http://localhost:5199.
Click the rope (or NEXT!) → patron steps up ×3 → **hover their shoes/torso/face** for
tooltips → click the ID in the tray to zoom it → ADMIT or NOT TONIGHT. Stamp someone to
see the signature interaction. Wait ~30 in-game min for Dazza's first rule text.
To fail on purpose: deny everyone (aggro riots by ~10:30 PM), or admit everyone (three
heat strikes, licence gone at 3 AM).
In the Claude preview pane, drive frames per CLAUDE.md "Dev notes" — `document.hidden`
freezes the Phaser loop at frame 0 there. **Drive a whole run inside ONE `javascript_exec`
call**: splitting the frame stepping across calls hands Phaser a multi-second delta and
its tweens/timers land in a state that looks like a game bug but isn't. Cost me an hour.

**Tests:** 229 passing / 0 failing.

**Bugs found by playing it that the test gate could never have caught** (all fixed):
1. Game opened on ~45 real seconds of empty street — the 9 PM arrival rate was honest and
   unplayable. Raised the curve, capped the max gap, and the QueueManager now seeds one
   patron at construction so somebody is always at the rope on the first frame.
2. `noLogos` fired on 47.6% of patrons, but `dollPlan.ts` only draws a logo for the TOP
   slot — ~28% of all patrons were deniable for a logo that was never on screen. Narrowed
   to visibly-rendered slots (see CCR-3). Dazza's own text says "no *visible* logos".
3. **One patron could be ruled up to 11 times.** Game state was being mutated inside the
   stamp's tween callback; if the tween didn't land, the patron stayed at a rope the
   player could keep scoring. All state now settles synchronously in `rule()` and the
   tweens only move pixels. This class of bug is why `busy` also has a token-guarded
   watchdog.
4. A correctly-played night still rioted at 1:37 AM. Aggro relief only fired on a
   completely empty street, which never happens during the slam. Relief now scales with
   queue slack — the pressure is "keep it moving", not "never deny".
5. Admit-everything logged **40** heat strikes (one per patron over capacity).
   CONTRACTS.md §3 says heatStrikes is "run-scoped, max 3"; now enforced, and a persistent
   breach is one write-up rather than one per patron.
6. One stubborn regular reappeared 6 times and ate a third of the night's denials.
   Comebacks capped at 2.
7. Dazza sent the identical lap text 5 times in a row. Seven phrasings now, picked by the
   same roll that sets the delay.
8. Determinism leak: denying a regular drew from the `arrivals` stream, so one player
   decision reshuffled every remaining arrival time. Moved to its own `comebacks` stream
   (CONTRACTS.md §6 exists precisely to stop this).
9. ID card modal covered the patron, making photo-vs-face impossible. Moved right.
   The genuine-hologram reference swatch now sits on the same cream backing as the card —
   a translucent teal reads as a different colour over dark laminate than over card stock,
   so the reference was lying.
10. Phone preview text spilled off the screen onto the desk; strike reasons on the summary
    ran off the right edge of the canvas. Both clamped.

**Broke / known-wonky:**
- `src/main.ts` is the one file I touched outside my lane — 8 lines, to register the three
  door scenes and boot into `Night` instead of `Parade`. Unavoidable for the "npm run dev
  → playable night" deliverable. Boot/Parade are still registered; `game.scene.start('Parade')`
  from the console still works. Flagging it explicitly for the merge.
- `src/data/strings/door.ts` is a NEW file in `data/strings/` — CONTRACTS.md §7 says Aussie
  strings live there, and it adds no types, but `data/` is nominally frozen-ish. Move it if
  you'd rather.
- With placeholder art, `noThongsSinglets` / `noWhiteSneakers` / `noBlazers` are not
  distinguishable by silhouette (a thong and a boot are both a 3×4 rect). They ARE
  readable via the hover tooltips, which is why I built the inspection surface before the
  stamp. It costs the player time, which costs aggro — that tradeoff is fine, arguably
  good. It stops being a compromise at the v0.4 art pass (CCR-3).
- Vibe pins at 100 for a competent player by ~midnight, so late-night vibe stops being a
  live pressure. Aggro and heat still are. Worth a tuning pass in Phase 2; I did not want
  to over-tune against a bot that plays with perfect information and zero deliberation
  time. A human is much slower and will not see these numbers.
- `NightScene` renders nothing and `DoorScene` draws the HUD. Deliberate, but it means the
  HUD moves to wherever Phase 2 puts the shell.
- No audio (LANE-JUICE). `StubBeatClock` runs and is consumed for nothing yet — no rhythm
  flourish landed; the stamp is tween-timed, not beat-timed.

**Contract change requests:**
1. **CCR-1 — `PatronFlags.handHolding?: boolean`.** Needed by `noHandHolders`, which is one
   of the 8 already-written Dazza rule texts and otherwise has no data to predicate on.
   Implemented as a `declare module` interface augmentation in `src/rules/doorTypes.ts`, so
   `src/data/types.ts` is byte-identical to main. QueueManager sets it on couples and
   DoorScene draws a linked-hands pixel between them so the rule is visible, not psychic.
   Request: fold the field into `types.ts` properly at integration and delete the augmentation.
2. **CCR-2 — `VerdictOutcome.hypeDelta?: number`.** Design §3.1 gives rope theatre a hype
   reward but `VerdictOutcome` has no hype channel, so `judge()` cannot express the 'wait'
   verdict's payoff. Same augmentation pattern, in `src/rules/judge.ts`. Same request.
3. **CCR-3 — renderer request, for LANE-0/art rather than contracts.** `dollPlan.ts` draws
   a logo rect for the TOP slot only, and gives thongs/boots, blazer/bomber, and
   sneaker/heel identical geometry. Four dress-code rules are therefore silhouette-invisible
   and I narrowed `noLogos` to compensate (`LOGO_VISIBLE_SLOTS` in `dressCode.ts`, one
   constant, widen it when the art lands). Requesting per-slot logo marks and distinct
   shoe/outer silhouettes in the v0.4 art pass.

**Questions for reviewer:**
1. Is the `declare module` augmentation pattern acceptable as the standing mechanism for
   additive contract extensions, or do you want every extension to go through you and land
   in `types.ts`? It kept the frozen files clean and I'd use it again, but it is a
   precedent and two other lanes will copy it.
2. `src/main.ts` and `src/data/strings/door.ts` — ruling on ownership, please. If either is
   off-limits I'll move the strings into `scenes/door/` and hand you a patch for main.ts.
3. Should heat strikes end the night immediately at 3, or accumulate to the summary as
   they do now? Design §2 says "3 strikes = licence pulled, run over" — I read that as
   run-scoped, so the night still finishes and the summary declares it. Say the word and
   it becomes a third fail state.
4. Vibe pinning at 100 (above) — worth retuning now, or leave it until there is a real
   playtester rather than a bot?

**Next session should:**
- Take the review findings from the four-lens pass (lane discipline, scene lifecycle,
  playability, balance) — I ran it but landed only what I could verify in-session.
- Swap `StampFx` for `ui/Stamp` + SFX once LANE-JUICE lands; hook a beat flourish to
  `beat:tick` (currently consumed by nothing).
- Guest-list clipboard and the UV pass-out stamp are specced in design §3.1 and NOT built —
  they were the right things to cut to get a full playable night.
- Sobriety line-walk (the third challenge) is still a stretch item; alphabet + riddle ship.
- Phase 2 integration should lift clock/meters/fail-state ownership out of `NightScene`
  into the real night-flow machine and replace the temporary summary.

---
### SESSION — LANE-DOOR (review-fix pass) — 2026-07-19 19:05
**Branch/commits:** lane/door @ 30162d3..(this commit)

Ran a four-lens adversarial review over the lane (discipline / scene lifecycle /
playability / balance) and landed the findings I could verify in-session.

**Fixed:**
1. **The game was answering its own question.** `DoorScene` lit up every rule a patron
   broke on the dress-code card the moment they stepped up. Optimal play was "read the red
   line", which deletes the inspection loop the entire scene exists for — all 8 dress-code
   rules were free. The highlight now fires AFTER the ruling, for ~2s, as feedback.
   This is the single most important fix in this pass.
2. **Tonight's date appeared nowhere in the game.** The card showed `04/12/2000` and the
   HUD showed `11:42 PM`; the player was asked to subtract a birthdate from a date they
   were never given. `underage` is the tell that files a heat strike and costs the
   licence, and it was pure guesswork. Now stamped on the card beside the DOB and under
   the HUD clock. Every boundary case `idCheck.ts` was carefully written for
   (`turnsEighteenTonight`, leap-day DOB) is finally reachable by a player.
3. **Determinism, again, worse than the one I already fixed.** In `spawnArrival` the
   partner's `generatePatron` call sat behind `if (enqueued)`, and enqueue fails at
   `maxQueue` — which is player-controlled. From the first overflow onward every
   remaining patron of the night differed. All draws are now unconditional. Same class as
   the comeback-stream bug; I fixed it for `couples` and left it in `patrons`.
4. **You could rule straight through the open sobriety modal.** Its backdrop was not
   interactive, so ADMIT/NOT TONIGHT stayed live underneath. Now eats clicks.
5. **Tooltips read as broken text** — `navy a blazer`, `white a singlet`, `cream a bucket
   hat`. Articles were baked into the noun tables and then colour-prefixed. Since the
   tooltips are the ONLY detection channel for four rules under placeholder art, the one
   system the player has to trust looked broken. Nouns are bare now.
6. `bumbag` and `chain` are drawn on the torso/waist but were described only in the head
   zone — hovering the visible item told you about a jacket. Both body zones now name them.
7. `NightScene.endNight` pushed to `state.heatStrikes` directly, bypassing `core/meters.ts`
   fifty lines after correctly emitting `heat:strike`. Now emits. CONTRACTS.md §4.
8. Dropped the one `as never` in the diff (`DoorScene`'s `dazza:text` handler) for an
   honest type guard — see CCR-4.
9. Test quality: `tests/inspect.test.ts` proved its vocabulary coverage by string-comparing
   output against the raw type id, which silently stopped working once a legitimate word
   ('skirt', 'cap') matched its own id. Replaced with an exact `hasWordFor` check.

**Tests:** 231 passing / 0 failing (+2). Re-verified in-browser after the changes:
competent play still runs to 3:00 AM; deny-everything still riots (aggro 100 @ 10:42 PM);
one patron can still only be ruled once, plus up to 2 regular comebacks.

**New contract change request:**
4. **CCR-4 — `dazza:text` payload should carry the lane's rule type.** `EventMap` types it
   `rule?: DressCodeRule`, so a rule this lane emits comes back without the `short` card
   label the lane added to it. Currently narrowed with a runtime type guard in
   `DoorScene`. Either widen the payload or accept the guard.

**Reviewer findings I did NOT land, with reasons:**
- **The economy is slack and dawdling is dominant.** Measured over 5 seeds: a competent
  night pours 749–1115 raw vibe into 50 points of headroom (≈94% discarded by the clamp),
  vibe pins at 100 by ~10:00 PM and stays there 81–86% of the night, and aggro peaks at
  5/100 because relief refunds ~10:1. `hype` is a pure ratchet — nothing in `src/` ever
  emits a negative hype delta — so stalling is paid twice (vibe multiplier + cash bonus)
  and a player who plays *slower* earns 24% more at identical accuracy. Recommended:
  `denyViolatorVibe` 6→1, `admitCleanVibe` 3→1, `QUEUE_TUNING.calmPerSec` 0.9→0.25.
  I did not apply these: it is a whole-economy retune, the measurements come from a bot
  with perfect information and zero deliberation time, and I would rather you tune it
  against a human than have me chase a number I can't feel. Flagging it as the top item
  for the next pass.
- **The queue never actually backs up.** Player throughput ≈0.83 arrivals/in-game-min
  against a curve that peaks at 0.42 — the "10:30–12:30 slam" is currently fiction
  (measured avg queue 0.1–0.2, max 3–5 against 8 visible slots). Fixing it means roughly
  doubling the curve to ~170 arrivals a night, which is a big feel change; wants a human
  playtest first.
- **`noSongRequesters` (activeFrom 240) and `noHandHolders` (280) are nearly decorative** —
  they arrive after 84% and 93% of the night's patrons have been served, and fire ~1.3 and
  ~0 times respectively. Their `fromMin` values come from `data/strings/dazza.ts`, which
  is not mine to edit. Suggest 240→~105 and 280→~135.
- **`noHandHolders` is still unfair even when it fires.** The linked-hands pixel only draws
  between adjacent QUEUE sprites; once a patron steps up to the rope there is no partner
  drawn and no tooltip mentions it, so at the moment of judgement the tell is invisible.
  CCR-1's justification ("rendered as a linked-hands pixel ... so the rule stays one the
  player can SEE") overclaims. Either draw the partner at the rope, add it to
  `describeZone`, or cut the rule.
- **Intoxication and contraband are worth zero points.** `judge()` reads neither. Denying
  a maggot is scored as denying a clean punter (+7 aggro), and a pat-down that finds a
  baggie changes nothing. The drunk channel is the most legible signal the game has — sway
  tween, bloodshot pixels, a whole sobriety minigame — and it does not score. This is the
  biggest *design* gap in the lane and I'd want your ruling on the intended numbers before
  inventing them.
- **The clicker is inert.** `clickerShown` is read only for a text colour and one summary
  row; `judge` uses the true `insideCount` for capacity. The brief calls the divergence
  "the game", so it needs a consequence — presumably the inspector reads the clicker, not
  reality, which is Phase-2 inspector work.
- Minor, unlanded: the ID modal's shade dims the desk hologram swatch but not the card's
  (the teal/magenta hue gap carries it today, but a subtler fake colour would break);
  `peelingLaminate` is cream-on-cream and near-invisible; `photoMismatch` degrades to a 1/6
  coin flip for hatted patrons; three player-facing strings are still inline in `judge.ts`
  against CONTRACTS.md §7, one of which ("Good story for them one day") reads as narrator
  rather than the game's voice.
### SESSION — LANE-FLOOR — 2026-07-19 18:50
**Branch/commits:** lane/floor @ 9805ff8..(this commit) — branched from main, worktree `../not-tonight-floor`
**Done:**
- `FloorDemoScene` (menu key **F** from Parade): 80x45-tile venue (1280x720, 2x screen,
  camera-follow), 44-patron crowd sim, flashlight cone, UV mode, all three v0.2
  infractions + the stamp check, playable end-to-end.
- Pure logic, all tested: `venueMap` (programmatic layout + anchors + stalls),
  `cone` (vision maths), `player` (heavy accel + wall slide), `crowdSim`
  (archetype-weighted anchor wandering, clock-driven intoxication drift, beat-driven
  dance bob, stall double-occupancy, escort-out), `bangJudge` (nearest-beat-boundary
  timing), `escalation` (5x3 cut-off matrix), `patDown` (pocket layout + scoring).
- Phaser layer: `FloorView` (baked tile texture, silhouette/detail split with a
  cone GeometryMask, neon glow pools, UV stamp dots) + three overlays
  (`CutOffOverlay`, `StallOverlay`, `PatDownOverlay`).
- All floor dialogue in `src/scenes/floor/strings.ts` (see deviation #1).
**How to see it:** `npm run dev` → press **F** at the Parade → WASD patrol, **TAB** UV,
**E** interact, **SPACE** bang, **ESC** cancel, **R** reseed. Walk into the dark until
the torch finds someone. In Claude's preview pane drive frames per CLAUDE.md.
**Tests:** 169 passing / 0 failing (49 pre-existing + 120 new floor tests).
**Verified in-browser, not just green:** cut-off (correct rung ejects, one rung too
soft on a maggot lets them stagger free), escort walks to the exit and logs an
ejection, stall busts on 3 on-beat bangs and *never* busts on 8 off-beat ones,
pat-down bins an item (+2 vibe, right flavour line) and leaks it on timeout (-1.5),
UV shows the stamp glow and marches an unstamped patron out with a Kayden radio line.
**Broke / known-wonky:**
- **Two bugs the test suite could not catch, both found by running it and both fixed:**
  1. *Ambient meter collapse.* The bump penalty had a per-agent cooldown but no
     sim-wide bound, so leak rate scaled with crowd size — an unattended venue
     pinned vibe to 0 and aggro to 100 by ~1AM before the player touched anything.
     Added a sim-wide gate (`BUMP_GATE_MS`); an unattended night now drifts
     50→27 vibe / 0→34 aggro. Regression test added.
  2. *Frozen beat behind modals.* `beat.update()` sat below the overlay early-return
     in the scene loop, so opening the stall minigame stopped the beat clock and the
     rhythm game could never receive a tick. Clock + beat now run regardless of overlays.
- Dwell times raised (2.5-9s → 7-22s): at the old values ~68% of the crowd was
  permanently in transit and the bar/dance floor read as empty.
- Tile palette + darkness alpha retuned; at the original values the venue
  multiplied down to near-black and you could not tell a dance floor from a wall.
- Light2D dropped in `FloorView` (documented in-file) — the pipeline swap fights the
  cone GeometryMask, and the mask is the actual gameplay. The look is carried by a
  darkness sheet + additive cone + neon glow pools instead.
- `GeometryMask` on a container is a WebGL-first path; fine under `Phaser.AUTO`,
  would misbehave if anything ever forced the Canvas renderer.
- The 16x16 top-down doll reads as a coloured ring from above (dark hair over
  outer-layer shoulders). Correct per Phase 0's `dollPlan`, just noting it.
- Stall interiors are deliberately sealed (`stall` walkable, `stallDoor` not), so
  patrons are moved in/out via `StallDef.inside`/`.door` rather than pathed.
**Contract change requests:**
1. **`incident:log` event.** `core` exposes no incident API and `EventMap` is frozen,
   so `FloorDemoScene` appends to `NightState.incidents` directly (marked
   `// TODO(contract):`). Three lanes writing that array directly is exactly the
   shared-mutable-state fight the bus discipline exists to prevent, and Phase 3's
   inspector/audit needs a single writer. Requesting
   `'incident:log': IncidentRecord` plus a handler in `core/meters.ts`.
2. **Aggro has no decay.** `Meters` only ratchets aggro upward, so it is a one-way
   trip to 100 over a long night. Ambient pressure is now bounded (above), but a slow
   downward drift on a quiet floor would make the meter a *state* rather than a
   countdown. Flagging as a design/contract question, not patching it unilaterally.
**Questions for reviewer:**
1. **Floor strings placement.** CONTRACTS.md §7 says Aussie dialogue lives in
   `src/data/strings/`, but `data/` is LANE-0's and lanes commit only in their own
   dirs. Parked in `src/scenes/floor/strings.ts`. Move it to
   `src/data/strings/floor.ts` on your word — it is a pure file move.
2. **`.gitignore` gap (affects every lane using a worktree).** It lists
   `node_modules/` with a trailing slash, which does not match a *symlinked*
   `node_modules` — worktrees that symlink it show `?? node_modules` as untracked.
   I staged paths explicitly, but a lane running `git add -A` would commit a symlink
   pointing at an absolute path on John's machine. One-char fix (drop the slash);
   left alone since `.gitignore` is not in my lane.
3. Escalation numbers (clean +4 vibe, scene -3/+5 per rung of overreach) are
   deliberately small so aggression never feels rewarded. Confirm the balance intent
   before LANE-DOOR mirrors it at the rope.
**Next session should:**
- Phase 2 integration: replace the self-populating spawner with the real admitted-patron
  list from LANE-DOOR (the scene already only ever consumes `Patron[]`).
- Swap `StubBeatClock` for LANE-JUICE's `TechnoEngine` — `StallOverlay` takes
  `beatIntervalMs` in its constructor and reads `beat:tick` off the bus, so it should
  be a one-line change.
- Fight-brewing infraction (design §3.2) is specced but NOT built — the only v0.2
  floor mechanic still missing.
- Replace `HudStub` with LANE-JUICE's real meter HUD.
### SESSION — LANE-JUICE — 2026-07-19 18:55
**Branch/commits:** lane/juice @ c262ac8..2eb3c94
**Done:**
- `audio/TechnoEngine.ts` — synthesized 128 BPM track (kick / off-beat hats /
  2-bar 16th bassline / 8-bar pad), look-ahead scheduler (25ms timer, 100ms
  horizon) as the beat authority. Emits `beat:tick {beatIndex, audioTimeMs}`
  with the **scheduled** time, plus `beat:bar` on downbeats. Drop-in replacement
  for `core/StubBeatClock`. Music → lowpass → master; SFX bypass the filter.
- `audio/Sfx.ts` — 10 procedural one-shots + `startRain`/`stopRain`. One shared
  noise buffer; voices disconnect on `ended`.
- `audio/scheduling.ts` + `audio/filterCurve.ts` — pure, node-testable beat grid
  and cutoff-ramp math (the WebAudio classes are thin wrappers over these).
- `ui/` — `Stamp`, `Phone`, `DialogueBox`, `Clicker`, `MeterHud`, plus
  `style.ts` (chunky widget kit) and `typo.ts` (deterministic drunk typos).
- `ui/JuiceDemoScene.ts` — exercises every widget and SFX; log-mapped cutoff
  slider, DOOR/FLOOR toggle routed through the bus, beat pad that flashes when
  `ctx.currentTime` reaches the scheduled time (not on event receipt).
**How to see it:** `npm run dev` → http://localhost:5199/#juice (or press **J**;
**P** returns to the parade). Click once to unlock audio, then: drag MUFFLE to
sweep the filter by hand, hit STEP INSIDE for the door→floor sweep, mash the
SOUNDS column, stamp someone with NOT TONIGHT.
**Tests:** 111 passing / 0 failing (was 49 at Phase 0; +62 this lane).
**Verified by ear:** NOTHING. No audio output available in this environment.
Everything below was logic-checked in a real browser via the exposed engine:
- door→floor sweep ramps 250Hz→18kHz over 600ms, monotonic, exponential
  (t=0.5 lands ~2.1kHz vs the ~9.1kHz a linear ramp gives).
- under a genuinely hidden/throttled tab: 52 consecutive beats, **zero**
  scheduled in the past, 75–98ms lead, max 1 beat scheduled per timer wake.
**John/Fable still need to ear-check:** does the kick read as a kick, is the
bassline musical, is `typeTick` quiet enough at the real mix level (it fires per
character), and does the sweep *feel* like a door rather than a filter.
**Broke / known-wonky:**
- `src/main.ts` is edited (2 lines + a ~12-line demo route). **Outside LANE-JUICE
  ownership** — see request 2 below. Without it the demo scene is unreachable
  and Vite tree-shakes it out entirely.
- `TechnoEngine` has no test file — it needs a real `AudioContext` and the suite
  runs in node. Its scheduling/filter math is tested via the pure modules, but
  the `resync` arithmetic is only reachable through the class. Extracting
  `resyncBeat(grid, nextBeat, nowMs)` into `scheduling.ts` would close that gap.
- `Phone` thread truncates an over-tall newest message rather than scrolling.
- `Stamp` decals are caller-owned now; the demo caps them at 5. The door scene
  must destroy them (or pass `parent`) or they accumulate over a night.
**Contract change requests:**
1. **Add `door:phoneTheatre: { durationMs: number }` to `EventMap`.** Emitted by
   `ui/Phone` when the player closes the phone, carrying how long they stared at
   it while a queue waited — the door scene rewards it with hype. It is a door
   event, not an audio one, so it is outside my namespace. **I did not edit
   `data/types.ts`.** `ui/juiceEvents.ts` declaration-merges it into `EventMap`
   instead: fully typed, zero edits to the frozen file, and folding it into the
   real contract later is a copy-paste. Same file also adds `audio:unlocked` and
   `beat:bar`, which ARE mine per LANE_JUICE.md but are declared together so the
   whole juice surface reads in one place.
2. **Sign-off requested on the `src/main.ts` edit** (scene registration + J/#juice
   route, marked `// TODO(integration)`). Happy to revert it if you would rather
   wire the demo from the Phase 2 shell yourself.
**Questions for reviewer:**
1. `TechnoEngine.unlock()` anchors beat 0 at `currentTime + 60ms` so beat 0 is as
   honest as every other beat. If a consumer assumes beat 0 lands exactly at
   `audio:unlocked.atMs`, that is a 60ms discrepancy — is that acceptable, or
   should `atMs` carry the anchored origin instead?
2. `MeterHud` counts heat strikes from `heat:strike` events it receives. On a
   mid-run scene swap it would restart from 0. Should it take an initial count in
   its options, or read `NightState.heatStrikes.length` at construction?
**Next session should:**
- LANE-DOOR: swap the local stamp for `ui/Stamp` (`// TODO(juice)`), and take
  `MeterHud` in place of `HudStub`.
- Someone with speakers should ear-check the mix and tell me what to retune.
- Consider extracting `resyncBeat` into `scheduling.ts` so the throttled-tab
  resync gets direct test coverage.

---
### REVIEW — Fable — 2026-07-19 (Phase 1 review & integration)
**Reviewed:** lane/door @ e0b5831 (231 tests), lane/floor @ 3eee843 (169),
lane/juice @ f5e1151 (111). All three gates verified green per-branch before merge.
**Merged to main:** yes — door, then floor, then juice; 413 tests green post-merge.
Verified in-browser after integration: Door night boots and plays (called a patron
up, stamped her, meters moved per the judge table), FloorDemo cone/crowd runs (F),
JuiceDemo runs with AUDIO LIVE @ 128 BPM after unlock (J).

**Quality note to all three lanes:** outstanding sessions. The play-found bug
lists (door's 10 + review pass, floor's meter-collapse and frozen-beat catches)
and juice's honest "verified by ear: NOTHING" are exactly what this file is for.

**Contract decisions (all folded into `src/data/types.ts` + CONTRACTS.md §4 by me):**
1. CCR-1 `PatronFlags.handHolding` — ACCEPTED, folded in; doorTypes.ts augmentation removed.
2. CCR-2 `hypeDelta` — ACCEPTED onto `VerdictOutcome` (not just DoorOutcome); judge.ts augmentation removed.
3. CCR-3 (per-slot logos, distinct shoe/outer silhouettes) — ACCEPTED for the
   v0.4 art pass; logged. `LOGO_VISIBLE_SLOTS` stays the guard until then.
4. CCR-4 — resolved by adding optional `short?: string` to `DressCodeRule`.
   Door's runtime guard may stay or simplify; either is fine.
5. JUICE-1 `door:phoneTheatre` + `audio:unlocked` + `beat:bar` — ACCEPTED, folded in;
   `ui/juiceEvents.ts` deleted (its three importers updated).
6. FLOOR-1 `incident:log` — ACCEPTED; `core/meters.ts` is now the single incident
   writer and all three direct `incidents.push` sites (floor demo, DoorScene,
   NightScene deferred) now emit the event instead.
7. FLOOR-2 aggro decay — deferred to the Phase-2 tuning pass, alongside door's
   economy findings (vibe pinning, hype ratchet, queue slack). One tuning pass,
   one owner, against a human player.

**Rulings on questions:**
- `declare module` augmentation: it was the right unblocking move, and both lanes
  executed it cleanly — but it does NOT become standing practice. Contract
  extensions land in `types.ts` at each review, augmentations are deleted on
  folding. One source of truth wins over lane autonomy here.
- `src/main.ts`: now integration-owned (reviewer/Phase-2). Lanes request routing
  changes via handover. Current dev routes: N night · P parade · F floor · J juice,
  same hashes.
- `data/strings/<lane>.ts`: ACCEPTED as the standing convention — new string
  files there are lane-owned, no sign-off needed (floor's moved:
  `src/data/strings/floor.ts`). `types.ts` stays frozen.
- Heat 3-strikes: keep summary-declared for v0.1; Phase 2's shell makes it an
  immediate "licence pulled" cut — logged for integration.
- Vibe-pins-at-100 / economy retune: HELD for a human playtest (John). Door's
  measured numbers are the starting point for that session, not auto-applied.
- Juice beat-0 anchor: `audio:unlocked.atMs` should carry the anchored beat-0
  origin — implement next juice session.
- MeterHud heat count: read `NightState.heatStrikes.length` at construction
  (single source of truth) — implement next juice session.
- Floor escalation balance ("aggression never feels rewarded"): CONFIRMED as
  design intent; door should mirror the shape at the rope.
- `.gitignore` slash: fixed.

**Also applied at integration:** dazza fromMin retunes per door's measurements
(noSongRequesters 240→105, noHandHolders 280→135; two pinned-order tests updated),
floor strings moved to `data/strings/floor.ts`, vite port now respects `$PORT`
(preview autoPort), 413 tests + lint + build green on main.

**Next:** Phase 2 integration is OPEN — one lane, `lanes/` file to follow. Big
rocks: night-flow state machine (door⇄floor interleave + radio pulls), TechnoEngine
replacing StubBeatClock in the night, ui/Stamp + MeterHud replacing door's local
versions, save/progression wiring, then the tuning pass with a human.

---
### SESSION — PHASE-2 INTEGRATION (executed by Fable, reviewer) — 2026-07-19 20:40
**Branch/commits:** main (integration is reviewer-owned per the Phase-1 ruling)
**Done:**
- **The night is whole.** Door and Floor run CONCURRENTLY for the entire night;
  the player toggles by clicking the venue doorway (it glows on radio calls) and
  pressing E at the floor's entry. `NightScene` is the flow machine: it owns
  clock, meters, audio, radio, Kayden's stamp accuracy, and the run save.
- **Kayden.** While you're inside, `DoorScene.kaydenRule()` works the REAL queue
  through the REAL `judge()` — admits 4 in 5, never tests, never clicks the
  clicker, forgets one UV stamp in seven. His mistakes cost actual vibe/heat and
  his unstamped admits are the floor's noStamp supply. Leaving him alone all
  night reliably ends in LICENCE PULLED (verified: 97 admits, 3 audit strikes).
- **Floor in night mode:** `FloorDemoScene` gained a second registration
  ('Floor') that adopts the NightContext — fed by real door admissions with a
  6–14s walk-in, MeterHud (initialHeat per ruling), keeps simulating while
  you're at the rope (maggots ripen → radio pulls), demo mode untouched.
- **Audio in the night:** TechnoEngine + Sfx created by NightScene; unlock on
  first click; StubBeatClock hands over on `audio:unlocked`; the lowpass sweeps
  on every door⇄floor toggle; stamp/clicker/rope/phone/radio SFX wired.
  `audio:unlocked.atMs` now carries the anchored beat-0 origin (juice Q1 ruling).
- **Run progression:** Thu 07-16 → Fri 07-17 → Sat 07-18 at The Royal, night
  seed = runSeed + index*101, heat carried run-scoped via save.ts, third strike
  = immediate LICENCE PULLED cut (new endReason + FAIL_TEXT.licence), run-over
  and week-done both clear the save into a fresh week.
- **Widget swaps:** door deny path now uses ui/Stamp (decal destroyed ~900ms
  post-slam); StampFx.ts deleted.
**How to see it:** `npm run dev` → play. Doorway = step inside; E at entry =
step out. To watch Kayden: step inside and just... stand there. Check INSIDE
count and the incident log fill up. Preview-pane note in CLAUDE.md still applies
(drive frames in ONE javascript_exec call).
**Tests:** 413 passing / 0 failing (no new pure logic — flow wiring; the night
was verified by driving full nights in-browser, including a complete 6-hour
Kayden-only night, the licence-pulled cut, and the Thu→Fri chain with carried
heat).
**Bugs found while verifying (both fixed):**
1. `kaydenRule` emitted `door:verdict` on top of `applyOutcome`'s own emission —
   every Kayden admission spawned TWO pending floor walk-ins. Caught by
   inside-vs-crowd arithmetic, not by tests.
2. Pre-existing: a deferred hit's vibe delta could end the night, and
   `endNight()` reassigns `this.deferred` while `fireDeferred` iterates it →
   crash at scale. Collect-then-process now; surfaced only by simulating a full
   unattended night.
**Broke / known-wonky:**
- Balance: a Kayden-only night ends licence-pulled (intended), but the
  alternation rhythm (how long CAN you stay inside) is untuned — that's the
  John-playtest tuning pass, together with the Phase-1 economy findings.
- Floor overlays (stall/pat-down) still silent — LANE-JUICE2/FLOOR2 item.
- DoorScene keeps its bespoke top-bar meters; MeterHud lives on the floor side
  only. Cosmetic unification optional (LANE_JUICE2 §4).
- `radio:call` maggot trigger only fires while at the door (by design — you can
  hear the floor, you can't see it).
**Contract changes:** none needed — Phase-1 folds covered everything.
**Next:** Phase 3 lanes are OPEN — LANE_CONTENT / LANE_FLOOR2 / LANE_JUICE2
(files in lanes/). All three parallel, worktrees, own branches, usual protocol.
If only one session: CONTENT first (it's the game's heart), FLOOR2, then JUICE2
— except run JUICE2 first if John is available for the ear pass.

---
### SESSION — LANE-CONTENT — 2026-07-19 21:35
**Branch/commits:** lane/content @ 0f7b948..(this commit), worktree `../not-tonight-content`

**Done — all four deliverables land, in priority order:**

1. **Guest list clipboard.** `rules/guestList.ts` (pure) + `door/ClipboardPanel.ts`. The
   desk's left panel is now tabbed **DAZZA'S RULES | GUEST LIST** — there is no spare
   real estate at 640×360, and the tab is honest about the cost: you cannot read the
   dress code and the list at once, so checking a name spends a beat of queue time.
   The mechanic works as designed and is *measured*, not asserted: at the generator's
   actual claim rates an **exact** match is a ~50/50, **near** leans legit (~66%), and
   **none** leans liar (~97%). That posterior test is the module's real deliverable.
   `knowsOwner` patrons now get their own line pool (`OWNER_HINTS`) that drops Terry's
   name — the trap needs a tell that isn't just confidence.
2. **Scripted moral encounters.** `rules/encounters.ts` + `data/strings/encounters.ts`.
   Four, three per night, injected by `QueueManager` ahead of the arrival curve (they are
   people who turned up at a moment, not draws from a distribution). Multi-beat speech at
   the rope. Verified in play: *"seven years ive been coming here. you knocked me back
   once, back in nineteen. fair enough too."*
3. **The Inspector.** `rules/inspector.ts` + `NightScene` wiring. Reads as a boring
   punter; admitted, they watch for 30 clock-min and convert any live breach's DEFERRED
   strike to an IMMEDIATE one. Denied: nothing, ever — paranoia is the tax, and that
   absent branch carries a comment so nobody "fixes" it. Verified in play: a night ended
   LICENCE PULLED at 12:47 AM with two of the three strikes reading *"inspector saw the
   room over licensed capacity"* / *"inspector saw a minor inside the venue"*.
4. **The Incident Report.** `rules/incidentReport.ts` + `door/IncidentReportScene.ts`,
   sitting between the summary and the next shift. 3 questions, one truthful account and
   one or two plausible-deniability ones, shuffled. Filed accounts go to
   `GameState.pastReports` and re-save, so Phase 4 audits what the player *claimed*.
   `NightScene` no longer dumps raw incidents there — a truth log has nothing to catch
   anyone out with.

**Also landed (my Phase-1 finding, now fixable because I own `rules/`):**
- **`judge()` finally reads intoxication and contraband.** Denying a `messy`/`maggot`
  patron, or one carrying severity-3 contraband, is now *justified* rather than scored as
  an arbitrary denial (+7 aggro). Admitting anyone already showing tells schedules a
  ripening hit 18–54 clock-min later with a Dazza line. `loose` is deliberately still a
  judgement call. This closes the "the most legible signal in the game is worth zero
  points" gap from the Phase-1 review, and it was the fix the encounters reviewer needed:
  `quietBeer` had admit dominating deny on BOTH meters, which made the humane choice
  secretly optimal — the one thing §4.3 forbids. Pinned by a test.

**How to see it:** `npm run dev` → play.
- Guest list: wait for someone who says *"i'm on the list"* — the GUEST LIST tab flashes,
  click it, and the clipboard tells you whether the name matches. It never tells you
  whether they are lying.
- Encounters: three a night, spaced 40+ min. They talk in beats and their name is not on
  any list.
- Inspector: admit a `punter` who is secretly the inspector and then breach something
  within 30 minutes. Or just play badly and find out.
- Report: play a night to 3 AM, click through the summary.

**Tests:** 518 passing / 0 failing (413 → 518, +105).

**Full 3-night run played** (careful player: refuse dodgy cards, minors, >0.62 intox, and
any dress breach). All three nights reached 3:00 AM; heat carried 1 → 2 → 2, so the week
was survived with one strike to spare. Encounters fired 3/3/3; guest-list claimants
13/19/13 a night; owner's mates 1–3.
**What it FELT like, honestly:** the door is much richer to *read* now — there is a reason
to look at a person rather than a checklist, and the clipboard genuinely creates doubt.
But **the Phase-1 economy findings are all still true and my content did not change them**:
vibe pinned at 97–98 on every night, aggro sat at 0 all three nights, hype maxed at ×3 every
night. The careful player is never under meter pressure; the only real tension left is heat.
Also, because late-night dress rules make most of the crowd deniable, the careful bot ends
up denying MORE than it admits (70 vs 55, 70 vs 41, 50 vs 39) — the room the player is
protecting stays half empty. That is the tuning pass John is holding, and it is now the
single biggest thing between this and a good game.

**Broke / known-wonky:**
- **The cross-night Kayden contradiction never fired in three nights of play**, because it
  needs a `kayden` incident and Kayden only works the rope while you are on the FLOOR — my
  playthrough never went inside. The path is unit-tested (5 tests) but has never been seen
  in a real game. Someone should play a night from the floor and lie about Kayden.
- Encounter outcome objects are module singletons; I clone `heatStrike` before emitting so
  a run cannot alias one strike, but the rest of the outcome is shared. Fine while nothing
  annotates them.
- `settleId()` in encounters leaves a fake ID's fabricated DOB, so ~1 in 50 scripted
  patrons carries a card whose age disagrees with them. Cosmetic today (nothing reads the
  mismatch); the proper fix needs `nightDate`, which `dress()` isn't given.
- I removed a stale `import '../../rules/doorTypes'` from `QueueManager` — a leftover
  side-effect import for the augmentation you folded in at review.
- One test run failed once and never reproduced across 4 subsequent runs; it coincided with
  the dev server rewriting a file mid-run. Recording it rather than pretending it didn't
  happen.
- Sobriety line-walk (deliverable 5, stretch) NOT built. 1–4 took the session.

**Contract change requests:** none. Everything went through existing events
(`meters:delta`, `heat:strike`, `dazza:text`, `incident:log`) and no `declare module`.

**Questions for reviewer:**
1. `RunInfo` gained a `game: GameState` field so the report form can file into the save and
   persist it. That hands a scene a mutable reference to the run — happy with that, or
   would you rather the report emit and let `NightScene` own the write?
2. The report re-saves after the night's own `saveGame`. Two writes per night. Cheap, but
   if you want one, the night's save needs to move after the paperwork.
3. Encounters are 3-per-night on a 4-entry table, so a week shows the same people every
   night with only the order changing. Worth gating repeats across nights via
   `GameState`, or is a regular being a regular fine?

**Next session should:**
- The tuning pass with John. My run data above is the current baseline; the Phase-1
  measurements still hold.
- Play a floor-side night and exercise the Kayden contradiction.
- Sobriety line-walk if anyone wants it.
- More encounters — the table wants 8–10 so a week does not repeat.
### SESSION — LANE-JUICE2 — 2026-07-19 21:50
**Branch/commits:** lane/juice2 @ d112bda..1a66c60 (worktree `../not-tonight-juice2`)
**Done:**
- **`audio/mix.ts` + `audio/arc.ts`** (new, pure, tested). The track's constants
  are now a live `TrackMix`, and the night has a shape. Same surface serves the
  ear pass, the arc, and the Phase-4 DJ control panel (BUILD_PLAN §4 rule).
- **`TechnoEngine`** drives every voice from the mix × arc scalars. Verified
  behaviour-identical at `DEFAULT_MIX`, constant by constant. Silent voices are
  skipped rather than clamped (9 PM bass builds zero nodes). `setMixValue`
  quantises integer paths so the dump can't export a value nobody heard.
- **`ui/MixDeskScene`** — the ear-pass rig. Solo/mute per voice with a
  per-voice listening prompt, every param on log-mapped sliders (5 pages), an
  arc scrubber, and DUMP MIX → pasteable `TrackMix` literal + clipboard.
  Restores the arc pin and solo state it found, so it can't un-tune its host.
- **`audio/Ambience`** — rain (outside), crowd murmur (inside, level follows the
  room), kebab-shop fluoro hum. See routing + headcount notes below.
- **`Phone`** thread scrolls: visible-window culling, binary-searched range,
  history cap, sticky-bottom, and a "NEW BELOW" cue when a text lands off-screen.
- **`JuiceDemoScene`** hosts all of it; arc pinned at PEAK on entry.
**How to see it:** the demo lives in this worktree, so
`cd ../not-tonight-juice2 && npx vite --port 5311` → http://localhost:5311/#juice
(or J). Click once to unlock audio, then **M** for the mix desk.
**Tests:** 460 passing / 0 failing (was 413; +45 across mix/arc, +2 from fixes).
**Verified by ear:** STILL NOTHING — no audio output in this environment. Logic
verified in a real browser: arc pins/restores across desk open+close, integer
quantisation, crowd headcount arithmetic (120 + 2 admits − 1 ejection = 121 with
the clicker event correctly ignored), zero console errors.
**THE ASK — the ear pass is now a ~10 minute job and I can't do it:**
Open the desk, SOLO each voice in turn, fix what's wrong, hit DUMP MIX, paste me
the literal. It goes straight over `DEFAULT_MIX` in `src/audio/mix.ts`.
`tests/mix.test.ts` pins the current values deliberately, so a retune will fail
that test — that's the tripwire working; update it in the same diff. Specifically
unjudged: does the kick read as a kick through the door filter, is the bassline
musical or just busy, is `typeTick` (fires per character) tolerable, does the
stamp sit right against the music, and does the door→floor sweep feel like a door.
**Broke / known-wonky:**
- `Ambience` rain uses `Sfx`'s own 1.2s/0.9s envelope, so it trails the 600/900ms
  filter sweep by ~300ms. Fixing it properly means a `fadeS` param on
  `Sfx.startRain/stopRain`. Reads fine; noting it rather than hiding it.
- `Ambience` assumes it is the sole owner of rain policy. If a door scene also
  drives `sfx.startRain()` they will fight.
- `MixDesk` is reachable only from JuiceDemo (key M) — see request 1.
- No `TechnoEngine`/`Ambience` test files; both need a real `AudioContext` and
  the suite is node. Their pure math is tested via mix/arc/scheduling/filterCurve.
**Contract change requests:** none. `door:phoneTheatre` was folded in at the
Phase-1 review; the crowd bed derives its headcount from existing events rather
than requesting `capacity.inside` in a payload (reasoning above).
**Requests for integration (main.ts is yours now, per the Phase-1 ruling):**
1. **A global route to the mix desk**, so the ear pass can also happen during a
   REAL night rather than only in the demo. `MixDeskScene` already falls back to
   `getActiveEngine()` when launched without data, so it attaches to whatever
   engine is running — it just needs a key. Suggest **M**, alongside N/P/F/J.
2. Optional: `MeterHud` door-bar variant is still not done (LANE_JUICE2 §4.3,
   explicitly "only if clean"). DoorScene's bespoke meters work; I left them.
**Questions for reviewer:**
1. The arc means the night no longer sounds the same at 9 PM and 1 AM — bass is
   fully out at 9 PM by design. Worth confirming that's wanted before John tunes
   against it, since it changes what "the mix" even means at a given hour.
2. `Ambience` currently has no owner in `NightScene` — I can't wire it there
   (scenes aren't mine). Want it created alongside TechnoEngine at integration?
**Next session should:**
- Run the ear pass with John and land the retuned `DEFAULT_MIX`.
- Wire `Ambience` into `NightScene` (integration-owned).
- LANE-FLOOR2 owns the cut-off overlay call site; `DialogueBox` drunk-typo mode
  is ready for it and needs no widget change — just pass `drunkenness` + `rng`.

---
### SESSION — LANE-FLOOR (round 2) — 2026-07-19 20:56
**Branch/commits:** lane/floor @ 3eee843..(this commit)
**Done:**
- **Fight-brewing infraction built** — the last missing v0.2 floor mechanic (design §3.2).
  Not a modal: two patrons square up, a 7s fuse burns through five escalating shove
  rungs, and you stop it by physically standing between them. `fight.ts` (pure:
  `isBetween` segment maths, `stepFight`, `scoreFight`, eligibility) +
  `fightDirector.ts` (lifecycle, one fight at a time, cooldown, pin/release) +
  a `setFight` tell in `FloorView` + scene wiring. `crowdSim` gained a
  `'squaringUp'` activity that pins an agent, plus `pinForFight`/`releaseFromFight`.
- **Adversarial review of the whole lane** (5 dimensions x ~3k lines), then I
  hand-adjudicated the findings and fixed six real defects — see below.
**How to see it:** `npm run dev` → **F** → patrol. A fight announces itself with a
pulsing marker and shoving silhouettes **visible outside the torch cone** (verified
by screenshot with the beam pointed away) — spotting it is the gameplay. Prompt
reads GET BETWEEN THEM; stand in the gap before the fuse burns.
**Tests:** 225 passing / 0 failing (+56 this round: `fight.test.ts`, `fightDirector.test.ts`).
**Verified in-browser:** fight breaks up (+6 vibe, both released, no heat) and swings
(−12 vibe, +20 aggro, exactly one heat strike, neither marked handled); shove rungs
escalate 0→4; the tell reads with the torch facing away; all four round-1 mechanics
still work after the fixes.

**Balance, measured rather than guessed** — same seed, whole night:
| | idle player | attentive player |
|---|---|---|
| vibe | 0 | 54 |
| aggro | 100 | 45 |
| heat strikes | 5 | 0 |
Doing nothing is ruinous; doing the job holds the room. That gradient is the point.

**Bugs found and fixed this round (all confirmed by reading the code, several in
code I wrote myself last session):**
1. `PatDownOverlay` checked `'Escape'` while the scene routes `'ESCAPE'` (it
   uppercases) — ESC was dead in the pat-down while the on-screen hint promised it.
   The other two overlays handled both spellings, which is why round 1 missed it.
2. The spawn cap and HUD counted `crowd.agents.length`, but CrowdSim deliberately
   keeps departed agents so `find()` works — so **every ejection permanently shrank
   the venue** and the floor drained over the night. Now counts live bodies.
   (Verified: culled 15 → refilled to 44.)
3. `FloorView`'s sprite cache is keyed by `patron.id` and survives `resetRun`, but
   `_resetPatronSerial()` recycles ids from `p0` — so **R** gave the new crowd the
   previous run's dolls, sway and stamps. Added `FloorView.reset()`.
4. `escalation`'s per-incident `resolved` was consumed as a permanent per-patron
   `handled`, so correctly watering a `loose` patron **retired them from all
   tracking for the night while they kept drinking**. The Quiet Messy One could
   never come back around. Now settled per-stage: they re-arm when they get worse.
5. Stall busts marked the **captured** occupant list handled, but the crowd keeps
   simulating behind the modal — unrelated patrons who had wandered off got retired.
   Now re-queries occupancy at resolution.
6. `floor:infractionSpotted` for a stall put a **stall id in the frozen `patronId`
   field** (resolves to nobody), and the dedupe key meant one report per cubicle per
   night. Now reports an occupant, so a fresh pair reports again.
7. Also wired `teardown()` to `Phaser.Scenes.Events.SHUTDOWN` — a `shutdown()`
   *method* is not invoked by Phaser, so the previous version leaked FloorView,
   CrowdSim, overlays and bus listeners every time you left the floor.
**Fight rebalance (found by running it, not by tests):** at a 20s cooldown an idle
night brewed **28 fights, all missed, 28 heat strikes** against a run cap of 3 —
fights swamped every other meter. Cooldown → 150s (≈5/night) and the heat strike is
now `deferred: true`, matching §3.2's "possible Heat" and the contract's
inspection/audit semantics. Two tests updated to match; the cooldown test now derives
its window from the constants so it can't silently pass vacuously again.
**Broke / known-wonky:**
- **My review harness under-reported and I overrode it.** 16 raw findings, and the
  adversarial verify stage confirmed **zero** — I had required both skeptics to fail
  to refute *and* told them to default to "refuted" when uncertain. That combination
  produced a 100% false-negative rate: six of the sixteen were real, including two
  found independently by three reviewers. I adjudicated them by hand instead.
  Flagging because the *pattern* matters for anyone reusing that harness: a
  refute-biased verifier plus unanimity is not a quality bar, it is an off switch.
- Fights burn their fuse through overlays. Deliberate — you can't be in a cubicle
  and between two blokes at once, and ESC always exits — but it means a fight
  starting during a 10s pat-down is a forced choice. Confirm you want that.
- An idle night still pins aggro at 100 by ~1AM. Aggro has no decay (see round 1
  request #2); with that in place the idle curve would breathe rather than saturate.
**Contract change requests:** both from round 1 still open and unchanged —
(1) `incident:log` event + single writer in `core/meters.ts`; (2) aggro decay.
No new ones: the fight reuses the existing `'fight'` infraction kind and `heat:strike`.
**Questions for reviewer:**
1. `patrons/doll.ts` caches textures by key and never releases them, so each reseed
   mints a fresh set with no upper bound (a long session leaks canvases). It is
   LANE-0's file and frozen, so I left it — worth a Phase 2 ticket.
2. Fight numbers (broken +6 / swung −12 vibe, +20 aggro, deferred heat) and the
   150s cooldown are my calls from the measured table above. Confirm before
   LANE-DOOR mirrors any of it.
3. Round 1's questions on floor strings placement and the `.gitignore` symlink gap
   are both still open.
**Next session should:**
- Phase 2 integration: real admitted-patron list from LANE-DOOR, LANE-JUICE's
  `TechnoEngine` for `beat:tick`, real meter HUD.
- All five v0.2 floor infractions are now built — the floor is feature-complete
  against design §3.2.

---
### REVIEW — Fable — 2026-07-19 (Phase 3 review & integration)
**Reviewed:** lane/content @ 37f9892 (518 tests), lane/floor @ 906a366 (225),
lane/juice2 @ 10462eb (460). All gates verified green per-branch. Merged
content → juice2 → floor; **621 tests green on main** post-merge + integration.
Verified in-browser: a Kayden-heavy night produced the full integrated strike
sheet (a fake-ID minor via Kayden, a hip flask, and an UNATTENDED FLOOR FIGHT —
cause: "someone said the kebab shop has gone downhill"), the incident-report
form runs between summary and next shift, and the mix desk overlays a live scene
via the new M route.

**Quality notes:**
- CONTENT: measured guest-list posteriors instead of asserting them, and the
  judge() intoxication fix closed the worst scoring gap in the game — the exact
  right person fixed it (rules owner). The §4.3 catch (humane choice must not be
  secretly optimal) is the review of the phase.
- FLOOR2: six real defects found by re-reviewing YOUR OWN previous session is
  the standard now set. The honest note that your verify harness had a 100%
  false-negative rate (refute-biased + unanimity) is going in the reviewer
  playbook: that pattern is an off switch, not a quality bar.
- JUICE2: the mix desk turns the ear-pass from a code task into a 10-minute
  human task. Correct instinct throughout.

**Protocol breach (FLOOR2) — no harm, don't repeat:** the session branched from
its OLD lane tip (3eee843), not main — so it never saw Phase 2, its handover
re-requests CCRs that were folded a phase ago, and its round-2 code imported a
file main had moved. I ported everything at merge (fight files re-pointed at
data/strings/floor, fights wired into NIGHT mode incl. the unattended path,
live-body cap applied to the night spawn path). Next session: `git fetch &&
git switch -c lane/<name> origin/main`, and read the CURRENT handover first.

**Integration applied (beyond the merges):**
- Aggro physiology (approved round-1 request) implemented: `tickCalm` in the
  floor — no brewing fight + no unhandled maggot → ~0.3 aggro/clock-min cools,
  both modes, player present or not. Numbers are tuning-pass fodder.
- Fight swings radio the door (urgency 3) when the player is at the rope.
- `Ambience` created/destroyed by NightScene beside the engine; starts on
  `audio:unlocked`.
- `MixDeskScene` registered globally; **M** toggles it as an overlay anywhere.

**Rulings:**
1. CONTENT Q1 (`RunInfo.game` mutable ref): ACCEPTED as-is. If a third writer
   to the save ever appears, we revisit with an event; two is fine.
2. CONTENT Q2 (double save): accepted, cheap. Leave it.
3. CONTENT Q3 (encounter repeats): YES — gate repeats across nights via
   GameState next content session, and grow the table to 8–10.
4. FLOOR2 (fight fuse burns through overlays): CONFIRMED as design. ESC exists;
   choosing what to abandon is the floor.
5. FLOOR2 (doll texture cache growth): bounded per-night by the crowd cap;
   acceptable until the v0.4 art pass, which owns texture lifecycle anyway.
6. JUICE2 Q1 (the arc changes what "the mix" means by hour): CONFIRMED wanted.
   The desk pinning PEAK on entry is the right tuning default.
7. JUICE2 Q2 (Ambience owner): NightScene, done.
8. Fight heat strike as `deferred: true` + 150s cooldown: good rebalance,
   approved retroactively.

**Open items carried:**
- The Kayden-contradiction path (lie about him in the report) has never fired in
  real play — someone play a floor-heavy night and lie about it.
- Stall/pat-down/bin SFX still unwired (overlay constructors want the handle).
- Cut-off overlay ↔ DialogueBox drunk-typo integration (widget ready, call site
  floor-owned).

**NO NEW LANES OPEN.** The bottleneck is no longer code: it is two sessions with
John — (1) the EAR PASS (press M in a real night, solo voices, DUMP MIX, paste
the literal back), and (2) the ECONOMY TUNING PASS (baselines: content's
3-night run data + door's Phase-1 measurements + floor's idle/attentive table).
After those land, the next lanes are content-expansion (encounters, repeat
gating), venue 2, and the v0.4 art pass per docs/ASSETS.md.

---
### SESSION — SOLO BUILD-OUT (Fable) — 2026-07-19 23:30
**Branch/commits:** main (three commits this session; deployed live)
**The headline: THE ECONOMY TUNING PASS IS DONE — against bots, held to bands.**
`tests/sim/economy.ts` is a full node-side bot night (QueueManager + judge +
Meters + deferred pipeline + churn model, no Phaser); `economy.test.ts` runs
careful/sloppy/dawdler bots over 5 seeds and asserts NINE feel-bands. Those
tests are now the economy's contract — a knob change that kills the feel fails
CI, not a playtest three weeks later. Where the numbers landed (careful bot):
admits 87 > denies 69 (was 48/68 — the room fills now), vibe pinned-share 0.34
(was 0.99 — the meter is alive), aggro peaks ~36 (was 6), slam queue avg 7
peak 16+ (was 1.0/8 — the 10:30 slam is REAL, verified live: 22 in the queue
at 12:33 AM), hype decays while you work (was a permanent x3 ratchet), sloppy
dies of vibe with 3 strikes, extreme dawdling riots before it profits.
**What the tuning changed (all commented at the definition):**
- judge: denyViolator 6→2, denyClean 2→1, admitClean 3→2, admitBadId 2→1.
- queue: calmPerSec 0.9→0.35, NEW hypeDecayPerSec 0.045 (theatre fades while
  you serve — kills the ratchet).
- arrivals: slam peak 0.42→0.72/min, night total ~70→~120.
- generator: logo rate 0.35→0.18; sunnies/bucketHat/blazer weights halved.
- NEW `core/meters.vibeCoolingPerMin`: the room cools above 50 (-0.4) and
  faster above 70 (-0.8), emitted per clock-minute — nobody stays impressed.
- **Dazza's attention span (`ACTIVE_RULE_CAP = 4`)**: only his last four rules
  are live; older ones get greyed on the card + a retraction toast ("dazza:
  forget the 'NO THONGS' thing. new priorities"). With all 8 cumulative, ~55%
  of the late crowd was deniable and rule-following EMPTIED the room.
- **Natural churn**: patrons go home after 50-140 clock-min (`floor:leave`,
  new contract event, CrowdSim emits, NightScene frees capacity). Without it
  capacity was a one-way ratchet and the door locked shut from 1 AM.
- **Capacity strike bug FIXED**: the strike reason embedded the patron id, so
  the reason-dedupe never collapsed and every over-80 admit filed its own
  strike — 3 admits = licence gone. Now one stable reason per night. (This is
  what was really killing the long Kayden nights in Phase 2.)
**Also this session:**
- Floor SFX wired: stall bangs (doorBang), pat-down bin drops (clickerClunk),
  via optional sfx handles on the overlay constructors.
- Cut-off dialogue now wears its blood-alcohol: `drunkify` on the patron's
  reply, deterministic per dollSeed.
- **Encounters 4 → 8** (eighteenToday, kaydensMate, bigNightOut, twoOfThem —
  register per the §4.3 rules, humane never secretly optimal) + cross-night
  repeat gating (`GameState.seenEncounters`, recorded off the encounter
  incident, excluded by the scheduler until everyone's been met). Scheduler
  reworked: end-order greedy + backward latest-safe pass — a random draw can
  no longer strand a pick; overflow drops gracefully.
- nightShift window tightened to [150,195] (its noLogos dilemma dies when the
  rule cap rescinds noLogos at 200).
- **Bug found & fixed in browser verification**: Dazza's texts lag the clock
  (send cooldown), so a rule could be rescinded before its card row existed —
  the one-shot guard then swallowed the retirement forever and the card LIED
  about what was enforceable. Card sync is idempotent now; verified converged
  under worst-case lag.
- **The Kayden contradiction has now fired in real play** (content's open
  item): floor stint → his 9:15 PM influencer admit → lied on the report →
  Friday's paperwork: "kayden wrote his version of p2 up too. it does not
  match urs. he is very proud of the log". Screenshot in session notes.
**Tests:** 632 passing / 0 failing (was 621). Gate clean. **Deployed to
https://monsterrobot.games/not-tonight/ (bundle index-W0Kf_Kh7.js, verified 200).**
**Broke / known-wonky:**
- The sim bot cheats (perfect information, no inspection time beyond a fixed
  cost) — bands are calibrated to it, not to humans. John's playtest may want
  the arrival curve ±15%; the bands make retunes safe to try.
- Dawdler test covers EXTREME stalling (6s); mild-stall dominance is killed by
  hype decay but has no dedicated band.
- Rule retractions land as toasts; a phone text would be richer (needs a dazza
  schedule slot — next content session).
- Sloppy bot dies by VIBE before aggro ever moves — arguably right (admit-
  everything tanks the room via laps/ripens) but worth a human sanity-check.
**Next:** John's ear pass (M in a live night) and a human feel-pass over the
new economy — the bands make it safe to turn knobs. Then venue 2.

---
### SESSION — SPACES (Fable, solo) — 2026-07-20 14:30
**Branch/commits:** main; deployed live.
**Done — the venue became a place (docs/SPACES.md is the plan):**
- **venueMap grew rooms**: sealed DJ BOOTH east of the dance floor (decks +
  mixer + monitor from the fleet's own gear lineage), fenced open-sky SMOKING
  YARD in the SW (smoke anchor moved inside — wanderers now actually go out for
  one; rain-blue light spill), sink row in the dunnies, glassie-station alcove
  (role-ladder hook), entry-corridor poster wall/cloak/stamp podium, one arcade
  cabinet. South booths shifted east of the yard. 5 new tile kinds; all
  reachability/orphan tests extended and green.
- **Props are DATA**: `venueMap.PROPS` (31 placements, tile footprints,
  optional emissive) + `LIGHTS` (zone light signatures — bar ambers, 3×3 dance
  grid that pulses on the ACTUAL beat via FloorView.onBeat, DJ blue, dunny
  green fluoro that flickers like dying fluoro, yard blue, entry pink).
  FloorView renders every prop from `gen:prop:<kind>` if a generated texture
  exists, else a baked pixel placeholder — the game is fully dressed TODAY.
- **The art drop-in contract**: `public/props/manifest.json` + PNGs; the floor
  loads the manifest before building (both modes, missing manifest = fine).
  `tools/props_import.py` (PIL) turns raw MODELBEAST/Blender output into
  game-ready sprites (crop→alpha→NEAREST resize→16-colour quantise→manifest).
- **John's generation queue** is docs/SPACES.md §5 — 13 prioritised items with
  pipelines, prompts, seeds and sizes ([GLB] prerenders of 90sDJsim gear +
  [MB] flux jobs). Raw drops: `art_incoming/`.
**Tests:** 637 passing (+5 venue). Gate clean. **Deployed + verified live**
(game 200, manifest 200). Screenshots: dance floor + DJ booth reading as real
space; bar run with amber shelf/taps/glassie racks/posters.
**Broke / known-wonky:**
- Prop art is placeholder until John's queue lands — by design.
- The pane's navigate() strips URL hashes (#floor) — dev routes fine in a real
  browser; in the pane use the N/P/F/J keys instead.
- Yard rain particles not yet keyed to 'yard' tiles (renderer knows the kind;
  particles are a small FLOOR follow-up with the smoking loop).
**Next:** John runs the §5 queue (I process + deploy as they land); the two
human passes (ear, economy feel) still open; then venue 2 planning can reuse
the whole PROPS/LIGHTS system as-is.

---
### SESSION — VENUE LADDER (Fable, solo) — 2026-07-20 17:10
**Branch/commits:** main; deployed live.
**Done — "WEEK SURVIVED" finally leads somewhere:**
- **`data/venues.ts`**: The Royal → Voltage → Elevate (design §6), each a week
  of Thu→Sat expressed as knobs on existing systems: licensed capacity
  (80/110/70), arrivalScale on the curve (0.8/1.0/1.1), Dazza ruleLimit
  (4/8/8 — a dive doesn't run the full book), inspector (off at the Royal).
- **Promotion flow**: survive the week → promoted, next venue, dates advance a
  calendar week, heat strikes CLEAR (new venue, new licence — regulars and
  met-encounters carry: same town). Survive Elevate → THE SEASON IS DONE.
  Licence pulled anywhere still kills the run. Each venue-week gets its own
  seed block (runSeed + venueIndex*1009 + night*101).
- Knobs threaded cleanly: arrivalScale → QueueManager → nextArrivalGapMin;
  ruleLimit → dazzaSchedule (chronological prefix of the rule book, live-rule
  cap applies on top); inspector gated in NightScene.onVerdict. Summary names
  venues and sells the promotion with Dazza's pitch line.
- **Sim knows venues**: simulateNight takes {arrivalScale, licensed}; new band
  asserts The Royal is a measurably gentler week than Voltage for the same
  careful hands (and still clock-survivable).
- **Floor garnish**: rain now falls inside the smoking yard (and only there —
  it keys off 'yard' tiles), and there is a DJ in the booth: headphones,
  hoodie, bobbing on the downbeat, paid in exposure.
**Verified in-browser end-to-end**: 3 Royal nights → promotion → Voltage boots
with capacity 110 on 2026-07-23; booth screenshot shows decks/mixer/DJ under
the torch. **Tests: 638. Deployed + live 200.**
**Broke / known-wonky:**
- Elevate is currently "Voltage but tighter" (smaller room, hotter curve). Its
  design identity (VIP politics, influencer density) wants a content pass —
  archetype weights per venue would be the natural next knob.
- NIGHT_DATES base stays exported for tests; live dates derive via nightDateFor.
- The bot-run paperwork in verification clicked through reports without
  reading them, exactly like Dazza.
**Next:** venue-flavoured archetype mixes (Elevate's identity), John's art
queue (SPACES §5), the two human passes (ear, economy feel) still open.

---
### SESSION — REGULARS & VENUE IDENTITY (Fable, solo) — 2026-07-20 19:40
**Branch/commits:** main; deployed live.
**Done:**
- **Venue crowd identity**: `VenueDef.archetypeWeights` overlays merged in the
  generator and threaded via QueueManager. The Royal is regulars-and-punters
  (influencers weight 2), Voltage the balanced baseline, Elevate a VIP circus
  (influencer 18, ownersMate 7, financeBro 14, regulars 3 — they can't get in
  either). Distribution tests pin the overlay behaviour.
- **CROSS-NIGHT REGULARS (design §4.1, the unkept promise, now kept):**
  - `patrons/regularCast.ts`: six run-seeded regulars — same faces, names and
    boring real licences every night and every venue (own SeededRNG, zero
    stream contamination). New contract flag `PatronFlags.regularId`.
  - QueueManager adopts a cast identity onto every rolled 'regular': tonight's
    outfit and drinks, THEIR face/age/card, grudge state from the run save.
  - NightScene records every regular denial into `GameState.regulars`
    (persisted); two denials anywhere in the run → next appearance arrives
    **disguised**: fake moustache + joke glasses on the SAME face — and
    deliberately NOT on the card photo, which still shows Thursday's
    clean-shaven bloke. Same name. Same walk. judge() already prices the
    grudge (denyGrudgedRegularVibe) off timesDeniedBefore.
  - Verified in-browser: planted a 2-denial grudge, rolled the queue — Lachlan
    Doyle stepped up in the disguise with his clean card open. Screenshot'd.
- **Rule retractions are Dazza texts now** (phone buzz + thread evidence), not
  toasts — re-entrant-safe (no rule payload → no card/recursion path).
**Tests: 646 (was 638). Gate clean. Deployed + live 200.**
**Broke / known-wonky:**
- Regulars have no bespoke step-up line about the disguise; the tell is purely
  visual + the judge note. A "back again" line pool keyed off
  timesDeniedBefore would be a nice content nibble.
- Cast size fixed at 6; Elevate's regular weight 3 means you may not meet one
  there — acceptable (that's the point of Elevate).
- Encounter patrons correctly bypass cast adoption (scripted people stay
  scripted).
**Next:** John's art queue (SPACES §5) + the two human passes remain the only
non-code items. Code-side: regular step-up lines, Elevate VIP guest-list
density (claims are archetype-driven so the mix already lifts them), and the
smoking-door slip-in (FLOOR2 §2's second noStamp source) are the next nibbles.

---
### SESSION — THE CLUB GETS DRESSED (Fable, solo) — 2026-07-20 22:15
**Branch/commits:** main; deployed live with the full art drop.
**Done:**
- **Ran the entire SPACES §5 generation queue myself on MODELBEAST** — 21
  flux_local jobs (all prop sprites + 3 posters + the street backdrop),
  processed through tools/props_import.py on ultra (PIL), 20 sprites now live
  in public/props/ + manifest. The drop-in contract worked exactly as designed:
  zero code changes for the floor props.
- **Two new consumers wired**: NightScene now preloads the manifest art BEFORE
  launching Door/Floor (textures are game-global), and DoorScene swaps its
  flat wall-of-night rect for the generated STREET BACKDROP plate — brick,
  layered gig posters, graffiti, rain streaks. The door scene went from
  placeholder rects to a real street in one image.
- **Dance floor light show**: two scan beams pivoting on the disco ball
  (baked wedge texture, additive, opposite rotations, beat-kicked brightness)
  and the grid cells now COLOUR-CYCLE by bar (pink/blue/amber, phase-offset
  per cell so the floor ripples instead of blinking).
- **Furniture pass**: pool table with low green cone (the Australian venue
  underneath the techno), plants, wet-floor sign, standing tables by the dance
  floor, third east booth table. New kinds registered end-to-end (map → sizes
  → placeholders → importer targets).
- **Bug fixed (found live)**: stepping inside during the floor's prop-load
  window meant the scene missed night:phaseChange and never learned the player
  was present (camera dead, input ignored). `present` now initialises from
  NightState.location at bind time.
- **Farm note**: first queue run lost all 20 downloads to a shell-quoting
  mangle (ssh single-quote stripping around ['id'] — same trap as the cover
  art). The renders survived on the farm; patched and refetched. If you script
  MB over ssh: heredoc with QUOTED delimiter, double-quoted keys, always.
**Tests: 646. Gate clean. Deployed; game + backdrop both 200 live.**
**Broke / known-wonky:**
- Generated sprites are first-pass flux at 16-48px — most read beautifully
  (decks/ball/backdrop/posters are transformative), a few are just fine
  (glassRack, stampPodium). Re-rolls are one importer command away; judge on
  John's screen, not mine.
- The GLB prerender path (real 90sDJsim gear via Blender) remains unused —
  flux versions landed well enough that it's now an upgrade, not a need.
- Speaker sprite is slight-angle rather than pure top-down; reads good in situ.
**Next:** John plays it (economy feel + ear pass, M works live); re-roll any
sprite that bugs him; regular step-up lines and the smoking-door slip-in stay
the next code nibbles.
