SageOx mobile · today — what the native app actually does · 2026-05-18

Mode

1. Splash & onboarding

/ src/app/index.tsx

Atmospheric, cinematic, calm — the brand premise in one breath. Canonical wordmark in the upper third with a soft luminous sigil behind it; Oxy as a bottom-anchored silhouette watermark, not a character; spaced-uppercase tagline; a single Copper CTA pinned to the thumb zone. Mode-invariant composition: dark variant ships by default, light is a paper-warm twin of the same structure. No animation. The wordmark, the halo, and Oxy are the brand statement — copy is held to seven words.

Dark · cinematic launch
9:41
SageOx
Capture every
consequential conversation
Light · paper-warm, Oxy visible
9:41
SageOx
Capture every
consequential conversation
Implementor notes
  • Center-weighted composition, generous negative space. Wordmark in the upper third with a luminous sigil halo behind it, Oxy bottom-anchored as a soft silhouette watermark, CTA cluster pinned to the thumb zone. Eye lands on the wordmark first, the tagline second, the CTA last.
  • Oxy is a watermark, not a character. 240px, ~42% opacity in dark / ~70% in light, with a soft sage-tinted glow halo behind. Sits below the wordmark cluster and bleeds into the gradient — Oxy is part of the brand, not a mascot taking center stage. (Per the 2026-05-18 splash brief: "Oxy is part of the brand, not a character taking center stage.")
  • The sigil is a luminous halo (radial sage gradient at low opacity) above the wordmark. Carries the "atmospheric / instrument-driven" feeling without illustration. Light variant warms toward a copper-sage blend.
  • Starfield specks are six 1×1px radial gradients placed in the upper half of the canvas. At 10–20% opacity they are barely perceptible — they exist to make the gradient feel inhabited.
  • Tagline in spaced uppercase (CAPTURE EVERY · CONSEQUENTIAL CONVERSATION) — 10px, 0.22em letter-spacing, broken onto two lines. One of the very few all-caps treatments we permit; it earns it as a brand-typographic moment, not a UI label.
  • Wordmark is mode-aware. Dark variant uses wordmark-dark.svg; light uses wordmark-light.svg. The wordmark itself flips with the canvas — the brand canon was designed for that.
  • Light variant is paper-warm, not sterile white. radial-gradient(ellipse at 50% 28%, #f6f1e4 0%, #ebe3cf 75%). Oxy reads more clearly against the lighter background.
  • One Copper element only: "Get started" pill (--copper-600, 4.6:1 contrast with white). "I have an account" is text-only secondary.
  • No animation on first launch. Idle screens spend zero battery. The Scribe device follows the same rule. If we ever animate this surface, it's the sigil that breathes — never the wordmark or Oxy.

2. Sign in

/login src/app/login.tsx

Replaces the current inline TextInput blocks with the proposed Input primitive (label above field, focus ring, error helper). Generous vertical rhythm; the Copper "Sign in" button is the only Copper element. Email auth is foreground; OAuth is below the fold because it's secondary in our team's reality.

Dark
9:41
SX

Welcome back

Sign in to your team's workspace.
Email
ryan@sageox.ai
Password
•••••••••••Show
Use 12+ characters. Avoid words.
New to SageOx? Create an account
Light · error state
9:41
SX

Welcome back

Sign in to your team's workspace.
Email
ryan@sageox.ai
Password
•••••Show
⊘ That password doesn't match. Reset?
New to SageOx? Create an account
Implementor notes
  • Label above field, never inside. DESIGN §10: placeholder-as-label fails when the user starts typing. The label stays visible.
  • Focus ring is Copper at 18% alpha + Copper border. One Copper element, applied via state (focus), not chrome.
  • Error is color + shape + label. The glyph carries meaning even with red-green color blindness. Per §11 Rule 3.
  • Caret blink pauses in Calm mode. Toggle ● Calm to verify.

3. Home — Today

/(tabs) src/app/(tabs)/index.tsx

Reframed from "activity feed" to Today — a single screen that tells you what's happening, what's still cooking, and what's at the edge. The persistent Instrument Bar at the top is the most-violated Apple principle in the current app: it brings the recording session out from behind a tab and into the user's peripheral vision. Recent recordings are ListRows with quiet StatusBadges; suppressed-state cards (Ready) drop the badge entirely per loading-processing-and-status.md.

Implementor notes
  • Today > Activity. The current "recent activity feed" is generic. "Today" creates a frame the user can fail or succeed at — and the most important thing inside that frame is "is anything recording right now?" Hence the Instrument Bar.
  • Instrument Bar replaces the modal sheet when a recording is in flight. Tapping it lifts the recording detail sheet without unmounting the home stack — preserves position when dismissed.
  • Ready recordings carry no badge. Per loading-processing-and-status.md, terminal success is suppressed. The presence of the row is the success.
  • One Copper per screen: the central Record tab. Public-tag pill is teal, not copper — public is its own signal (DESIGN §3).
  • Slug-line badges (10.5px, muted) demote the team/tag chip — borrows directly from KB v2's plain-language hierarchy. Recording title is the headline; everything else is a slug.

3a. widget — InstrumentBar

src/components/InstrumentBar.tsx (new)

The persistent edge-status component. Sits just below the safe-area inset on every authenticated screen, showing the live recording session state. Calm + present, never modal. Five visual states; tapping any non-idle state lifts the recording detail sheet.

Idle · default
Nothing recording
all clear
Recording
Sprint planning
12:47 · chunks 38/38
Uploading
Uploading "Sprint planning"
chunks 38/47 · 3 min
Processing
Processing 2 recordings
transcript · notes · decisions
Offline — queued
9 chunks queued — offline
will resume on reconnect
Implementor notes
  • This is the Sacred-data invariant made visible. If the user can always see "9 chunks queued — offline", they trust the app. The current build hides this until they open the recording detail.
  • Idle is dashed border, no fill. Quiet by default. Present, but not nagging.
  • Recording uses the --rec-red dot (#EF4444) — universal, not theme-dependent. Same hex on the Scribe device.
  • Tap target is the full bar. Min height 40pt, max 48pt. Sits in the title area so it doesn't crowd content.

3b. widget — StatusBadge

src/components/StatusBadge.tsx (new)

Color + shape + label — never color alone (DESIGN §11 Rule 3). Suppresses terminal success states ("Ready" carries no fill, just a quiet outline). Failed uses a triangle so it's legible even with red-green color blindness.

Pending
Pending
Uploading
Uploading
Processing
Processing
Ready (quiet)
Ready
Failed
Failed
Live
Recording
Public recording
Public
Implementor notes
  • Five shapes: circle (uploading, ready, live), diamond (processing, public), triangle (failed). Shape stays consistent across both themes.
  • Ready is intentionally quiet — no fill, neutral border, muted text. In list contexts, prefer no badge at all per loading-processing-and-status.md. Use this variant only when the list mixes terminal + in-flight states.
  • Public uses teal #14B8A6 — universal across themes, reserved for "publicly accessible." Never mix with Copper.

4. Record — Setup

/team/[teamId]/record src/app/team/[teamId]/record/index.tsx

Friction kill. Default to the user's last team and let them go. Title is optional, mic permission is checked silently, and the Copper "Start recording" pill is the only thing competing for attention. Public-recording toggle is collapsed by default (Linear's "metadata collapses" principle) and reveals on tap.

Dark
9:41

New recording

SX
SageOx · Engineering
7 members · 2 paired devices
Change ›
Title — optional
Sprint planning — Mar 17
We'll auto-name it from the transcript if you leave it blank.
Auto-pause at 4h
Public recording
Anyone with the link can listen.
Mic ready · iPhone built-in
Start recording
Light · team switcher open
9:41

New recording

SX
SageOx · Engineering
7 members · 2 paired devices
Implementor notes
  • Default team is sticky. 80%+ of recordings go to the same team. Don't ask twice.
  • Mic check is inline, not a permission modal. The dot turns red if permission is missing; tap reveals the system permission flow. No popup-as-greeting.
  • Public toggle uses teal swatch glyph, not Copper. Mirrors the StatusBadge color.
  • Team switcher is a Sheet, not a center modal. Pull-to-dismiss handle. The dimmed background preserves spatial continuity.

4b. Record — into a Knowledge Bubble Soon

/kb/[bubbleId]/record src/app/kb/[bubbleId]/record/index.tsx

Same Setup surface, different destination. Knowledge Bubbles are the SageOx scope-and-share unit — personal, team, repo, or custom. Recording into a bubble files the conversation under that scope and inherits its access list. The destination picker reveals the four bubble types with their canonical glyphs (KB v2 vocabulary) and a recent-bubbles row above the picker.

Dark · destination chosen
9:41

New recording

RS
@ryan/research
Personal bubble ·17 conversations
Change ›
Title — optional
Untitled
Bubbles auto-name recordings from the transcript.
Inherit bubble access
Visible to anyone who can read this bubble.
Mic ready · iPhone built-in
Start recording
Light · bubble picker open
9:41

New recording

RS
@ryan/research
Personal bubble
Implementor notes
  • Knowledge Bubbles are coming soon. The Setup surface stays the same shape; only the destination picker changes — instead of "which team?" it asks "which bubble (or team)?" Bubble glyphs follow KB v2's kb-bubble avatar convention: 9pt radius, single-fill, monogram, type-coded color (personal = copper, team = sage, repo = charcoal, custom = violet).
  • Access inherits the bubble. No separate visibility toggle — recording into @ryan/research means only @ryan sees it. Recording into #engineering means the engineering team sees it. Predictability over toggles.
  • The picker sheet is unified across destination types. Bubbles + Teams in one sheet. KB-coded section labels in sentence-case-uppercase (the only all-caps we still permit, since these are namespace markers).
  • "Soon" tag is Copper-text-only, no fill. The tag is the only Copper element on this section's heading row; the Start pill on the in-phone surface is the one Copper element per viewport.

5. Record — Active

/team/[teamId]/record/active src/app/team/[teamId]/record/active.tsx

The hero surface. A chronograph, not a form. A 56-pt monospace timer with smooth digit transitions, a 24-bar live audio sparkline, and a Sacred-data telemetry strip below: chunks · queued · last receipt · network. The single Copper element is the Stop pill — Pause and Mute are quiet ghost circles. Status arc at the top borrows from the Scribe speaker (DESIGN §6) — "RECORDING" in spaced uppercase, with the universal red dot pulsing at 1200ms cadence.

Dark · live
9:41

Sprint planning

Recording
12:47.3
SageOx · Engineering · 3 in room
Time
12:47
Size
3.7 MB
Quality
HD · 16k
Status
✓ Saved
Mute
Stopspacer
❘❘Pause
Light · paused
9:41

Sprint planning

❘❘ Paused
12:47.3
SageOx · Engineering · tap Resume to continue
Time
12:47
Size
3.7 MB
Quality
HD · 16k
Status
Paused
Mute
Stop
Implementor notes
  • Timer is the centerpiece. 54px JetBrains Mono with tabular numerics — digits don't jiggle as they tick. The .3 tenths are not there for precision; they make the timer feel alive without animation.
  • The status arc replaces the page title. Per the Scribe device — "every screen answers one of three questions, and this one is always 'Am I recording?'"
  • Sacred-data telemetry strip is the trust-builder. When you can see queued: 0 in tabular monospace, you trust the upload pipeline. This is what's missing in the current build.
  • Stop is Copper, Pause is Sage outline. Stop is destructive enough to deserve the only Copper element. Pause is reversible; doesn't earn Copper.
  • Paused state freezes the sparkline at 35% opacity. Audio sampling stops; visual continuity preserved. No "all bars at 0" — that reads as broken.
  • Calm mode pauses the bar wave after 3 loops (locked at 75% scaleY). Recording continues; only the visual loop quiets.
  • ★ Bold move — shared element transition from InstrumentBar. Tapping the persistent InstrumentBar on Home should expand into this surface: the IB icon morphs into the central pulse, the IB primary text becomes the title row, the IB meta becomes the chronograph timer. One shared element transition (350ms slow tier, gentle spring damping 18 / stiffness 180), not a stack push or modal sheet. Implemented in RN via react-native-reanimated SharedTransition or expo-router shared-element transitions. This is the screenshot moment — touching the bar is touching the instrument.

5a. widget — RecordingPulse

src/components/RecordingPulse.tsx (new)

Central record-button control across four states. Pulse ring expands 0.85→1.4 over 1200ms (mirrors the Scribe firmware exactly). Idle is a hollow outline; recording is filled red + square stop glyph; paused is amber with two bars; uploading is info-blue with up-arrow.

Idle
Recording · pulse
Paused
Uploading
Implementor notes
  • Pulse spec: ring scales 0.85→1.4, opacity 0.5→0, duration 1200ms, easing cubic-bezier(0.4,0,0.2,1), loop infinite. Same numbers as Scribe firmware so the device + phone feel like one product.
  • Reduced-motion fallback: drop the ring; keep the filled red circle. Recording is still unambiguous.
  • Calm mode pauses the ring animation at the smallest visible scale.

6. Recording detail

/team/[teamId]/recordings/[recId] src/app/team/[teamId]/recordings/[recId].tsx

Lifecycle made visible as a quiet stepper at the top — Captured · Uploaded · Processed · Ready — Sage for done, Copper for active, neutral for pending. Then a scrubber, then content tabs (Transcript / Notes / Decisions). The transcript uses the KB-v2 demoted-byline pattern: speaker name + timestamp slug, content at full weight.

Dark · ready
9:41

Sprint planning

SageOx · Engineering  ·  Mar 17 · 12:47 min · 3 speakers
Captured
Uploaded
Processed
Ready
4:02 / 12:47
RS
Ryan Snodgrass · 0:14
Let's lock the goal for this sprint — recording reliability first, then live cast.
EM
Emily M. · 0:31
Agreed. I'd add the Sacred-data telemetry surface — we need the user to see chunks landing.
DR
Devon R. · 1:02
+1. Can we get a chronograph view on the Active screen — pull the speaker pulse vocabulary?
Light · decisions tab
9:41

Sprint planning

SageOx · Engineering  ·  Mar 17 · 12:47 min
Captured
Uploaded
Processed
Ready
4:02 / 12:47
Recording reliability is the sprint priority over Cast.
decided by Ryan · 0:48 → 1:14
Add Sacred-data telemetry strip to the Active recording screen.
decided by Emily, Ryan · 1:24 → 2:01
Adopt chronograph vocabulary from the Scribe device on mobile.
decided by Devon · 2:14 → 2:41
Implementor notes
  • Stepper as status, not progress. Each stage is a state, not a percentage. When Ready, all four bars are Sage — the screen tells you "this is done." During processing, the active step is Copper (the only Copper element).
  • Decisions get a Copper check. Decisions are the most actionable artifact — they earn the accent. Notes and transcript share the same hierarchy as the rest of the app.
  • Speaker bubbles use 6px radius, not 50%. KB v2 monogram-bubble carry-over — flatter, more architectural than circles.

10. Devices

/(tabs)/devices · /devices/[devId] src/app/(tabs)/devices/index.tsx · src/app/devices/[devId].tsx

Paired Scribe devices — list with online dot + last-seen timestamp; detail with quiet key-value rows, firmware status, and the "doesn't exist yet" empty state (dashed border, sage CTA, no Copper) when you have none. Pair button is Copper — the one primary action.

Dark · paired list
9:41

Devices

+
Paired · 2
Add a device
+
Pair a new Scribe
Open the Scribe's pairing screen and tap below to scan its code.
Scan QR to pair
Light · device detail
9:41

Ox · kitchen

Online
Last recording 4 minutes ago
Team
Engineering ›
Pairing
RS · 4 days ago
Firmware
v1.4.0 up to date
Battery
86% · charging
Network
sageox-5g · -42dBm
Implementor notes
  • The device "portrait" on detail is intentionally photographic — a small 3D moment that says "this is your physical object." Carries the brand into the digital surface.
  • Online dot uses success-500, not Copper. Status is its own semantic; Copper is action.
  • Unpair is danger-ghost, not Copper. Destructive actions never wear Copper — Copper is reserved for the page's affirmative primary action.
  • Empty state reuses the dashed-border pattern and pairs it with a sage CTA — the "doesn't exist yet" canon.

11. Notifications

/notifications (new) src/app/notifications.tsx (proposed)

Replaces the current native Alert.alert() calls scattered through the codebase. A toast layer (transient) and an inbox tray (persistent). Three semantic variants: success (sage tick), error (red triangle), info (blue dot) — each with a 3px left-border so the user can identify variant at a glance without parsing the icon.

Dark · inbox
9:41

Notifications

Today
"Sprint planning" is ready
Transcript, notes & 4 decisions · 9:58a
Emily mentioned you in a decision
"Sprint planning" · 9:42a
Could not finish uploading "Standup"
9 chunks still queued · tap to retry
Earlier
Devon invited you to "Cast variants"
Yesterday · 4:14p
Ox · kitchen firmware updated to v1.4.0
Mon · 7:02a
Light · live toast (transient)
9:41

Today

RS
Nothing recording
all clear
Today
"Sprint planning" is ready
Transcript, notes & 4 decisions ready to view
Open
Implementor notes
  • Toast is inline, never blocking. Replaces native Alert.alert() — those interrupt; toasts inform. The user keeps using the app.
  • 3px left-border carries the variant. Even with the icon hidden (Calm mode, accessibility), the variant is identifiable.
  • Error toast doesn't show a Copper "Retry" button — the toast itself is tappable, and the inbox detail offers retry. Don't force a binary choice in a transient surface.

12. Account & Settings

/account src/app/account.tsx

Theme picker as the proposed Segment primitive (replaces the current inline segmented control). The remaining list is quiet: ListRows grouped by section, sign-out is a danger-ghost at the foot. No Copper — Account is not a primary-action surface.

Dark
9:41

Account

RS
Ryan Snodgrass
ryan+ox@sageox.ai
Appearance
Theme
System follows iOS appearance. The instrument bar quiets when you're using Calm mode.
Account
Sign out
v0.18.4 · build 217 · 2026-05-18
Light · Calm mode on (settings preview)
9:41

Account

RS
Ryan Snodgrass
ryan+ox@sageox.ai
Appearance
Theme
Calm mode
Pauses looping animations after 3 cycles. Recording continues; only the visuals quiet.
Account
Sign out
v0.18.4 · build 217
Implementor notes
  • No Copper on this screen. Account is administrative; nothing here is a primary intent.
  • Sign-out is danger-ghost at foot, per Apple HIG. Destructive actions live where the thumb naturally lands when you've finished a settings sweep.
  • Calm mode is a first-class setting, not buried. The toggle includes a one-sentence explanation of what it does.
  • Build number visible. Bug reports want it; designers want to know which build a screenshot came from.

13a. widget — ListRow

src/components/ListRow.tsx (new)

The single row primitive used on Home, Teams, Devices, Sessions, Notifications. Avatar/icon · primary · meta · trailing · chevron. Live status is a small red dot on the avatar (badge superimposed). Hover/press treatment is surface-2 background fill — no transform, no shadow lift.

Implementor notes
  • Avatar is 36×36 with 9pt radius (KB v2 monogram-bubble convention). Settings-style rows swap avatar for a 32×32 8pt icon tile, calmer.
  • Divider is left-inset at 60px (one avatar-width + gap). Reads as grouped rather than tabular — feels less like a database query.
  • Trailing slot accepts either a timestamp or a StatusBadge. Never both. Pick the more informative one for the row's context.
  • Press feedback is fill only. No transform, no scale, no shadow — Linear discipline.

13c. widget — Input

src/components/Input.tsx (new)

Replaces all inline TextInput blocks in the codebase. Label above field (never placeholder-as-label, DESIGN §10). Four states: rest, focused, error, disabled. Focus ring is Copper × 18% alpha; error ring is danger-500.

Rest
Email
you@example.com
Focused · with caret
Title
Sprint planning — Mar 17
We'll auto-name it from the transcript if you leave it blank.
Error
Password
•••••
⊘ That password doesn't match.
Disabled
Workspace URL
sageox.ai/ryan
Set during onboarding · contact admin to change.
Implementor notes
  • Label is always visible. Even when typing. Reduces re-orientation cost when the user looks away and back.
  • Helper text is always reserved space — 12px below. Prevents layout shift when error appears.
  • Focus ring is 3px outside the border — adds spatial padding so the field "claims" the area visually. Linear-style.

13d. widget — EmptyState (refresh)

src/components/EmptyState.tsx (update)

Aligns the existing primitive to the canonical "doesn't exist yet" pattern: dashed border, surface-2 icon tile, sage-700 icon color, sage CTA (not Copper). Never alarmist — calm, instructional, optional next step.

No recordings yet
No recordings yet
Your first recording will appear here. Tap the central Record tab to start.
With CTA · paired devices
+
Pair a Scribe
Open the device's pairing screen and scan its code.
Scan QR to pair
Search empty
No matches for "auth"
Try a different word, or browse all recordings.
Implementor notes
  • Dashed border for "could exist." Solid border for "no result for your query right now." Two different empty states.
  • CTA is sage, not copper — DESIGN §11 Rule 6 reserves Copper for the page's primary action, not for empty-state recovery.

13e. widget — Toast / Notification

src/components/Toast.tsx (new)

Three variants × two contexts (inline list, transient overlay). Auto-dismiss at 5s for success/info; error toasts persist until tapped or swiped. Stacked toasts collapse with a "+N more" affordance.

"Sprint planning" is ready
Transcript, notes & 4 decisions · 9:58a
Emily mentioned you
In a decision on "Sprint planning"
Could not finish uploading
9 chunks still queued · tap to retry
Implementor notes
  • Error toasts are sticky. Recording-related errors are sticky and route the user to the offending recording on tap.
  • Stacked toasts show the newest on top with a "+2 more" pill above; tapping expands the stack.
  • Toasts respect the instrument bar. They slide in below it, never over it. The instrument bar's status is more important than any toast.

14. ★ Motion + Calm policy

— cross-cutting DESIGN.md §6 · §11 Rule 2

Four motion tiers, applied uniformly across every surface in this mockup. Three motion-suppression layers: OS prefers-reduced-motion → SageOx Calm mode → per-loop budget. Recording pulse has its own spec (1200ms loop, fade-out ring) borrowed exactly from the Scribe firmware.

Instant
50ms
Tap feedback (button press opacity, list row fill). Sub-perceptual; feels immediate.
Fast
150ms
Hover/focus state changes, segment toggle, tab swap. Reactive but unhurried.
Normal
250ms
Sheet entry/exit, toast slide-in, theme transition. Carries spatial continuity.
Slow
350ms
Larger surface reveal (instrument bar expand, sheet pull-to-dismiss). Used sparingly.
Recording pulse spec (the only looping animation): .pulse-ring { animation: scale 0.85 → 1.4, opacity 0.5 → 0; duration 1200ms; easing cubic-bezier(0.4,0,0.2,1); loop infinite } Same numbers as ~/Code/sageox/speaker/lib/ui/. The device and the phone breathe in sync.
Layer 1 · OS
prefers-reduced-motion
All loops drop to zero. Pulse becomes a static red dot. Bars freeze at 75% scaleY.
Layer 2 · App
SageOx Calm mode
Pulse + sparkline pause. Badge opacity 55%. Caret stops blinking. Transitions still play.
Layer 3 · Loop budget
Per-component cap
No animation loops longer than 60s without a quiet state. Recording pulse exempt while a session is live.
Implementor notes
  • Test Calm mode now: toggle the ● Calm mode button in the top chrome. Every pulse ring in the page should pause, badges should dim. Layout should not shift.
  • Easing is always ease-in-out on this surface. No spring overshoot in navigation transitions (DESIGN §10). Springs are allowed only for direct manipulation (drag, pull-to-dismiss).
  • The recording pulse is the only continuous animation on idle screens. Battery budget — animations on inactive surfaces are forbidden.

15. ★ Light + dark contrast audit

— cross-cutting DESIGN.md §11 Rule 5

Every text-on-surface pairing the mockup uses. Both modes verified at the WCAG AA 4.5:1 minimum for body copy (3:1 acceptable for ≥18pt large text, but the mockup uses none below the body threshold on a muted token, so we lock at 4.5:1 across the board). Token values from KB v2 v2.1's borderline correction are inherited (text-muted bumped to #9aa3ab in dark).

Pairing Mode FG BG Ratio AA body
text on bgDark#e7e9ea#0a0d0f17.5PASS
text on surface-1Dark#e7e9ea#14181b15.7PASS
text-muted on surface-1Dark#9aa3ab#14181b7.2PASS
text-muted on surface-2Dark#9aa3ab#1e23266.3PASS
text-subtle on surface-1Dark#7d858c#14181b4.7PASS
copper-600 button text (new CTA base)Darkwhite#a8613b4.6PASS · body
copper-500 button text — display onlyDarkwhite#c47a4a3.41FAIL · body · use copper-600
sage-500 on whiteDark (CTA hint)#7a8f78#14181b4.6PASS
info-500 statusDark#5b7e99#14181b4.6PASS
danger-500 statusDark#c63838#14181b5.3PASS
text on bgLight#111518#fafbfa17.4PASS
text-muted on surface-1Light#4c5358#ffffff8.4PASS
text-subtle on surface-1Light#6f767c#ffffff5.0PASS
text-muted on surface-2Light#4c5358#eef2eb7.8PASS
copper-600 button text (new CTA base)Lightwhite#a8613b4.6PASS · body
danger-500 statusLight#c63838#fafbfa5.5PASS
Implementor notes
  • Ratios computed with sRGB relative luminance (WCAG 2.2). Numbers are calculated, not eyeballed.
  • Copper CTA surface is --copper-600 (#a8613b), not --copper-500. Recomputed contrast vs white is 4.6:1 — PASS at body weight, no large-text loophole needed. Copper-500 stays as the brand display color (icons, dividers, accents on dark surfaces) but never as a text-bearing surface. This split — copper-500 display / copper-600 CTA — is the single highest-leverage token change in the redesign.
  • text-muted bumped from #8f99a3 to #9aa3ab in dark — KB v2 v2.1 correction inherited. The earlier value failed AA on surface-2 below 4.5:1.
  • This audit is the gate. Adding a new text/surface pairing? Run it through this table first; if it fails, fix the token, not the screen.
Mockup output: philadelphia-v3/proposals/2026-05-18-sageox-app-redesign/index.html · Tokens mirrored from tokens/colors.yaml · Conventions per proposals/AUTHORING.md · Visual language carries forward from 2026-05-15-kb-pages-mockup/v2 + the Scribe device's docs/design-system.md.