# pixtuoid/tui — renderer agent guide

The **terminal render pipeline**: ratatui `App` + `TuiRenderer` (the `Renderer`
trait impl). Owns cross-frame state, the half-block pixel pass, widgets, themes,
the per-agent motion-timing authority, and A\* pathfinding. This is where the
headless `SceneState` becomes pixels. Parent guides:
binary [`../../CLAUDE.md`](../../CLAUDE.md); workspace
[`../../../../CLAUDE.md`](../../../../CLAUDE.md); headless lib
[`../../../pixtuoid-core/CLAUDE.md`](../../../pixtuoid-core/CLAUDE.md).

## Layout

```
tui/
├── anim.rs         centralized easing curves + eased_progress(start, duration_ms, easing, now) free function —
│                   used by floor slide, A* walk path ease, and version popup entrance/dismissal animations
├── renderer.rs     draw_scene orchestrator (DrawCtx struct), half-block flush, terminal lifecycle
├── widgets/        ratatui widget paint fns, split into sub-modules:
│                   mod.rs (TickerQueue, shared helpers), hud.rs (footer, wall display,
│                   elevator indicator, theme picker), tooltip.rs (hover, cat, coffee,
│                   furniture, labels, chitchat bubbles), help.rs (paint_help_overlay)
├── hit_test.rs     mouse hit-test: agent hover, coffee machine click, furniture tooltips, pet
├── tui_renderer/   Renderer trait impl, split production vs tests:
│                   mod.rs (TuiRenderer: cross-frame state — RgbBuffer, FrameCache, Router, PoseHistory,
│                   TickerQueue, Theme, cached Layout; Vec<FloorCtx> each carries .motion + .door_anim_max_ms;
│                   #[cfg(test)] frame_buffer/floor_motion/floor_buf/inject_coffee test seams),
│                   harness.rs (#[cfg(test)] mod: ~38 headless integration tests driving the real
│                   render()/navigate_floor() path via ratatui TestBackend — output-first: buf() pixels + frame_buffer cells)
├── theme/          color theme system — one file per theme, Theme struct in mod.rs
│                   mod.rs (struct defs + ALL_THEMES registry), normal.rs, cyberpunk.rs,
│                   dracula.rs, tokyo_night.rs, catppuccin.rs, gruvbox.rs
├── motion/         per-agent walk-timing state, split production vs tests:
│                   mod.rs (MotionState: entry/exit/snap_back/wander_*/walk_path fields — exit and
│                   snap_back are WalkLeg{started_at,profile,from} structs (named fields, was a
│                   3-tuple carrying the from-Point); entry stays a 2-tuple (SystemTime, WalkProfile);
│                   walk_path = frozen per-leg A* polyline via
│                   WalkPathSnapshot; WanderPhase enum Seated/WalkingOut/AtWaypoint/WalkingBack;
│                   octile_path_len; advance_wander drives the elastic wander timeline, idempotent
│                   per now via last_advanced_at; owned as HashMap<AgentId, MotionState> on FloorCtx.motion),
│                   tests.rs (#[cfg(test)] mod tests)
├── pose/           routed pose + motion-timing authority, split production vs tests:
│                   mod.rs (PoseHistory, derive_with_routing, snap-back; snapshots A* path length once at
│                   walk-start → freezes WalkProfile → drives t_x1000 per-frame via physics::walk_progress;
│                   the leg's A* polyline *shape* is also frozen once per leg via walk_path, re-snapshotted
│                   only when (from,to) changes and only for cornered routes >2 points, so per-frame overlay
│                   churn can't reroute an in-flight walker (no flash); octile_distance promoted to
│                   pub(in crate::tui); re-exports core::pose),
│                   tests.rs (#[cfg(test)] mod tests: unit + frame-by-frame continuity guards)
├── pathfind.rs     Router trait + AStarRouter with selective cache invalidation
├── floor.rs        FloorCtx (per-floor render state), FloorTransition, LightingState, build_floor_scene
├── pet.rs          PetKind (Cat, Dog) + per-kind static data; Pet{kind,name} (a configured office pet) + Pet::defaulted; select_pet_for_floor(&[Pet])->Option<&Pet>; PetState (heart-anim interaction)
├── chitchat.rs     venue-keyed group/solo speech bubbles (VenueKey::Room vs ::Waypoint)
└── pixel_painter/  pure-pixel pass — split into focused child modules:
                    mod.rs (PixelCtx struct, orchestrator), background/ (weather, sunset, skyline,
                    time_of_day.rs, lighting.rs), ambient.rs (sun spot + dust motes + ceiling halos),
                    drawable.rs (y-sort Drawable enum + dispatch), effects.rs (glow/z's/steam/dust/bubble),
                    palette.rs (agent palette + recolor + tool_glow_tint), anchors.rs (breath, walk position,
                    character_anchor; the per-pose anchor fns take `sprite_w` — the pack's character width,
                    resolved ONCE per frame [8 bundled / 10 robot] — so a non-8-wide pack centers correctly),
                    furniture.rs (coffee table, area rug, side table, pantry table/chair),
                    glass.rs (frosted-glass room-divider walls: consts + stitch + paint_glass_wall_h/v),
                    seat.rs (SeatView orientation single-source + seat_sprite + settle_seat_view + paint_character_at),
                    tests.rs (sibling unit suite, extracted from mod.rs)
```

> Furniture drawables y-sort via `core::layout::z_sort_row` (the south base row, tied to the mask's
> `anchored_top_left` so the sprite and its blocked ground can't drift); `center_pin_south_offset` remains
> only as the offset primitive for shadow/halo placement. See `core::layout::placement`.

## Known sharp edges (don't be surprised by these)

- **`draw_scene` is called through `TuiRenderer`** (the `Renderer` trait impl), which owns the cross-frame state (RgbBuffer, FrameCache, Router, OccupancyOverlay, PoseHistory) and assembles a per-frame DrawCtx borrow (RgbBuffer, FrameCache, Router, OccupancyOverlay, PoseHistory, theme, mouse state). `draw_scene` returns `Result<Option<Layout>>` — the computed layout is cached on `TuiRenderer.cached_layout` so hit-test functions can use it without recomputing. During floor transitions, `cached_layout` is cleared to `None`.
- **`recolor_frame` substitutes by RGB equality.** Works because each palette key in the default pack maps to a unique RGB. If you add a sprite pack where two keys share a color, swap to a palette-key-indexed approach instead. (There is no automated check enforcing the uniqueness invariant on the embedded pack — be careful editing palette RGBs.)
- **EXIT walks are time-compressed to fit the GC window; entry/wander/snap-back are not.** Walk duration is normally pure physics (`distance ÷ speed`), but an exiting slot races a removal deadline: the reducer GCs it after `EXIT_GRACE_WINDOW = 4500 ms`, so when the exit's physics duration exceeds the window `derive_with_routing` scales `elapsed` so `walk_progress` reaches 1000 by the window edge — without this the sprite would **vanish mid-corridor** when the deadline fires (a real regression, fixed with a test). Don't delete the exit compression as "redundant." **Snap-back is NO LONGER compressed** (it used to be, to fit a 900 ms window): it now runs by **pure physics** with a brisk SnapBack profile (`physics::V_CRUISE_SNAPBACK` faster cruise + `WALK_ACCEL_SNAPBACK` ≈ 3× accel — an "urgent rush back"), and `SNAP_BACK_MS = 900 ms` is now just the **ARM window** (only fire a snap-back for a recent flip), NOT a render cap — so a far snap-back renders to completion as a real ≈ 1.3 s walk instead of a hard-compressed 900 ms dash. Entry has no cap (nothing GCs an entering agent) and must stay uncompressed so far desks genuinely take longer.
- **A walk leg's A\* polyline shape is frozen once per leg, not re-routed per frame.** `route_walking_pose` snapshots the route into `MotionState.walk_path` keyed on `(from, to)` and reuses it until the endpoints change. This is NOT redundant with `AStarRouter`'s own cache: the router cache is *invalidated* by per-frame occupancy-overlay churn (another agent toggling a waypoint obstacle), and without the freeze a mid-leg re-route remaps the frozen-progress `t` onto a differently-shaped polyline → the sprite **jumps** (the "flash") and the frame pays a fresh A\* cost (the periodic stutter). Only cornered routes (>2 points) are frozen; straight 2-point walks re-route each frame (cheap, and self-healing if A\* transiently fell back to a straight `[from,to]`). The accepted trade-off: a frozen walker won't dodge an agent that steps into its path mid-leg (rare, cosmetic, legs are seconds). Don't "simplify" the `walk_path` check away as duplicate caching. The occupancy overlay is still built from the *stateless* `core::pose::derive` (for cache-signature stability) — that intentional divergence from the tui motion timeline is what made the freeze necessary.

## Where to look

- "Which side does an agent approach furniture from?" → `core::layout::approach_point` (`layout/approach.rs`) is **A\*'s goal**; `stand_point` is the obstacle render anchor (they're equal for obstacles). A waypoint's `pos` is the furniture's blocked CENTER. Both pick the side **nearest the agent's home desk**, but `approach_point` adds two filters the old `walk_target` lacked: (1) the side must be in `FurnitureDef.approach` (`ApproachSides`, rotated by the seat's `facing` — a North-facing couch excludes its south backrest); (2) the chosen cell must be **A\*-reachable** (`ReachSet.reaches`, a coarse-cell BFS mirroring the router's grid coarsening) so A\* never targets a walled-off cell. Pure geometry over `WalkableMask` + `ReachSet`, no live A\*. Called by `core::pose::idle_pose` (overlay walk dest) + `tui::motion::pick_wander_dest` (routed walk dest) with the same `origin = home desk` so they can't drift.
  - **Obstacle kinds** (pantry/vending/printer): `approach_point == stand_point` — the agent stands AT the approach point, render anchor == A\* goal.
  - **Seat kinds** (`occupies_pos`: Couch/MeetingSofa/MeetingStand): A\* routes to the approach point (a cell off an *allowed* side), then a post-A\* **settle** appends `seated_foot_cell(furniture, pos)` (the seat's render-anchor *inverse* — `back_couch` seats → `pos+5`, waypoint stands → `pos`) to the walk polyline, so the sprite glides onto the seat **pop-free** (the settle endpoint == the render anchor by construction). The seat cell stays *blocked* (the sprite sits ON the furniture); the approach/settle split is what replaced the old "A\* snaps adjacent + sprite pops to a fixed north anchor." Don't collapse the settle back into a single `pos` target. **NO approach-side fallback:** if no allowed+reachable side exists (a seat boxed in to only its backrest — only happens in the degenerate tiny 120×96 meeting room), `approach_point` returns the blocked `pos` as a **"no valid approach" sentinel** and the caller (`pick_wander_dest`/`idle_pose`) **skips the furniture this cycle** (ambles aimlessly) rather than routing into it — a back-approach (standing behind the backrest then sitting facing north) is physically impossible, so it's never chosen. Invariant pinned by `seat_approach_is_never_behind_the_backrest_on_real_layouts`.
  - **The home desk** is a seat whose chair (`desk_walk_anchor` == `seated_foot_cell(Desk)`) is *inside* its blocked footprint, so it can't be A\*'s goal directly — aiming the router at it makes `find_path` snap the blocked goal to the NEAREST walkable coarse cell, which for a south-facing chair is the SOUTH/corridor front, so the agent routes up THROUGH the desk front (the "walk through the table" / approach-from-south bug). **EVERY desk-touching leg — entry, wander-out, wander-back, and the exit DEPARTURE — therefore shares ONE `pose::desk_leg_endpoint(desk, layout)` helper** that returns `(routing_endpoint, chair_settle)`: the routing endpoint is `desk_approach_cell` (= `approach_point(Furniture::Desk, …, Facing::South)` — a reachable N/E/W cell, the south front excluded by `DESK_APPROACH`), and `chair_settle = Some(chair)` is the cell to SETTLE onto. So A\* always gets a real reachable goal and a post-A\* settle glides the sprite onto the chair; `desk_walk_anchor` stays the seat render anchor + the settle target, NEVER the raw router goal. The arrival glide renders front-facing at the desk's seated z-key (`desk.y+4`, below the desk furniture's `desk.y+8`) via `SeatView::Front`, so it sorts behind the desk. A wander leg uses `Settle::Both` (rise off one seat AND glide onto another — e.g. wander-back rises off the waypoint seat then glides onto the chair); entry/exit use single-ended `Settle::End`/`Settle::Start`; the motion-side wander/exit profiles add the chair-glide length so the duration matches (no pop). **Snap-back joins the unified path too** (the last exception erased): the urgent Idle→Active return routes via the approach cell + settle like the rest, run by **pure physics** with a brisk SnapBack profile — no 900 ms time-compression. Two things make that safe: (1) its arm gate measures distance to the **chair** (`desk_walk_anchor`), NOT the desk origin — the chair is +(6,4) from the origin, so a desk-origin gate re-fired forever once the agent settled ON the chair (10 px ≥ MIN); (2) it arms ONCE per state transition and renders from the FROZEN origin, so a K-call within a frame can't flip Walking↔Seated. The ONLY non-caller is a mid-wander EXIT, which starts from its live wander position rather than the chair. Degenerate fallback: if `desk_approach_cell` returns `None` (every allowed side walled off), the leg reverts to the direct chair target with no settle. So `approach_point(Furniture::Desk)` is called in production by every desk leg (entry/wander/exit/snap-back); seat render for the desk stays its own arms (4 work-states: typing/thinking/sleeping/`StandingAtDesk` at z=`desk.y+0`), NOT folded into `SeatView::seated_sprite` — only the arrival glide unifies. Pinned by `desk_entry_routes_around_the_desk_then_settles_onto_the_chair` + `wander_legs_approach_the_desk_via_an_allowed_side_not_through_the_front`.
  - **Debug (debug builds only):** toggle `w` (`pixel_painter/debug_overlay.rs`) to see the mask (red), approach points (green) vs seat cells (magenta), and live A\* routes (cyan). The `w` dispatch arm and help entry are `#[cfg(debug_assertions)]`-gated — `w` does nothing in release builds. The snapshot example's `--debug-walkable` renders the same overlay + a BFS connectivity report.
- "How is the office rendered (pixel pass)?" → `tui::pixel_painter::render_to_rgb_buffer`. The pixel pass is split: `pixel_painter/background/` (floor/walls/windows/clock/corridor/lighting/shadow via `mod.rs` + `time_of_day.rs` + `lighting.rs`), `pixel_painter/drawable.rs` (y-sort `Drawable` enum + dispatch), `pixel_painter/effects.rs` (chair-behind/screen glow/sleep z/steam/dust/bubble), `pixel_painter/palette.rs` (agent palette + recolor + `tool_glow_tint` for per-tool monitor color), `pixel_painter/anchors.rs` (per-pose sprite anchors + breath + walking_position + character_anchor), `pixel_painter/furniture.rs` (procedural furniture paint helpers). The terminal-flush pass (`half-block + widgets + status footer`) is `tui::renderer::draw_scene`.
- "How do the room dividers render (frosted-glass partitions)?" → the `room_walls` loop in `render_to_rgb_buffer` (`pixel_painter/mod.rs`), painted AFTER `dim_floor_overlay` so the glass isn't dimmed. Each layout segment is a **frameless frosted-glass** strip — no hard outline: a cool gradient across the short axis (a bright specular edge → tinted body → soft slate edge, all three derived from `room_wall_trim_light` and alpha-composited via `glass_over` (`palette::blend`) so the floor glows through), plus a subtle brighter **seam** every `GLASS_SEAM_STRIDE` px for panel rhythm. The vertical's joint stitching is the pure `stitch_vertical_wall` helper. Thicknesses: **`WALL_THICK_H_PX=6` (E-W, shows its face) vs `WALL_THICK_V_PX=3` (N-S, edge-on)** — the 2:1 ratio sells the top-down fake-3D. `WALL_THICK_H_PX` is **defined as `pixtuoid_core::layout::WALL_THICK_H`** (the mask footprint) — one source of truth, so the visible face and the blocked ground can't drift; the vertical's 3 px visual is intentionally wider than its 1 px walkable footprint (top-down ground-projection rule, invariant #6). **Paint order / occlusion:** the VERTICAL wall paints in the background pass (`paint_glass_wall_v`); the HORIZONTAL wall is emitted as a `DrawableKind::RoomWallH` into the y-sorted drawable pass (`paint_glass_wall_h`), anchored at its **south (front) base** — so a character standing just north of it is composited *behind* the glass (occluded), same depth trick the `Door` drawable uses. To make that occlusion visible in a pure top-down projection, the horizontal glass rises `GLASS_CAP_PX=6` px NORTH of its footprint (a translucent "back cap" over the meeting-room floor — visual only, not in the mask; `GLASS_CAP_PX` is defined as `WALL_THICK_H_PX` so the cap grows with the wall-face thickness — was 3, which only grazed the single feet row), so a walker's feet/legs read behind the frosted pane. The loop also **stitches the vertical's joints** the terminal-agnostic layout can't know about: a segment starting at `top_margin` is raised to `top_wall_h` (connect to the window band), a segment sitting just below a horizontal wall is bridged up to it (the dual-meeting layout offsets the lower segment ~6 px to clear the cross wall), and a segment whose bottom meets a horizontal row extends down by the horizontal's thickness to fill the inside corner. Covered by `harness.rs::meeting_glass_partition_connects_at_window_and_corner` + `pixel_painter::tests::glass_wall_h_back_cap_composites_over_a_character_behind_it`.
- "How does the neon wall display work?" → `pixel_painter/background/lighting.rs::paint_neon_panel` paints the dark panel with pulsing cyan border in the pixel buffer; `widgets/hud.rs::paint_wall_display` overlays ratatui text (branding, state dots, scrolling ticker); `widgets/mod.rs::TickerQueue` manages the persistent scrolling message buffer.
- "How do pets work?" → `tui/pet.rs::PetKind` enum (Cat, Dog) with per-kind static data (sprite names, hitboxes, behavior). **`config::resolve_pets` resolves config into `Vec<Pet>` ONCE at startup** — each `Pet { kind, name }` carries its display name (custom from the `[[pets]]` stanza's `name` field, else `PetKind::default_name()`). One pet per floor is selected via `select_pet_for_floor(floor_seed, &[Pet]) -> Option<&Pet>`; the picked `&Pet` flows into `DrawCtx.floor_pet`, so the tooltip reads `floor_pet.name` directly — **no per-frame name lookup, no parallel kind→name map**. Config: `[[pets]]` stanzas (`kind` required string, `name` optional; absent section = all kinds with default names; `pets = []` = no pets; unknown `kind` = warn+skip, non-fatal). `pixel_painter/drawable.rs::pet_position` — 40s cycle, picks a destination from all spots (desks, pantry, sofas, couch, corridor), walks there (35%), sits/sleeps (65%). **Walk routing RESPECTS the walkable mask**: `find_path(&layout.walkable, &OccupancyOverlay::new(), corridor, prev, dest)` — A* on the STATIC mask with a throwaway EMPTY overlay, arc-length-sampled (`sample_polyline`) for uniform speed. The empty overlay is deliberate: the pet ignores live-agent occupancy so every frame of a leg calls A* on identical inputs → bit-identical polyline → no flash, no per-frame state (contrast agents, whose `walk_path` freeze exists only because they route on the DYNAMIC overlay). Raw spots are pre-snapped via `pathfind::snap_point_to_walkable` (a static-mask wrapper over the private `snap_to_walkable`) and written over the polyline endpoints — necessary because `find_path`'s `reconstruct` puts the RAW (often-blocked) `from`/`to` at the ends. Same snapped anchor for walk-end and rest → no pop at the leg boundary. No-path fallback: straight lerp between the snapped anchors (never panics, still better than the old raw-spot lerp). Cat sleeps with z's near idle agents; both pets sleep when all agents are idle. Sprites per kind: `*_walk` (8×6), `*_sit` (6×6), `*_sleep` (6×4). Click to pet → hearts animation via `PetState` on `TuiRenderer`. `hit_test_pet` / `paint_pet_tooltip` parameterized on `PetKind` (kind drives sprite/hitbox); the tooltip's NAME comes from `DrawCtx.floor_pet`. A floor's name is shared by every floor showing that kind (the model is per-KIND, not per-instance).
- "How does desk personalization work?" → `drawable.rs::paint_desk_personalization` — procedural pixel items appear on desks based on `session_age_secs`: coffee cup (event-driven, after pantry visit), plant (30min), photo frame (1hr).
- "How does the coffee run work?" → `Pose::Walking.carrying_coffee` set in `idle_pose` walk-back from Pantry → `walking_coffee` sprite selected in pixel_painter → `coffee_holders: HashSet<AgentId>` on `TuiRenderer` tracks which agents have visited the pantry (inserted when the pixel pass sees `carrying_coffee: true`) → cup persists on desk until agent exits → exit walk overrides `carrying_coffee` from `coffee_holders` in the pixel painter → `coffee_fetched_at` timestamps drive 120s steam window.
- "How do atmosphere / ambient effects work?" → `tui/pixel_painter/ambient.rs` is the orchestrator, called between background and y-sorted drawables. Five effects: `paint_sun_spot` (East/West walls only — South is the window strip, see `sun_on_wall` in `background/time_of_day.rs`); `paint_dust_motes` (anchored to `window_spill_columns` returned by `background/mod.rs`); `paint_ceiling_halos` (gated on `Theme::kind == ThemeKind::Dark`, color from `palette::tool_glow_tint`). Weather floor tint is in `background::weather_floor_tint`, applied in `paint_floor_and_walls` at 0.15 blend.
- "How does the theme system work?" → `tui/theme/mod.rs` defines the `Theme` struct (~110 color roles in 8 groups, incl. `ApplianceColors` — vending/printer/coat-rack colors; these were hardcoded RGB literals in `drawable.rs` so corridor appliances rendered with the NORMAL palette on every theme until each theme supplied its own harmonized set, guarded by `appliance_palette_is_legible_for_every_theme`). Each theme is a `pub static Theme` in its own file (e.g. `theme/cyberpunk.rs`). `ALL_THEMES` is the registry slice. `--theme` CLI flag resolves via `theme_by_name()`. The `&'static Theme` threads through `TuiRenderer` → `draw_scene` → `render_to_rgb_buffer` → all paint functions. Press `[t]` in the TUI for a live preview picker (j/k or ↑↓ to navigate); each row shows a 2-cell swatch (`hud.rs::theme_swatch` → `neon_brand` + `carpet_base`) drawn from that row's own palette. `set_theme()` flushes the `FrameCache` so character recolors update immediately. 6 themes: normal, cyberpunk, dracula, tokyo-night, catppuccin, gruvbox.
- "How does the `?` help overlay work?" → `tui/widgets/help.rs::paint_help_overlay` paints a centered rounded Block listing every shortcut. Open state on `TuiRenderer::help_open` (toggled by `?`; Enter/Esc/`?` dismiss — the close guard runs BEFORE the version-popup/theme-picker key handlers in `tui/mod.rs`). Threaded through `DrawCtx.help_open`, painted last in `draw_scene`.
- "How does the footer color-code state?" → `widgets/hud.rs::status_segments` builds `(text, role)` pieces once; `build_status_spans` (production) tints active/waiting/idle counts by hue (`label_active`/`waiting`/`idle`); `build_status_summary` (`#[cfg(test)]`) concatenates the same pieces as the byte-identical text-contract oracle for the insta snapshots. Tier fallback (full→medium→min→quit-only) keys off byte width as before.
- "Why does the version popup border breathe?" → `hud.rs::pulse_border_color` lerps the border between 60%–100% of `neon_brand` toward `tooltip_bg` on a ~3s sine pulse, deterministic in `now` (threaded to `paint_version_popup` from both the normal and floor-transition paint paths). Tooltips share a rounded frame via `widgets/tooltip.rs::framed_tooltip`.
- "How does weather work?" → `pixel_painter/background/time_of_day.rs::weather_state` picks from 8 variants (Clear/Rain/Storm/Snow/Fog/Overcast/Windy/Smog) via splitmix64 hash of `wallclock / 600` (changes every 10 min). Effects paint on window glass after the skyline. `sunset_strength()` adds a time-based golden-hour tint at ~6am/6pm, scaled down by existing twilight intensity to avoid double-orange. Smog amplifies the sunset blaze (1.5×) for a sodium-lit golden hour. **`weather_light(weather) -> { intensity, beam_strength, night_sky }`** is the physically-grounded light model: `intensity` (0..1) is daytime diffuse sun (drives spill + `darkness = 1 − max(day_eff, night_glow)`); `beam_strength` (0..1, replaced the old `has_direct_beam` bool) scales the wall sun-spot + dust motes — full under Clear, faint through haze/thin-cloud/snow-glare (Snow/Smog/Fog), zero under thick overcast/rain/storm; `night_sky` (0..1) is night-side luminance (moon/stars/snow-glow/sodium haze) so a clear/snowy night reads brighter than a storm night (`night_glow = night_sky·(1−day)`) — without it every weather rendered an identical pitch-black night. `background::skyline_haze(weather)` murks the city behind the glass under fog/storm/rain/smog (fog is a near-total white-out). Storm lightning: `lightning_envelope` (two-pulse flicker, every `LIGHTNING_PERIOD_MS=15000` ≈15s, lasting `LIGHTNING_FLASH_MS=90`) drives BOTH the bright on-glass bolt (`paint_floor_to_ceiling_window`) and the room-wide ambient bounce (`paint_lightning_flash`, painted LAST in the pixel pass so the whole interior flares). City light twinkle is ~0.6–1.4s cycles at 75% lit.
- "How does the meeting room come alive (sitting + group talk)?" → `core::layout::compute_waypoints` pushes per-room **slots** into `layout.waypoints`: **3** `MeetingSofa` seats per sofa (`dx ∈ {-6,0,+6}` on the 20px sofa) + 2 `MeetingStand` spots per table, each carrying a `Facing` (north seat faces South/viewer, south faces North/away; stands face the table centre) and a `room_id`. They're ordinary waypoints, so the existing `waypoint_index_for_cycle` sends idle agents there emergently (no coordinator). Render (`pixel_painter` AtWaypoint dispatch): `MeetingSofa` Facing::North → `back_couch` sprite, else front `seated`; `MeetingStand` → `standing` + `flip_x` by facing. Group chat: `tui::chitchat` is venue-keyed — `VenueKey::Room{floor,room_id}` merges all of a room's slots into ONE conversation with `participants: Vec<AgentId>` round-robin (refreshed each frame for join/leave). The **lounge couch** is also 3 seats (`WaypointKind::Couch`, `room_id = None`) and ALSO group-chats: its seats collapse to one venue via `chitchat::venue_wp_idx` (the first couch's `wp_idx`) rather than overloading the meeting-only `room_id` (which indexes `meeting_tables`); pantry/vending/printer stay single-point `Waypoint`. The couch sprite/rug/side-table paint ONCE, centred on `SceneLayout::couch_sprite_center` (3 seat waypoints would triple-paint). Both sofas share the 20px `meeting_sofa` sprite. A seated occupant's y-sort key uses `anchor_no_breath.y` (not the breathing anchor) so the ±1px breath can't flip the sofa over its sitter. The whole pipeline is covered headlessly by `tui_renderer/harness.rs::meeting_room_fills_and_hosts_group_chitchat`.
- "How does the thinking pose work?" → `core::pose::derive` returns `Pose::SeatedThinking` when an Idle agent's `last_event_at` is within `THINKING_WINDOW_SECS = 20s` AND `last_event_at > created_at` (excludes freshly spawned agents). Renders as the `seated` sprite + screen glow, with **no overhead indicator** — the pose holds the agent at its desk for the 20s window before it wanders/sleeps. (The old animated `···` thinking-dots `effects::paint_thinking_dots` were removed as visually distracting; only the silent seated pose remains.) Screen glow only paints when the agent's derived pose is seated (precomputed pose map avoids double A*).
- "How do tooltip stats work?" → `AgentSlot.tool_call_count` increments on `ActivityStart` (excludes Task delegation). `AgentSlot.active_ms` accumulates on the next `ActivityStart` (measuring the previous span) and on `expire_pending_idles` (measuring to `pending_idle_at`, not `now`, to avoid grace-window inflation). Tooltip shows `⏱ duration · N calls · X% active`. Fresh agents (<5s) show `--% active`. (The accounting lives in the reducer — `pixtuoid-core/state` — the tooltip render is `widgets/tooltip.rs`.)
- "How does the coffee machine Easter egg work?" → `hit_test.rs::hit_test_coffee_machine` checks if a click falls on the coffee machine section of the pantry counter sprite (x offset 11–18 for large, 8–13 for small). Hover shows `widgets/tooltip.rs::paint_coffee_tooltip` ("☕ Buy Ivan a coffee"), click opens BMC via `open::that`. Both take `&Layout` (cached on `TuiRenderer`).
- "How do furniture hover tooltips work?" → `hit_test.rs::hit_test_furniture` tests mouse coords against all layout positions (desks, waypoints, plants, wall decor, pod decor, meeting sofas/table, coat rack, doormat, water cooler, trash bin, elevator). Returns `Option<&'static str>` label. `widgets/tooltip.rs::paint_furniture_tooltip` renders it. Checked after agent tooltip and coffee machine in the draw closure priority chain.
- "How do the corridor appliances work?" → Vending machine (4×6) and printer (5×4) are painted as y-sorted `Drawable` variants in `pixel_painter/drawable.rs`. Both are `WaypointKind` variants so idle agents can wander to them. Placement is conditional on corridor height (vending ≥10px, printer ≥9px). Positions stored as centre-point waypoints (same convention as Pantry/Couch).
- "How does per-agent motion state work?" → `tui::motion::MotionState` (in `motion/mod.rs`) holds all per-agent walk-timing: `entry: Option<(SystemTime, WalkProfile)>` (door→desk; a 2-tuple — no `from`); `exit: Option<WalkLeg>` (`WalkLeg { started_at, profile, from }`) — `from` is the agent's position when `exiting_at` fires (its current wander location if out on a trip, else the desk anchor) so the exit walk starts where the sprite actually is, not by teleporting to the desk; `snap_back: Option<WalkLeg>`; `walk_path: Option<WalkPathSnapshot>` — the **frozen A\* polyline** for the current walk leg (re-snapshotted only when the `(from, to)` endpoints change, and only for genuinely cornered routes >2 points). All profiles freeze *duration*; `walk_path` freezes *geometry*. Plus the elastic wander timeline — `wander_phase` (`WanderPhase`: Seated/WalkingOut/AtWaypoint/WalkingBack), `wander_phase_started_at`, `wander_profile`, `wander_cycle_n`. Each `FloorCtx` owns `pub motion: HashMap<AgentId, MotionState>` and `pub door_anim_max_ms: u64` (max in-flight entry/exit `duration_ms + pause_ms`, updated each frame via `recompute_door_anim_max_ms()`, replaces the hardcoded `ENTRY_ANIMATION_MS` in door cosmetics). Evicted via `fctx.motion.retain(|id,_| scene.agents.contains_key(id))`. `octile_path_len(&[Point]) -> u32` sums per-segment `octile_distance`.
- "What is the elastic wander timeline?" → `advance_wander()` in `motion/mod.rs` drives the cyclic wander via explicit per-phase clocks anchored to `wander_phase_started_at`. Walk legs (WalkingOut/WalkingBack) use `physics::walk_profile` — variable duration proportional to path length; Seated/AtWaypoint dwell is **absolute per-spot**: `seated_dwell_ms(id)` for the desk beat (15-30s) and `dwell_ms(kind, id)` for the waypoint beat (sofa/meeting seat 20-40s, pantry 10-18s, vending/printer 4-8s). The same `dwell_ms`/`seated_dwell_ms` are read by core's stateless `idle_pose` for overlay coherence, BUT `idle_pose`'s own cycle period uses fixed estimates (`WANDER_WALK_EST_MS`/`WANDER_DWELL_EST_MS`/`est_wander_cycle_ms`) since exact coherence is impossible (core has no router). Total cycle wall-time is elastic — it grows with actual path length — but `wander_cycle_n` is deterministic so destination selection is unchanged from core's stateless wander. **Idempotency**: transitions fire only when `now > last_advanced_at`. **Bootstrap**: fresh Idle agents (UNIX_EPOCH sentinel on `wander_phase_started_at`) fast-forward `cycle_n = elapsed_idle / est_wander_cycle_ms(id)`; the phase clock anchors at `now` (clean Seated start) so the machine never rushes through expired legs (a desk↔waypoint teleport). **Stale resume**: the TRIGGER is `now - last_advanced_at > cycle_ms_for(id)` (7-13s — a frame-cadence-vs-frozen-floor sentinel, NOT a dwell detector: on-screen `advance_wander` runs every ~33ms even during a 40s lounge dwell, so the gap never approaches it; only an off-screen floor or a pause trips it). On trip it re-runs the same analytic bootstrap instead of replaying the whole backlog one transition per frame. Do NOT raise the trigger to "max dwell" — that would let 13-60s off-screen gaps replay. **Seated render authority**: in `tui::pose::derive_with_routing` the `WanderPhase::Seated` arm returns `Pose::SeatedIdle` DIRECTLY — it must NOT fall through to core's stateless `idle_pose`, whose independent fixed-fraction timeline drifts from this machine and would render the agent at a waypoint while the tui clock says Seated (a teleport). Don't "simplify" the Seated arm back to a fallthrough.

## When refactoring

The render path is exercised by the headless harness (`tui_renderer/harness.rs`,
~38 tests) plus dense `motion/tests.rs` + `pose/tests.rs` unit suites with a real
A\* router and overlay churn. Changes to `derive_with_routing`, `MotionState`, or
the pixel passes should add or update a frame-by-frame continuity guard — the
flash/teleport/replay regressions documented above all came back as failing
tests first.
