---
path: /tara/sit/[id]
surface: customer
domain: tara
auth: signed-in
source: apps/oshun/web/src/app/tara/sit/[id]/page.tsx
status: walked
last_walked:
  '2026-06-27 targeted automated coverage — /tara/sit/when-the-mind-wanders:
  route-level Playwright covers idle/running/paused transport, aligned 15s
  keyboard scrub, reflection save/queue states, and 44px return-link targets;
  component tests cover completed/partial/drifted states. Earlier 2026-05-29
  automated runtime walk verified populated render, HTTP 200, and 0 page/console
  errors. 2026-06-28 deepening adds Space/k global shortcut coverage, focused
  control bypass coverage, and storage-unavailable offline reflection copy.
  2026-07-03 addendum adds a clock-controlled browser pass for the 90-second
  long-idle drift state, scrub recovery to paused, and resume telemetry.
  2026-07-03 follow-up covers an unknown sitting id rendering the app-level 404
  UI, no player render, usable search, and browser-back recovery to /tara.
  2026-07-03 second follow-up returns the offline reflection branch online and
  confirms the queued UI plus `tara:reflection:<id>` localStorage draft remain
  because no reconnect drain is wired yet. Evidence:
  first-tara-sit-deepening.spec.ts, user-flag-to-review.spec.ts,
  TaraSitPlayer.test.tsx, WALKTHROUGH/results/runtime-sweep-2026-05-29.md'
---

# Tara · sit player

## Purpose

The single-sitting player. Resolves `[id]` against `getTara()` — either
`todaysSit` or one of the `sittings` rows — and renders the `TaraSitPlayer`
client component: timer, audio controls, drift detection, completion threshold,
reflection capture, and cross-domain step-out. Lands here from `TaraRoom` "Begin
sitting", from a notification deep link, or from a resumed session card.

## Entry points

- **Linked from `/tara`** via `TaraRoom` "Begin sitting" button on the
  today-card and the sittings rail
- **Deep link from notification** — Tara ritual reminder push (`/tara/sit/<id>`)
- **Direct URL / bookmark** — yes (auth required); 404 if `id` resolves to
  neither `todaysSit` nor a row in `sittings`
- **Resumed-session card** on `/` continuation rail (when wired)
- **Telegram mini-app share / web share target** — same URL shape
- Not in shell nav; not in PWA `manifest.json` `shortcuts[]` (verify)
- `robots: { index: false, follow: false }` — never SEO-indexed

## Layout regions

`page.tsx` is a thin server component that resolves the sitting then renders
`<TaraSitPlayer>` inside `<LWebShell top={<LCustomerNav active="today" />}>`.
The player is one centered column, `maxWidth: 760`.

- **Header**: `LCustomerNav` (active=`today`); on the player view itself a top
  bar with a "← Tara" back link and the `TrustSafetyFlagButton`
- **Hero / above-the-fold**: eyebrow status (`Ready when you are` / `In session`
  / `Paused` / `Paused, gently` / `Reflection`), large display title, byline
  (`teacher · pathLabel · session NN of M · sitting <index>`)
- **Main (running)**: large mono timer (remaining), elapsed/total/percent
  eyebrow, scrub slider, transport row (play/pause, ±15s, End sitting), drift
  status bar (conditional), voice-speed + ambient-mix sliders, captions /
  transcript / large-text toggles, captions panel, transcript panel
- **Main (complete)**: completion blessing copy (variant by `completed` vs
  `partially-completed`), mood-after chips (settled / tender / restless /
  awake), reflection textarea, Save / saved-status row, "Done · back to Tara"
  link, then a "Next steps" rule + cross-domain chip row (Arete / Nisaba / Nyx)
- **Footer**: none beyond `LWebShell` chrome

## States

The player's `data-session-state` attribute reflects the active state — useful
for E2E selectors.

- [ ] **Loading** — server-rendered after `await getTara()`; no client-side
      skeleton; verify Suspense / `loading.tsx` boundary exists on the parent
      segment
- [x] **Not started (`not-started`)** — heading "Ready when you are"; play
      button shows the triangle glyph; timer shows full duration; scrub slider
      at 0
- [x] **Running (`started`)** — heading "In session"; play button shows the
      pause glyph; `setInterval(…, 1000)` advances `elapsed`; drift watcher
      armed
- [x] **Paused (`paused`)** — heading "Paused"; tick interval stopped; scrub
      still works; resuming returns to `started`
- [x] **Drifted (`drifted`)** — auto-entered after `DRIFT_IDLE_SECONDS = 90s`
      with no interaction; soft status bar "The session paused itself. Take a
      breath; resume when you're ready."; scrubbing returns to `paused`.
      Playwright now installs the browser clock, starts the real route player,
      fast-forwards past the idle threshold, asserts the `drifted` state and
      copy, scrubs forward to recover to `paused`, then resumes with
      `data-telemetry-event="tara_sit_resumed"`.
- [x] **Completed (`completed`)** — `elapsed >= total` OR finish-early with
      ratio ≥ `COMPLETION_THRESHOLD = 0.8`; reflection panel mounted; blessing
      copy "A sit, kept…"
- [x] **Partially completed (`partially-completed`)** — finish-early with ratio
      < 0.8; reflection panel mounted; blessing copy "You crossed part of the
      way…"
- [x] **Reflection submitting (`submitting`)** — Save button shows "Saving…";
      disabled
- [x] **Reflection saved (`saved`)** — green eyebrow "Saved · attached to today"
- [x] **Reflection queued (`queued`)** — offline path: writes
      `tara:reflection:<sitId>` to `localStorage`; eyebrow "Saved locally, will
      sync when you're back online". If durable storage throws, the state
      becomes `memory-queued` and the user is told the draft is kept in this tab
      only instead of being told it persisted locally. Playwright now also
      returns the browser online after the offline save and confirms the queued
      UI plus `tara:reflection:<sitId>` localStorage entry remain because no
      client-side drain exists yet.
- [x] **404 (sitting not found)** — `id` resolves to neither `todaysSit` nor a
      `sittings` row → Next `notFound()` → app-level 404 UI. Playwright now
      deep-links to `/tara/sit/not-a-real-sitting-id` from `/tara`, captures the
      current dev-shell document status as `200`, and asserts "Page not found",
      exact `404`, no `[data-tara-sit-player]`, the 404 search box routing to
      `/?q=morning%20clarity`, and browser-back recovery to `/tara`.
- [x] **Reduce-motion** — `prefers-reduced-motion: reduce` matches → play-button
      transition is `none`. Evidence: `first-tara-sit-deepening.spec.ts`
      emulates reduced motion and polls the real route player's play button
      transition until it collapses to `none`.
- [ ] **Error (recoverable)** — `getTara()` throws → segment `error.tsx`
      boundary; retry button preserves `id`
- [ ] **Offline (post-load)** — once the player mounted, the timer ticks
      regardless of network; reflection submit detects
      `navigator.onLine === false` and queues to `localStorage`
- [ ] **Offline (cold load)** — `/tara/sit/<id>` not in SW precache; verify SW
      runtime caches the rendered HTML for the today-sit so cold offline launch
      works
- [ ] **Gated** — `auth: signed-in` enforced by middleware (verify); no role
      gating today
- [ ] **Standalone PWA** — cold-launch as relaunch target renders cleanly;
      safe-area insets respected; `LCustomerNav` not clipped
- [x] **Storage unavailable** — Safari private mode / quota exceeded: catch
      around `localStorage.setItem` flips state to `memory-queued` (component
      state keeps the draft visible). E2E now forces `Storage.prototype.setItem`
      to throw for `tara:reflection:*`, verifies
      `data-tara-reflection-sync-state="memory-queued"`, verifies the copy says
      "Kept in this tab only", and verifies no `tara:reflection:*` durable entry
      was written.

## Interactions

### Top bar

- [x] **"← Tara" back link** (link)
  - Function: navigates to `/tara`
  - Keyboard: standard `<a>`; in tab order before timer controls
  - Screen reader: announces "Back to Tara" (`aria-label`)
  - Touch target: ≥ 44×44 CSS px (`minHeight: 44`, verified in Playwright)
  - Mobile: stays in top-left, eyebrow caps
  - Disabled when: never
  - Offline: same (client nav)
  - Telemetry: none
- [x] **Trust & safety flag button**
      (`<TrustSafetyFlagButton targetKind="content" targetId="tara-sit-<id>">`)
  - Function: opens flag/report dialog
  - Keyboard: button; Enter / Space activates
  - Screen reader: per shared component
  - Telemetry: per shared component
  - Evidence: `user-flag-to-review.spec.ts` opens the Tara sitting report
    dialog, asserts `targetKind="content"`, target id
    `tara-sit-when-the-mind-wanders`, target label metadata, 44px touch target,
    category select, rationale field, submit enablement, and cancel close.
  - See [`shell/01-app-shell.md`](../../shell/01-app-shell.md) and
    `apps/oshun/web/src/components/TrustSafetyFlagButton.tsx` for shared
    behavior

### Title block

- [x] **Heading** — non-interactive; visual variant of session state
- [x] **Display title** — non-interactive; mirrors `sitting.title`
- [x] **Byline eyebrow** — non-interactive;
      `teacher · pathLabel · session XX     of M · sitting <index>`

### Timer + scrubber (only while not complete)

- [x] **Timer readout** (`role="timer" aria-live="polite" aria-atomic="true"`)
  - Function: announces remaining as `MM:SS`; polite live region (does not
    interrupt)
  - SR announcement cadence remains a manual AT follow-up if users report flood.
- [x] **Scrub slider** (`<input type="range">`)
  - Function: sets `elapsed` to chosen seconds (clamped 0..total)
  - Keyboard: arrow keys move ±1s by default; verify desired step
  - Screen reader: `aria-label="Scrub session position"`,
    `aria-valuemin/max/now`, `aria-valuetext="<MM:SS> elapsed"`
  - Touch target: native range; verify mobile thumb is hit-friendly
  - Mobile: full-width
  - Disabled when: completed/partial — slider not rendered
  - Offline behavior: same (local state)
  - Telemetry: none today (consider `tara_sit_scrubbed`)
- [x] **Play / pause toggle** (circular button)
  - Function: toggles `started ↔ paused`; if `not-started`, starts the session
    and tick interval
  - Keyboard: Enter / Space; also bound to `Space` and `k` globally via `window`
    keydown (ignored when focus is inside `<textarea>` / `<input>`)
  - Screen reader: `aria-label` flips between "Pause sitting" / "Resume sitting"
  - Touch target: 56×56
  - Mobile: large circular target
  - Disabled when: state is `completed` / `partially-completed` — early-return
    in `togglePlay`
  - Offline behavior: works (local state)
  - Telemetry: `data-telemetry-event` flips between `tara_sit_started`,
    `tara_sit_paused`, `tara_sit_resumed`;
    `data-telemetry-surface="tara_player"`
- [x] **"« 15s" rewind** (button)
  - Function: `scrub(elapsed - 15)`
  - Keyboard: also bound to `ArrowLeft` globally through
    `SCRUB_STEP_SECONDS = 15`
  - Screen reader: `aria-label="Rewind 15 seconds"`
  - Touch target: `minHeight: 44`
  - Telemetry: none today
- [x] **"15s »" forward** (button)
  - Function: `scrub(elapsed + 15)`
  - Keyboard: bound to `ArrowRight` through `SCRUB_STEP_SECONDS = 15`
  - Screen reader: `aria-label="Forward 15 seconds"`
- [x] **"End sitting"** (button)
  - Function: `finishEarly()` — completes with `partially-completed` if ratio <
    0.8, else `completed`
  - Keyboard: button; Enter / Space
  - Screen reader: `aria-label="Finish session early"`
  - Disabled when: never (verify intent — should pressing during `not-started`
    count as `partially-completed`?)
  - Telemetry: `tara_sit_finished_early`

### Drift status bar (conditional)

- [x] **Drift advisory** (`role="status" aria-live="polite"`) — appears only
      when `state === 'drifted'`; copy: "The session paused itself. Take a
      breath; resume when you're ready."

### Audio sliders

- [x] **Voice speed slider** (`<input type="range">`, 0.85–1.25, step 0.05)
  - Function: stores `voiceSpeed`; verify the value is wired into the audio
    element when audio backend ships
  - Screen reader: `aria-label="Voice playback speed"`, `aria-valuenow`
  - Telemetry: none
- [x] **Voice / ambient mix slider** (`<input type="range">`, 0–1, step 0.05)
  - Function: stores `ambientMix`; verify wiring when audio backend ships
  - Screen reader: `aria-label="Voice ambient mix"`

### Display toggles

- [x] **Captions toggle** (button)
  - Function: toggles caption panel (live SR-friendly caption-by-progress)
  - `aria-pressed` reflects state
- [x] **Transcript toggle** (button)
  - Function: toggles transcript panel; `aria-controls="tara-transcript"`
  - `aria-expanded={showTranscript}` reflects state (line 533) — note this
    toggle uses `aria-expanded` (disclosure semantics), **not** `aria-pressed`
    like the captions / large-text toggles
- [x] **Large-text toggle** (button)
  - Function: bumps caption font from 17→22 px and transcript font from 14→17 px
  - `aria-pressed` reflects state
  - Verify the toggle does not leak into other panels (mood chips, reflection
    textarea)

### Captions / transcript panels

- [x] **Captions panel** (`role="status" aria-live="polite"`) — text chosen by
      `captionForProgress(progress)`; updates as session advances
- [x] **Transcript panel** (`#tara-transcript`) — list of `TRANSCRIPT` lines
      with `LDots` marker
  - Verify the transcript panel scrolls / does not break the column on long
    transcripts

### Completion: mood + reflection

The completion block (`data-tara-completion`, line 600) opens with an `LEyebrow`
"Thank you for sitting" (line 602) above the blessing copy, and the reflection
field is introduced by a `<label htmlFor="tara-reflection">` "Reflection · one
or two lines is plenty" (line 645).

- [x] **Mood chips** (`settled` / `tender` / `restless` / `awake`)
  - Function: toggles single-select `moodAfter`
  - Keyboard: each button; Enter / Space
  - `aria-pressed` reflects active mood
  - Telemetry: none today (consider `tara_reflection_mood_set`)
- [x] **Reflection textarea** (`<textarea id="tara-reflection">`)
  - Function: stores `reflection` text
  - Keyboard: tab into field; Enter inserts newline (Space/k global handler
    correctly bails out — verify)
  - Placeholder: "What did you notice?"
- [x] **Save reflection** (button)
  - Function: calls `submitReflection()` — if offline writes to localStorage and
    flips `queued`; if online simulates a 300ms POST and flips `saved`
  - Disabled when: `reflection.trim().length === 0` OR status is `submitting` →
    cursor `not-allowed`, opacity `0.4`
  - Screen reader: `aria-label="Save reflection"`
  - Telemetry: `data-telemetry-event="tara_reflection_saved"`
- [x] **Saved eyebrow** — `Saved · attached to today` (green)
- [x] **Queued eyebrow** — `Saved locally, will sync when you're back online`
      (accent); Playwright verifies the current reconnect behavior keeps this
      eyebrow visible and keeps `tara:reflection:<sitId>` in localStorage. A BFF
      drain that clears the local queue on reconnect is not wired yet.
- [x] **"Done · back to Tara" link** (link)
  - Function: navigates to `/tara`
  - Keyboard: standard `<a>`
  - Touch target: ≥ 44×44 CSS px (`minHeight: 44`, verified in Playwright)

### Next-steps cross-domain row (post-complete only)

- [x] **Arete chip-link** → `/arete` — telemetry `tara_cross_domain_arete`; copy
      "Add to today's routine"
- [ ] **Nisaba chip-link** → `/nisaba` — telemetry `tara_cross_domain_nisaba`;
      copy "Read a related passage"; verify the
      [`tara-to-nisaba-handoff`](../../journeys/tara-to-nisaba-handoff.md)
      passage-companion bridge (`buildTaraNisabaPassageCompanion` /
      `NisabaPassageCompanionCard`) is reachable from this chip rather than a
      generic `/nisaba` landing
- [ ] **Nyx chip-link** → `/nyx` — telemetry `tara_cross_domain_nyx`; copy "Step
      outside, tonight"; verify the
      [`nyx-to-tara-bridge`](../../journeys/nyx-to-tara-bridge.md) is the mirror
      entry

### Global keyboard handlers

- [x] **Space / k** — toggle play/pause; bypassed when focus is in `<textarea>`
      / `<input>` (verify the early-return covers `<select>` and `<button>` too
      if they ever appear in-flow). E2E now verifies `k` starts from page
      context, `Space` pauses from page context, focused transcript/captions
      buttons keep native Space activation without toggling the session, and
      focused range input ignores the `k` shortcut.
- [x] **ArrowLeft** — `scrub(elapsed - 15)` via `SCRUB_STEP_SECONDS`; aligned
      with the visible 15s rewind button
- [x] **ArrowRight** — `scrub(elapsed + 15)` via `SCRUB_STEP_SECONDS`; aligned
      with the visible 15s forward button

## Automated coverage

- `apps/oshun/web/e2e/first-tara-sit-deepening.spec.ts` covers the real
  `/tara/sit/when-the-mind-wanders` route for not-started, running, paused,
  timer/scrub, aligned ArrowLeft/ArrowRight 15-second jumps, rewind/forward
  buttons, clock-controlled long-idle drift and scrub-to-paused recovery,
  unknown-id 404 routing/search/browser-back recovery, transcript/captions/
  large-text toggles, reflection mood + textarea + submitting/saved states, the
  return links' 44px hit areas, current cross-domain chip hrefs, Space/k page
  shortcuts, focused-control keyboard bypass, the partially-completed
  early-finish branch, the durable offline queued-reflection branch, the
  reconnect non-drain behavior for `tara:reflection:<id>`, and the
  storage-unavailable in-tab-only reflection queue branch.
- `apps/oshun/web/e2e/user-flag-to-review.spec.ts` covers the Tara sit report
  affordance target metadata and dialog preparation path for
  `/tara/sit/when-the-mind-wanders`.
- `apps/oshun/web/src/components/lilith/TaraSitPlayer.test.tsx` covers the
  component's title/byline/back-link render, play/pause timer progression,
  completed and partially-completed branches, drift auto-pause after 90 idle
  seconds, scrub-from-drift back to paused, caption/transcript disclosure
  semantics, 15-second global arrow shortcuts, and the post-complete bridge link
  render.
- `apps/oshun/web/e2e/arete-streak-recovery.spec.ts` covers the Arete recovery
  handoff into `/tara/sit/when-the-mind-wanders` and observes the completion
  POST to `/v1/tara/sittings/when-the-mind-wanders/complete`.
- `apps/oshun/web/e2e/tara-session-player.spec.ts` covers the newer
  `/domains/tara?...path=` immersive player, including Tara-to-Nisaba, Metis,
  Arete, Nyx, and assistant handoffs. That spec is related Tara journey
  coverage, but it does not replace `/tara/sit/[id]` route coverage above.

## Data & contracts

- **Reads**: `getTara()` from `@/lib/lilith-data/tara` (server). The page
  resolves the `[id]` param against `data.todaysSit.id` and
  `data.sittings[].id`. No client fetch for the session itself.
- **Writes (completion — wired)**: on the state transition into `completed` /
  `partially-completed`, a `useEffect` POSTs once to
  `/v1/tara/sittings/<id>/complete` (`buildOshunBffUrl`, `method: 'POST'`,
  `credentials: 'include'`, headers include `await csrfHeaders()`), body
  `{ durationSeconds: elapsed, reflectionText: reflection || null }` (lines
  122–161). A `completionPostedRef` guard makes it fire exactly once per
  session; the endpoint is idempotent on the sitting id + session. On a thrown
  fetch it falls back to a localStorage queue under key `tara:completion:<id>`
  with the payload plus `queuedAtIso`.
- **Writes (reflection — today)**: the separate reflection **submit** button
  (`submitReflection`) is still a 300 ms stub
  (`await new Promise((r) => setTimeout(r, 300))`, line 252) — verify the real
  BFF route and contract. Add to `libs/contracts/` when wired. (Note: the
  reflection text is already carried to the BFF by the completion POST above.)
- **Writes (offline)**: reflection-submit offline path writes
  `tara:reflection:<sittingId>` localStorage key; the completion POST's offline
  fallback writes `tara:completion:<id>`
- **Realtime**: none today; future: Psyche session envelope push (`session.tick`
  / `session.completed` / `session.drifted`) — verify event names
- **Caching**: SSR per request; SW runtime cache should pin the today-sit HTML
  so the cold-offline launch works; verify cache key includes `[id]`
- **Auth/role check**: middleware enforces signed-in; no role gating; Lilith
  tone policy applies to teacher / transcript copy upstream of this view, not at
  render time

## Cross-references

- Shell: [`shell/01-app-shell.md`](../../shell/01-app-shell.md),
  [`shell/03-pwa-behavior.md`](../../shell/03-pwa-behavior.md),
  [`shell/06-keyboard-a11y.md`](../../shell/06-keyboard-a11y.md)
- Sibling Tara routes: [`tara.md`](./tara.md),
  [`domains-tara-programs.md`](./domains-tara-programs.md),
  [`domains-tara-teachers.md`](./domains-tara-teachers.md),
  [`domains-tara-courses-id.md`](./domains-tara-courses-id.md),
  [`domains-tara-collections.md`](./domains-tara-collections.md),
  [`domains-tara-sounds.md`](./domains-tara-sounds.md),
  [`domains-tara-search.md`](./domains-tara-search.md),
  [`domains-tara-analytics.md`](./domains-tara-analytics.md)
- Component sources: `apps/oshun/web/src/components/lilith/TaraSitPlayer.tsx`,
  `apps/oshun/web/src/components/lilith/TaraSitPlayer.test.tsx`,
  `apps/oshun/web/src/components/TrustSafetyFlagButton.tsx`,
  `apps/oshun/web/src/lib/lilith-data/tara.ts`
- Feature spec: [`V1/features.md`](../../../V1/features.md#tara)
- Architecture: [`V1/ARCHITECTURE.md`](../../../V1/ARCHITECTURE.md#tara)
- Journeys traversing this view:
  - [`journeys/first-tara-sit.md`](../../journeys/first-tara-sit.md) —
    end-to-end first sit; cites this view as the player landing
  - [`journeys/tara-daily-ritual.md`](../../journeys/tara-daily-ritual.md) —
    daily ritual lands here
  - [`journeys/tara-to-nisaba-handoff.md`](../../journeys/tara-to-nisaba-handoff.md)
    — post-complete Nisaba chip is the bridge entry
  - [`journeys/offline-first-time-use.md`](../../journeys/offline-first-time-use.md)
    — reflection queue path exercised here

## Open questions / known gaps

- [x] **Runtime walk (2026-05-29) — walked with a real id:** the earlier
      placeholder-id 404 was expected; walking `/tara/sit/when-the-mind-wanders`
      (a real id from the `/tara` hub: also first-sit, the-breath-observed,
      the-looker, a-small-space, the-body-settled) renders the populated
      `TaraSitPlayer` with transport controls. HTTP 200, no notFound, 0 errors.
      id hit "Page not found" (demo id -> not found; needs a real id to walk
      populated state (h1="Page not found")). Re-walk with a real id from mock
      data to verify the populated player state.
- [x] Keyboard scrub step is aligned with the 15s button labels through
      `SCRUB_STEP_SECONDS = 15`; covered by `TaraSitPlayer.test.tsx` and
      `first-tara-sit-deepening.spec.ts`.
- [x] Voice-speed and ambient-mix sliders are stateful and covered for keyboard
      updates; voice speed is still not wired to a voice-audio backend. Ambient
      mix is wired to the `/v1/tara/sessions/<id>/ambient.wav` audio element.
      Evidence: `first-tara-sit-deepening.spec.ts` presses ArrowRight on each
      slider and polls `aria-valuenow` for the controlled value change.
- [ ] Reflection BFF route + contract not yet defined — add to `libs/contracts/`
      and wire `submitReflection`
- [x] Localstorage `tara:reflection:<id>` queue has no client-side drain on
      reconnect. Evidence: `first-tara-sit-deepening.spec.ts` returns the
      browser online after an offline reflection save and asserts the queued
      eyebrow plus localStorage draft remain. The offline-first-use "queued
      writes flush on reconnect" claim must not be applied to this Tara
      reflection path until a drain is implemented.
- [x] `notFound()` path is reachable and falls through cleanly to the app-level
      404 UI. Evidence: `first-tara-sit-deepening.spec.ts` captures the current
      dev-shell document status as `200` and asserts no player render, 404
      search routing, and browser-back recovery to `/tara`. A Tara-themed 404
      remains a possible product refinement, not a coverage gap for the current
      shipped route.
- [ ] SW precache scope: confirm `apps/oshun/web/src/sw.ts` (or equivalent)
      caches `/tara/sit/<todaysSitId>` so cold-offline launch works for the
      today-sit
- [ ] `End sitting` while still in `not-started` produces `partially-completed`
      with 0% — Playwright verifies the current branch; product intent remains
      open (ledger entry vs no-op).
- [ ] Cross-domain chip links go to domain landing pages, not the specific
      bridge targets — wire `?from=tara-sit-<id>` or use
      `buildTaraNisabaPassageCompanion` directly so the receiving view knows the
      origin
