SageOx · iPad / tablet experience 2026-06-14
Mode

iPad / tablet experience

SageOx is 100% phone-locked today. This exploration designs the iPad as the calm board in the middle of the room — a fundamentally different posture from the phone-as-instrument. Width-driven breakpoints (not Platform.isPad) ensure Split View and Stage Manager are first-class. The loved record button is preserved exactly: it lives on the bottom action bar, pops UP, and is present on every screen in both orientations. The nav rail (iPad) and the nav bar (phone) carry navigation only — never the record action.

Date: 2026-06-14 Status: complete · for review Breakpoints: <600 compact 600–839 medium 840–1179 expanded ≥1180 xl Type scales: in-hand (phone tokens) table-mode 28/500/1.5 (cloud /cast renderer requirement, not native)

1 Nav rail + bottom action bar — the corrected model

The nav rail carries navigation only: Home, Conversations, Bubbles, Settings pinned bottom — no record button. The bottom action bar spans the content column with the record button bottom-center, popping UP, in both landscape and portrait. This is the identical phone layout preserved at expanded width. Both dark and light, both orientations.

Record button states — bottom action bar, bottom-center, pops UP (identical to phone)
Ready
Recording
Paused
Public team
Same button art, same UP pop, 64×64 on the bottom bar (vs 56×56 phone). Nav rail never touches this.
Dark · iPad 13" landscape · nav rail (nav only) + bottom action bar (record pops UP)
9:41
100%
Good morning
3 conversations this week
Product sync — in progress
12:34 elapsed · 4 participants
Design system sync
Today · 47 min · 5 participants
2h ago
Engineering standup
Yesterday · 18 min · 8 participants
1d ago
Quarterly planning
Monday · 1h 22 min
3d ago
Home
Conversations
Bubbles
Profile

Uploads

Design sync
47 / 52 chunks

Available TVs

Conference Room A
Ready
Board Room
Table Mode

Quick join

Light · landscape
9:41
100%
Good morning
3 conversations this week
Design system sync
Today · 47 min
2h ago
Engineering standup
Yesterday · 18 min
1d ago
Home
Conversations
Bubbles
Profile

Quick join

Dark · iPad 13" portrait (≥840pt → two panes + bottom bar)
9:41
100%
Good morning
Design system sync
47 min
2h
Engineering standup
18 min
5h
Home
Chats
Bubbles
Profile
13" portrait ≥840pt → two panes (split preserved). Bottom bar present.
Phone (≤600pt) — identical bottom bar
9:41100%
Good morning
Design system sync
47 min
Home
Chats
Bubbles
Profile
Phone — same bar art, same UP pop
Android delta — M3 navigation rail vs iPad nav rail
Same bottom-action-bar record button on both platforms. The nav rail differs in visual idiom only — not in record button placement.
Android · M3 Expressive nav rail
Home
Chats
Bubbles
Settings
Content…
Active-indicator pill (M3), ~48dp targets, squircle shapes. No FAB — the raised record button IS the primary action.
iPad · Apple sidebar idiom
Content…
Rounded-rect active slot, no label (icon-only at 64px width). Same raised record button on the bottom bar.
Critical for Android engineers: do not replace the raised record button with a Material 3 FAB. The bottom action bar with the raised record button is SageOx's primary action surface on both platforms. The FAB pattern would place it at the bottom-right, disconnecting it from the center-focused bar. Android system back pops the pushed WebView detail back to the list (no custom gesture needed).
Implementor notes — Nav rail + bottom action bar
  • Rail = nav only, always. The nav rail (or sidebar) carries only navigation destinations: Home, Conversations, Bubbles, Settings. The record button is never in the rail. This rule holds in both orientations and all breakpoints above compact. Violation means a second code path for useRecordFlow() that doesn't exist and shouldn't.
  • Bottom action bar persists at all widths >600pt. At expanded+ the bar's content is constrained to maxWidth:720pt centered, but the bar background spans full width. This prevents the record button from stranding at a far edge on a 13" landscape display. The constraint is on the flexDirection:'row' inner container, not the bar background view.
  • Record button: 60×64px at expanded, 48×48px in portrait narrow. The punch-ring gap and mic icon scale with the button. In RN: width:60, height:60, borderRadius:30 + position:'absolute', bottom:28, alignSelf:'center' relative to the action bar.
  • 13" portrait keeps two panes (split preserved). At 1032pt logical, even in portrait, the split view fires. The nav rail stays visible at 52px icon-only. This is the key 13" vs 11" difference — see Scene 8 for the full portrait treatment.
  • Never Liquid Glass. Rail background is theme.surface.default, 1pt border right. No blur, no frosted glass. Stated rule from memory.
  • Android M3 rail vs iPad Apple sidebar: active indicator is a filled pill (M3) vs a rounded-rect slot fill (Apple). Both are icon-only at this width. The critical shared invariant is that the record button lives on the bottom action bar in both cases — never replaced by a FAB, never moved to the rail.
  • Android system back: the predictive back gesture pops the WebView detail back to the list. No custom animation needed. The nav rail does not change during back navigation.
  • One Copper per viewport: "Join meeting" CTA in the Now Rail is Copper. The record button is green (state.success), not Copper.

2 Table Mode — iPad as cast endpoint

Table Mode = the iPad mints and plays the same signed /cast HLS stream a TV receives, full-screen, via expo-video (LandscapeCastFullscreen). The board — transcript, decisions, reactions, layout, typography, motion, light/dark — is rendered server-side by the cloud /cast renderer (sageox-mono) and is dynamic: it responds to room interactions in real time. The mobile app's only job is mint + full-screen play. One stream, multiple endpoints: a TV and the iPad can receive the same cast simultaneously. UX label: "Table Mode" / "Show on this iPad."

Cloud /cast renderer requirement — 1:1 true scale
The /cast renderer must produce transcript copy at this size and weight. Not a native mobile spec.
Person A
Let's say the API freeze is end of this week, and anything after that goes through a formal change request.
28pt / weight 500 / line-height 1.5 — readable at ~1.5m across a table. Speaker label at 20pt above. APCA Lc 75 min on body; Lc 90 on the live line. Column-constrained to ~50 CPL (max-width ~680px in the renderer).
Light — cloud /cast renderer output (glare-resistant default; light/dark chosen by /cast renderer, not native code)
Product planning — Q3
23:41
Recording
LIVE · playing /cast
Board content (layout · transcript · decisions · reactions · motion) rendered server-side by cloud /cast renderer — not native UI. Lives in sageox-mono.
Person A
So the question is whether we lock the API surface before the design review or allow changes to come through
Person B
I think we need to lock it. Every late API change ripples into the mobile client and costs us a sprint.
Person C
Agreed. Let's say the API freeze is end of this week, and anything after that goes through a formal

Decisions

Decision
API surface locked end of week. Changes after require formal change request.
Decision
Mobile client ships v1 without real-time sync; add in v1.1.

Open concerns

Concern
Sprint velocity may not support both API freeze and design review in the same week.

Raised hands

B
Person B
D
Person D

Reactions

👍 7
💡 3
2
Dark — cloud /cast renderer output, dark theme (suitable for dim rooms; /cast renderer chooses theme, not native code)
Product planning — Q3
23:41
Recording
LIVE · playing /cast
Board content (layout · transcript · decisions · reactions · motion) rendered server-side by cloud /cast renderer — not native UI. Lives in sageox-mono.
Person A
So the question is whether we lock the API surface before the design review or allow changes to come through
Person B
I think we need to lock it. Every late API change ripples into the mobile client and costs us a sprint.
Person C
Agreed. Let's say the API freeze is end of this week, and anything after that goes through a formal

Decisions

Decision
API surface locked end of week. Changes after require formal change request.
Decision
Mobile client ships v1 without real-time sync; add in v1.1.

Open concerns

Concern
Sprint velocity may not support both API freeze and design review in the same week.

Raised hands

B
Person B
D
Person D

Reactions

👍 7
💡 3
2
Resolved — single model

iPad is a cast endpoint — it plays the signed /cast stream

The iPad mints a real pre-signed cast.* stream (same path as casting to a TV) and plays it full-screen via expo-video. Cast-proof rule satisfied: the stream is served by a cast.* host. UX entry point: "Show on this iPad" / "Table Mode" — a distinct affordance from the TV cast picker, though it uses the same mint path.

  • One stream, multiple endpoints: a TV and the iPad receive the same cast simultaneously.
  • isPresignedCastHost(url) passes — the cast.* stream URL satisfies the cast-proof contract in .claude/rules/cast-presigned-endpoint-is-the-only-proof.md.
  • Board content (layout, type, motion, light/dark) is server-defined and dynamic — secret sauce stays in sageox-mono, not in native client code.
  • No "cast confirmed" green indicator on the iPad — the iPad is the playback surface, not a remote. The cast-proof verdict applies to the stream URL, not to a UI badge on the iPad itself.
  • Entry affordance is separate from the TV cast picker to avoid conflating iPad-glass with TV-glass in the device list. The picker shows external receivers; "Show on this iPad" is a self-cast.
Implementor notes — Table Mode (cast endpoint)
  • The mobile app's only job: mint + full-screen play. Call POST /api/v1/meetings/{rec_id}/cast/token to mint a cast token, resolve the cast.* stream URL, then hand it to LandscapeCastFullscreen / expo-video for full-screen playback. That's it. No native board layout, no native transcript rendering, no native decisions panel.
  • Board content = server-side, dynamic. The transcript lines, decision cards, raised-hands queue, reactions ticker, layout, typography, motion, and light/dark are all rendered by the cloud /cast renderer in sageox-mono. They change in real time as participants interact. The mobile client draws none of this — it plays a video stream.
  • Legibility (28pt / 500 / 1.5 / 50 CPL) is a cloud /cast renderer requirement, not a native mobile spec. Cross-link to sageox-mono when implementing. The type specs shown in this mockup are requirements the renderer must meet; the mobile app merely plays back what the renderer produces.
  • Light/dark choice belongs to the /cast renderer. The perceptual review finding — light survives glare on bright-lit tables; dark works in dim rooms — is a renderer requirement. The mobile app passes a preference hint at mint time if the API supports it; otherwise the renderer decides. Do not hardcode light/dark in native Table Mode code. Validate on real hardware before specifying in DESIGN.md.
  • APCA contrast targets are renderer requirements. Body copy (28pt): Lc 75 min, Lc 90 on the live line. Large text (64pt title, 72pt timer): Lc 75. Non-text: Lc 45. These are acceptance criteria for the cloud renderer, not for native component styling.
  • Cast-proof contract satisfied. The stream URL is served by a cast.* host, passing isPresignedCastHost(url). The iPad is a genuine cast endpoint. No app-mcxv gap — no fake protocol constant needed, no "never light green confirmed" carve-out (the iPad plays the stream; it does not surface a cast-confirmed badge to the user).
  • One stream, two endpoints. A TV and the iPad can receive the same cast.* stream simultaneously. The mint is shared; each endpoint establishes its own playback session.
  • Table Mode entry point. "Show on this iPad" / "Table Mode" is a distinct affordance on the active recording screen (cast rail or explicit card) — separate from the TV cast picker, which lists external receivers. Tapping it mints the stream and calls LandscapeCastFullscreen. The LandscapeCastFullscreen component in src/components/cast/ is the seed; it already handles the expo-video full-screen path.
  • One Copper per viewport. The stream content is server-rendered; if "End meeting" appears as a native overlay control, it is the one Copper element the mobile app injects. Do not add additional Copper overlays. If "End meeting" is rendered inside the stream itself, the mobile app injects no Copper at all — confirm with the renderer team.
  • Reactions, recording pulse, timer. These are all rendered server-side inside the stream. The mockup shows them as illustrative reference for the renderer team's requirements — they are not native components.

3 Conversations → Recording detail — flagship master-detail

Nav rail (nav only) + two-pane. Conversations list (~3 cols) with selected-row state | CloudReaderView WebView detail (~6 cols) — the responsive web reader fills the detail pane; no native transcript tabs. Empty-right-pane state shown alongside. Both light and dark.

Dark · row selected · detail loaded
9:41
100%
Conversations
Today
Product planning — Q3
47 min · 4 participants
2h
Engineering standup
18 min · 8 participants
5h
Yesterday
Quarterly planning
1h 22 min · 12 participants
1d
Design system sync
34 min · 5 participants
1d
Product planning — Q3
Today · 47 min · 4 participants
web reader · CloudReaderView WebView
Transcript
Notes
Decisions
Person A
The question is whether we lock the API surface before the design review or allow changes to come through after.
Person B
We need to lock it. Late API changes ripple into the mobile client and cost a sprint.
Person C
Agreed. API freeze end of this week. Formal change request after that.
15:58 / 47:02
Light · empty right pane state
9:41
100%
Conversations
Today
Product planning — Q3
47 min · 4 participants
2h
Engineering standup
18 min
5h
Select a conversation
Transcript, notes, and decisions appear here
Implementor notes — Conversations master-detail + WebView detail pane
  • Detail pane = CloudReaderView WebView, not native tabs. The RN WebView component fills the detail column. The web frontend's responsive CSS (cloud reader) renders Transcript / Notes / Decisions tabs and the playback bar. Native code provides only: the pane container, the webview boundary, and the header strip showing title + "web reader" label.
  • Flag for the web team: confirm the CloudReader's responsive breakpoints at intermediate pane widths (~500–900pt logical). The pane is ~62% of the usable screen width at expanded; the web reader must reflow at these intermediate sizes rather than assuming a full-browser viewport.
  • Exception — live-recording detail: when the selected row is a recording currently in uploading status (live meeting), the detail pane renders a native join card instead of the WebView reader. The WebView is for completed recordings only.
  • Nav rail is nav-only. No record button anywhere in the rail. The dashed blue border on the WebView region is a mockup annotation, not a rendered element.
  • Selected-row state: background: surface-2 + 2pt leading border in copper. The playback CTA inside the web reader is also Copper — both are scoped to the same selected item. Together they count as one Copper use per viewport.
  • Pane split ratio: list ~3 cols / detail ~6 cols at 840+. In RN: SplitView with listFlex={3} / detailFlex={6}.
  • Empty right pane: shown when no row is selected. Do not auto-select on first load. On narrow iPad (Split View half <840pt), collapse to single column (see Scene 8).

4 Active Recording on iPad

Single-column centered (max-width ~640pt) — one primary intent, not split. Nav rail present (nav only). Full-width taller live waveform. Record button on the bottom action bar (bottom-center, recording state red fill). Cast/Table-View rail as a right-side column in landscape; collapses above controls in portrait.

Dark · recording state
9:41
● REC
Engineering standup
Design team · public
Recording
08:23
Uploading · 12 of 14 chunks · secure
Home
Chats
Bubbles
Profile

Available TVs

Conf. Room A
Ready
Board Room
Offline

This iPad

Show on this iPad
Table Mode — plays the live /cast stream full-screen on this device
Rotate to landscape
Light · paused state
9:41
Engineering standup
Paused
08:23
Recording paused · audio safe on device
Home
Chats
Bubbles
Profile

Available TVs

Conf. Room A
Ready

This iPad

Show on this iPad
Table Mode — plays /cast stream full-screen
Implementor notes — Active Recording on iPad
  • Single-column centered, max-width 640pt. Recording is one primary intent. Extra horizontal space goes into padding/breathing room, not a second pane. Same principle as the phone — the waveform just runs wider. maxWidth:640, alignSelf:'center', width:'100%' on the recording container.
  • Four trust signals always visible: red dot, "Recording" text label, live waveform (real amplitude bars), and the running timer. All four — redundancy here is intentional per Sacred data policy.
  • Waveform height: 56pt on iPad (vs 34pt phone) — same bar width (2pt) and gap (1.5pt), more bars across the wider display, taller overall. Drive bar height from real expo-av metering level, not faked sine. If real amplitude is unavailable, show the timer alone (no fake waveform).
  • Record button on the bottom action bar. Recording state = red fill + stop icon. Paused state = amber fill + pause icon. Ready state = green fill + mic icon. The nav rail is unchanged regardless of recording state.
  • Cast rail side column: 180pt fixed width. Shows available TVs + "Table Mode / Show on this iPad" card. "End & upload" is the one Copper CTA per viewport.
  • Table Mode entry: the "Show on this iPad" / "Table Mode" card mints a pre-signed cast.* stream (same path as casting to a TV) and starts full-screen playback via LandscapeCastFullscreen / expo-video. The board content is server-rendered by the cloud /cast renderer — the mobile app plays the stream, it does not compose the board natively. This entry point is separate from the TV cast picker (which targets external receivers), but uses the same mint token.
  • Upload status line: "12 of 14 chunks · secure" — always specific, never a spinner. Sacred data: user must know their audio is landing safely.
  • Paused state: amber record button on the bottom action bar (amber fill, pause icon), amber "Paused" label, waveform frozen at 35% opacity, "audio safe on device" reassurance strip. The nav rail shows no state change during recording or pause.

5 Devices — paired TVs + per-TV detail

Paired + nearby mDNS list | per-TV detail (rename, auto-cast policy, density, Forget). Both themes.

Dark · TV detail loaded
9:41
Devices
Conference Room A
Online · AppleTV
Board Room
Offline · Chromecast
Living Room TV
AirPlay · unpaired
Conference Room A
AppleTV 4K · Online · paired 3 months ago
Name
Conference Room A
Auto-cast when recording
Stream density
Available to team
Light · nearby scan state
9:41
Devices
Conference Room A
Online
Discovering local devices via mDNS…
Living Room TV
AirPlay · audio-only? Checking…
Select a device
Rename, cast policy, and pairing options
Implementor notes — Devices two-pane
  • mDNS scan in "Nearby" section: driven by classifyAirplay in src/services/cast-tv/airplay/airplayCapability.ts. Audio-only devices (HomePod, AirPort Express) are hidden before reaching the list — the "Checking…" row reflects the async classify step. Never show an audio-only device as a cast target.
  • Auto-cast segmented control: Off / Ask / Always — maps to the existing PATCH /devices/{id} endpoint. "Ask" is the safe default; "Always" is an opt-in power-user preference.
  • Stream density: 1080 / 720 / 540 — maps to the res field currently hardcoded in src/services/cast-tv/stream-fetch.ts. Phase 3 work extends the cast mint body with the selected density.
  • One Copper per viewport: the selected-row left border uses Copper as a selection indicator (structural, not a CTA). The "Pair" button on the nearby row is secondary style — it demotes so the Copper selection marker remains focal. "Forget device" is danger-outline, not Copper.

6 Join-as-companion — quiet controller

The non-disruptive participant surface: reactions, raise-hand, chat, live insights feed. Mic muted, non-interrupting. iPad layout plus a phone-vignette for comparison (phones are the usual companion device).

Dark · iPad companion view
Product planning — Q3
4 participants · 23:41 · recording
Mic off
React
👍
Agree
💡
Idea
Question
Hand
Live insights
Decision
API surface locked end of week.
Concern
Sprint velocity may not support both.
Chat
Person A
Let's lock it end of week then.
You
+1 — I'll draft the change request template.
Message…
Phone companion — same endpoints, compact layout
Product planning — Q3
● Recording · 23:41
👍
Agree
Raise hand
💡
Idea
Question
Chat
Let's lock it end of week.
Message…
Same join/room endpoints — phone is the usual companion; iPad gets more screen for intel feed + chat.
Implementor notes — Join-as-companion
  • Mic-off is non-negotiable. The companion surface is explicitly non-interrupting. The record button on the bottom action bar is dimmed to 35% opacity and pointer-events:none when in companion mode. The "Mic off" pill is always visible in the header — never hidden, never needs a tap to reveal.
  • Reaction button size: the 4-reaction grid uses flex:1 equal columns. On the phone frame they're larger relative to the screen (the 2×2 grid fills more of the viewport) — this is intentional: reaction buttons must be tappable without looking at the phone.
  • Live insights feed: driven by the same SSE endpoint (GET /meetings/{rec_id}/cast/events) that Table Mode uses. Decisions and concerns surface in real time. On the phone, this is collapsed behind a tab; on iPad it's always visible in a dedicated section.
  • One Copper per viewport: the chat send button is the single Copper CTA. Reaction buttons are surface-2 fills — deliberate secondary style. The active/selected reaction briefly highlights in sage on send.
  • Raise hand endpoint: POST /meetings/{rec_id}/join/hand — the "✋" reaction button calls this, not the generic reaction endpoint. The button label reads "Hand" not "Raise hand" at this density.

7 Compact fallback — Stage Manager narrow window

At <600pt width, the exact phone layout is preserved byte-for-byte: bottom action bar, raised record button. A Stage-Manager-style window containing the phone layout, plus an identical phone-vignette beside it. Proves "replaced" never happens.

Dark · Stage Manager window ≈550pt wide (<600 breakpoint)
SageOx
9:41100%
Good morning
3 conversations this week
Today
Product planning — Q3
Today · 47 min
2h
Engineering standup
Yesterday · 18 min
1d
Home
Conversations
Profile
Settings
Window width ≈550pt → useBreakpoint() returns compact → phone layout, bottom action bar, record pops UP. No sidebar rendered.
identical to
Phone (<600pt · unchanged)
9:41100%
Good morning
3 conversations this week
Product planning — Q3
47 min
Home
Conversations
Profile
Settings
Phone <600pt — identical layout, record pops UP
Implementor notes — Compact fallback
  • Width-driven, not device-driven. useBreakpoint(width) returns compact when width < 600. This fires on: phone (always), iPad in a Stage Manager window narrowed below 600pt, iPad in a Split View half at any size below 600pt. Platform.isPad is never consulted for layout decisions — it gives the wrong answer in all multitasking scenarios.
  • "Loved phone layout preserved byte-for-byte" means: the ActionBar component renders unchanged, useRecordFlow is called unchanged, the record button's translateY(-28) pop is unchanged. The iPad layout changes are gated at expanded+; compact is a pass-through with zero new code paths.
  • Stage Manager window chrome (traffic lights) is mockup-only illustration. It represents any narrow window, not just Stage Manager specifically — Slide Over, Split View 1/3, and Stage Manager small all hit the same <600pt breakpoint.
  • One Copper per viewport: the record button is green (state.success). No Copper element is present in the compact Home view — the Copper CTA appears only when a user initiates a task (recording, joining). This is correct for the idle Home screen.

8 Portrait variants — first-class, width-driven

Portrait is first-class: 13" portrait (~1032pt) keeps two panes (split preserved) + bottom action bar. 11" portrait (~834pt) drops to single-column push (Bear collapse). Width-driven via useBreakpoint(), never Platform.isPad. Right detail pane = CloudReaderView WebView. Bottom action bar spans the content column in both states.

Dark · 13" iPad portrait (~1032pt) — two panes preserved · width ≥840pt
9:41
100%
Conversations
Today
Product planning — Q3
47 min · 4 participants
Engineering standup
18 min · 8 participants
Yesterday
Quarterly planning
1h 22 min
Home
Chats
Bubbles
Profile
Product planning — Q3
Today · 47 min · 4 participants
web reader · CloudReaderView WebView
Transcript Notes Decisions
Person A
The question is whether we lock the API surface before the design review or allow changes.
Person B
We need to lock it. Late API changes cost a sprint.
15:58 / 47:02
13" portrait (≥840pt): list + detail side by side, same as landscape. Bottom action bar spans the content column at max-width 720pt centered. No collapse, no push — free rotation with landscape is the same two-pane split.
11" iPad portrait (~834pt) — single-column push (Bear collapse, <840pt threshold)
Portrait · list (no selection)
9:41
100%
Conversations
Today
Product planning — Q3
47 min · 4 participants
Engineering standup
18 min · 8 participants
Yesterday
Quarterly planning
1h 22 min
Home
Chats
Bubbles
Profile
Portrait · detail pushed over list
Conversations
Product planning — Q3
Today · 47 min · 4 participants
web reader · CloudReaderView WebView
Transcript Notes Decisions
Person A
The question is whether we lock the API surface before the design review or allow changes.
Person B
We need to lock it. Late API changes cost a sprint.
15:58 / 47:02
Home
Chats
Bubbles
Profile
Implementor notes — Portrait variants
  • Width-driven, not device-driven: portrait two-pane vs. single-column is decided by useBreakpoint(width) — never by Platform.isPad. An 13" iPad in portrait (~1032pt) is ≥840pt and holds two side-by-side panes. An 11" iPad in portrait (~834pt) is borderline and collapses to single-column push (Bear pattern) at the 840pt threshold. Landscape is always two panes on iPad at any size (≥840pt). Free rotation = layout is always width-computed on every frame.
  • Bear 3-pane collapse pattern (11" portrait): the right pane is not rendered alongside the list — it slides over with a standard push transition (react-native-screens stack navigator inside the detail slot). The back affordance uses the Copper back-chevron + "Conversations" label. The nav rail persists as a sibling to the stack, fixed at 52px, and does not slide with the push.
  • Nav rail stays visible in portrait at the narrow 52px icon-only width — nav items only, no record button. The nav rail does not hide behind a hamburger at any iPad breakpoint.
  • Bottom action bar persists in every portrait state: both the list state and the detail-pushed state show the bottom action bar with the raised record button at bottom-center. The bar sits below the stack content column, not below the nav rail (which has no bar). In 13" portrait where two panes coexist, the bar spans both panes at the same max-width constraint as landscape (720pt centered).
  • Detail pane = CloudReaderView WebView: the pushed detail (and the right pane in 13" portrait) is a WebView rendering the responsive web reader. Tabs (Transcript / Notes / Decisions) and the playback scrubber are rendered inside the web reader, not as native components. Flag for web team: verify the reader's responsive CSS at intermediate pane widths (~400–900pt) covering the range from an 11" single-column push to a 13" portrait detail pane.
  • Back chevron color: Copper (var(--p-copper)). This is the one Copper element in the 11" portrait detail state — it is the primary actionable affordance (returning to the list). The web reader's playback CTA is also Copper, rendered inside the WebView — both are in the same detail viewport, satisfying the "one Copper per viewport" rule as a single related action group.
  • Transition: tapping a row does a standard horizontal push (list translates left, detail translates in from right). No modal, no sheet. In RN this is a Stack.Screen inside the detail slot of the SplitView primitive.

9 First-run onboarding on iPad — tablet reflow

The 6-step phone onboarding flow (Welcome → solo/team branch → see-it-work example → what-to-capture → land-on-Home → lazy-mic) reflowed for a larger canvas. Steps 1 + 2 (Welcome + branch) share one iPad screen; step 3 (example loop) gets the full-width canvas. Steps 4–6 follow the phone layout. Aligns with the onboarding worktree — does not redesign the flow.

Dark · Steps 1+2 — Welcome + branch on one canvas
SageOx
Your team's discussions, turned into context your AI coworkers can use.
Example · what SageOx captures
"Let's ship the cast retry fix Thursday, and make the transcript export public…"
Decision
Ship cast retry fix Thursday
Summary
3 bullets extracted automatically
Transcript
Full verbatim · searchable
Your AI coworker can prime on this.
Nothing to do yet — this is a real example from a team like yours.
How will you use SageOx?
We'll tailor the next step.
🧑‍💻
Solo
Just me, for now
👥
With a team
Join or create a team
Already have an account? Sign in
Steps 3–6 follow phone layout unchanged — shown as phone-vignettes for reference
3 · See it work
Example
See what SageOx captures
Real discussion, turned into context
"Ship cast retry fix Thursday…"
Decision · ship Thu
Summary · Transcript
Unchanged from phone · already the right density on mobile
4 · What to capture
What's worth capturing?
Anytime a conversation holds a decision or context worth keeping.
🧭 A decision you just made
🎨 A design or planning chat
🗣 A standup or 1:1
No pressure to record now.
5 · Land on Home
Welcome back
Get started
✓ Saw how it works
◦ Capture your first discussion
💡 Tap here when a discussion is worth keeping
6 · Lazy mic
🎤
Capture this discussion?
SageOx records and securely streams your audio to your private workspace.
Allow microphone
Not now
Implementor notes — First-run onboarding on iPad
  • Coordinates with onboarding worktree (granola-docs-onboarding-gaps). That worktree owns app/onboarding/first-run.tsx, components/Coachmark.tsx, and the telemetry client that reports solo/team choice to cloud. This iPad reflow is a tablet-responsive variant of that same screen — not a new screen. Implement as a breakpoint branch inside first-run.tsx: at expanded+, render the two-column Welcome+Branch layout; at compact/medium, render the phone filmstrip unchanged.
  • Steps 1+2 merged onto one canvas on iPad. The phone shows Welcome (step 1) full-screen then navigates to Branch (step 2). On iPad, both fit side-by-side: Welcome + capture→context example on the left, Branch choices on the right. The user never sees a transition between these two steps — they land on the full picture. This is the main tablet-specific change.
  • Capture→context example (step 3 on phone) is shown on the Welcome panel (left side) on iPad. It's shown large and early — the "show, don't force" principle works better with space. On phone it's step 3; on iPad it's visible at step 1. Do not duplicate the screen; the data is the same seeded example.
  • Steps 3–6 unchanged. The phone filmstrip for steps 3 (see it work), 4 (what to capture), 5 (home + coachmark), and 6 (lazy mic) renders unchanged on iPad. Extra white space at medium/expanded widths is acceptable — these steps are short and centered. Do not pad them artificially.
  • Telemetry: solo/team choice. The onboarding worktree reports this as a coarse enum to the cloud. The iPad tablet reflow does not change this signal — it fires at the same point (tapping Continue with a branch selected), regardless of which layout was shown.
  • One Copper per viewport: the "Continue" CTA is the only Copper element on the Welcome+Branch canvas. The solo/team border on the selected choice is copper as a selection indicator — that is the same Copper role (selection + action are unified), same rule as Scene 3.
  • Light variant: toggle Mode to light to verify — the example card uses surface backgrounds which render correctly in both modes via data-pmode vars. The wordmark uses p-copper which is lighter in dark mode, darker in light mode per the pmode token set.