Cross-platform brand guide · iOS · watchOS · Android · Wear OS · Web

The right card,
before you ask.

Hopscotch turns dumb barcode loyalty cards into a smart wallet that surfaces the right card automatically when you arrive. This is the single source for how the brand looks and behaves across every surface — shared tokens first, then the components specific to each platform.

Design essence — invisible when working

The best interaction is no interaction: the right card just appears when you walk into the store. Everything follows from four commitments — fast to add (scan a card in under 10 seconds), instant to use (a tap to the barcode), forgiving (easy to edit, reorganize, delete — no data anxiety), and accessible (high-contrast barcodes, large tap targets, VoiceOver / TalkBack throughout).

Visually that reads as utility, not flourish. Calm neutrals, one owned hue, generous space. The interface gets out of the way of the one thing the user came for: the barcode.

Color strategy — stand out, then survive

Teal is the owned hue — a deliberate non-blue in a category of generic blue-gray wallet apps. It is how you recognize a Hopscotch screen at a glance. But a single deep teal can't carry text contrast alone, so the system layers it: a six-step ramp for tints and depth, and a two-token accent split so the same brand color works both as a mark on a surface and as a fill under white. Distinctiveness first; legibility engineered in second.

The hero brand artifact — the white barcode card

On every surface — iPhone, Apple Watch, Android, Wear, web mockups — the scannable card is forced white with a continuous-rounded container and a monospace number. Two reasons, one decision: a store scanner needs maximum white luminance to read a barcode, and forcing it white everywhere makes the card the single recognizable "Hopscotch object." On watchOS it even overrides dark mode (.preferredColorScheme(.light)) so the clock disappears behind a clean white face. The components section leads with it.

§2 · Identity

Brand mark & wordmark

The mark is five rounded bars of varying height — a barcode distilled to its essence, the scannable object the whole product is built on. It is delivered as a single monochrome silhouette in three fills, never a multicolor logo. Monochrome keeps it legible at complication size, on a Wallet pass, and reversed on a dark nav.

The three marks — one shape, three fills

Black

On light backgrounds, light Wallet pass, print.

White

Reversed on dark UI, dark Wallet pass, splash.

Green · #047568

Brand moments only — app icon, marketing hero.

Note — production assets are canonical

The production assets are the canonical files, not these silhouettes. App / Wallet marks: Assets/App Icons/ and Assets/Wallet Logos/Hopscotch Brand Mark - {Black,White,Green} - No BG.{png,svg}. Web wordmarks: hopscotch-landing/assets/{icon-wordmark-nobg.svg, icon-light.svg, wordmark-dark.png}. Always pull from those for production; the marks above inline the real five-bar SVG, recolored per fill, so the guide stays self-contained.

Clear space & minimum size

  • · Clear space: keep a margin of at least one bar-width on all sides.
  • · Minimum size: 20pt in-app (nav, header), 16pt for the watch complication. Below that, drop the wordmark and show the mark alone.
  • · Wordmark: "Hopscotch" is a fixed serif lockup (Fraunces) — a logo asset, not interface type; the web nav wordmark renders at 32px height.

Monochrome rule — do / don't

Do
  • Use one fill per placement (black, white, or green)
  • Reverse to white on any dark surface
  • Let the mark stand alone below 20pt
Don't
  • Recolor with card-palette or gradient fills
  • Add shadows, outlines, or container shapes
  • Stretch, rotate, or place green on a dark background (use white)
§3 · Color, shared core

Color — the shared system

Identical across iOS and Android; web aligns on the ramp and diverges only where noted (§4). Every token carries a light and a dark value — and dark is a deliberate re-derivation, not an inversion. Use the Dark toggle in the nav to preview the whole document in dark mode.

Brand teal ramp

One owned hue, six tonal steps. Tints (100/200) build containers and selected states; 300 is the dark-mode mark; 500 is the web's marketing lead; 600/700 carry depth and the primary light-mode accent.

Teal-100#E6F4F2
Teal-200#BFE6E1
Teal-300#5BC7B8
Teal-500#1BAA98
Teal-600#036157
Teal-700#047568

100 background · 200 container / tint · 300 dark-mode mark + success · 500 mid / web primary · 600 deep · 700 primary brand teal & light accent.

The two-token accent split — the signature insight

The same brand teal plays two opposite contrast roles, so it is wired as two tokens. In light mode they're identical; in dark they split. A teal used as a mark on a surface (text, an icon, an arrow) wants to be lighter for contrast against night. A teal used as a fill under white content (the + button, a toggle track) wants to be deeper so the white glyph stays legible. Get this backwards with one teal and you make either the small text or the white glyph mushy.

Aa ↗ link #047568 on #FFFFFF
Aa ↗ link #5BC7B8 on #0F1111
Accent (mark) Text · links · arrows · nav · selection
+ white on #047568
+ white on #05A08E
Accent Fill + button · toggle track · card fallback

The contrast math

Dark-mode tealAs a mark on #0F1111White on it (as a fill)Therefore
#5BC7B8 ≈ 9:1 ✓ ≈ 2.04:1 ✕ Use for marks — text, icons, arrows. Never put white on it.
#05A08E ≈ 5:1 (dim as small text) ≈ 3.27:1 ✓ large/bold Use for fills — white glyph on teal. Only large/bold white may sit on it.
The one rule

Only large or bold white content may sit on Accent Fill (button glyphs, bold labels). Never put small white caption text on teal — put the caption on the dark surface instead. In light mode both tokens are #047568; the split only exists in dark.

Semantic colors

Success is intentionally teal, not a generic green — in Hopscotch a confirmation ("added," "In Wallet") is a brand moment. Error and warning are the only non-teal hues the product UI uses.

Light#047568
Dark#5BC7B8
SuccessBrand teal, not green
Light#E8A80C
Dark#F4C542
WarningStale / needs refresh
Light#D63B6E
Dark#F2708B
ErrorValidation · destructive

Error legibility: #D63B6E clears ~4.5:1 on light only at semibold or heavier — always render error text ≥ semibold. #F2708B is dark-mode only (it fails on white, passes ~6.3:1 on black). For a soft negative outline, use Error at 25% opacity rather than inventing a muted red.

Card user-color palette

Eight vivid accents the user picks per card — they color the icon tile so a card is recognizable at a glance. These are user data, not brand semantic colors; they render at their literal hex on every surface and are never touched by Material You. (Canonical hexes: Android CardColors.kt.)

Teal#047568
Blue#2563EB
Purple#7C3AED
Pink#DB2777
Red#DC2626
Orange#EA580C
Yellow#CA8A04
Green#16A34A

Element → token map

The contract between the brand and the code. Every colored element maps to exactly one token — start here when wiring a new surface.

ElementTokenLight / Dark
+ / primary action button (fill)accentFill#047568 / #05A08E
Card-color background fallbackaccentFill#047568 / #05A08E
Toggle "on" track (white knob on it)accentFill#047568 / #05A08E
Bottom-nav icons (active + inactive)accent#047568 / #5BC7B8
Text, links, the ↗ arrowsaccent#047568 / #5BC7B8
Selection / active stateaccent#047568 / #5BC7B8
Geofence radius circle (map)accent#047568 / #5BC7B8
Map location pinaccent#047568 / #5BC7B8
Success check, "In Wallet" badgesuccess#047568 / #5BC7B8
Error / validation text (≥ semibold)error#D63B6E / #F2708B
Delete / Remove tint, swipe-to-deleteerror#D63B6E / #F2708B
Stale-pass / needs-refresh indicatorwarning#E8A80C / #F4C542
Focus ring / outlinefocus#047568 / #5BC7B8
§4 · Color, per surface

Color — surfaces & per-platform wiring

The accent and semantic tokens above are identical across iOS and Android. What differs per surface is how they're delivered — code tokens on iOS, a full Material 3 ColorScheme on Android, CSS variables (light-only, plus a gradient) on web.

Surfaces — dark is an elevation ramp

In light mode every surface is white and depth comes from shadow. In dark mode there are no shadows — depth comes from lighter surfaces moving forward: night base at the back, card a step up, elevated a step above that, with an explicit border token because shadows can't separate near-blacks.

TokenLightDarkUse
surface.base#FFFFFF#0F1111 NightApp canvas / window background
surface.card#FFFFFF#171A1ACards, rows, list items
surface.elevated#FFFFFF#1F2323Sheets, menus, raised chrome
surface.border#2A2E2EHairline separation in dark only
surface.ink (hi-contrast)#151515Max-contrast ink on light

Text ramp

Never pure #000 or #FFF — primary ink is a near-black, primary dark text a soft off-white. Three weights of de-emphasis cover everything.

Light
Primary text#151515
Secondary text#6B7278
Tertiary text#9BA1A8
Dark
Primary text#F6F6F6
Secondary text#C9CED3
Tertiary text#7A8086

iOS / watchOS — tokens live in code

There's no asset-catalog color set driving the brand. The hexes live in Shared/Theme.swift as BrandHex raw values, exposed as adaptive Color tokens that the app, watch, and both widgets all compile. Change a hex once → it changes everywhere. An AccentColor asset mirrors them so system controls auto-tint.

Swift tokenLightDarkHelper
Color.brandAccent#047568#5BC7B8Marks, text, nav glyphs, selection
Color.accentFill#047568#05A08E.brandProminentButton() · .brandToggleTint()
Color.semanticSuccess#047568#5BC7B8Checkmarks, "In Apple Wallet"
Color.semanticError#D63B6E#F2708BError text (semibold), destructive
Color.semanticWarning#E8A80C#F4C542Caution / needs-attention
Color.brandTealDeep#047568#047568Fixed teal for forced-white watch surfaces

The helpers exist so white-on-fill can't be forgotten: .brandProminentButton() applies a capsule shape and tints with .accentFill (the deeper teal), not the lighter mark accent. On watchOS, Color(lightHex:darkHex:) always resolves to the dark value (the watch has no light mode), except the barcode screens which force .colorScheme(.light).

Android — Material 3 ColorScheme + three brand overrides

The brand tokens (HopscotchColors.kt) are wired into a full static M3 ColorScheme in Color.kt. The accent mark is colorScheme.primary; the accent fill is a separate accentFill() composable (M3's primary is the lighter mark teal and would make white-on-fill mushy).

M3 roleLightDarkSource
primary#047568#5BC7B8accent mark · brandTeal700 / 300
onPrimary#FFFFFF#0F1111
primaryContainer#BFE6E1#036157teal-200 / teal-600 · also status-bar tint
onPrimaryContainer#036157#BFE6E1teal-600 / teal-200
surface#FFFFFF#0F1111Night base
surfaceContainer#F4F8F7#171A1Ateal-tinted, not M3 purple-gray
surfaceContainerHigh#EDF4F3#1F2323Night-elevated
surfaceContainerHighest#E6F4F2#2A2E2Eteal-100 / Night-border
error#D63B6E#F2708BsemanticError
accentFill()#047568#05A08Ecomposable, NOT in the scheme — for + / toggles
Override 1 — dynamic color off

dynamicColor = false by default. The theme supports Material You (hybrid), but ships off so the user's wallpaper never overrides teal. Brand consistency wins.

Override 2 — teal-tinted surfaces

surfaceContainer is #F4F8F7 light / #171A1A dark — a teal-leaning neutral, not M3's baseline purple-gray. Keeps nav bar & menus in-brand.

Override 3 — nav indicator + status bar

Bottom-nav indicator bumped to brandTeal-200 (#BFE6E1); the status bar is tinted to primaryContainer (teal-200 light / teal-600 dark).

Web — light-only, gradient lead, its own neutral ramp

The landing site (hopscotch-landing/styles.css) carries tokens as CSS :root vars. It diverges from the apps in three deliberate ways — marketing energy, not drift — and all three are flagged in the open-decisions section.

Web primary = mid-teal + gradient

The site leads with Teal-500 #1BAA98 and a gradient for CTAs and nav, where the apps use flat deep #047568. Reconciliation: apps anchor on Teal-700 flat fills; web may use the Teal-500→700 gradient on marketing surfaces only.

linear-gradient(145deg, #1BAA98, #047568)
#1BAA98
#047568

Web neutrals vs app text tokens

The site uses an Apple-ish cool-gray ramp, close to but not identical to the app text tokens. The mismatch is small and currently intentional — surfaced as decision #4.

RoleWebApp equivalent
text primary#1D1D1F#151515
text secondary#515154#6B7278
text muted#8E8E93#9BA1A8
surface#F5F5F7#FFFFFF
border#E0E0E4

Web has no full dark mode — only a single dark "privacy island" section (--bg-privacy #1A1A1F) used for the data-privacy block. Decision #5.

§5 · Type

Typography

Type splits cleanly into two worlds, by design. In the apps, Hopscotch is fully native to each OS — SF Pro on iOS / watchOS, Google Sans Flex on Android — so the product feels like it belongs to the phone. Fraunces + Outfit are the web / advertising voice, where we're meeting someone for the first time and the brand is allowed to perform. This is no longer fragmented or unresolved; it's a deliberate native-product / expressive-web split (locked — see §15). Specimens below are live: Outfit and Fraunces load from Google Fonts; Google Sans Flex is self-hosted; SF Pro is stood in by -apple-system and labeled as such.

The principle — the brand gets out of the way in the product

Hopscotch is a utility. People open it to get one thing done and leave. Native type and native icons make the app feel instant and like it belongs to the phone — not like a brand performing in someone else's house. The expressive brand voice (Fraunces, Outfit, the web gradient) lives on web and advertising, where a first impression is the job. This is the same "invisible when it's working" principle that governs the whole system, applied to type and icons: in the product the brand disappears; on the website it performs.

Two worlds of type

The web / advertising families — where the brand performs
Web headlines · Fraunces
Fraunces

A variable "old-style" serif (opsz 9–144) for all web headlines — warmth and editorial confidence for the first impression. Web / advertising only. Weights 600/700. (This guide's own headings use it.)

Web body / UI · Outfit
Outfit

Geometric, rounded, friendly. The web body and UI face — and the typeface the fixed wordmark lockup is set in. Web / advertising only; not an in-app interface face. Weights 300/400/500/600/700.

The app families (native) — where the brand gets out of the way
iOS / watchOS · SF Pro
SF Pro

Apple's system typeface across the entire app — display, body, controls, all watch text. SF Pro monospaced for barcode / code values. No Outfit in the iOS app. (Specimen stood in by -apple-system.)

Android · Google Sans Flex
Google Sans

Google's own variable brand + system typeface (GRAD, opsz, wght axes) across the whole M3 scale — the Material 3 Expressive direction. FontFamily.Monospace for barcode data. No Outfit in the Android app.

One exception: the wordmark is a fixed logo lockup (the Fraunces serif) and may appear as a brand asset on the app splash and the Wallet pass — that's the logo, not interface type.

iOS / watchOS — native: SF Pro + SF Symbols

The entire iOS scale is SF Pro — display, body, dense UI, captions, all controls, and all watch text. No Outfit anywhere in the app. Barcode / code values are SF Pro monospaced — O/0 legibility matters when a number is read aloud at checkout. This is Dynamic Type, San Francisco, and SF Symbols working together so the app feels like the OS, not a skin laid over it.

display · hero (stand-in)SF Pro 600 · 34pt
data-card hero numeral, empty-state title
3491 0027
display · card name (stand-in)SF Pro 600 · 20pt
widget & barcode card title
Blue Bottle Coffee
body · SF Pro (stand-in)SF Pro 400 · 17pt
all body, list rows, controls
Walk into the store and the card appears.
caption · SF Pro (stand-in)SF Pro 400 · 13pt
secondary, metadata
Last used 2 days ago · 0.4 mi away
code value · SF Pro monoSF Pro monospaced · 15pt
O/0 legibility
7 8 9 0 1 2 3 4 5 6 7

Specimens above are stood in by -apple-system (the live SF Pro stand-in); on device these render in true San Francisco at every weight and optical size, with code values in SF Pro mono.

Android — Google Sans Flex across the M3 scale (Google's variable brand + system typeface)

Android uses Google Sans Flex for the whole Material 3 type scale — Google's own variable brand and system typeface (GRAD, opsz, wght axes), the Material 3 Expressive direction. No Outfit in the app. This is not a divergence from iOS: both apps are native to their OS, which is exactly the point — SF Pro on Apple, Google Sans Flex on Android, each app feeling like it belongs to the phone. Barcode data overrides to FontFamily.Monospace. The role/size values below are the font-agnostic M3 spec, verbatim from Type.kt.

RoleSizeWeightLine heightTracking
displayLarge57spNormal64sp−0.25sp
displayMedium45spNormal52sp0
displaySmall36spNormal44sp0
headlineLarge32spSemiBold40sp0
headlineMedium28spSemiBold36sp0
headlineSmall24spSemiBold32sp0
titleLarge22spSemiBold28sp0
titleMedium16spMedium24sp0.15sp
titleSmall14spMedium20sp0.1sp
bodyLarge16spNormal24sp0.5sp
bodyMedium14spNormal20sp0.25sp
bodySmall12spNormal16sp0.4sp
labelLarge14spMedium20sp0.1sp
labelMedium12spMedium16sp0.5sp
labelSmall11spMedium16sp0.5sp

Correction vs. brief: the brief implied the M3 default weights — the actual Type.kt overrides all headline/title roles to SemiBold/Medium (only display + body stay Normal). Values above reflect the source.

Live specimen — Google Sans Flex across the M3 roles

displaySmallGoogle Sans Flex 400 · 36sp
lh 44 · tracking 0
Your card is already out.
headlineSmallGoogle Sans Flex 500 · 24sp
lh 32 · tracking 0
It shows up when you show up
titleMediumGoogle Sans Flex 500 · 16sp
lh 24 · tracking 0.15
Blue Bottle Coffee
bodyLargeGoogle Sans Flex 400 · 16sp
lh 24 · tracking 0.5
Scan a card, set the store, and Hopscotch does the rest.
labelMediumGoogle Sans Flex 500 · 12sp
lh 16 · tracking 0.5 · CAPS
Add to Wallet
barcode data (override)FontFamily.Monospace · 16sp
O/0 legibility
4471 9920 3318

Tracking shown in em (sp ÷ size) so it renders true at these px sizes. Live Google Sans Flex is self-hosted from the local TTF (GRAD, opsz, wght variable axes); the only role that breaks family is barcode data, which overrides to monospace for the same O/0 reason iOS uses SF Pro mono.

Web — Fraunces headlines + Outfit body, fluid scale

Fluid clamp() sizing, font-display: swap, text-wrap: balance on headings / pretty on body.

hero (Fraunces)clamp(36px, 5vw, 68px)
600 · −0.025em
The right card, before you ask.
h2 (Fraunces)clamp(28px, 3.5vw, 48px)
600
Never fumble for a card again
body (Outfit)16–18px · 400 · lh 1.6–1.7
Scan a card, set the store, and Hopscotch does the rest.
eyebrow (Outfit)13px · 600 · 0.08em · UPPERCASE
Smart wallet

Dead asset to retire: Regards.otf is loaded via @font-face in the landing CSS but never referenced — remove it.

§6 · Spacing & layout

Spacing & layout

One 8pt grid across every surface, with 4pt for fine-tuning inside components. The governing rule: internal spacing ≤ external spacing, so elements never feel like they're escaping their containers.

The 8pt scale — proportional

Nine steps, each bar drawn to true scale against the 96 maximum so the rhythm is visible, not just listed. The same token does the same job on every surface — --sp-4 is a card's inner padding whether it's drawn in SwiftUI, Compose, or CSS.

Token
Value
Scale (rel. to 96)
Primary usage
--sp-14px
Fine-tune · icon-to-label gap
--sp-28px
Row inset (vertical) · chip inner pad
--sp-312px
Related items · chip gap · row r12
--sp-416px
Card padding · row H inset · card r16
--sp-624px
Loosely related groups · sheet padding
--sp-832px
Between sections · container gutter
--sp-1248px
Major blocks · mobile section padding
--sp-1664px
Screen sections (web) · web nav height
--sp-2496px
Web section padding (vertical, desktop)
Component spacing reference
  • · Card padding: 16
  • · List row insets: ~8 vertical / 16 horizontal
  • · Data-card field spacing: 14–18
  • · Chip gap: 8–12
  • · Internal ≤ external on every component
Web content width
  • · Content max-width: 1100px
  • · Section padding (V): 96px desktop / 48px mobile
  • · Prose measure: ≤ ~68ch for readability
  • · Breakpoints: 640 · 768 · 960 · 1024
§7 · Shape

Shape & radius

One rule decides every corner: actions are pills or circles; containers are rounded rectangles. A rounded-rect signals "container," not "tap me" — so there are no rounded-rect buttons anywhere in the system.

Actions — pill & circle
Add to Wallet Edit

CTAs & secondary buttons → capsule. The primary + and small icon affordances (close, back) → circle.

Containers — rounded rectangle

Cards / sheets r16 · medium containers & rows r12 · chips / swatches / small tiles r8–9. iOS uses .continuous corners throughout.

Radius scale

4
watch barcode · M3 extraSmall
8–9
chips · swatches · tiles
12
rows · banners
16
cards · sheets
28
M3 bottom sheet · extraLarge
pill
all buttons (capsule)
ElementiOS (.continuous)Android (M3 Shapes)
Cards / sheets / large sections16large · CardShape 16dp
Medium containers / rows / banners12medium 12dp
Chips / swatches / small tiles8–9small 8dp
Buttons / CTAs (prominent + secondary)capsuleButtonShape · 50%
Primary + / icon affordancescircleBadgeShape · 50%
Bottom sheet (modal)16 topBottomSheetShape · top-28
Watch barcode container4extraSmall 4dp

Nested radii always shrink: a chip (r8) inside a card (r16) inside a sheet (r16) reads as a clean stack of containers.

§8 · Elevation

Elevation & shadow

Native surfaces stay quiet — a single soft shadow, never a stack. The web is allowed richer, layered depth for marketing presence. Wearables use no shadows at all; the true-black background does the separating.

Native — subtle, single soft shadow

subtle · rows
6% · blur 6 · y3
medium · cards
8% · blur 8 · y4
FAB · colored
12–15% · blur 6–8 · y2–4

In dark mode shadows do nothing on near-black — depth there comes from the surface ramp (§4), not shadow.

Web — layered + device shadow

--shadow-card
--shadow-card-hover
--shadow-device

Buttons get a teal-tinted hover shadow (rgba(27,170,152,0.35)) — the colored-shadow polish that makes a CTA feel lit rather than dropped.

§9 · Iconography

Iconography

One semantic set, three native renderers — SF Symbols on iOS, Material Icons (Filled) on Android, inline stroke SVG on web. Categories map 1:1 across platforms so the same card always wears the same glyph. Icons are native for the same reason type is: in the product, the app should feel like the OS, not a brand skin.

CategoryiOS · SF SymbolsAndroid · MaterialWeb · SVG
🛒 Grocerycart.fillShoppingCartcart (stroke)
Coffeecup.and.saucer.fillLocalCafecup
🏋 Fitnessdumbbell.fillFitnessCenterdumbbell
Pharmacycross.caseLocalPharmacycross-case
📚 Librarybooks.verticalLocalLibrarybooks
🍴 Restaurantfork.knifeRestaurantfork-knife
🏪 StorestorefrontStorefrontstorefront
💳 DefaultcreditcardCreditCardcard
Sizes
  • · Standard: 24
  • · Card icon-tile glyph: 20 (in a 40–44 tile)
  • · Geofence dot: 8–10
  • · Empty-state: 48
Consistency rules
  • · Same semantic set on every platform; never mix renderers within a surface.
  • · Category glyphs use the filled variant in tiles, the outline variant in inactive nav.
  • · Brand mark (mono black/white/green) on splash, Wallet pass, web nav — see §2.
§10 · Voice

Voice & messaging

Hopscotch sounds like a calm, capable friend who already handled it. The product is invisible when it's working, so the words are too — short, plain, never asking for credit. Confident but not corporate; a little playful (the name earns it); privacy stated as fact, not pitched. The throughline: describe the relief, not the technology.

Calm
It already happened

State the outcome in the past tense — the work is done, the user just arrives. No exclamation marks, no urgency. The tone of a thing that quietly works.

"Your card is already out."
"Get instant access to your cards now!"
Precise
One idea per sentence

Short sentences, concrete nouns, real places. No stacked value-props, no "seamless / powerful / revolutionary." Name the gym, the coffee shop, the register.

"Walk into your gym. Your card is waiting."
"Seamlessly leverage your loyalty ecosystem."
Privacy-first
Say what we don't do

Trust comes from understatement. State the absence flatly — no account, no tracking, no server. Don't dramatize it; the restraint is the point.

"No account. No tracking. It stays on your phone."
"Bank-grade encryption protects your data in the cloud."
Helpful
Own the moment, not the blame

When something fails, the app takes responsibility and points to the next step. Never scold the user, never leave them stuck. Calm for errors, brief for success.

"Couldn't read that barcode. Try better light, or type it in."
"Error: invalid barcode input. Please try again."

Message templates

The voice, applied. Six real moments across the journey — written once here so they read the same on iPhone, Watch, Android, and the web.

Tagline
The right card, before you ask.
The whole product in six words — outcome first, no feature named. Headline alt: "Your card is already out."
Empty state — no cards yet
Add your first card. Scan the barcode on any loyalty or membership card and it's saved in seconds.
Explains why it's empty and the one next action — never just "No cards."
Permission ask — location
Hopscotch uses your location to surface the right card when you arrive — processed on your device, never sent anywhere.
Names the benefit and the privacy guarantee in the same breath. Asked in context, right after a location is set.
Error — scan failed
Couldn't read that one. Try better light, or enter the number by hand.
What happened + what to do. Two recovery paths, no fault assigned. Error copy is always ≥ semibold for contrast.
Nudge — set a location
Want this card to show up automatically? Drop a pin on the store and Hopscotch takes it from there.
Optional, not nagging. Frames the upgrade as relief ("show up automatically"), not a chore.
Success — added to Wallet
In your Apple Wallet. It's on your lock screen now.
Brief, past tense, tells them where it went. Success never slows the user down — it confirms and gets out of the way.
Never say

"Your loyalty cards, organized" — every competitor says it, and Hopscotch isn't about organizing, it's about auto-surfacing. Also retire: seamless, revolutionary, game-changing, powerful yet simple, and any sentence in the passive voice. Lead with the moment the card appears.

§11 · Components

Signature components

Every component below is rendered in CSS from the brand tokens — no screenshots. Each is shown in both light and dark (use the nav toggle). We lead with the hero: the white barcode card.

① The white barcode card — forced white on every surface

The recognizable Hopscotch object. White background (scanner legibility), r16 continuous container, category icon tile in the user's card color, name in Outfit, and the code value in SF Pro mono. Both panels below force the card white even though the right one sits on a night surface — exactly as the apps do.

On light surface
Blue Bottle
Coffee
7 8 9 0 1 2 3 4 5
CODE 128
On night surface — card stays white
City Library
Library
L 4 4 9 0 0 2 7
QR
Do — the barcode card
  • Force the background to white on every surface — even dark mode and watchOS (.preferredColorScheme(.light))
  • Keep the r16 continuous container and the monospace number, always centered
  • Tint only the small category icon tile with the user's card color
Don't — the barcode card
  • Let it inherit the night surface — a dark or tinted barcode field can fail the store scanner
  • Color the card body with the card's accent or a teal fill
  • Set the code value in Outfit — use mono so O/0 and 1/l stay unambiguous when read aloud

② Data card

A card with no barcode — membership numbers, PINs, account fields. Outfit hero number; SF Pro mono for each code value (read-aloud legibility); 14–18 field spacing.

Rewards+ Member
Membership
Member number
4471 9920 3318
Tier
Gold

③ Card row (deck)

The list item in the deck. 36×36 icon tile (r8) in the card color, name in body semibold, sub-line in secondary text, geofence dot trailing when a location is set.

Blue Bottle
Coffee · 0.2 mi
CVS ExtraCare
Pharmacy
Whole Foods
Grocery · 1.1 mi

④ Buttons — the four-state matrix

Every button is a capsule — a rounded-rect is a container, never an action. For each surface, Primary and Secondary are shown in all four states: default · hover/pressed · focus · disabled. iOS & Android fill with flat accentFill (the deeper teal, so white labels stay legible in dark); web fills with the marketing gradient. Hover lifts and brightens; pressed sinks and dims; focus shows the 2px teal ring; disabled drops to 40% and can't be tapped.

iOS · Primary
accentFill, capsule
DefaultAdd to Wallet
PressedAdd to Wallet
FocusAdd to Wallet
DisabledAdd to Wallet
iOS · Secondary
outline pill
DefaultSet location
PressedSet location
FocusSet location
DisabledSet location
Android · Primary
accentFill, 50% pill
DefaultAdd to Wallet
HoverAdd to Wallet
FocusAdd to Wallet
DisabledAdd to Wallet
Android · Secondary
tonal (teal-200)
DefaultSet location
HoverSet location
FocusSet location
DisabledSet location
Web · Primary
gradient, pill 100px
DefaultGet Hopscotch
HoverGet Hopscotch
FocusGet Hopscotch
DisabledGet Hopscotch
Web · Secondary
outline pill
DefaultFor businesses
HoverFor businesses
FocusFor businesses
DisabledFor businesses
shape  capsule / pill — radius = height ÷ 2 (never a rounded-rect)
padding  ~11px vertical · ~22px horizontal (H ≈ 2× V) · min height 44pt touch target
label  Outfit 600 (iOS/Android) · 15px · one verb-led phrase ("Add to Wallet", not "Submit")
fill  accentFill #047568 / #05A08E — never the lighter mark accent (white label would go mushy in dark)
focus  2px focus teal ring, 3px offset · disabled 40% opacity, no pointer events

Hover/pressed are simulated via classes here so both render statically; in product they're driven by :hover, :active, and :focus-visible at --dur-quick. There is exactly one Primary per screen region.

Do — buttons
  • Make every button a pill (capsule); the primary + and icon affordances are circles
  • Fill with accentFill so white labels clear contrast in dark mode
  • Show a visible focus ring and keep one primary action per region
Don't — buttons
  • Use a rounded-rectangle button — that shape reads as a container, not a tap target
  • Fill with the lighter mark accent (#5BC7B8) — white-on-it fails at ~2:1
  • Stack two primary CTAs side by side, or drop the focus ring to "clean it up"

⑤ Category chip

All Coffee Grocery Fitness

Selected uses teal-200 fill / teal-600 text. Pill shape, r100.

⑥ FAB / Add

Circle, accentFill, colored shadow. The single most-used affordance — scan a card.

⑦ Banners

⑧ Navigation — per surface

Three native patterns, one accent. iOS tab bar (glyph + label, accent active), Android bottom nav (teal-200 pill indicator), web glass header (blurred, gradient CTA).

iOS tab bar
Deck
Nearby
Settings
Android bottom nav
Deck
Nearby
Settings
Web glass header
Hopscotch For businessesGet the app ▾
Do — navigation
  • Keep surfaces teal-tinted (#F4F8F7 / #171A1A) so nav stays in-brand
  • Use the accent mark for the active tab; the Android indicator is teal-200
  • Respect each platform's native chrome — tab bar, bottom nav, glass header
Don't — navigation
  • Let Material You repaint the bar — dynamic color is off so wallpaper never wins
  • Fall back to M3's default purple-gray surfaceContainer
  • Force one nav pattern across all three surfaces in the name of "consistency"

⑨ Web — hero, feature card, compare, footer

Marketing surfaces the apps don't have: a Fraunces hero, the teal-glow feature card (r24, 44×44 icon box), a compare table using the Error-at-25% negative outline, and the dark privacy island.

Smart wallet
The right card, before you ask.

Scan a card, set the store, and Hopscotch surfaces it automatically when you arrive.

Get Hopscotch
Geofenced auto-surfacing

Walk in, and the card is already on your screen — no searching.

Privacy island
Your cards stay on your device.

The one dark section on the otherwise light site — #1A1A1F.

CompareHopscotchPlastic wallet
Surfaces the right card automatically
Works on watch
Can be left at home

The "bad" comparison column uses a soft Error outline (Error at 25%) rather than a separate muted red — one negative token, two intensities.

§12 · Parity

Cross-surface parity matrix

Where the three surfaces agree, diverge-by-design, or have a gap. shared · aligned but differs · missing.

DimensioniOSAndroidWebNote
Primary tealapps flat #047568; web leads #1BAA98 + gradient
Two-token accent splitweb has no dark mode, so no split needed
Dark modeweb = single privacy island only
Neutral rampweb on its own Apple-gray ramp
App type (native)native per OS by design — iOS SF Pro; Android Google Sans Flex; web is the shared expressive face below
Web display / bodyFraunces headlines + Outfit body — web / advertising only, not in-app
Monospace (code values)SF Pro mono / FontFamily.Monospace / Fira-style
Card radius (16)r16 continuous / CardShape 16dp / --radius-md
Button shape (pill)capsule everywhere
Nav patterntab bar / bottom nav / glass header — native each
Icon setone semantic set, native renderers by design — SF Symbols / Material / SVG
Motionease-out/in shared; web adds float
Gaps to close

· Web has no dark mode — only the privacy island. (Decision #5.)

· Web neutral ramp ≠ app text tokens — close but not unified. (Decision #4.)

· Web primary = Teal-500 + gradient, not 700 flat — reconcile marketing-only vs in-app. (Decision #3.)

· Type is resolved, not a gap — apps are native per OS (SF Pro / Google Sans Flex); Fraunces + Outfit are web / advertising only. Intentional divergence, marked ◐ above. (Decisions #1 & #2 — locked.)

§13 · Motion

Motion

Motion communicates, it doesn't decorate. Every animation answers a question: where did this come from, what changed, did my tap work. Closing is always faster than opening, and nothing uses linear easing except progress bars.

Duration tokens

Four named durations, drawn from the same scale on every surface. Hover a row to watch the bar fill at its real speed — the difference between --dur-instant and --dur-gentle is the difference between "felt" and "noticed." (Reconciles with the landing CSS, which already runs 0.15s / 0.20s transitions.)

Token
Value
Speed (live — hover)
Use
--dur-instant0.10s
Tap-down, checkbox flip — reads as immediate
--dur-quick0.15s
Hover, toggle, micro-interaction
--dur-default0.20s
The workhorse — most state changes
--dur-gentle0.25s
Sheets, popovers, the surfaced card
MomentDurationEasing
Micro-interaction (tap, toggle)100–150msease-out
Popover / sheet enter200–300msease-out
Sheet / overlay exit150–200msease-in
Card surfaced (geofence)300msspring (gentle)
Web float (device mockups)4s loopease-in-out
Progress / scanlinear (only here)
Easing tokens
  • · Enter: cubic-bezier(0.16, 1, 0.3, 1) — fast start, gentle landing
  • · Exit: cubic-bezier(0.4, 0, 1, 1) — slow start, fast off
  • · Surfaced: cubic-bezier(0.34, 1.4, 0.64, 1) — slight overshoot
Web float (live)
card
card
card

Animate only transform and opacity (GPU). prefers-reduced-motion kills the float and smooth scroll — verify with your OS toggle.

§14 · Accessibility

Accessibility

Accessibility is a design requirement, not an afterthought — it's why the accent is split and why the barcode card is forced white. The audited contrast pairs below are the ones the system actually relies on.

PairContextRatioVerdict
#151515 on #FFFFFFprimary text, light~17:1AAA
#6B7278 on #FFFFFFsecondary text, light~4.8:1AA
#047568 on #FFFFFFaccent text/link, light~4.9:1AA
#5BC7B8 on #0F1111accent mark, dark~9:1AAA
#FFFFFF on #05A08Ewhite on accent fill, dark~3.27:1AA large/bold only
#FFFFFF on #5BC7B8white on mark teal (wrong)~2.04:1FAIL — don't
#D63B6E on #FFFFFFerror text, light (semibold)~4.5:1AA at ≥ semibold
#F6F6F6 on #0F1111primary text, dark~16:1AAA
#C9CED3 on #0F1111secondary text, dark~11:1AAA
The accent-fill rule

Never put small white text on teal. Only large/bold white may sit on Accent Fill. Captions go on the dark surface, not on the fill.

No color alone

Geofence state pairs the teal dot with a label; success/error pair color with an icon; category uses color and glyph. Error text carries weight, not just hue.

Targets & focus

44pt minimum touch targets (FAB, rows, chips). Visible focus ring in focus teal on every interactive element; keyboard-operable web; VoiceOver / TalkBack labels on icon-only controls.

§15 · Open decisions

Open decisions to lock

The two typography decisions (#1, #2) are now locked — they're kept here as the record of what was decided and why. The remaining three (#3–#5) are genuinely unresolved: the code shows real divergence, so the guide surfaces it rather than silently picking. Each open item has a recommendation and its tradeoff; Rob decides.

01Fraunces as a brand display face — in-app?Locked
Decision
No. Fraunces stays web / advertising only — all web headlines. It never enters the apps. The apps carry no expressive display face at all; they use the OS system font for display (SF Pro on iOS, Google Sans Flex on Android).
Why
Hopscotch is a utility — people open it to do one thing and leave. Native type makes the app feel instant and like it belongs to the phone, not like a brand performing. Fraunces' editorial warmth is exactly right for the website, where a first impression is the job, and exactly wrong in a product that should disappear while it works.
Note
The fixed wordmark lockup (the Fraunces serif) may still appear on app splash / the Wallet pass — that's a logo asset, not interface type.
02Outfit scope — is Outfit an in-app face?Locked
Decision
No Outfit in either app. Both apps are fully native: iOS / watchOS → SF Pro + SF Symbols; Android → Google Sans Flex (Google's own variable brand + system typeface, Material 3 Expressive direction) + Material Symbols. Outfit is a web / advertising body/UI face only (plus the wordmark logo lockup).
Why
This is the same "invisible when it's working" principle that governs the whole system, applied to type and icons: in the product the brand gets out of the way and the app feels like the OS; on the website it performs. Going native per platform is not fragmentation — SF Pro vs Google Sans Flex is a deliberate, aligned-intent split (both apps native to their OS — Apple's typeface / Google's typeface), shown as ◐ in the §12 parity matrix.
Note
Supersedes the earlier draft where Android ran Outfit across the M3 scale. The §5 specimens and §12 matrix now reflect this.
03Web primary teal — gradient vs flatOpen
Conflict
Web leads with #1BAA98 + a Teal-500→700 gradient; the apps use flat #047568.
Recommend
Lock the gradient as marketing-only. Apps stay on flat Teal-700 fills; the web gradient is allowed on CTAs/nav as marketing energy, not pulled in-app.
Tradeoff
Keeping the split means the brand teal looks slightly different on the site than in the product (acceptable — context differs); unifying to flat would calm the site but cost some marketing punch.
04Web neutral ramp vs app text tokensOpen
Conflict
Web uses an Apple-ish cool-gray ramp (#1D1D1F / #515154 / #8E8E93); the apps use #151515 / #6B7278 / #9BA1A8.
Recommend
Unify the web to the app text tokens. The mismatch is small and buys nothing; one neutral ramp makes screenshots and embeds feel native to the brand.
Tradeoff
A one-time CSS edit; risk is purely that the slightly cooler Apple gray currently flatters the device mockups — verify before/after.
05Web dark modeOpen
Conflict
There's no web dark mode — only the single dark privacy island.
Recommend
Defer a full web dark mode until post-launch. The app dark palette already exists and ports cleanly when it's worth the effort; a marketing site rarely needs it day one.
Tradeoff
Shipping without it is fine for a landing page, but prefers-color-scheme users see a bright site at night — low-cost to add later using the tokens already defined in this guide.
§16 · Reskin

Reskin & white-label

The system is built so a spin-off — say Hopscotch for Business — can re-theme by swapping one thing: the accent ramp. Everything that makes a Hopscotch screen feel like Hopscotch (the shape language, the forced-white barcode card, the type roles, the spacing rhythm) stays locked. Because every swatch is driven by tokens, a hue change ripples through correctly in both light and dark with no per-screen edits.

What swaps vs. what stays locked

Swaps — the brand layer
  • The accent ramp (teal-100…700) → the new hue's six steps
  • accent + accentFill light/dark pair (re-run the contrast math so white-on-fill still clears ~3:1)
  • focus ring & success (both track the accent — success stays "brand-colored, not generic green")
  • Teal-tinted surfaceContainer & nav indicator, recomputed from the new hue
  • Wordmark + brand-mark fill (the green mark becomes the new accent)
Locked — the system layer
  • The forced-white barcode card — scanner legibility is non-negotiable on every theme
  • Shape rules — actions are pills/circles, containers are rounded-rects; radius scale 4/8/12/16
  • Type roles — native OS face for all interface type (SF Pro on iOS, Google Sans Flex on Android), mono for code values
  • The 8pt spacing rhythm and internal ≤ external rule
  • The two-token accent split pattern itself (only the hex values inside it change)
  • Error / warning semantics — negative/caution hues are not part of the brand swap

Before / after — a single hue swap

Hopscotch teal → a "for Business" indigo. Same ramp positions, same components, same shapes — only the accent token family moves. The barcode card is shown in both to make the point: it does not move.

Hopscotch — teal accent
Add to Wallet accent #047568
Blue Bottle
Coffee
7 8 9 0 1 2 3 4 5
Hopscotch for Business — indigo accent
Add to Wallet accent #2540C2
Blue Bottle
Coffee
7 8 9 0 1 2 3 4 5

The indigo ramp is illustrative, not a committed brand. Note the barcode card is byte-for-byte identical in both — only the icon tile (a card-color slot) picks up the new accent. Same buttons, same pill shape, same number in mono. That's the test of a real system: the spin-off is recognizably the same product, just wearing a different hue.