---
path: /legal/privacy
surface: customer
domain: onboarding-public
auth: anon
source: apps/oshun/web/src/app/legal/privacy/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; manual
  screen-reader and real telemetry-delivery checks pending. Evidence:
  WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content re-verified
  2026-06-03 against current source; 2026-06-28 public-legal-pages Playwright
  pass covered anonymous routing, exact section inventory, legal nav
  matrix/active states, TOC/back-to-top/print shell controls, 390 px
  no-overflow, 44 px touch targets, offline-after-load stability, no telemetry
  hooks, and axe. Manual AT pass and real telemetry delivery remain pending.'
---

# Legal · Privacy Policy

## Purpose

OSHUN's full privacy policy, rendered in the Lilith manuscript register (cream
paper, terracotta accent, Cormorant Garamond serif). Fifteen titled sections
delivered as a single `<article>` inside `LegalPageShell`, which adds cross-page
legal nav, scroll-spy TOC, back-to-top, and a print stylesheet.

## Entry points

- Footer link "Privacy Policy" on `/welcome` and `/landing`
- Sidebar nav inside any other `/legal/*` page (the shell renders a fixed
  `LEGAL_NAV` list)
- Direct URL / bookmark — yes (anon)
- Linked from in-product privacy controls (e.g., profile settings, cookie
  consent banner)
- Email footer "Privacy" link

## Layout regions

`page.tsx` declares a `SECTIONS` array of 15 anchor ids and wraps the article
body in `<LegalPageShell sections={SECTIONS}>`. All visual styling is injected
by the shell as a `<style>` block; the article itself uses only semantic HTML
and `data-legal-date` / `data-legal-table-wrap` / `data-legal-mono` hooks.

- **Header (shell)**: minimal `[data-legal-header]` with brand link and a back
  link (rendered by `LegalPageShell`)
- **Skip link**: `[data-legal-skip-link]` jumps to main content
- **Sticky sidebar**: `[data-legal-sidebar]` — cross-legal nav (7 entries) plus
  in-page TOC button list ("On this page")
- **Main article**: `[data-legal-content] article` — h1 "Privacy Policy",
  `data-legal-date` "Last updated: May 26, 2026", then 15 `<section>` anchors
  (`introduction`, `information-we-collect`, `how-we-use`, `data-sharing`,
  `data-retention`, `data-security`, `your-rights`, `regional-notices`,
  `dsar-companion`, `data-residency`, `moderation-reporting`,
  `international-transfers`, `childrens-privacy`, `changes`, `contact`)
- **Floating control**: `[data-legal-back-to-top]` — 44px circular button fixed
  bottom-right, fades in after 300px scroll

## States

- [x] **Loading** — N/A; static server component, no async data fetched
- [x] **Default (populated)** — all 15 sections render with serif body type
- [x] **TOC click / active section** — clicking a TOC button scrolls to that
      section and marks it `data-active="true"`; passive scroll-spy remains
      driven by `IntersectionObserver` in `LegalPageShell`
- [x] **Back-to-top visible** — appears once `window.scrollY > 300`
- [x] **Narrow viewport (≤768px)** — sidebar collapses to a static block above
      the article; in-page TOC section is hidden via `display: none !important`
- [x] **Print** — `@media print` removes sidebar, header, back-to-top; switches
      background to white, text to black, expands the article to full width, and
      prints anchor hrefs after each link
- [x] **Error (unrecoverable)** — N/A; no data dependencies, only segment
      `error.tsx` would catch
- [x] **Offline after load** — once the route is loaded, the URL, heading,
      section inventory, and active legal nav state remain stable when the
      browser context goes offline
- [x] **Gated** — N/A; anonymous-readable

## Interactions

### Cross-legal sidebar nav (`[data-legal-nav-link]`)

Seven `<Link>` entries hardcoded in `LegalPageShell`'s `LEGAL_NAV`: Privacy
Policy, Terms of Service, Cookie Policy, Accessibility, CCPA, DPA, Lilith Terms.

- [x] **Each sidebar nav link** (link)
  - Function: navigates to the sibling legal page
  - Active styling: `data-active` attribute when `pathname === href`
    (italicised, terracotta, left-border accent)
  - Hover styling: paper background on `onMouseEnter`
  - Keyboard: Tab focuses; Enter activates
  - Touch target: ≥ 44 px height and width on the 390 px mobile viewport

### On-this-page TOC (`[data-legal-toc-link]`)

Fifteen `<button>` elements, one per `SECTIONS` entry.

- [x] **Each TOC button** (button)
  - Function: calls `scrollToSection(id)` →
    `el.scrollIntoView({ behavior: 'smooth', block: 'start' })`
  - Active styling: terracotta + left border when clicked immediately or when
    `IntersectionObserver` flags that section as intersecting
  - Hover: paper background
  - Keyboard: Tab into list, Enter / Space activates
  - Hidden when viewport ≤ 768px

### Back-to-top button (`[data-legal-back-to-top]`)

- [x] **"↑" back to top** (button, `aria-label="Back to top"`)
  - Function: `window.scrollTo({ top: 0, behavior: 'smooth' })`
  - Visibility: opacity 0 with `pointer-events: none` until
    `window.scrollY > 300`, then opacity 1
  - Position: `fixed; bottom: 28; right: 28; 44×44`

### Body links

Anchors inside the article use the shell's serif/terracotta-underline style. The
visible link here is an inline mailto-style reference to `privacy@oshun.app`
rendered as plain text, not an anchor element — verify during walk whether it
should be clickable.

## Data & contracts

- **Reads**: none — static text only
- **Writes**: none
- **Realtime**: none
- **Caching**: server-rendered static; loaded-route offline stability is
  asserted. Cold offline navigation / per-legal SW path caching is not asserted,
  and current `sw.js` does not path-cache `/legal/*`.
- **Auth/role check**: anonymous; each legal page is listed individually in
  middleware `PUBLIC_PATHS` (e.g. `/legal/privacy`, `/legal/terms`,
  `/legal/cookies`, `/legal/accessibility`, `/legal/ccpa`, `/legal/dpa`) — there
  is no `/legal` prefix entry
- **Metadata**: `title: 'Privacy Policy'`,
  `description: 'How OSHUN collects, uses, and protects your personal information.'`

## Cross-references

- Shell: [`shell/01-app-shell.md`](../../shell/01-app-shell.md)
- Sibling legal pages (all share `LegalPageShell`):
  - [`legal-terms.md`](./legal-terms.md)
  - [`legal-cookies.md`](./legal-cookies.md)
  - [`legal-accessibility.md`](./legal-accessibility.md)
  - [`legal-ccpa.md`](./legal-ccpa.md)
  - [`legal-dpa.md`](./legal-dpa.md)
  - [`legal-lilith.md`](./legal-lilith.md) (uses `LegalPage`, not
    `LegalPageShell` — different chrome)
  - [`legal-lilith-privacy.md`](./legal-lilith-privacy.md) (also uses
    `LegalPage`)
- Component sources:
  - `apps/oshun/web/src/components/legal/LegalPageShell.tsx`
  - `apps/oshun/web/src/design-system/lilith` (tokens: `LV`, `SERIF_L`,
    `MONO_L`)

## E2E coverage

- Backed by
  [`apps/oshun/web/e2e/public-legal-pages.spec.ts`](../../../apps/oshun/web/e2e/public-legal-pages.spec.ts)
  — covers the public legal route matrix with anonymous access, exact section
  inventory, legal nav inventory/active state, TOC click and active state,
  back-to-top behavior, print chrome suppression, 390 px no-overflow, 44 px
  nav/back-to-top targets, offline-after-load stability, no telemetry hooks, and
  axe.

## Open questions / known gaps

- [ ] Cold offline navigation / per-legal SW cache is not asserted and current
      `sw.js` does not path-cache `/legal/*`; PWA shell cache coverage remains
      in the offline-first / PWA walkthroughs.
- [ ] Body contains a plain-text email reference (`privacy@oshun.app`) — confirm
      intent: should it be a mailto link?
- [ ] `LAST_UPDATED` string is hard-coded (`'May 26, 2026'`) — confirm the
      update process when policy changes
- [ ] No "I accept" or consent affordance on this page; consent UX lives on the
      cookie banner shell — verify scope expectation
- [ ] No localisation; en-US only — confirm whether i18n is in scope
