| name | Unit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| description | The fastest, most trustworthy gym logging tool for athletes who already know how to train. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| colors |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typography |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rounded |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spacing |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| components |
|
Creative North Star: "The Lab Notebook"
Unit is the gym notebook a serious lifter has been writing in for a year — quiet, page-like, and trusted because it never gets in the way. The visual system is built around a single belief: under fatigue, the only things that should compete for the eye are the numbers the lifter is reading and the one button they need to press next. Everything else recedes into milk-grey paper and bond-white cards.
The aesthetic is iron and paper: solid Ink (#0A0A0A) accents on Bond (#FFFFFF) cards floating on a Milk (#F5F5F5) page. There is no glow, no gradient, no decorative motion in the hot loop. Cards separate from the page through fill contrast, never through drop shadows. Numerics use monospaced digits and dominate visual hierarchy — weight, reps, and timers are the loudest elements on every screen because they are the data the lifter is actually reading mid-set. Copy is small, secondary, and quiet.
This system explicitly rejects the visual category of "fitness tracker" software. It is not a Strong/Hevy/Jefit spreadsheet, not a Strava feed, not a Whoop dashboard. There are no neon data viz arcs, no gradient hero metrics, no recovery rings, no badge animations, no stock-photo athletes. If a pixel doesn't help log faster or read state more clearly under fatigue, it does not belong.
Key Characteristics:
- Light-first, system-honoring (light mode is the design baseline; dark adapts via system trait, never drives decisions)
- Flat by doctrine — separation through fill contrast, never shadows
- Numerics-first hierarchy (monospaced digits at display size dominate every workout screen)
- One Ink-black primary CTA per stress screen, full-width, 60pt tall
- Surface area kept small: 12 colors, ~14
AppFontcases (Geist sans + Geist Mono), 3 radii, 8 spacing steps — and growing them requires a Gym Test justification
The palette is named like materials in a workshop, not like UI states. There is one accent (Ink), and it carries the identity. Everything else is a neutral or a system signal.
- Ink (
#0A0A0A): The single accent. Used on the primary CTA background, on body and title text, and as the workout numeric color. It is the loudest element on screen by definition — its rarity is the point.
- Milk (
#F5F5F5): The page. Bars and the root background share it so the screen reads as one continuous sheet of paper. Also reused as the inner fill ofcard-rowrecesses inside cards. - Bond (
#FFFFFF): Card and sheet surfaces. Cards lift off Milk through pure value contrast — no shadow, no border, just a 6.25% step in lightness. - Pumice (
#E8E8E8): Secondary controls. Steppers, secondary buttons, capsule chrome. A flat, slightly chalky neutral that sits between Milk and the Ink accent without competing. - Hairline (
#E5E5E5): The 1pt divider. Used at 55% opacity insideAppDivider, never raw — it should be felt, not seen. - Ash (
#595959): Secondary and supporting text. Section labels, helper copy, metadata. - Mist (
#949494): Disabled text and disabled-state foregrounds. Lighter than Ash so disabled never reads as merely "secondary". - Chalk (
#F6F6F6): The text color on Ink CTAs. Slightly off-white for less optical glare against pure black.
- Verde (
#34C759): Success. PR detection, set-logged confirmation. Always paired with an icon or label, never carries meaning alone. - Amber (
#FF9500): Warning. Soft cautionary states. Always paired. - Signal (
#FF3B30): Error and destructive. Delete confirmations, validation. Always paired.
- Spark (
#FF4400): The splash-screen orange. Reserved for the launch animation and home-screen icon only — forbidden in the app shell. There is noAppColortoken for it; the splash asset carries the value directly.
The One Ink Rule. Ink is reserved for the single primary action on a stress screen. If a screen has two Ink-filled buttons, one of them is wrong. Secondary actions take Pumice. Tertiary actions take Ghost (text-only, no fill).
The Flat-By-Default Rule. Cards never carry a drop shadow or a stroke. Separation between Milk page and Bond card is the 6.25% lightness step and nothing more. A hairline border at full opacity is the same failure mode as a shadow — both add chrome that fill contrast already provides. If a card needs more weight, the card is in the wrong place — not under-shadowed and not under-bordered.
The Spark-Is-Splash Rule. #FF4400 orange is permitted in the splash screen and nowhere else. It is the legacy of a previous brand decision, retained for launch identity, banned everywhere downstream.
The Soft-Pair Rule. Verde / Amber / Signal never carry state alone. They always pair with an icon (success.circle, exclamationmark.triangle, xmark.octagon) or a text label. Color blindness is the test, not the exception.
Sans Font: Geist (bundled .ttf in Unit/Resources/Fonts/, weights Medium / SemiBold / Bold)
Mono Font: Geist Mono (numerics, primary CTAs, set-result rows — fixed-width digits under fatigue)
Both ship as bundled .ttf and are registered via UIAppFonts in Info.plist. Always reach typography via the AppFont enum — never Font.custom("Geist*") or Font.custom("GeistMono*") in feature code.
Character: Geist is a humanist neo-grotesque — quiet, neutral, with just enough warmth to keep the page from reading as clinical. Numerics, primary CTAs, and the workout-signature rows pivot to Geist Mono so weight and rep columns stay aligned at every Dynamic Type size — the lifter scans down a column, not across a row.
- Splash title (Geist Bold 56, line-height 1.05, tracking -1.2): Splash welcome only.
- Headline / large title (Geist Bold 22, tracking -0.4): Screen-level titles. One per screen.
- Product heading (Geist Bold 24, tracking -0.4):
ProductTopBarheadings, hero copy on empty states. - Title (Geist Bold 20): Section titles, exercise names on workout cards.
- Section header (Geist SemiBold 17): 17pt semibold section headings, list-row titles (program / template / routine names), and the canonical button-label style for sans buttons (no separate
labelcase). Semibold (not bold) so row titles don't out-shout numerics on workout-adjacent lists and density reads closer to iOS-native. - Body (Geist Medium 17, line-height 1.35): Default reading copy. List rows, descriptions, paragraph text.
- Caption (Geist Medium 15): Secondary metadata, helper text, timer subtitles.
- Muted (Geist Medium 13): Footnotes and very secondary copy.
- Overline (Geist SemiBold 10): Top-of-card overline labels.
- Small label (Geist Medium 11, tracking +1.0): Tiny uppercase "WAS" / "MOST POPULAR" style chips.
- Numeric display (Geist Mono Bold 36, tracking -0.6): Weights, reps, rest-timer countdown. The workout signature.
- Product action (Geist Mono Bold 17): Primary CTA labels and top-bar text actions.
- Performance (Geist Mono SemiBold 15): Set-result / PR rows in History.
- Step indicator (Geist Mono SemiBold 14): Set step counters in
SetProgressIndicator.
The public website uses the same Geist families, medium-weight floor, and
Ink/Ash hierarchy with a responsive editorial scale. These are named web
tokens in app/globals.css, not feature-level one-offs:
- Hero (
.h-hero): 40 / 52 / 60px, line-height 1.0, tracking -0.035em. - Campaign heading (
.h-campaign): 34 / 44px, line-height 1.02. - Display CTA (
.h-display): 40 / 54px, line-height 1.04. - Section heading (
.h-section): 26 / 32px, line-height 1.1. - Narrative body: 16px minimum; 18–20px is reserved for hero/supporting leads.
- Metadata: 12–14px only for dates, status, compact data labels, and controls.
Uppercase mono .eyebrow labels are metadata, not general section decoration.
Navigation, footer headings, and section introductions use sentence-case Geist.
The Medium-Floor Rule. No text in the system uses regular (400) weight. The minimum is medium (500). At gym lighting and one-handed glance distance, regular weight goes mushy; medium reads at the same speed as bold without the heaviness.
The Numerics-First Rule. On any workout screen, the largest and heaviest element is a number — never a label, never an exercise name, never an icon. If something else is winning the visual hierarchy on a logging screen, the screen is wrong.
The No-Decoration Rule. No gradient text. No background-clip: text. No letter-spaced uppercase decoration. Type carries weight through size and weight contrast alone.
Unit is a flat system. There are no shadow tokens. There is no AppShadow enum. Cards lift off the page through fill-value contrast (Bond #FFFFFF on Milk #F5F5F5, a 6.25% lightness step) and through generous internal padding. Sheets use the system presentation chrome — corner shape comes from iOS, not from a token, since no callsite needed a custom value.
There is one exception: the appCardElevation modifier exists for the rare moment a card sits over photographic or busy imagery (onboarding splash). It is not used in the workout shell and not part of the day-to-day vocabulary.
The Flat-By-Default Rule. Surfaces are flat at rest. Depth is conveyed through value, padding, and the 1pt AppDivider hairline (#E5E5E5 at 55% opacity). If you reach for a shadow, you are solving the wrong problem.
The Soft-Edge Rule. Content scrolling behind a fixed bar fades through the canonical appScrollEdgeSoft(top:bottom:) modifier — never a hand-rolled LinearGradient or .mask. This is the only legitimate "elevation" technique in the system, and there is exactly one implementation of it.
The component set is deliberately small. Every primitive lives in Unit/UI/DesignSystem.swift — there is no Atoms/ folder splitting, by choice. New components require explicit user approval.
- Shape: rounded rectangle,
AppRadius.md(14px), iOS continuous corners, 60pt tall, full-width on stress screens. - Primary (
AppPrimaryButton): Ink (#0A0A0A) background, Chalk (#F6F6F6) label, Label typography. One per screen on logging flows. Disabled state: Pumice background, Mist label. - Secondary (
AppSecondaryButton): Pumice (#E8E8E8) background, Ink label. Tones:.default,.accentSoft,.destructive(text-only Signal red). - Ghost (
AppGhostButton): No fill, no border, Ink label. Used for "Add exercise" / "Add set" triggers inside cards. TheaccentSofttone of Secondary is banned for "Add X" actions — that's Ghost's job. - Pressed:
ScaleButtonStyleis the single custom pressed state: 0.96× scale, 14% opacity dim, and 8% brightness reduction over 150ms. Use it on every custom tappable card, row, and button. Native toolbar, alert, toggle, picker, and menu controls keep iOS feedback. Never add a screen-local pressed treatment.
- Shape:
AppRadius.lg(22px) iOS continuous corners. - Background: Bond (
#FFFFFF). - Internal padding:
AppSpacing.lg(24px) horizontal and vertical by default. - Elevation: none (see Elevation section).
- Concentric geometry rule: an Ink button placed inside a card uses
AppRadius.md(14px) so the inner radius equals card-radius minus padding (22 − 8 = 14). Nested radii must stay concentric.
Elements nested inside a card use AppColor.cardRowFill (Milk #F5F5F5) + AppRadius.sm (10px) + AppSpacing.sm (8px) padding. This creates a quiet recess inside the Bond surface — the card "page" with a Milk row "callout" inset into it. This is the single canonical row-on-card recipe. Do not invent parallel patterns with controlBackground or other fills.
AppDividedList: rows separated by 1ptAppDivider(Hairline#E5E5E5at 55% opacity). No outer chrome.AppCardList(data) { row }: the only sanctioned list-in-card primitive. Bakes the canonical 8/24 inset recipe and the divider. Never composeAppCard { AppDividedList(...) }by hand — the harness blocks it.- List rows: 52pt minimum height, Body typography (17px medium),
AppSpacing.lg(24px) horizontal padding. - Per-row shadowed cards are forbidden. A list of N items is one Bond card with N divider-separated rows — never N stacked Bond cards.
AppStepper: Pumice background, AppRadius.md (14px) capsule, 44×44pt minimum tap targets on each ± button. Compact values use Body (17px medium) with monospaced digits; prominent workout values retain Numeric Display. The fundamental "tweak a number" affordance for weight and rep adjustment.
AppDivider: 1px height, Hairline color at 55% opacity. The only divider primitive in the system. Raw Divider() is banned.
ProductTopBar: 64pt tall, Title typography (24px semibold rounded, tracking -0.3), title in Ash (secondary text), 16pt spacing between leading/title/trailing slots. Sizes:.mdand.large.- Root tab bar: native SwiftUI
TabView/UITabBarchrome. Today and Programs keep their navigation state, system selection feedback, Dynamic Type, and accessibility behavior. Do not recreate a custom tab bar. - Toolbar buttons: defer to iOS-native chrome.
.weight()modifiers onToolbarItembuttons are banned — they fight the system styling.
WorkoutCommandCard is the screen's center of gravity during a session. It uses appWorkoutPanelChrome(), displays the exercise name in Title, the working numeric (weight × reps) in numericDisplay (36px monospaced bold), and one Ink primary action ("Done"). An optional rest timer divides off the bottom with a 1pt divider at 32% opacity. There is no decorative chrome, no progress arc, no "next exercise" preview competing with the current action.
These guardrails carry PRODUCT.md's anti-references through into pixels. Every "Don't" maps to a real failure mode the system has rejected.
- Do use Ink (
#0A0A0A) as the single primary CTA color, and exactly one Ink CTA per stress screen. - Do separate Bond (
#FFFFFF) cards from the Milk (#F5F5F5) page through fill contrast alone — never with a drop shadow, never with a stroke. - Do use monospaced-digit numerics (
AppFont.numericDisplay/numericLarge) for every weight, rep count, and timer value, so columns align at every Dynamic Type size. - Do use
AppCardList(data) { row }for any list-inside-a-card pattern. It bakes the canonical 8pt vertical / 24pt horizontal inset recipe. - Do use
appScrollEdgeSoft(top:bottom:)whenever content scrolls behind a fixed bar. - Do use the
cardRowFillrecess recipe (Milk fill + 10px radius + 8px padding) for any nested element inside a Bond card. - Do keep
AppRadiusconcentric — a 14px button inside a 22px card with 8px padding (22 − 8 = 14). - Do pair Verde / Amber / Signal with an icon or label every time. Color is never the only carrier of state.
- Do keep touch targets ≥ 44×44pt everywhere. The Gym Test (one-handed, sweaty, fatigued) is the floor, not the ceiling.
- Do honor
accessibilityReduceMotion— cross-fades and opacity only, no parallax or elastic.
- Don't introduce a parallel
LinearGradientor.maskfor scroll-edge fades —appScrollEdgeSoftis the single canonical implementation. - Don't invent a new
struct X: View/ViewModifier/ variant when the existing canonical primitive inDesignSystem.swiftcovers ~80% of the case. Extend the canonical withstyle:/variant:/tone:instead. Parallel implementations are the worst drift in the system. - Don't use
chevron.right/chevron.forwardanywhere. The system has no row-disclosure chevrons by doctrine. - Don't use raw
Divider(). UseAppDivider(Hairline at 55% opacity). - Don't apply
.weight(.semibold/.bold/.heavy)toToolbarItembuttons. Toolbar chrome defers to iOS-native. - Don't put
ScrollVieworAppCardas the root child of.sheet { }. Sheet roots are plainVStack. - Don't use Spark orange (
#FF4400) anywhere outside the splash screen. The accent is Ink. - Don't use
regular(400) font weight. The medium-floor is non-negotiable. - Don't use a side-stripe border (
border-left> 1px as a colored accent) on any card or row. Forbidden universally. - Don't use gradient text or
background-clip: textfor emphasis. Use weight or size. - Don't ship per-row shadowed cards stacked vertically. A list is one card with N rows, never N cards.
- Don't add a hairline stroke to card chrome. Bond on Milk is the separation; a 1pt border at full opacity is the same failure mode as a drop shadow.
- Don't show "0 kg" for bodyweight exercises. Show "BW". Don't use
–or—placeholder copy. - Don't build a target-vs-actual weight column UI. Last-time pre-fill only.
- Don't copy the Strong/Hevy/Jefit spreadsheet aesthetic — parallel target/actual columns, dense numeric rows, busy timer chrome. The whole "gym tracker app" visual category is the trap.
- Don't copy Strava / Nike Training Club patterns — feeds, badges, friend activity, share prompts, motivational hero copy, lifestyle photography of athletes mid-jump.
- Don't copy Whoop / Oura aesthetics — dark dashboards, neon data viz arcs, gradient hero metrics, recovery rings, sleep-score chrome. Unit is not a wellness product; it is a working tool.
- Don't add SaaS landing-page clichés to the marketing surface — stock-photo athletes, "transform your training" hero copy, three-icon feature grids, pricing tiers above the fold.