5cb2351cdb
Data model + projection function for the participant grid: RoomMember — one row in the grid. Combines the static `p`-tag role (kind-30312) with the dynamic presence flags (kind-10312 aggregator). Carries an `absent` Boolean for "member never joined" — nostrnests greys them out and we keep parity by surfacing the flag for the UI to decide. ParticipantGrid — onStage / audience split. buildParticipantGrid(participants, presences) — pure projection. Stage placement is `canSpeak() && onstage != false`, so a speaker who explicitly emits `onstage=0` (Tier 1 Step 1's "step off the stage" tag) drops to audience without losing their speaker role. Pure-audience members (present in the ledger but not p-tagged) show up in audience with `role = null`. Tests: * Host + speaker on stage with matching presence * Speaker with onstage=0 drops to audience * Pure listener (no p-tag) lands in audience with role=null * P-tagged speaker without presence is on stage with absent=true * Empty inputs produce an empty grid (no crash) The actual `LazyVerticalGrid` rendering is a follow-up — current `StagePeopleRow`s already cover the on-stage/audience split visually; the data model is the substantive piece. Wiring the VM's `participantGrid: StateFlow<ParticipantGrid>` and switching the room screen to a grid layout can ship later without changing the wire contract or tests.