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:
- Type-led. Large Newsreader display headlines are the primary anchor on every screen; body copy is sans-serif. Hierarchy comes from type scale, not from added chrome.
- Reduced chrome. No decorative ribbons, no vibes-only status pills ("On track"), no folio marks — unless the element earns its line by carrying unique information.
- Gold thread is precious. One flourish or one gold (
warmBright) accent per screen, never more. Gold means "the one thing that matters here" (a live indicator, a hairline catch-light, a primary action). - Ink is the brand moment. The near-black
inktoken is reserved for a single feature card per screen — Today's lesson hero, Profile's coach card, the Live Class full-screen background. It is not the default hero treatment on every screen. - Mono is numbers only.
sakMono(DM Mono) is used exclusively for timestamps, durations, and citations (e.g. "AL-MULK · 67:1"). Every uppercase label — eyebrows, section titles, chips, pills, tab labels — is sans-serif with letter-spacing, never mono. - Verse closer. Arabic + citation only, no English translation, and only on Today. English translation appears only on lesson-detail screens.
- Editorial structure. Masthead → feature → rail, repeated per screen.
- One editorial/brand line per screen (R8). Poetic or brand-voice copy (e.g. "Four sessions, one deadline.") is budgeted at one instance; a screen cannot have both a headline like this and a verse epigraph.
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
- Primary CTA pill: height 50–54px,
t.accentbackground,#ffftext, plum shadow0 6–8px 18–20px rgba(101,2,118,0.28). - Secondary CTA pill: height 40–46px, transparent background,
1px solid ${t.ruleStrong}border,t.textStrongtext. - Warm CTA (completion states, onboarding "Continue"):
t.warmBrightbackground,t.warmInktext, warm-tinted shadow (e.g.0 6px 14px rgba(255,183,11,0.32)). - Strip: always represents the full 15-week term —
<SakStrip total={15} done={7} current />— never a partial count. - Verse closer: Arabic + citation only; English translation reserved for lesson-detail screens.
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:
- Inline expanding shelf — tap a row to reveal detail without navigating away (session rows; see Component inventory).
- "Show N more" / collapse-at-N —
SakFeedbackNotes({ notes, collapseAt = 3 })truncates a notes list and exposes the rest behind a control. - Scroll-inside-card —
SakPassageCardcaps multi-āyah passages atmaxHeight(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
- Fine/light fonts are a known problem. Stakeholder feedback on the prior mockup
(
FEEDBACK-SYNTHESIS.md) explicitly flags: "Reduce color variety, single theme. Accessibility: fine/light fonts hard to read." This is a standing constraint on Sakīna, not a resolved item — when adding new screens, avoidfontWeightbelow 500 for any body or label text, and avoid text below the locked 10–11px floor (Section- except for the smallest meta labels, which should stay short and high-contrast.
- Minimum sizes. Per the locked scale, nothing should render smaller than 10px
(
MetaCelllabel is the current floor). Eyebrows/labels sit at 11px as the practical minimum for anything a user is expected to read rather than glance past. - Contrast implications of the paper palette.
paper(#FBF7F2plum /#FAF8F4ink) andcard/bg(#FFFFFF) are close in value by design (a warm-white layering effect), which means borders rely on theruletoken (rgba(42,11,48,0.10)/rgba(20,22,28,0.10)) rather than fill contrast to separate surfaces — at 10% opacity these hairlines are already faint. Any new surface-on-surface composition (e.g. a card onpaperonbg) should be checked against this — don't rely solely on a 0.5px hairline where a genuinely low-vision user needs a value or color break.textMuted(#8C7C8Eplum /#878A91ink) againstbg/paper/cardis the site's built-in "already borderline" text color — do not introduce anything lighter thantextMutedfor text that must be read, and do not usetextMutedfor body copy of any length (its intended use is short eyebrows/timestamps only). - Dark mode is a straight token swap, not a redesign —
SAK_DARKprovides its own accent/warm/text values already tuned for a dark background (e.g.accentbecomes a lightened#C97DD6rather than the light-mode#650276, which would fail contrast on a dark surface). Any new component must resolve all colors throughgetSakTokens()rather than hardcoding a palette value, or it will silently break in dark mode.