Skip to content

Sunshine Design System

Specification for developers building on or alongside Flux Student Directory.

Version 1.0Updated October 16, 2026

1. Purpose and design principles

1.1 Purpose

Flux Student Directory is a school directory for parents and school staff. People open it for quick, practical reasons (who is in my child's class, what is for lunch, is there school tomorrow) usually on a phone, often in a hurry. The visual system exists to make those lookups fast, make different kinds of information easy to tell apart, and make the product feel friendly and trustworthy to a parent.

1.2 Principles

  1. One bright color per area. Each area of the app (Notices, Calendar, Lunch Menus, and so on) has a single color used by its sidebar icon, its count pill, its primary button, and its selection states. Color is how a parent learns where they are.
  2. Color labels, it never carries meaning alone. Every color is paired with text, an icon, or a shape. A parent who cannot distinguish red from green must still be able to use every screen.
  3. Native first. On Apple platforms the system provides the type, the materials, the navigation chrome and the light and dark behavior. The Sunshine layer only adds color and shape. It never fights the platform.
  4. Show what we have, hide what we do not. UI that depends on optional data (a category, a location, a photo) appears only when the data is present. There are no empty placeholders for data a school never entered.
  5. Warm, not loud. Colors are bright but the surfaces are calm: generous padding, large rounded shapes, soft tints, short text.
  6. Schools can brand it a little. A school's theme can tint navigation chrome, selection and prominent buttons. It never recolors the Sunshine labelling colors (see section 4.8).
  7. Accessible by default. WCAG 2.2 AA is the target for web, and Apple's Human Interface Guidelines accessibility guidance for native. See section 12.
  8. The same thing looks the same everywhere. A category, a grade or a meal type has one color on every screen and every platform.

2. Scope and platform matrix

2.1 Surfaces

Surface Technology Audience Sunshine status
iPhone SwiftUI Parents, staff, admins Full
iPad SwiftUI (same target, split view) Parents, staff, admins Full
Mac SwiftUI (same codebase, macOS 26.5+) Parents, staff, admins Full
Apple Watch SwiftUI (watchOS 26.5+) Parents Not applied. The watch app is not restyled with the Sunshine layer yet (section 15.4)
App plane web Next.js 16, Tailwind v4 Parents, admins Foundation applied; Sunshine layer applied to categories, filter chips and pills only

2.2 Platform capability matrix

Capability iOS iPadOS macOS watchOS Web (app)
Sunshine colored sidebar and count pills Yes Yes Yes No Not applicable (see 15.5)
Category pills and filter chips Yes Yes Yes No Yes
Filled SF Symbols (symbolVariant(.fill)) Yes Yes Yes Limited Lucide icons
Dynamic Type Yes Yes Yes (system text styles) Yes Browser zoom and rem units
Light and dark System System System System prefers-color-scheme only
Biometric lock Yes Yes No No No
School theme (tint) Yes Yes Yes No Yes
Photo editing Yes Yes Yes No Yes

3. Brand voice and tone

Friendly, optimistic, plain. A school directory should sound like a helpful front-office person, not a bank.

3.1 Voice rules

Rule Do Do not
Plain words "No notices right now." "There are no notices to display at this time."
Say what happened and what to do "Couldn't load categories. Check your connection and reopen this form." "Error 500."
Name things the way parents do "Lunch Menus", "Notices", "Sign-Ups", "Staff Directory" Internal names ("audiences", "memberships")
Keep labels short "Send Email", "Open Full PDF Document", "Add to Apple Calendar" Sentences on buttons
Sentence case for body, Title Case for navigation and buttons "Permission Slips" "PERMISSION SLIPS" except where a style below says uppercase
No blame "Unlock canceled. Try again, or sign out to use a different account." "You failed authentication."
Neutral family language "Parent", "Guardian" Gendered relationship assumptions (the Contact Export feature deliberately uses neutral labels)

3.2 Casing exceptions

  • The High Priority banner is all caps: HIGH PRIORITY NOTICE on native and web. All caps on purpose, and "Notice" rather than "Notification" so the product term is the same everywhere.
  • Detail section labels are uppercased small text: DOCUMENT PREVIEW, EVENT DETAILS, staff and guardian Email / Phone section labels (.uppercased() in code).
  • Everything else is Title Case for navigation and buttons, sentence case for descriptive text.

3.3 Things the product does not say

  • No meal times. The Lunch Menu never shows times; schools differ and the system does not collect them.
  • No "Issued by". Notices do not show an author line.
  • No designation of a "Primary Guardian".
  • No "Home Address" label; it is just "Address".

4. Color

4.1 Model

Color in the system has four layers, from most to least stable:

  1. Semantic text and surface colors from the platform (primary, secondary, background, grouped background). Never overridden.
  2. Flux brand foundation: the accent (Flux blue) and the supporting warm colors (sun, coral, leaf).
  3. Sunshine area colors: one system color per area of the app.
  4. Content colors: category colors chosen by a school admin from a fixed palette, and derived colors (grade, meal type, calendar category).

An optional school theme (section 4.8) tints a small set of chrome elements on top of layers 1 and 2.

4.2 Flux brand foundation

app/app/globals.css, SwiftUI/flux-student-directory/Assets.xcassets/.

Token Light Dark Role Text-safe on light canvas
flux-blue (AccentColor in SwiftUI) #2E7FCB #74B4EF Primary actions, links, focus, tint No: 4.06:1 on #FFFBF3. Use flux-blue-text for small text
sun (SunColor) #FFB020 #FFC856 Highlights, fills, icons Never as text (1.77:1)
coral (CoralColor) #FF6B4A #FF8A6E Warm emphasis Large text and fills only (2.73:1)
leaf (LeafColor) #3FA66B #6FCB93 Success Large text and fills only (2.96:1)
warning #D97706 #FBBF24 Status, warnings See contrast notes
danger #DC2626 #F87171 Destructive, errors AA on white
flux-blue-text #1D6BB0 = flux-blue Small blue text 5.38:1
coral-text #C43F1F = coral Small warm text 4.99:1
leaf-text #2C7A4E = leaf Small success text 5.09:1
sun-text #8A5A00 = sun Small highlight text 5.74:1

The three colors are used for their foundation roles on SwiftUI, glyph-only (never as text): SunColor for the star on a starred student's roster row (highlight), CoralColor for the warm "not linked to a student" glyph on the guardian detail (warm emphasis), LeafColor for success: the "Added to your calendar" check, the "Saved" indicators and the invite-confirmation check.

4.3 Neutral and surface colors

Web, app plane:

Token Light Dark Use
--background #FFFBF3 (warm paper) #1E1A2E (plum charcoal) Page canvas
--surface #FFFFFF #272238 Cards, inputs, header
--foreground #2B2013 #F5EEDE Body text
--foreground-muted #6B5E4D #C9BFAE Secondary text
--border #F0E4D0 #3A3350 Hairlines
Token Light Dark
--background #FAFAFC #0D0F14
--foreground #171717 #EDEDED

SwiftUI: no custom neutrals. The app uses .primary, .secondary, .tertiary, .background, .background.secondary, and Color(.secondarySystemGroupedBackground), so Increase Contrast, dark mode and future OS palette changes come for free.

4.4 Sunshine area colors

Web
Icon tiles in each area color, as used by the web sidebar and screen headers.

FluxArea in FluxStyle.swift. Each is a SwiftUI system color, so it adapts to dark mode automatically. The sidebar icon, the count pill, the primary action button on that area's screens, and its selection states all use the same color.

Area Color SF Symbol in sidebar Notes Web counterpart (Lucide)
Starred yellow star.circle.fill star
Lunch Menus green fork.knife utensils
Calendar blue calendar calendar
Directory Map teal map map
Notices red megaphone Same hue as the Urgent treatment megaphone
Polls pink chart.bar chart-bar
Permission Slips mint checklist list-checks
Documents indigo doc.text file-text
Staff Directory orange person.text.rectangle Keeps orange contact
Sign-Ups cyan hand.raised hand
Students ("Whole School") and Grades purple person.3 users-round
My Lists brown (list symbol)
Organizations rust per-category symbol Rust, a brown-tinged orange: #B7410E light, #E0764A dark. Palette key rust
Classes viridian rectangle.stack.person.crop Custom color (ViridianColor asset; web --cat-viridian): #40826D light, #5FB89B dark. The 13th palette key. White on the light fill is 4.53:1 graduation-cap
Departments phthalo green building.2 Deep emerald (PhthaloColor asset; web --cat-phthalo): #005C32 light, #17A058 dark. The 15th palette key. Chosen to be clearly different from viridian and from system green (distances in 4.5) building-2
School Settings gray house.and.flag.fill school
Access indigo person.badge.key.fill key-round

Every sidebar symbol is rendered with symbolVariant(.fill) and foregroundStyle(area.color); the list applies symbolRenderingMode(.hierarchical).

Staff Directory keeps orange and Organizations became rust so the two never share a hue; Departments became phthalo green so Classes and Departments differ; Access keeps indigo, shared with Documents. Indigo is the only remaining shared hue, and the two screens are never adjacent. If more areas are added the palette is not extended again without a second differentiator (an icon).

4.5 Content category palette

Web
Every palette key as a category pill, plus the grade-only mauve and the neutral pill.

The palette is defined once and shared by every platform; the machine-readable snapshot is published as tokens.json.

A category stores a palette key, never a hex value. The server and both platforms map the key to a color. The 15 category keys are the 12 Apple system colors (blue, purple, orange, green, pink, teal, indigo, red, mint, cyan, brown, yellow) plus three custom colors: viridian (Classes), rust (Organizations) and phthalo (Departments). A sixteenth color, mauve, is grade-only (Grade 12) and is not offered to categories.

Each key carries five values, all in the snapshot:

Role Used for Rule
Fill (light / dark) Icons, dots, tiles, tinted backgrounds The Apple color, or the custom value
Pill text (lightText / darkText) Label on a 16% tint of the fill At least 4.5:1 on every web and native surface in both appearances
Solid fill (solid) Selected chip, initials circle, action button The fill itself, or its pill-text color where white on the fill would be under 4.5:1
Label on solid (onSolid) Text on the solid fill Dark #1C1C1E or white, whichever the rule below gives, at least 4.5:1

Values (hex, light / dark). Pill text and solid columns are the accessible variants; fills are unchanged.

Key Fill Pill text Solid fill Label on solid
blue #007AFF / #0A84FF #005DC2 / #51A8FF #005DC2 / #0A84FF white / dark
purple #AF52DE / #BF5AF2 #9126C8 / #D189F6 #9126C8 / #BF5AF2 white / dark
orange #FF9500 / #FF9F0A #945600 / #FF9F0A #FF9500 / #FF9F0A dark / dark
green #34C759 / #30D158 #1E7233 / #30D158 #34C759 / #30D158 dark / dark
pink #FF2D55 / #FF375F #BC0024 / #FF7490 #BC0024 / #FF375F white / dark
teal #30B0C7 / #40C8E0 #1D6B79 / #40C8E0 #30B0C7 / #40C8E0 dark / dark
indigo #5856D6 / #5E5CE6 #4240B5 / #9B9AEF #5856D6 / #5E5CE6 white / white
red #FF3B30 / #FF453A #BF0A00 / #FF7A72 #BF0A00 / #FF453A white / dark
mint #00C7BE / #63E6E2 #00706B / #63E6E2 #00C7BE / #63E6E2 dark / dark
cyan #32ADE6 / #64D2FF #126991 / #64D2FF #32ADE6 / #64D2FF dark / dark
brown #A2845E / #AC8E68 #755F44 / #BEA789 #755F44 / #AC8E68 white / dark
yellow #FFCC00 / #FFD60A #7A5F00 / #FFD60A #FFCC00 / #FFD60A dark / dark
viridian #40826D / #5FB89B #2F6B58 / #6ABDA2 #40826D / #5FB89B white / dark
rust #B7410E / #E0764A #A43A0D / #E79471 #B7410E / #E0764A white / dark
phthalo #005C32 / #17A058 #005C32 / #1BBF69 #005C32 / #17A058 white / dark
mauve (grade-only) #B07AA1 / #C792B9 #885178 / #CE9FC2 #885178 / #C792B9 white / dark

Rules:

  • Light mode follows the readable-label rule: the label on a selected chip, initials circle or action button is dark #1C1C1E on yellow, mint, cyan, green, orange and teal, and white on the rest. Where white on the base fill is under 4.5:1 (blue, purple, pink, red, brown, mauve) the solid fill is the darker pill-text color instead, so white passes. Dark mode: the system fills are bright in dark mode, so white fails on all of them except indigo. In dark mode the label is therefore dark on every key except indigo.
  • Name-derived fallback (null or unknown key): both platforms now use the same rule. Web now matches native bit for bit (native was not changed, because it drives every shipped avatar color): lowercased, trimmed name, well-known-name table first, else djb2 over UTF-8 with 64-bit wrapping, indexed into the original 12 keys. A shared sample-name test (fallbackSamples in the snapshot) fails either side on drift. viridian, rust, phthalo and mauve are never fallbacks, so existing uncolored names keep their color.
  • Default categories seeded per school: Policy indigo, Forms brown, Admissions purple, Athletics orange, Transportation teal, Academic blue, General cyan.
  • A Notice's "urgent" status is not a category color. It uses the danger/red treatment.

New colors and how distinguishable they are (CIEDE2000 distance, then the smaller of the deuteranopia and protanopia simulated distances; 10 or more reads as clearly different, 20 or more as obviously different):

Pair Light Light, color-blind Dark Dark, color-blind
phthalo vs viridian 16 16 14 15
phthalo vs green 38 38 15 15
phthalo vs mint 41 42 28 31
phthalo vs teal 42 43 34 37
viridian vs green (existing) 26 26 17 19
rust vs orange 29 24 20 13
rust vs brown 24 14 17 6
rust vs red 16 8 12 3
mauve vs pink (Grade 12 vs K) 24 26 24 27
mauve vs purple 18 16 20 18

Phthalo is deliberately deep and saturated (light) and mid-lightness (dark) so it is distinct from viridian's gray-green and system green's brightness; in dark mode it is as close to viridian as viridian already is to system green, so the Classes and Departments rows are told apart by their icons as well as color (Principle 2). Rust and red are the closest pair in dark mode for color-blind users; they never sit together and are labelled.

4.6 Derived colors

Grade colors (FluxColors.color(forGrade:)). One color per grade, shared by the grade filter chip, the student's initials circle and the grade pill:

Grade K 1 2 3 4 5 6 7 8 9 10 11 12
Color pink red orange yellow green mint teal cyan blue indigo purple brown mauve

Grade 12 is mauve (#B07AA1 light, #C792B9 dark) so it is no longer the same pink as Kindergarten; mauve is a grade-only palette color (not a category key) with its own pill, solid and text variants (4.5). Distance from pink is 24 (CIEDE2000). No grade (alumni or unknown) is gray. Students who have graduated show the "Alumni" label.

Meal type colors and symbols:

Meal Symbol Color Web counterpart (Lucide)
Breakfast sunrise.fill orange sunrise
AM Snack carrot.fill pink carrot
Lunch sun.max.fill green sun
PM Snack moon.stars.fill indigo moon-star
Other fork.knife teal utensils

Calendar category colors and symbols. The calendar uses a fixed set of four categories plus a fallback, mapped onto the well-known names so colors match the rest of the app:

Category id Label Color (via name) Symbol Web counterpart (Lucide)
general General blue (default symbol)
performance Performance purple (Arts) theatermasks drama
closure Closure red (No School) xmark.circle circle-x
athletic Athletic orange (Athletics) sportscourt trophy
anything else name-derived

Well-known name colors (FluxColors.known): academic/academics/general blue; athletics/sports orange; arts purple; community/lunch green; holiday/holidays/no school red; meeting/meetings/transportation teal; fundraiser pink; field trip mint; admissions/policy indigo; forms/form brown. These apply when a category has no server palette key.

Person-name colors: a person with no photo whose color is not grade-driven (a parent, a staff member) gets FluxColors.color(for: fullName), which is stable per name.

4.7 Status and feedback colors

Meaning Color Where
Urgent / error / destructive red (Color.red, web danger) Urgent banner and row icon, Urgent Only chip, unread dot, error text, Delete buttons
Success green "Added to your calendar" confirmation
Today orange, everywhere Today tile in the month grid, the "Today" link in the calendar header, the Lunch Menus week strip
Selected the area color as a solid fill, with its readable label (dark or white, per 4.5) Chips, calendar day, menu day tile
Unread red dot (8 pt) Notices list

4.8 School and organization themes

AppTheme in Models/AppTheme.swift (native) and lib/theme/* (web). A school, and optionally an organization, can supply a derived set of tokens:

accent, accentHover, onAccent, accentText, accentMuted, background, surface, border, headerBackground, headerText, and optional highlight, highlightText, onHighlight. Each has a light and a dark value (Color.themed(light:dark:)).

Rules:

  1. What a theme may change: the tint (links, selection, prominent buttons: appTheme(_:) sets .tint(theme.accentText)), the faint page background behind lists and forms (themedBackground()), and the navigation bar (themedNavigationBar(), a no-op on macOS where the title bar is the system's).
  2. What a theme must not change: Sunshine area colors, category colors, grade colors, meal and calendar colors, and status colors. The source comment says it directly: "these colors only label things", and "Status colors are never themed."
  3. A school with no theme gets the standard Flux look. The sign-in screens and the watch app are never themed.

4.9 Contrast and WCAG AA targets

Targets:

Content Minimum
Body text, small text (< 18.66 px bold or < 24 px regular) 4.5:1
Large text 3:1
UI component boundaries, focus rings, icons that convey information 3:1

Pills, chips, count badges and initials are small text and are held to 4.5:1. Area-colored icons, tiles and dots are decorative or sit beside a text label (Principle 2), so they are not held to 3:1; where an icon is the only signal it needs a text alternative instead.

Measured pairings (computed by the unit tests, which fail the build below 4.5:1; the lowest ratio over every surface is shown: web canvases #FFFFFF, #FFFBF3, #1E1A2E, #272238 and native .background/.background.secondary in both appearances). "Pill text" is the pill-text color on a 16% tint of the fill; "selected label" is the label on the solid fill of a selected chip, initials circle or action button.

Key Pill text, light Pill text, dark Selected label, light Selected label, dark
blue 4.62 4.65 6.28 4.66
purple 4.68 4.72 6.33 4.83
orange 4.66 5.00 7.74 8.28
green 4.77 5.09 7.66 8.42
pink 4.78 4.61 6.64 4.83
teal 4.79 5.10 6.61 8.55
indigo 5.76 4.75 5.65 5.06
red 4.71 4.63 6.44 4.99
mint 4.72 6.25 8.03 11.30
cyan 4.78 5.64 6.69 9.89
brown 4.62 4.73 6.04 5.53
yellow 5.12 6.59 11.25 12.05
viridian 4.64 4.71 4.53 7.14
rust 4.71 4.68 5.56 5.56
phthalo 5.65 4.69 8.15 5.03
mauve 4.61 4.72 6.01 6.68

5. Typography

5.1 Principles

  • Native: SF Pro through Dynamic Type text styles. Never fixed point sizes for text that carries content.
  • Rounded for warmth: the rounded design (.fontDesign(.rounded)) is applied to display-level text and initials; it only changes glyph shape, not sizing.
  • Weight carries hierarchy more than size: titles bold, row titles semibold or regular headline, metadata regular.

5.2 SwiftUI type scale

Role Style Weight Where
Screen title (student) .largeTitle, rounded bold Student detail name
Screen title (event) .largeTitle bold Calendar event detail
Detail title .title bold Notice, document, staff, guardian detail
Menu date .title / .title2 bold Menu day
Staff row name .title3 semibold Staff directory
Row title .headline regular (notice: medium when read, semibold when unread) Student, guardian, document, notice rows
Row secondary .subheadline regular, .secondary Guardian names, address, document description
Metadata .caption regular, .secondary Dates, page count, size
Pill label .footnote semibold FluxPill
Pill icon .caption2 bold FluxPill
Filter chip label .subheadline semibold FluxFilterChips
Count badge .caption bold, capped at 99+ FluxCountBadge (the only count badge; the private sidebar one was removed)
Section label .footnote semibold, uppercase, .secondary One style everywhere (FluxSectionLabel). System List section headers keep the system style
Action button .headline regular (headline) FluxActionButtonStyle
Banner .subheadline bold, uppercase High Priority banner
Sidebar section header .subheadline, .secondary "My Lists"
Initials .system(size: diameter × 0.4, weight: .bold, design: .rounded) bold FluxInitialsCircle (merged, one font rule everywhere including watchOS)

Initials text scales with the circle, not with Dynamic Type.

5.3 Dynamic Type

Text styles everywhere except initials and icon tiles, which scale with their container (a deliberate exception).

Every list row must remain legible at the largest accessibility sizes. At AX sizes:

  • pills may wrap to their own line under the title instead of sitting at the trailing edge,
  • avatars stay at their fixed diameters,
  • filter chips stay a single horizontally scrolling row.

5.4 Web type

--font-sans: -apple-system, BlinkMacSystemFont, Helvetica, var(--font-geist-sans), sans-serif. Apple devices render SF Pro; others render the Geist Sans webfont. --font-mono is Geist Mono.

Role Classes
Category pill text-xs font-medium, px-2 py-0.5, rounded-full
Filter chip text-xs font-medium, px-3 py-1, rounded-full, 1 px border
Form fields on phones 16 px minimum below 768 px

A rem-based web type scale mirrors the native roles in 5.2, so "row title", "metadata" and "pill" are the same relative size on web and scale with the browser's default font size and zoom. Tokens are in app/app/globals.css (@theme), used as Tailwind utilities in the shared components only (components/sunshine/*); pages that carry their own text-sm/text-xs classes were deliberately not churned.

Role Native (5.2) Web token and utility Size
Row title .headline --text-row-title, text-row-title 1.0625rem (17 px), line 1.5rem
Row secondary .subheadline --text-row-secondary, text-row-secondary 0.9375rem (15 px), line 1.375rem
Metadata .caption --text-meta, text-meta 0.75rem (12 px), line 1rem
Pill label .footnote --text-pill, text-pill 0.8125rem (13 px), line 1rem
Filter chip .subheadline --text-chip, text-chip 0.875rem (14 px), line 1.25rem. One deliberate difference: native is 15; web chip rows keep 14 so a row fits at 320 px
Section label .footnote, semibold, uppercase, secondary .section-label class 0.8125rem, weight 600, uppercase, 0.04em tracking

Count badge uses the metadata role (12 px bold).6).

5.5 Text rules

  • Titles in rows are single line (lineLimit(1)), truncated at the tail. Notice and document descriptions show up to 3 lines of preview.
  • Previews are plain text from the rich-text body: whitespace-collapsed (FluxPreviewText.plain).
  • Dates: abbreviated in lists (Oct 13, 2026), long in detail metadata ("Last Updated").
  • Detail body text is selectable (.textSelection(.enabled)), a deliberate app-wide convention.
  • Numbers in count pills are digits, capped at 99+.

6. Iconography

6.1 Native (SF Symbols)

  • Use filled variants for area and identity icons (symbolVariant(.fill), or explicit .fill names such as doc.fill, star.fill, house.fill, phone.fill, envelope.fill, mappin.circle.fill, graduationcap.fill).
  • Sidebar and Settings lists use hierarchical rendering (.symbolRenderingMode(.hierarchical)).
  • Area icons are tinted with the area color; contact icons are tinted by action (phone green, message teal, email blue); neutral metadata icons are .secondary.
  • Decorative icons are hidden from VoiceOver (accessibilityHidden(true)); informative icons carry a label (for example the urgent exclamation has the label "Urgent").
  • No custom icon assets, no icon fonts.

Common symbols:

Concept Symbol Web counterpart (Lucide)
Urgent (row) exclamationmark.circle.fill (red) circle-alert
Urgent (banner, chip) exclamationmark.triangle.fill triangle-alert
Category pill (notice detail) tag.fill tag
Category pill (document detail) folder.fill folder
File size pill internaldrive.fill hard-drive
Class of (student) graduationcap.fill graduation-cap
Address house.fill house
Location mappin.and.ellipse (events), mappin.circle.fill (staff) map-pin
Send email button paperplane.fill send
Open document arrow.up.right.square square-arrow-out-up-right
Add to calendar calendar.badge.plus calendar-plus
Starred star.fill, yellow star
Organization pill person.3.fill users
Link guardian link.badge.plus link
Registered / pending guardian checkmark.circle.fill / clock.fill circle-checkclock
Add plus; add in capsule: plus.circle.fill pluscircle-plus
Remove in capsule xmark.circle.fill circle-x
Empty states the area symbol (doc.text, megaphone, calendar, person.text.rectangle, fork.knife.circle, person.3, star) file-textmegaphonecalendarcontactutensilsusers-roundstar

6.2 Web (Lucide)

Web uses Lucide (lucide-react) with rounded stroke caps. Lucide is the web analogue of SF Symbols: same concept, similar silhouette, not shared assets.

6.3 Sizing

Context Size
Inline with .footnote text .footnote or .caption font
Pill icon .caption2 bold
Icon tile size × 0.48 semibold inside a size square (default 40 pt)
Contact row icon .title3 in a 28 pt wide frame
Chevrons in toolbars .title3 semibold in a 44 × 44 pt hit area

7. Emoji usage

No emoji appears in any UI string. Iconography is SF Symbols and Lucide only.

  1. Chrome never uses emoji. Navigation, buttons, pills, banners, empty states, errors and form labels use symbols from section 6. Emoji render differently per platform and per OS version, cannot be tinted, and do not scale with the type system.
  2. User-authored content may contain emoji. Notice bodies, document titles, list names, category names and student or organization names render emoji exactly as typed. Do not strip, replace or restyle them. Never truncate in the middle of a multi-scalar emoji (use grapheme-aware truncation, which Swift's lineLimit and CSS text-overflow already provide).
  3. Emoji is never the only signal. If an admin puts a warning emoji in a title, the urgent flag, not the emoji, is what drives the Urgent treatment.
  4. Initials ignore emoji. The initials circle uses the first letter of the first and last name and skips non-letter leading characters (emoji, digits, punctuation); a word with no letter is dropped.
  5. No emoji in push notifications, email subjects and calendar exports, which are copied to places the system does not control. A school can still type them into a title; the product does not add them.
  6. Accessibility: emoji read aloud by VoiceOver as their names ("red heart"). Avoid runs of emoji in generated text.

8. Spacing, layout, shape and elevation

8.1 Spacing scale

Value (pt) Use
2 Tight vertical padding in a "Linked" badge, staff row padding
4 Gap in a pill (icon to text), row vertical padding, count badge vertical padding, stack spacing between lines in a row
6 Gap between an inline badge and a name, pill vertical padding
8 Gap between chips, vertical padding of the chip scroller
10 Section content spacing, chip vertical padding (with a 44 pt hit area)
12 Gap from avatar to text in a row, pill horizontal padding, card inner stack spacing, banner vertical padding
16 Card padding (including the calendar event card), screen gutter, chip horizontal padding, banner horizontal padding, stack spacing between cards
20 Staff header padding
24 Bottom padding on the calendar

The spacing scale is 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48 (points on native, pixels on web; Tailwind 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10, 12), even numbers only. Web publishes the radii and tints as CSS variables next to the palette (--radius-*, --tint-*).

8.2 Layout grid

Native:

  • Single column on iPhone (stack), three columns on iPad and Mac (NavigationSplitView: sidebar, content, detail). The sidebar column is navigationSplitViewColumnWidth(min: 180, ideal: 220).
  • Screen gutter 16 pt (list rows use the system inset; detail screens use .padding() = 16 pt).
  • Detail screens are scrolling stacks of cards separated by 16 pt.

Web (app plane): Tailwind responsive utilities. The shell switches at lg (1024 px): below it a mobile header with a menu, above it a sticky sidebar. A separate rule raises form fields to 16 px below 768 px. The named breakpoints are phone below 768 px, tablet 768 to 1023 px, desktop 1024 px and up; component behavior aligns to them.

8.3 Corner radii

Radius Use
Capsule (fully rounded) Pills, chips, count badges, contact action buttons ("Call", "Email")
8 Document preview thumbnail
10 Month-grid selected-day tile (34 × 34)
12 Calendar event date tile (54 × 54)
14 Action buttons, High Priority banner, menu day tile
16 Menu image
18 Cards (fluxCard), staff and guardian header panels, calendar event cards
size × 0.28 Icon tiles (11.2 pt at the default 40 pt)
Circle Avatars and initials

Radius tokens: pill (capsule), tile (12), control (14), card (18), image (8 for thumbnails, 16 for hero images). The near-identical 10, 12 and 14 choices for a "small tile" are now 12 for tiles and 14 for controls (buttons, the banner).

Tokens: --radius-card, --radius-panel.

8.4 Elevation and surfaces

No explicit shadows. Depth comes from surface color:

  • Cards use .background.secondary (which resolves to a slightly elevated grouped surface in both appearances).
  • The menu week strip uses .background.secondary.
  • Standard components (toolbars, tab bars, sidebars, sheets) use the OS's Liquid Glass automatically. Do not reimplement system chrome; use glassEffect / GlassEffectContainer only for genuinely custom floating controls, and never hard-code blur or tint values.

fluxCard() now draws the soft border its comment always promised (1 pt separator on the 18 pt rounded rectangle), and the calendar event card uses it. Web Card has the same soft border.

8.5 Sizes of repeated elements

Element Size
List avatar (student, guardian) 48 pt
Staff row avatar 56 pt
Parent row avatar in Student Detail 60 pt
Staff and guardian detail avatar 112 pt
Student detail photo 260 pt
Watch student detail photo 55% of screen width (screenBounds.width * 0.55)
Default initials circle 44 pt
Default icon tile 40 pt
Action button full width, 50 pt minimum height
Month grid cell 44 pt minimum height; selected tile 34 × 34
Menu day tile equal width, 10 pt vertical padding, 6 pt dot
Event date tile 54 × 54
Calendar event dot 4 pt
Toolbar chevron hit area 44 × 44 pt
Unread dot 8 pt
Document preview max width 420 pt
Menu image 250 pt tall

9. Components

Each component lists its purpose, anatomy, specification, behavior and accessibility.

9.1 FluxPill

Vector sample at iPhone width: category pills in every palette color, grade pills from K to 12th and Alumni, pills with icons, and count badges.iPhone
Vector sample at iPad width: category pills in every palette color, grade pills, pills with icons, and count badges.iPad
Category, grade and icon pills, and count badges, on iPhone and iPad. Native samples show SF Symbols as plain squares: the symbol names are listed in the iconography tables.

Purpose: a small tinted label for a category, grade, relationship, or file size.

Anatomy: optional leading symbol, text, capsule.

Text .footnote semibold, one line; symbol .caption2 bold, 4 pt gap; padding 12 horizontal, 6 vertical; foreground = the tone's pill-text color; background = the tone's fill at 0.16 in a Capsule.

Usage:

  • One pill per fact. Place at the trailing edge of a row, or at the top of a detail header.
  • Detail-header pills carry an icon (tag.fill, folder.fill, internaldrive.fill, graduationcap.fill); row pills do not.
  • A pill with no text is not rendered.

Variants in use: category (palette color), grade (grade color), "Class of YYYY" (blue), relationship (name-derived color), "Parent" (purple area color), registration state (with checkmark.circle.fill or clock.fill), file size (secondary).

Accessibility: the text is read as-is.

Web: components/sunshine/Pill.tsx, rounded-full px-3 py-1.5 text-[13px] font-semibold on color-mix(in srgb, var(--cat-<key>) var(--tint-pill), transparent), the same size and 16% tint as native.

9.2 FluxCountBadge

Web
Count badges in several area colors, including the 99+ cap.

Purpose: a count on a sidebar row, color-matched to the row's icon.

.caption bold, 8 pt horizontal and 4 pt vertical padding, the tone at 0.16 in a capsule (same rule as a pill; a count badge is a capsule label), digits capped at 99+. Web: components/sunshine/CountBadge.tsx, same rule, with a screen-reader label.

9.3 Filter chips (FluxFilterChips)

Chips with one selected, with nothing selected, and with more than twelve options folded behind More; then the native chips alongside action buttons, contact capsules and the banner. Native samples show SF Symbols as plain squares: the symbol names are listed in the iconography tables.

Purpose: a single-select filter row above a list (grade, category, urgent, calendar category).

  • Horizontally scrolling, no indicators, 8 pt gaps, 16 pt side padding, 8 pt vertical padding.
  • First chip is "All" (title configurable). Selecting the selected chip again clears to All, except All itself.
  • Chip: .subheadline semibold; padding 16 / 9; optional leading symbol (.caption bold); capsule.
  • Unselected: the tone's pill-text color on the tone at 0.16. Selected: the tone's solid fill with its readable label (dark text on yellow, mint, cyan, green, orange and teal; white on the rest in light mode; 4.5). Chips are visually about 40 pt tall with a 44 pt hit area.
  • accessibilityAddTraits(.isSelected) on the selected chip.
  • scrollEdgeEffectHidden(true, for: .all), because iOS 26's soft scroll-edge effect would otherwise fade a short scroller under the navigation bar out of sight.

Rules:

  • Show chips only when there is something to filter. Document and notice category chips appear only when at least one item has a category; the Urgent Only chip appears only while an urgent notice is visible.
  • Chip order: All, then Urgent Only (notices), then categories A to Z (localizedCaseInsensitiveCompare).
  • Only one chip is selected at a time.

Web: components/sunshine/FilterChips.tsx renders links (?category=<id>, plus urgent=1), wrapping rather than scrolling. Unselected chips are tinted fills like native (the colored outline is gone); the selected chip is the solid fill with a check mark and aria-current. "All" takes the area color. Past 12 chips the rest fold behind a native <details> "More", and a pinned "Mine" chip stays next to All. Tap targets are 44 px. Because the chips are links, they work without JavaScript. CategoryFilterChips is built on it.

9.4 Section header (FluxSectionHeader)

A Label with a filled SF Symbol tinted in the area color, used for groups in Forms and Settings (for example "Face ID unlock"). Title in the standard section-header style.

9.5 Initials circle and avatars

Web
Vector sample at iPhone width: initials circles in several colors, icon tiles in area colors, a card with a staff member and department pills, and Today and Selected day tiles.iPhone
Vector sample at iPad width: initials circles, icon tiles, a card, and Today and Selected day tiles.iPad
Initials circles in grade colors and name-derived colors; native initials, icon tiles, a card and the Today and Selected day tiles. Native samples show SF Symbols as plain squares: the symbol names are listed in the iconography tables.

Web: components/sunshine/InitialsAvatar.tsx, the same concept.

  • Initials by the shared rule (first letter of the first and last word, skipping non-letters, uppercased), .system(size × 0.4, bold, rounded), in the tone's onSolid color on its flat solid fill (no gradient, so contrast is testable), in a circle. A tint parameter picks the tone; with none, the watch's accent-tinted variant is still available. Hidden from VoiceOver. A (firstName, lastName) form takes each field's first letter, also skipping non-letters.
  • Photo views (StudentPhotoView, GuardianPhotoView, StaffPhotoView) show the photo when present, otherwise the initials fallback.
  • Color rules: student = grade color; parent and staff = FluxColors.color(for: name).

Rules: a person row always has a leading 48 pt avatar (56 pt for staff). The photo is a circle. Never show an empty gray circle. Initials are decorative and the name is always adjacent text.

9.6 Icon tile (FluxIconTile)

Web
Icon tiles tinted in each area color.

A rounded square holding an SF Symbol: symbol size × 0.48 semibold in the tile's color, on the tone at 0.14, corner radius size × 0.28 continuous. Default 40 pt. Used for the document row (doc.fill in the category color, or the Documents area color without a category). The meal and event icons use plain colored symbols or the date tile.

9.7 Cards (fluxCard())

Web
A standard card (16 px radius, 16 px padding) and a large panel (24 px radius).

Padding 16, full width, leading aligned, .background.secondary in a continuous 18 pt rounded rectangle with a soft border. Used for grouped detail sections (header card, body card, attachments card, metadata card) and the calendar event card.

9.8 Action button (FluxActionButtonStyle)

Full-width action buttons in eight area colors.

Purpose: the one big primary call to action on a detail screen: "Open Full PDF Document", "Send Email", "Add to Apple Calendar".

Full width, minimum height 50 pt, .headline, the tone's readable label on its solid fill, 14 pt continuous radius, 0.8 opacity while pressed.

Rules: at most one per section; color is the area color (documents indigo, calendar blue, staff = the person's name color). 50 pt exceeds the 44 pt minimum touch target.

Contact action capsules: "Call", "Email", "Text" use a capsule filled with the action color (phone green, message teal, email blue), .subheadline semibold white label. A contact capsule is at least 44 pt tall (minHeight: 44, still capsule-shaped); web capsules are min-h-11.

9.9 High Priority banner

FluxHighPriorityBanner, Label("HIGH PRIORITY NOTICE", systemImage: "exclamationmark.triangle.fill"), .subheadline bold, the red tone's readable label on its solid fill, 16 / 12 padding, full width, 14 pt continuous radius. The web notice page shows the same banner and copy. Appears above the notice header card, only for notices whose flags contain urgent.

Do not use this banner for anything that is not an urgent notice.

9.10 Notice and document header card

Anatomy: a card with (top row) a category pill with a leading symbol on the left and either the date (notice) or the size pill (document) on the right, then the title in .title bold, then a metadata line (Expires … for notices; doc.fill + filename + page count for documents).

Notice header also shows the audience tag next to the category pill.

9.11 Document preview card

An uppercase "DOCUMENT PREVIEW" section label, then the first page of the PDF rendered with PDFKit (scaled so the long edge is 900 px), centered, maximum 420 pt wide, 8 pt continuous corner radius, white page background. Tapping opens the full document. The preview is cached by attachment id in the Caches directory so a document viewed once previews offline. Multi-page documents show page 1 only; the page count appears in the header and in the metadata row. If the preview cannot be produced (offline on first view, not a PDF), the section is not shown.

The metadata block beneath is label/value rows (File Size, Page Count, Last Updated) separated by dividers, label .secondary, value trailing.

9.12 Rows

Web
Web
A student row, a student row without optional data, a staff row with department pills and contact actions, and two content rows.

Student row (StudentRowView): 48 pt photo or grade-colored initials; name .headline; a "Linked" badge (accent, .caption semibold, 6 / 2 padding) when the student is linked to the viewer; guardian names, street and city/state/zip each on their own .subheadline secondary line; trailing grade pill; trailing yellow star.fill only when starred (never show an empty star).

Guardian member row (GuardianMemberRowView): same anatomy as the student row; trailing "Parent" pill in the purple area color; optional contact line.

Staff row (StaffDirectoryView): 56 pt avatar; name .title3 semibold; title .subheadline secondary; tappable email and phone rows with leading symbols; context menu (Copy, Email, Call).

Document row: icon tile, .headline title, .subheadline description (secondary), category pill, .caption metadata (pages, size).

Notice row: optional urgent exclamationmark.circle.fill (red) and unread dot (8 pt red), .headline title (medium read, semibold unread), trailing category pill (or the audience tag when there is no category), up to 3 lines of .subheadline preview, .caption date. Leading swipe toggles read (matches Apple Mail), trailing swipe is Trash.

Event row (CalendarView): a 54 × 54 date tile (.title2 bold day number over .caption month, color.opacity(0.14), 12 pt radius), title .headline, category pill with the category symbol, time line .subheadline secondary, optional location (mappin.and.ellipse) and optional result summary (trophy) in .caption or .subheadline secondary. The whole row is an 18 pt rounded card on .background.secondary, 14 pt padding.

Row rules: vertical padding 4 pt inside the list's own insets; one primary line, up to three secondary lines; the trailing area holds at most one pill plus one status icon.

9.13 Empty, loading and error states

  • Empty: ContentUnavailableView(title, systemImage:) with the area symbol (doc.text, person.text.rectangle, person.3, star, fork.knife.circle), or a short secondary line ("No notices right now."). In the calendar, an italic tertiary line: "No events on this day", "No upcoming events". In the menu: "No menu listed" italic tertiary.
  • Not found: ContentUnavailableView("Notice not found", systemImage: "megaphone"), "Event not found" (calendar), "Document not found" (doc.text.magnifyingglass).
  • Error: "Something went wrong" with a retry action; inline errors in .caption red.
  • Loading: ProgressView; the menu image shows "Imagining what this looks like…" in secondary text.

9.14 Forms and pickers

Category picker (CategoryPickerRow): a menu Picker titled "Category" with "None" plus one option per school category, each shown as circle.fill tinted with the category color. It is always shown on the Document and Notice edit forms so an item with no category can be given its first one. It shows the last cached list immediately, refreshes on appear (online only), keeps a good list if the refresh fails, and shows a footnote when the list is empty: "No categories yet. A Senior Admin can add them on the web in School Settings." or, after a failed fetch with an empty cache: "Couldn't load categories. Check your connection and reopen this form." Choosing None sends a null. An unchanged selection sends nothing (CategorySelection.keep).

Mark as urgent toggle on notice create and edit. Hidden for Organization Editors, who send neither category nor flags.

Web: CategorySelect, CategoriesManager (School Settings, Senior Admin) for managing the categories and their palette colors.

Forms in general: use native Form/List sections on Apple platforms; labels sentence case; errors announced (web role="alert").

9.15 Calendar and week strip

Month grid (CalendarMonthGrid): weekday labels .caption medium secondary; day cells 44 pt minimum height; selected day = 34 × 34 rounded square (radius 12) filled with the Calendar area color (blue) with its readable label; today = orange 20% tint with orange number; days with events use .subheadline semibold and a 4 pt dot beneath; other days .subheadline. Selected, today and has-events are conveyed to VoiceOver as a value plus a selected trait (not by color alone).

Header: previous and next chevrons in 44 × 44 pt hit areas, month label .headline, a "Today" link .caption semibold in orange when the visible month is not the current one.

Below the grid: category filter chips (Calendar color for All), the selected day's events, then a forward-looking "upcoming" list. The category filter chips sit above the day list.

Menu week strip (MenuWeekView): month label .caption semibold secondary above the week range .title3 bold, "Jump to Today" .subheadline medium in the accent color, 44 pt chevrons. Seven equal tiles: weekday .caption semibold, day number .title3 bold, a 6 pt dot (hidden when the day has no menu). Selected = Lunch Menus green solid fill with dark text (the rule for green) and dot; today = orange (14% tint, orange text; it was green); other = secondary.opacity(0.1). Radius 12, vertical padding 10. The strip sits on a .background.secondary band.

Selected day content: the full date .title2 bold, then one block per meal type: icon (.title2, meal color), meal name .title3 bold, chevron, items in .body. No times (section 3.3).

9.16 Lists restyle (custom Lists)

Student members: the same row as the Starred and Students rows (initials circle sized 48 in grade color; name .headline); tapping opens Student Detail.

Parent members (GuardianMemberRowView): 48 pt photo or colored initials, name .headline, secondary line (relation or the linked students' names, or "Not linked to a student yet"), optional contact line, trailing "Parent" pill. Tapping opens the linked student's detail when a link resolves; otherwise it opens an independent guardian detail (GuardianMemberDetailView).

Guardian link resolution order (ListGuardianLinkResolver): the linked student id, then an allowlist or email match, then a unique full-name match.

Guardian detail: 112 pt avatar in a header panel tinted at 0.14 (18 pt radius), name .title bold, relation pill and a registration pill (checkmark.circle.fill registered / clock.fill pending), then contact rows (phone green "Call", message teal "Text", email blue "Email"), then the linked-students section with graduationcap.fill.

9.17 Detail headers for people

Web
A person's detail header: tinted panel, large avatar or photo, name, pills and contact capsules.

Student detail: photo 260 pt centered; name .largeTitle bold rounded centered; grade pill (grade color) and "Class of YYYY" pill (blue, graduationcap.fill) side by side; then sections "Parents and Guardians" (one list section per guardian, name .headline, relation pill, then contact rows), Address (house icon, standard postal format: street on one line, "City, State Zip" below), organization and list capsules.

Staff detail: a header panel tinted at 0.14 (radius 18, padding 20) with a 112 pt photo or colored initials, name .title bold, title .title3 secondary; then Email (with the full-width "Send Email" action button in the person's color), Phone, Location (mappin.circle.fill) sections with uppercase .caption section labels. The biography appears as a plain section without a title.

10. Screen patterns

Each pattern lists the structure and the Sunshine elements it must use.

Web admin screens: School Settings, Manage Access, Audiences, Notifications, Account, Audit Log and API Docs use tokens and shared components only (Card, SectionHeader, Pill, 44 px controls, rounded-2xl, neutral gray area tone for settings, indigo for Access) and keep their denser form layouts. Swagger UI on the API Docs page sits on a white panel because its theme is light-only.

Screen Structure Sunshine elements
Sidebar A list of areas, each with a filled colored icon and a matching count pill. Students and grades; My Lists with a "+" in its header; Organizations; school switcher and Profile at the bottom Area colors, FluxCountBadge, hierarchical symbols
Documents list Optional category filter chips; rows with icon tile, title, description, category pill, page and size metadata Category chips and pills, icon tile in the category color
Document detail Header card (category pill, size pill, title, filename, page count); a description section; metadata rows; big "Open Full PDF Document" button; document preview Action button in indigo, preview card
Notices list Chips: All, Urgent Only (when any urgent), then categories; rows with urgent icon, unread dot, title, category pill, 3-line preview, date Urgent red, category pills
Notice detail Optional High Priority banner; header card (category pill with tag.fill, audience tag, date, title, expiry); rich-text body in a card with no section title; attachments card Banner, cards
Calendar Month grid, category chips, selected day's events, upcoming list; event detail with category pill, largeTitle title, location, times, EVENT DETAILS, Add to Apple Calendar button, results section Calendar blue, orange today
Lunch Menus Week strip with Jump to Today; selected day with a block per meal; meal detail retains the generated image Green area, meal icons and colors; no times
Staff directory Rows with 56 pt avatar, name, title, tappable email and phone Orange area; name-derived avatar color; department pills (up to two, then +N) and department filter chips when Departments is active
Students list Grade filter chips (color-matched to grade), rows with guardians, address and grade pill; colored placeholders for missing photos Grade colors, purple All chip
Student detail Large photo, name, grade and class-of pills, parents section, address, organizations, lists Pills, contact capsules
Lists List detail with student and parent members styled as in 9.16 Row reuse
Polls, Permission Slips, Sign-Ups Lists and details in their area colors (pink, mint, cyan) Area colors, count pills
Directory Map Whole School / grade / organization chips above a map; teal area MapFilterBar chips

11. Motion

  • Selecting a menu day uses withAnimation (the default curve).
  • Buttons dim to 0.8 opacity while pressed (FluxActionButtonStyle).
  • Spinners for loading; the launch screen has its own animation and honors Reduce Motion (the only file with a Reduce Motion check).
  • Navigation transitions, sheets and list changes use the system defaults.
  • No custom haptics.

Rules:

  1. Use system transitions and spring defaults; no bespoke durations.
  2. Any non-system animation must check Reduce Motion (accessibilityReduceMotion on native, prefers-reduced-motion on web) and replace movement with a fade or an instant change.
  3. Nothing animates continuously except a progress indicator.
  4. Haptics: a light selection haptic on chip and day selection is acceptable on iPhone; none on macOS or watchOS beyond system behavior. Not currently implemented.

Every custom animation on native honors Reduce Motion (the menu day selection, chip and calendar changes). A single @media (prefers-reduced-motion: reduce) rule in globals.css collapses every transition and animation to an instant change and turns smooth scrolling off. The custom motion that exists is the sidebar chevron rotation, hover color changes on buttons and the lunch image spinner (a static ring under Reduce Motion, next to its text label). A unit test fails if a component adds an animate-* utility without motion-reduce:animate-none, and the route crawl asserts no element keeps a running transition under reducedMotion: reduce.

12. Accessibility

12.1 Requirements

Area Rule
Contrast 4.5:1 text, 3:1 large text and UI
Color is not the only signal Selected chips carry .isSelected; calendar days expose today, has-events and selected as values; unread has the label "Unread"; urgent has the label "Urgent" and an icon
Touch targets At least 44 × 44 pt (native), 24 × 24 CSS px minimum (WCAG 2.5.8) with 44 preferred
Dynamic Type Text styles only; layouts reflow rather than clip
VoiceOver Every icon-only button has a label; decorative symbols are hidden; avatars hidden when the name is adjacent; images labeled when informative
Reduce Motion See section 11
Keyboard (macOS, web) All controls reachable and operable; visible focus; the web has a skip link
Forms Every field has a programmatic label; errors announced
Language lang="en"
Increase Contrast, Bold Text Rely on system colors and text styles
Voice Control Use real Buttons, not tap gestures

12.2 Component accessibility contract

  • A pill is text; it must have at least 4.5:1 against its tint (see 4.9).
  • A chip is a button with a selected trait and a name equal to its label.
  • An avatar is decorative and hidden; the name beside it is the accessible content.
  • An icon tile is decorative; the title beside it is the content.
  • A banner is a single accessible element whose label is its text.
  • A count badge reads as part of its row ("Notices, 3 unread" style). Confirm the combined label.

13. Light and dark mode

13.1 Rules

  1. Follow the system. No in-app theme toggle. On Apple platforms there is no preferredColorScheme override in production code (the launch screen comment says so explicitly). On the web, dark mode is driven only by @media (prefers-color-scheme: dark), with no JavaScript.
  2. Never hard-code a color that does not adapt. Use system colors, asset-catalog colors with a dark variant, or CSS variables with a dark override. Fixed white is allowed only for text on a saturated fill.
  3. Tinted fills keep the same alpha in both modes (16% for pills, chips and count badges; 14% for tiles and panels), because the system colors brighten in dark mode and the tint stays legible against the darker canvas.
  4. Dark surfaces are warm purple-charcoal on the app plane (#1E1A2E), never pure black.
  5. Text-safe variants differ by mode. In light mode, small colored text uses the darker -text token; in dark mode the base token already clears AA, so the -text token equals the base.
  6. Images: document previews render a white page in both modes (the page is the document, not UI). Student photos are never inverted or dimmed.
  7. Shadows and borders are subtle in dark mode: prefer a border change over a shadow.

13.2 Token pairs

Token Light Dark
--background (app) #FFFBF3 #1E1A2E
--surface (app) #FFFFFF #272238
--foreground (app) #2B2013 #F5EEDE
--foreground-muted (app) #6B5E4D #C9BFAE
--border (app) #F0E4D0 #3A3350
AccentColor / --flux-blue #2E7FCB #74B4EF
--accent-text (web default) #2563EB #60A5FA
--accent-muted (web default) #EFF6FF #172554
--header-icon #71717A #A1A1AA
--highlight #FFB020 #FFC856
--highlight-text #8A5A00 #FFC856
--cat-<key>, --cat-<key>-text palette table, section 4.5 palette dark column; indigo text #8E8CFF
--danger #DC2626 #F87171
--warning #D97706 #FBBF24

Web secondary text in dark mode uses text-zinc-400 or lighter (the accessibility pass replaced text-zinc-500 on dark surfaces, 3.5:1, with dark:text-zinc-400).

13.3 Verification

Increase Contrast is handled by @media (prefers-contrast: more) in globals.css: border and secondary text tokens get stronger values in both appearances (muted text at least 7:1, borders at least 3:1, unit-tested), while hues, tints and tiles are unchanged. The route crawl (e2e/tests/app/sunshine-w5-route-crawl.spec.ts, in npm run test:a11y) signs in as a Senior Admin and as a parent of a scratch school with every area seeded, discovers every sidebar destination plus up to three detail or form pages under each, and runs axe (WCAG 2.2 AA) on every page in four appearances: light, dark, light with Increase Contrast, dark with Increase Contrast. Lighthouse accessibility scored 100 on all six pages sampled.

14. Data-absent rules and content rules

The UI is data-driven. These rules decide what appears when data is missing.

Data When absent Where
Document or notice category No category pill; no category chips if no item has one; the notice row shows the audience tag instead of a pill Documents, Notices
Notice flags No urgent icon, banner or Urgent Only chip Notices
Event location The location line does not render Calendar
Photo Initials circle in the grade or name color Everywhere
Page count (preview) Page count omitted Document detail
Document description The description line does not render Documents list
Staff title, email, phone, location The line or section is omitted (StaffDirectoryFormatting.nonBlank) Staff
Guardian name Falls back to the email (or "Member") Lists
Student address Address section omitted Student detail
Star Only starred rows show a star Rosters
Meal "No menu listed", no dot on the day tile Menus
Department Up to two department pills (phthalo, the Departments color), then a neutral "+N", on each Staff row, and one department filter chip per department that has staff; shown only while the Departments feature is active and the person is in at least one department (SwiftUI additionally requires features.departments to be explicitly true). No empty pill, no chips otherwise Staff
Subject expertise Not shown: no data; a location section is used instead Staff
Meal times Never shown Menus
Issued by Never shown Notices

Content guidelines

  • Titles: title case or sentence case as the author wrote them; the UI never changes author text.
  • Descriptions: first lines appear as a preview; authors should lead with the point.
  • Category names: short (one or two words); chips scroll, but long names crowd the row. Limit 24 characters.
  • Counts: digits only.
  • Dates: abbreviated in lists, long in detail, localized by the system.
  • Placeholders: examples must be fictional.

15. Platform specifications

15.1 iOS

  • Navigation: stack navigation from the sidebar list (the sidebar is the root screen on iPhone). Inline navigation title in the sidebar (navigationBarTitleDisplayMode(.inline)).
  • Chrome: system navigation bars, tab and toolbars with Liquid Glass; school theme may tint the bar (themedNavigationBar).
  • Controls: leading swipe for read and star (matches Apple Mail), trailing swipe for destructive actions; context menus for copy actions.
  • Biometrics: Face ID or Touch ID unlock overlay on foreground when enabled, with a "Use Passcode" button and a "Sign Out" escape. iOS and iPadOS only (#if os(iOS)). The usage string is "Use Face ID to quickly and securely unlock the directory."
  • Widgets: none.
  • Push: APNs; tapping a push opens the web app externally (no native deep link yet).
  • Safe areas: respect them everywhere; filter scrollers sit under the navigation bar with the scroll-edge effect disabled.
  • Minimums: iOS 26.5; no availability fallbacks for older OSes.

15.2 iPadOS

  • Layout: a three-column NavigationSplitView (sidebar, content list, detail). Sidebar width min 180, ideal 220.
  • Detail: card-based detail scrolls within the detail column; cards stay at full column width (no max width today). Constrain readable text to about 700 pt wide and center it in very wide detail columns.
  • Input: support pointer hover on rows and chips (system default for Button), keyboard shortcuts for primary actions.
  • Multitasking: Split View and Slide Over supported through the standard size classes. The layout must work in compact width; chips and pills must not rely on a regular width.
  • Presentation: sheets as form sheets on iPad; popovers for pickers where the system does so.
  • Biometric lock is the same as iOS.

15.3 macOS

  • Navigation: the same split view; the sidebar uses the system's source-list look (hierarchical symbols, colored icons preserved).
  • Windows: School Settings, Access and the Directory Map open in their own windows (WindowGroup(id:) keyed by a context value to dedupe), because they are too wide for a split-view column. A Settings scene provides the standard Settings window (Cmd+,).
  • Input: pointer and keyboard first. Context menus on rows (Copy, Email, Call). Text in details is selectable. No swipe actions as the primary affordance; expose the same actions in context menus and toolbars. Full Keyboard Access for tap-gesture rows is a known limitation.
  • Biometrics: none; Touch ID is not used on Mac in this app.
  • Chrome: the title bar is the system's; the school theme does not tint it.
  • Sizes: sheets declare minimum sizes (for example 420 × 480). Pill and chip sizes are the same as iOS; hit areas can be smaller on a pointer but the specification keeps the iOS sizes for consistency.
  • Menus: standard menu bar; no custom menu extras.
  • Not available: Contact Export, message sending, biometric lock.

15.4 watchOS

The watch app is read-only. It lists students (name, grade, and a star indicator), shows a Contacts-style detail (a photo at 55% of the screen width, or the initials fallback; guardian sections with tap-to-call, long-press-to-text and tap-to-email; address to Maps; read-only private note), a school switcher for multi-school parents, and a static-launcher complication (all four accessory families, app icon only). Data comes from the phone through WatchConnectivity.

Sunshine status: not applied. The watch uses InitialsAvatarView without a tint (accent-tinted initials, not grade colors), .headline and .caption text, and a yellow star. The Sunshine layer is not applied to the watch app yet.

15.5 Web: app plane

  • Shell: header and sidebar. Below lg (1024 px) a mobile header with a menu; at lg and above a sticky sidebar. A skip link and main#main-content landmark exist.
  • Canvas: warm paper #FFFBF3, surfaces white, plum-charcoal in dark mode.
  • Shape: rounded-2xl and rounded-3xl cards, pill buttons, warm-tinted shadows.
  • Typography: SF Pro on Apple, Geist Sans elsewhere; 16 px form fields below 768 px.
  • Icons: Lucide.
  • Sunshine on web today: the shared tokens (--cat-*, --area-*, --grade-*, --tint-*, --radius-*) and the components in app/components/sunshine/ (Pill, CountBadge, FilterChips, InitialsAvatar, IconTile, SectionHeader, Card, StudentRow, PersonRow, ActionButton), plus CategoryPill, CategoryFilterChips and the Staff department pills and chips. Admin screens get tokens and components only for now. components/sunshine/DetailHeader.tsx (name-derived panel at --tint-tile, 160 px photo or initials on Student Detail, 128 px on Staff Detail), ContactCapsule.tsx (44 px, the number or address itself, call green, text teal, email blue, tint layered over the surface so it holds 4.5:1 on a tinted panel) and Pill solidBase; the section cards use Card and SectionHeader with the area icon. The web sidebar uses one filled Lucide icon per area in its area color, CountBadge for counts with a spoken name, and the area-colored selected row with aria-current="page".
  • Dark mode: prefers-color-scheme only.
  • PWA: manifest.json in /public, referenced from the root layout so /login and /register get it too; icons icon-192.png, icon-512.png, icon-maskable-512.png, apple-touch-icon.png (opaque). No service worker, by design. Installability only, no offline caching or push on the web. Do not add next-pwa.
  • Responsive: phone-first for parents; admin screens remain usable at tablet width. Tables collapse or scroll horizontally on phones. Category chips wrap (flex-wrap) on web rather than scroll.
  • No JavaScript dependence for filters: chips are links.
  • Map: Google Maps on web, Apple MapKit on Apple platforms (a deliberate difference).

16. Localization and internationalization

English only (lang="en"). Strings are inline in views, not in a string catalog.

17. Extending the system

17.1 Definition of done for a new component

  • Uses only tokens from this specification.
  • Has an entry in section 9 with sizes, states and accessibility.
  • Handles absent data (section 14).
  • Passes the contrast targets in 4.9.
  • Works in light, dark, large type and VoiceOver.

Appendix A. Token reference

A.1 SwiftUI

Token Definition
FluxArea.<area>.color See 4.4
FluxColors.palette blue, purple, orange, green, pink, teal, indigo, red, mint, cyan, brown, yellow (the name-derived fallback uses only these)
FluxColors.color(for:) Known-name color, else djb2 index into palette
FluxColors.color(forGrade:) Grade table in 4.6
CategoryColor.keys / .color(forKey:) / .color(key:name:) Palette key to color, with name fallback
MealType.icon, MealType.color Meal table in 4.6
CalendarCategory.color(_:) Calendar table in 4.6
FluxPill 12 / 6 padding, footnote semibold, 0.16 tint, pill-text color, capsule
FluxCountBadge 8 / 4 padding, caption bold, 0.16 tint, 99+ cap
FluxFilterChips 16 / 8-10 padding, subheadline semibold, 0.16 tint, selected solid + readable label
FluxInitialsCircle size × 0.4 bold rounded, solid fill + readable label, circle (watchOS too)
FluxIconTile 0.48 symbol, 0.14 tint, radius 0.28 × size
FluxActionButtonStyle 50 pt min height, radius 14, headline, solid + readable label
fluxCard() padding 16, radius 18, .background.secondary, soft border
FluxTone / CategoryColor.textColor, solidColor, onSolidColor The adaptive fill, pill-text, solid and on-solid colors for a palette key
FluxSectionLabel, FluxHighPriorityBanner, withFluxAnimation One section-label style; the banner; Reduce Motion aware animation
AccentColor, SunColor, CoralColor, LeafColor Asset catalog colors with dark variants
AppTheme School theme roles with light/dark pairs

A.2 Web

Token Light Dark
--background, --surface, --foreground, --foreground-muted, --border Section 4.3 Section 4.3
--flux-blue, --sun, --coral, --leaf, --warning, --danger Section 4.2 Section 4.2
--flux-blue-text, --coral-text, --leaf-text, --sun-text Section 4.2 = base
--accent, --accent-hover, --on-accent, --accent-text, --accent-muted, --highlight, --highlight-text, --on-highlight, --header-bg, --header-text, --header-icon Defaults reproduce the standard blue look; a school theme overrides
--cat-<key>, -text, -solid, -on-solid Section 4.5 Section 4.5
--area-<name>, --grade-<n> (each with -text, -solid, -on-solid) Section 4.4, 4.6 same
--tint-pill, --tint-tile, --radius-* 16%, 14%; 4.8 radii same
--font-sans, --font-mono SF Pro / Geist Sans stack; Geist Mono

A.3 Shared

Token Rule
Pill, chip, count badge tint 16% on both platforms; icon tile and panel 14%
Selected chip, initials circle, action button the tone's solid fill with its readable label (4.5)
Urgent red / danger, never a category color
Touch target 44 pt minimum
Corner radius capsule for pills and chips; native 12 tile, 14 control, 18 card; web rounded-2xl cards and inputs, rounded-3xl large panels
Spacing scale 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48

Colophon and icon licenses

Web icons. The icons drawn on this page are Lucide icons, shown as inline SVG, so they stay sharp at any size. Lucide is released under the ISC License; some icons derive from Feather (MIT License, Copyright (c) 2013-present Cole Bemis).

Native icons. SF Symbols are named in the tables, never drawn: Apple's SF Symbols license does not allow redistributing the glyph artwork outside of apps and mockups. In the native (SwiftUI) samples every symbol is a plain square, and all sample text is outlined shapes, so no font file is embedded or distributed. Use the symbol names with Apple's own SF Symbols app and system fonts.

Lucide ISC License

Copyright (c) 2026 Lucide Icons and Contributors

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.