← QuranFlow — the redesign

QuranFlow — Sakīna Design System

Reference for the visual language of the redesigned app, extracted from the working mockup at ../mockup/. Source of truth: src/sak-base.jsx (tokens, shared components) and AUDIT.md (locked conventions, the 8 binding rules). This document restates both for implementation — every value below is copied from the code, not invented.

Direction name: Sakīna ("stillness/tranquility"). Style label used internally: Manuscript-Editorial.


1. Design principles

The Sakīna direction blends manuscript typography (large serif display type, a verse closer, one gold accent) with editorial restraint (content-first hierarchy, reduced chrome, one dark "feature card" per screen). From AUDIT.md:

The 8 binding rules (build rules)

These are the enforceable checks; treat each as a lint rule when building or reviewing a screen.

# Rule What it means when building
R0 Progressive disclosure first Default-collapse status, duration, counts, and body previews. Reveal behind a tap target, chevron, or "Show N more." Never render a full expanded state before the user asks.
R1 One week reference per screen SakStrip/eyebrow "Week N of 15" appears exactly once per screen — pick one slot (hero eyebrow or footer, not both).
R2 One primary action per screen Exactly one hero-weight CTA. All other actions render smaller/quieter below it.
R3 No decorative pills A pill must encode unique info (status, count, label). Delete any pill that only restates tone ("On track", "EST").
R4 No eyebrow + title when the title is self-explanatory If the title alone identifies the screen/person (e.g. "Ustadha Amina"), drop the eyebrow above it.
R5 No text restating a progress visual If a SakStrip/SakDots already encodes "Week 8 of 15," do not also print that as a caption.
R6 No section header for a single-item list Section headers require ≥ 2 items below them, or a non-obvious grouping.
R7 No instructional subtitle under self-evident UI Don't caption a list of tappable rows with "Tap a row to edit."
R8 At most one editorial/brand line per screen See above — budget of one poetic/brand-voice line total.

Build-time application of the loop in AUDIT.md: for any new screen, run Structure (can the user answer "where am I / what can I do / where can I go" in under 2 seconds?), Squint (exactly one visual anchor should survive defocusing), Prune (walk every element against R0–R8), then Consistency (does it use SakNavRoot/SakNavDrill, the locked eyebrow style, and the locked CTA pill shapes?).


2. Color tokens

Two palettes (Plum, Ink/Charcoal), each with a light mode (its own values) and a shared dark-mode overlay (SAK_DARK, identical for both palettes). Resolved at runtime by getSakTokens() in src/sak-base.jsx, which reads window.__sakTheme ({ palette: "plum" | "ink", mode: "light" | "dark" }) and merges SAK_DARK over the palette's light values when mode === "dark".

Plum palette — light mode

Token Value Role
bg #FFFFFF Screen background
paper #FBF7F2 Warm paper surface (passage cards, hero cards, closers)
panel #F6F2F7 Recessed panel background
card #FFFFFF Card surface (SakCard)
ink #2A0B30 Brand-moment dark surface (hero cards, tab bar active pill)
inkDeep #1F0824 Deeper ink variant (gradients, shadows)
textStrong #1F0824 Primary text
textBody #4A3F4F Secondary/body text
textMuted #8C7C8E Tertiary text, eyebrows, muted labels
rule rgba(42,11,48,0.10) Hairline borders
ruleStrong rgba(42,11,48,0.18) Emphasized borders (secondary CTA outline)
accent #650276 Primary action color (plum)
accentDeep #4C0459 Accent gradient partner
accentSoft #EADDEE Accent tint background (chips, pill fills)
warm #E0A20A Gold — text/icon weight
warmBright #FFB70B Gold — fill weight (CTA pills, gold hairline)
warmInk #1F0824 Text/icon color on top of warm/warmBright
warmSoft #FFF4D2 Gold tint background
good #2E8B57 Success/positive status (green)
danger #B91C1C Error/destructive status (red)

Ink palette — light mode

Token Value Role
bg #FFFFFF Screen background
paper #FAF8F4 Warm paper surface
panel #F3F1ED Recessed panel background
card #FFFFFF Card surface
ink #1A1B20 Brand-moment dark surface
inkDeep #0E0F12 Deeper ink variant
textStrong #15171B Primary text
textBody #43464D Secondary/body text
textMuted #878A91 Tertiary text, eyebrows
rule rgba(20,22,28,0.10) Hairline borders
ruleStrong rgba(20,22,28,0.18) Emphasized borders
accent #1A1B20 Primary action color (near-black, this palette has no purple)
accentDeep #0E0F12 Accent gradient partner
accentSoft #E5E3DE Accent tint background
warm #C68A1E Gold — text/icon weight
warmBright #E7A52A Gold — fill weight
warmInk #0E0F12 Text/icon color on warm/warmBright
warmSoft #FBEFCE Gold tint background
good #2E8B57 Success/positive
danger #B91C1C Error/destructive

Dark mode overlay (SAK_DARK, applied to either palette)

Token Value Role
bg #171018 Screen background
paper #1E1620 Warm paper surface
panel #241B26 Recessed panel background
card #211A24 Card surface
ink #0D070F Brand-moment dark surface
inkDeep #000000 Deeper ink variant
textStrong #F6F2F7 Primary text
textBody #CDBED1 Secondary/body text
textMuted #8C7C8E Tertiary text, eyebrows
rule rgba(255,255,255,0.10) Hairline borders
ruleStrong rgba(255,255,255,0.18) Emphasized borders
accent #C97DD6 Primary action color (lightened plum for dark bg)
accentDeep #9D4FAA Accent gradient partner
accentSoft rgba(201,125,214,0.18) Accent tint background
warm #FFC93C Gold — text/icon weight
warmBright #FFD55C Gold — fill weight
warmInk #1F0824 Text/icon color on warm/warmBright
warmSoft rgba(255,213,92,0.18) Gold tint background
good #5DC08A Success/positive
danger #E96A6A Error/destructive

Note: dark mode replaces both palettes' surface/text/accent tokens with the same SAK_DARK set — i.e. Plum-dark and Ink-dark render identically. good and danger are the only tokens that don't vary between the two light palettes.


3. Typography

Two font stacks plus one Arabic face, declared in sak-base.jsx. There is no monospace stack — DM Mono was removed in the Jul 2026 typography pass (see below).

Variable Stack Used for
sakDisplay 'Newsreader', 'Fraunces', Georgia, serif Display headlines (h1s), hero numerals, meta-cell values
sakSans -apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif Everything else: body copy, all uppercase labels/eyebrows/chips/pills/tab labels, tabular-figure numbers (timestamps, durations, counts), and spaced-small-caps citations
'Amiri Quran', serif (body), 'Amiri', serif (āyah-number glyph) Arabic Qur'anic text

Locked size scale (Jul 2026 "typography quiet pass")

Half-step sizes (9.5, 10.5, 11.5, 12.5, 13.5, 14.5, 15.5) were collapsed onto this scale across 114 sites — do not introduce new sizes off this list:

Size Use
10 Smallest meta labels (MetaCell label)
11 Eyebrows, section titles, chips, pills, tab-bar labels, tabular-figure timestamps/citations
12 Secondary meta text, sans captions
13 Body copy, secondary CTA text, meta values
15 Primary CTA text, row titles
16–17 Nav-drill title (16), meta-cell display values (17)
19 SakNavRoot title
22–34 Display type: passage card Arabic (22), session/lesson h1 (26–30), Today hero h1 (34)

"No monospace anywhere" rule

Prior to the Jul 2026 typography pass, uppercase labels were inconsistently set in sakMono. A first pass locked every uppercase label — eyebrow, section title, chip, pill, tab label — to sakSans, letterSpacing: 0.08em, fontWeight: 600 and carved out sakMono for numeric/technical values only (clock times, durations, āyah citations, week counters shown as raw text). 86 label sites were converted under that rule.

A second pass, later the same month, removed sakMono entirely — closing the carve-out. Numbers now render in sakSans with fontVariantNumeric: 'tabular-nums' wherever they tick, align in columns, or sit opposite other numbers (letter-spacing 0, not the old mono tracking). Citations under Arabic are spaced small caps (fontVariantCaps: 'all-small-caps', letterSpacing: '0.1em', fontWeight: 500, sentence-case source text, e.g. "Al-Mulk 67:1–2" rather than "AL-MULK · 67:1"). Mono prose captions (e.g. "This review · logged automatically") were rewritten as plain sans sentences.

Letter-spacing conventions

Element Spec
Eyebrow (locked, AUDIT.md) fontFamily: sakSans, fontSize: 11, letterSpacing: "0.16em", textTransform: "uppercase", fontWeight: 700, color: t.textMuted (or t.accent for "current"/"now" state — at most one accent eyebrow per screen)
Section title Same eyebrow style, paddingInline: 20, only rendered when ≥ 2 items follow (R6)
Nav title (SakNavRoot) fontSize: 19, fontWeight: 700, letterSpacing: "-0.3px"
Nav title (SakNavDrill) fontSize: 16, fontWeight: 700, letterSpacing: "-0.25px", centered
Hero h1 fontFamily: sakDisplay, fontSize: 26–36, lineHeight: 1.08–1.15, letterSpacing: "-0.02em", fontWeight: 400
Body No explicit letter-spacing override (system default); no body italics

Locked visual conventions


4. Component inventory

All components below are defined in src/sak-base.jsx unless noted, and exported onto window for use by every screen file. TweaksPanel (in tweaks-panel.jsx) is a dev-only theme-switcher for the mockup canvas and is excluded from this inventory — it ships with no production equivalent.

SakNavRoot({ eyebrow, title, action })

Top-level tab-screen header (Today, Learn, Sessions, You). Layout: eyebrow (11px, 0.16em, muted or accent) stacked above a left-aligned 19px/700 title, with an optional 38px circular action (typically SakIconBtn) right-aligned at the baseline. paddingTop: 58 (clears the status bar/Dynamic Island), paddingInline: 20, paddingBottom: 14. Use on the four root tabs only.

SakNavDrill({ eyebrow, title, action })

Drilldown-screen header (Lesson, Recording, You sub-pages, Session detail). Layout: 38px circular back-chevron button (left) + centered eyebrow/title stack + 38px action or an empty 38px spacer (keeps the title mathematically centered). paddingInline: 16, paddingBottom: 14. Title is 16px/700/centered; eyebrow 10px.

SakIconBtn({ icon, dot, onDark })

38×38px circular icon button. card background / rule border by default; on dark surfaces (onDark) switches to translucent white fill + border. Optional dot renders a 7px gold (warmBright) notification dot at the top-right corner, ringed in the surface color so it reads as "sitting on top."

SakCard({ children, style })

Base card surface used throughout: card background, 0.5px solid ${rule} border, borderRadius: 22, subtle shadow (0 1px 2px rgba(42,11,48,0.04)), overflow: hidden. Accepts a style override for one-off cases. This is the default container for rows (session lists, meta rows, coach panels).

SakSectionTitle({ children, right })

Section label row: eyebrow-style label (11px, uppercase, muted, sans) left-aligned, optional right slot (12px, sans, fontVariantNumeric: 'tabular-nums') for a trailing numeric/date value. paddingInline: 20, paddingBottom: 8. Per R6, only render when the section has ≥ 2 items below it.

SakStrip({ total, done, current })

Thin (4px tall) segmented progress bar — the canonical "week N of 15" visual. One segment per week: filled accent for done weeks, warmBright for the current week (if current is true), rule (hairline) for remaining weeks. Always called with total={15} for the term-long strip (R1/R5 — this replaces "Week 8 of 15" captions everywhere).

SakDots({ total, done, current, size, gap, doneColor, currentColor })

Row of small circles for short counts (e.g. onboarding step dots, attempts remaining). Filled accent (or doneColor) for done, hollow-with-border for remaining, warmBright (or currentColor) ring for the current item.

SakTabBar({ active }) / SakCoachTabBar({ active }) (student vs. coach app)

Floating glass tab bar, iOS-26 "liquid glass" treatment: heavy backdrop blur (blur(32px) saturate(180%) brightness(1.08)), translucent white/dark fill (rgba(255,255,255,0.58) light / rgba(36,27,38,0.55) dark), inset top highlight + bottom shadow for depth, and a 1px gold hairline (warmBright at 0.55 opacity) across the top edge — the one "gilt" detail tying the tab bar to the manuscript vocabulary. Active tab renders as a solid ink pill with white icon/label; inactive tabs are transparent with muted icon/label. Each tab is a stacked cell — icon over label, 19px icon and a 10.5px/600 (student) or 11px/600 (coach) letter-spaced label. Student tabs: Today / Learn / Sessions (Sessions label, calendar icon) / You. Coach tabs (sak-coach.jsx): Queue (headphones) / Students / Schedule / You — same shell, no gold hairline variance, slightly simpler shadow (no dark-mode-specific box-shadow branch).

One shell, two tab sets. The walkthrough renders both sets from a single component (../walkthrough/app/src/app/TabBar.tsx): the set that renders is whichever one owns the current tab, so coach mode is emergent from tab ownership rather than held as a mode flag. All eight stacks exist from boot, entering the coach app is a re-root onto Queue and leaving it a re-root onto You, and no chrome anywhere announces a "mode."

Count badge (Queue tab). A tab may carry a numeric count, and exactly one does: the coach Queue tab shows how many submissions are waiting. Spec: a 16px-tall pill riding the icon's top-right corner, sans with fontVariantNumeric: 'tabular-nums', warmBright fill with warmInk digits when that tab is active (gold on the ink pill), ink fill with white digits everywhere else. Never red. These are counts, not alarms — the app has no red badge vocabulary, and a backlog a coach is already working through must not be drawn as an error.

The badge is live, not decorative (Aug 3 2026): it reads the demo store, so sending the one review drops it 5 → 4 in the same beat the queue's headline and Sarah's row change. A count badge must never disagree with the list it counts — the queue headline counts its own rendered rows for the same reason.

Recorded divergence — the co-05 roster (logged Aug 3 2026). The Claude Design source for the coach roster (../walkthrough/build-notes/design-sources/co-05-roster.dc.html) draws its active tab as a horizontal pill — ink-deep fill, borderRadius: 100, padding: 10px 22px, icon and a 13px/700 label side by side. That is a divergence, not a variant: the stacked treatment above is canonical, and the walkthrough follows it. Logged here for the pending Design-project sync; do not propagate the horizontal form.

CTA pills (primary / secondary / warm)

Not standalone components — inline style blocks repeated per-screen against the locked spec (Section 3). Primary = filled accent pill, 50–54px tall, plum shadow. Secondary = outline-only pill (1px solid ruleStrong), 40–46px tall, no shadow. Warm = filled warmBright pill with warmInk text, used for completion/onboarding "Continue" actions. All are full or partial pill shape (borderRadius: 100).

Chips

Also inline, not componentized: small borderRadius: 4 (not full pill) uppercase labels used as session "type" tags (QRC / Class / Hours), each with its own tint pair defined in a local typeColors map (e.g. QRC = rgba(46,139,87,0.14) bg / good fg; Class = accentSoft bg / accent fg; Hours = rgba(224,162,10,0.14) bg / warm fg). Same map is reused in SessionRow (Today) and the Sessions tab so a chip reads identically in both places.

SakPassageCard({ ayat, cite, compact, maxHeight }) (sak-feedback.jsx)

THE Qur'an-text component — every Arabic passage on the recording/feedback loop routes through it. ayat shape: [{ num, segments: [{ text } | { text, mark: { n, tone } }] }]. Single āyah renders centered at 26px (or 22px if compact); multiple āyāt render as a justified paragraph with āyah-end medallions (﴿n﴾ in warm, Amiri font) and scroll internally past ~5 lines (maxHeight, default 240) so the screen never grows unbounded. Word-level mark: { n, tone } renders PassMark — an underlined span with a small numbered badge (warm or danger depending on tone) marking a specific correction point. Card surface is paper with a top gold-hairline gradient accent and a cite line below in spaced small caps (fontVariantCaps: 'all-small-caps', default "Al-Mulk 67:1").

SakVerseCard({ compact }) (sak-recording.jsx)

Thin wrapper that delegates to SakPassageCard — kept as the name used in the recording flow for backward compatibility/clarity at call sites.

FbWave({ bars, played, height, activeColor, restColor }) (sak-feedback.jsx)

THE audio-waveform primitive, shared by the feedback ink card, recording review, and voice-message bubbles. Renders bars (default 30) deterministically-seeded bars (Math.sin-based pseudo-random, not Math.random, so the waveform is stable across re-renders); bars before the played fraction render in activeColor (default accent), the rest in restColor (default rgba(42,11,48,0.18)).

FbPlayBtn({ size, onInk }) (sak-feedback.jsx)

Circular play button, 40px default. Two variants: default (light surfaces) uses accentSoft fill + plum-tinted border + accent play glyph; onInk variant (for use on the dark ink feature card) uses translucent white fill/border + white play glyph. Per REVIEW.md resolution D2, SakRecReview's previously-bespoke play circle was replaced with this component so all playback affordances match.

SubStatusPill({ status, pov }) (sak-extras.jsx; walkthrough: ui/kit.tsx)

Renders one submission status via SakPill. pov chooses whose words the pill speaks — 'student' (the default) or 'coach' — and it changes only the label, never the color. This is the single source of the status vocabulary described in Section 5 — always reference this component rather than re-deriving the label/color pairing, and never hand-write a coach-side status label at a call site.

Recut Aug 18 2026 (U-435), and this is now the whole set: awaiting → "Awaiting review" / "Needs review" (filled gold), inreview → "In review" in both seats (the same gold as a 1px outline on transparent ground — further along, not a different kind of thing), reviewed → "Feedback ready" / "Feedback sent" (plum), rejected → "Returned" in both seats (gold at a deeper wash — it asks without accusing, and never red), revoked → "Taken back" in both seats (panel ground, body ink — the quietest chip in the set). Unsubmitted is the absence of a row, so it has no pill. The earlier heard and practiced greens are retired: they described the student's habit, not the submission, and whether a student has listened is a receipt on the feedback, not a state of the recording. Neither word may reappear — a build assertion on the shipped HTML fails if it does. The legacy review value normalizes to awaiting.

Session row anatomy (SessionRow in sak-core.jsx, TimelineRow in

sak-schedule.jsx — matching vocabulary) Compact row (always visible): left time gutter (44px wide, left-aligned, no divider — sakDisplay 18px time + sans 11px tabular AM/PM beneath), a type chip + optional "Live now" indicator, title (15px/600) and "with {coach}" line (13px muted). Trailing control is either a Join pill (live sessions — no expand affordance) or a chevron (chevronRight/chevronDown depending on expanded state). Tapping a non-live row toggles an inline expansion shelf (see Patterns, below) containing a coach mini-card, a "What's covered" paragraph, and a primary "Open Zoom" + secondary "Remind me" CTA pair. Live rows render the row in a faint accent-tinted background (rgba(101,2,118,0.05)) and are not expandable.

`SessionDetailFrame({ title, chip, coach, coachInitial, time, ampm, duration,

when, location, primaryCTA, primaryIcon, children }) (sak-schedule.jsx) The canonical full-page session-detail shell, reused by SakSessionDetail(Fluency drills) andSakSessionDetailQRC(Sisters' Reading Circle) — adding a third session type is a content change against this frame, not a new design pass. Anatomy:SakNavDrill header → title block (chip + "when" label + Newsreader h1) → meta row (SakCardwith threeMetaCells: Time / Duration / Where, divided by MetaDiv hairlines) → "With" section (SakSectionTitle+ coach mini-card) →children` slot for type-specific content (what-we-cover, cohort, materials) → primary/secondary CTA pair.

Expanding shelf (progressive disclosure pattern, not a standalone component)

Implemented identically in SessionRow (Today) and TimelineRow (Sessions tab) via local React.useState — tapping a non-live row flips expanded, which conditionally renders a bordered panel (borderTop: 0.5px solid rule, padding: 14px 14px 14px 60px — indented to align under the title, not the time gutter) containing the coach mini-card, "What's covered" copy, and Open Zoom/Remind me actions. Both call sites are described in AUDIT.md as intentionally sharing "the exact same expansion vocabulary" so the two screens feel like the same component.

SakToggle({ on })

46×28px pill switch, reused in notification settings and the manage-subscription screen. accent fill when on, rgba(120,110,130,0.25) when off; white 22px knob slides between left: 3 and left: 21.

SakPill({ children, bg, color, style })

Generic pill-badge primitive underlying SubStatusPill and any other status/eyebrow chip: paddingInline: 9, paddingBlock: 4, borderRadius: 100, 11px/600/uppercase text, default accentSoft bg / accent fg, overridable via bg/color props.

SakFlourish({ inline })

The one-per-screen decorative gold-dot flourish: two 22px hairlines flanking a 4px warmBright dot. Used sparingly per the "gold thread is precious" principle — never more than once on a screen.

QFDevice / SakStatusBar

Device-chrome primitives (iPhone bezel, Dynamic Island, 9:41 status bar with signal/wifi/battery glyphs, home indicator) used to frame every screen in the mockup canvas. Not part of the shipped app's UI, but documents the safe-area assumptions (paddingTop: 58 in nav headers accounts for the status bar + Dynamic Island).

Icon({ name, size, color, strokeWidth, fill, style })

Single SVG sprite sheet component covering every glyph used across Sakīna screens (arrows, bell, book, calendar, camera, check, chevrons, clock, headphones, mic, play, sparkles, user icons, video, x, etc.) — lucide-style, round linecap/linejoin, strokeWidth default 1.8. New icons should be added to this single component's paths map (in alphabetical blocks) rather than introduced as one-off inline SVGs.


5. Patterns

Timezone display

One banner per screen, never per-item suffixes. Per REVIEW.md resolution B3, prior per-row "EST"/"EDT" suffixes were removed app-wide. The locked pattern is a single line, once per screen that has times on it:

"All times shown in Toronto (EDT)"

(seen verbatim in sak-feedback.jsx's SakWeekAtGlance and sak-schedule.jsx's Sessions tab). Coach-facing screens use the coach's own city/zone in the header eyebrow instead (e.g. "Tue · 17 Apr · Cairo (EEST)" in SakCoachSchedule). The You tab's timezone field is normalized to "Toronto · Eastern (EDT)". Do not reintroduce per-row zone suffixes — that was an explicit R3-adjacent cut.

Progressive disclosure (R0)

The default state of any screen shows only what answers "what's next?" Three concrete mechanisms recur across the codebase:

  1. Inline expanding shelf — tap a row to reveal detail without navigating away (session rows; see Component inventory).
  2. "Show N more" / collapse-at-NSakFeedbackNotes({ notes, collapseAt = 3 }) truncates a notes list and exposes the rest behind a control. The mechanism stands for any long list; the five-note feedback list it was built for does not. A coach's point is now a word plus the short voice note she spoke onto it (PRODUCT-SPEC, corrected Aug 4 2026), so the shipped Submission — feedback ready shows both points uncollapsed — reach for this pattern where a list actually runs long, not to reinstate that one.
  3. Scroll-inside-cardSakPassageCard caps multi-āyah passages at maxHeight (240px default) and scrolls internally rather than growing the page.

Status vocabulary (submission/feedback loop)

Five states — recut Aug 18 2026 (U-435). Defined once in SubStatusPill (sak-extras.jsx; the walkthrough's copy is ../walkthrough/app/src/ui/kit.tsx) and reused everywhere a submission's state is shown. One status value, two seats: the pov prop picks the label column, and both columns share the color.

Status key Student label (pov="student") Coach label (pov="coach") Color
awaiting Awaiting review Needs review Gold (warmSoft #FFF4D2 bg / #8A6D00 fg)
inreview In review In review Gold outline (transparent bg / #8A6D00 fg / rgba(138,109,0,0.32) border)
reviewed Feedback ready Feedback sent Plum (accentSoft bg / accent fg)
rejected Returned Returned Deep gold (rgba(224,162,10,0.22) bg / warmInk fg)
revoked Taken back Taken back Muted (panel bg / textBody fg)

Unsubmitted is the absence of a row, so it has no pill — a week with nothing sent says "Not sent yet" in plain text. The retired heard and practiced greens described the student's habit rather than the submission, and whether a student has listened is a receipt on the feedback, not a state of the recording; neither word may reappear, and a build assertion on the shipped HTML fails if either does. The legacy review value normalizes to awaiting.

Colors keep the design canon set in the port pass (../reconciliation/FINAL-DESIGN-REVIEW.html: "In review = gold, Feedback ready = plum, sentence case"), and they satisfy REVIEW.md A2: waiting-on-the-coach and waiting-on-the-student must stay visually distinct — do not collapse these to the same weight. The two waiting states share the gold thread because from the student's side they are one moment of the week (sent, not answered), separated only by whether the coach has it open; rejected sits deeper in the same family because it is the one state that wants something back, and never red — red in this app is only the recording dot and the LIVE dot. Neither waiting state carries alarm styling (U-406). The flow is awaiting → inreview → reviewed, with rejected and revoked as exits; a submitted recording always routes to a detail/pending screen (SakSubmissionPending, which owns the take-back) rather than a dead-end confirmation.

One state, two seats — the perspective flip (logged Aug 3 2026; states recut Aug 18 2026). The two labels that differ are exactly the two that describe someone else's next move. awaiting is work the coach owes and the student is waiting on; reviewed is work the coach has finished and the student has yet to open. So each seat reads the same fact from its own side: Sarah's Week 8 submission says "Awaiting review" to Sarah and "Needs review" to Ustadha Amina, off one status value. The other three — inreview, rejected, revoked — name a move whose author is unambiguous from either chair, so they read identically to both and get no second label. The colors never flip — a coach and a student looking at the same submission see the same swatch, so the pill stays learnable when a coach crosses into her own student app (coach-you.tsx → "Switch to your student app"). Gold means the week is still in flight, plum means the note is waiting on the student. Coach-side surfaces carrying the flip: the roster (coach-students.tsx) and the student record's submission history (coach-student.tsx) — and because one of the coach labels names a move that isn't hers, that record prints a plain-sans legend under the history card ("Feedback sent — she has your note. Returned — it came back with your written reason, and it never counts against that week. The receipt on each feedback row says whether she has played it."). Student labels are unchanged.

Reminder bell affordance

Root-tab notification entry point is SakIconBtn icon="bell" dot in SakNavRoot's action slot (seen on Today) — the 7px gold dot indicates unread notifications. Per-session reminders use a secondary "Remind me" pill (calendarPlus icon) inside the expanding shelf / session detail CTA row, never a bell — the bell is reserved for the notification inbox, the calendar-plus icon for "add this specific event's reminder."

Empty states

Named/tracked states live in sak-states.jsx (SakTodayRest, SakProfileFirstWeek, SakSubmissionsEmpty, SakNotifEmpty, SakRecPermission) and are treated as first-class screens with their own anchor per the audit loop — not a single generic "nothing here" placeholder reused everywhere. Each empty/edge state keeps the same nav header (SakNavRoot/SakNavDrill) and tab bar as its populated counterpart so the app frame never jumps.


6. Accessibility notes