← 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

Three font stacks plus one Arabic face, declared in sak-base.jsx:

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
sakMono "DM Mono", "SF Mono", ui-monospace, monospace Numbers only: timestamps, durations, 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, mono numbers (timestamps/citations)
12 Secondary meta text, mono 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)

"Mono is numbers only" rule

Prior to the Jul 2026 pass, uppercase labels were inconsistently set in sakMono. Locked rule: every uppercase label — eyebrow, section title, chip, pill, tab label — is sakSans, letterSpacing: 0.08em, fontWeight: 600. sakMono is reserved for numeric/technical values only: clock times, durations, āyah citations ("AL-MULK · 67:1"), week counters shown as raw text. 86 label sites were converted under this rule.

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, Profile). 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, Profile 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, sakMono) 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. 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).

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 sakMono (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 }) (sak-extras.jsx)

Renders one of the four feedback-loop statuses via SakPill: review → "In review" (muted gray), reviewed → "Feedback ready" (gold/warm), heard → "heard" (green/ good), practiced → "practiced" (accent/plum). 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.

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 + sakMono 11px 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). Profile'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.
  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)

Exactly four states, defined once in SubStatusPill (sak-extras.jsx) and reused everywhere a submission's state is shown:

Status key Label Color
review In review Muted gray (rgba(140,124,142,0.14) bg / textMuted fg)
reviewed Feedback ready Gold/warm (rgba(224,162,10,0.14) bg / warm fg)
heard heard Green (rgba(46,139,87,0.14) bg / good fg)
practiced practiced Accent/plum (accentSoft bg / accent fg)

Per REVIEW.md A2, SubStatusPill must visually distinguish in-review (muted, waiting on the coach) from feedback-ready (warm/gold, waiting on the student) — do not collapse these to the same visual weight. The submission's flow is strictly review → reviewed → heard → practiced; a submitted recording always routes to a detail/pending screen (SakSubmissionPending, which owns the revoke-window countdown) rather than a dead-end confirmation.

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