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.
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.
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.
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.
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.
On light backgrounds, light Wallet pass, print.
Reversed on dark UI, dark Wallet pass, splash.
Brand moments only — app icon, marketing hero.
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.
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.
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.
100 background · 200 container / tint · 300 dark-mode mark + success · 500 mid / web primary · 600 deep · 700 primary brand teal & light accent.
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.
| Dark-mode teal | As a mark on #0F1111 | White 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. |
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.
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.
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.
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.)
The contract between the brand and the code. Every colored element maps to exactly one token — start here when wiring a new surface.
| Element | Token | Light / Dark |
|---|---|---|
| + / primary action button (fill) | accentFill | #047568 / #05A08E |
| Card-color background fallback | accentFill | #047568 / #05A08E |
| Toggle "on" track (white knob on it) | accentFill | #047568 / #05A08E |
| Bottom-nav icons (active + inactive) | accent | #047568 / #5BC7B8 |
| Text, links, the ↗ arrows | accent | #047568 / #5BC7B8 |
| Selection / active state | accent | #047568 / #5BC7B8 |
| Geofence radius circle (map) | accent | #047568 / #5BC7B8 |
| Map location pin | accent | #047568 / #5BC7B8 |
| Success check, "In Wallet" badge | success | #047568 / #5BC7B8 |
| Error / validation text (≥ semibold) | error | #D63B6E / #F2708B |
| Delete / Remove tint, swipe-to-delete | error | #D63B6E / #F2708B |
| Stale-pass / needs-refresh indicator | warning | #E8A80C / #F4C542 |
| Focus ring / outline | focus | #047568 / #5BC7B8 |
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.
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.
| Token | Light | Dark | Use |
|---|---|---|---|
| surface.base | #FFFFFF | #0F1111 Night | App canvas / window background |
| surface.card | #FFFFFF | #171A1A | Cards, rows, list items |
| surface.elevated | #FFFFFF | #1F2323 | Sheets, menus, raised chrome |
| surface.border | — | #2A2E2E | Hairline separation in dark only |
| surface.ink (hi-contrast) | #151515 | — | Max-contrast ink on light |
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.
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 token | Light | Dark | Helper |
|---|---|---|---|
| Color.brandAccent | #047568 | #5BC7B8 | Marks, text, nav glyphs, selection |
| Color.accentFill | #047568 | #05A08E | .brandProminentButton() · .brandToggleTint() |
| Color.semanticSuccess | #047568 | #5BC7B8 | Checkmarks, "In Apple Wallet" |
| Color.semanticError | #D63B6E | #F2708B | Error text (semibold), destructive |
| Color.semanticWarning | #E8A80C | #F4C542 | Caution / needs-attention |
| Color.brandTealDeep | #047568 | #047568 | Fixed 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).
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 role | Light | Dark | Source |
|---|---|---|---|
| primary | #047568 | #5BC7B8 | accent mark · brandTeal700 / 300 |
| onPrimary | #FFFFFF | #0F1111 | — |
| primaryContainer | #BFE6E1 | #036157 | teal-200 / teal-600 · also status-bar tint |
| onPrimaryContainer | #036157 | #BFE6E1 | teal-600 / teal-200 |
| surface | #FFFFFF | #0F1111 | Night base |
| surfaceContainer | #F4F8F7 | #171A1A | teal-tinted, not M3 purple-gray |
| surfaceContainerHigh | #EDF4F3 | #1F2323 | Night-elevated |
| surfaceContainerHighest | #E6F4F2 | #2A2E2E | teal-100 / Night-border |
| error | #D63B6E | #F2708B | semanticError |
| accentFill() | #047568 | #05A08E | composable, NOT in the scheme — for + / toggles |
dynamicColor = false by default. The theme supports Material You (hybrid), but ships off so the user's wallpaper never overrides teal. Brand consistency wins.
surfaceContainer is #F4F8F7 light / #171A1A dark — a teal-leaning neutral, not M3's baseline purple-gray. Keeps nav bar & menus in-brand.
Bottom-nav indicator bumped to brandTeal-200 (#BFE6E1); the status bar is tinted to primaryContainer (teal-200 light / teal-600 dark).
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.
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.
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.
| Role | Web | App 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.
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.
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.
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.)
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.
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.)
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.
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.
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 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.
| Role | Size | Weight | Line height | Tracking |
|---|---|---|---|---|
| displayLarge | 57sp | Normal | 64sp | −0.25sp |
| displayMedium | 45sp | Normal | 52sp | 0 |
| displaySmall | 36sp | Normal | 44sp | 0 |
| headlineLarge | 32sp | SemiBold | 40sp | 0 |
| headlineMedium | 28sp | SemiBold | 36sp | 0 |
| headlineSmall | 24sp | SemiBold | 32sp | 0 |
| titleLarge | 22sp | SemiBold | 28sp | 0 |
| titleMedium | 16sp | Medium | 24sp | 0.15sp |
| titleSmall | 14sp | Medium | 20sp | 0.1sp |
| bodyLarge | 16sp | Normal | 24sp | 0.5sp |
| bodyMedium | 14sp | Normal | 20sp | 0.25sp |
| bodySmall | 12sp | Normal | 16sp | 0.4sp |
| labelLarge | 14sp | Medium | 20sp | 0.1sp |
| labelMedium | 12sp | Medium | 16sp | 0.5sp |
| labelSmall | 11sp | Medium | 16sp | 0.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.
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.
Fluid clamp() sizing, font-display: swap, text-wrap: balance on headings / pretty on body.
Dead asset to retire: Regards.otf is loaded via @font-face in the landing CSS but never referenced — remove it.
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.
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.
1100px96px desktop / 48px mobileOne 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.
CTAs & secondary buttons → capsule. The primary + and small icon affordances (close, back) → circle.
Cards / sheets r16 · medium containers & rows r12 · chips / swatches / small tiles r8–9. iOS uses .continuous corners throughout.
| Element | iOS (.continuous) | Android (M3 Shapes) |
|---|---|---|
| Cards / sheets / large sections | 16 | large · CardShape 16dp |
| Medium containers / rows / banners | 12 | medium 12dp |
| Chips / swatches / small tiles | 8–9 | small 8dp |
| Buttons / CTAs (prominent + secondary) | capsule | ButtonShape · 50% |
| Primary + / icon affordances | circle | BadgeShape · 50% |
| Bottom sheet (modal) | 16 top | BottomSheetShape · top-28 |
| Watch barcode container | 4 | extraSmall 4dp |
Nested radii always shrink: a chip (r8) inside a card (r16) inside a sheet (r16) reads as a clean stack of containers.
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.
In dark mode shadows do nothing on near-black — depth there comes from the surface ramp (§4), not shadow.
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.
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.
| Category | iOS · SF Symbols | Android · Material | Web · SVG |
|---|---|---|---|
| 🛒 Grocery | cart.fill | ShoppingCart | cart (stroke) |
| ☕ Coffee | cup.and.saucer.fill | LocalCafe | cup |
| 🏋 Fitness | dumbbell.fill | FitnessCenter | dumbbell |
| ✚ Pharmacy | cross.case | LocalPharmacy | cross-case |
| 📚 Library | books.vertical | LocalLibrary | books |
| 🍴 Restaurant | fork.knife | Restaurant | fork-knife |
| 🏪 Store | storefront | Storefront | storefront |
| 💳 Default | creditcard | CreditCard | card |
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.
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.
Short sentences, concrete nouns, real places. No stacked value-props, no "seamless / powerful / revolutionary." Name the gym, the coffee shop, the register.
Trust comes from understatement. State the absence flatly — no account, no tracking, no server. Don't dramatize it; the restraint is the point.
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.
The voice, applied. Six real moments across the journey — written once here so they read the same on iPhone, Watch, Android, and the web.
"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.
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 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.
.preferredColorScheme(.light))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.
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.
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.
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.
+ and icon affordances are circlesaccentFill so white labels clear contrast in dark mode#5BC7B8) — white-on-it fails at ~2:1Selected uses teal-200 fill / teal-600 text. Pill shape, r100.
Circle, accentFill, colored shadow. The single most-used affordance — scan a card.
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).
#F4F8F7 / #171A1A) so nav stays in-brandsurfaceContainerMarketing 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.
Scan a card, set the store, and Hopscotch surfaces it automatically when you arrive.
Get HopscotchWalk in, and the card is already on your screen — no searching.
The one dark section on the otherwise light site — #1A1A1F.
| Compare | Hopscotch | Plastic 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.
Where the three surfaces agree, diverge-by-design, or have a gap. ✓ shared · ◐ aligned but differs · ✗ missing.
| Dimension | iOS | Android | Web | Note |
|---|---|---|---|---|
| Primary teal | ✓ | ✓ | ◐ | apps flat #047568; web leads #1BAA98 + gradient |
| Two-token accent split | ✓ | ✓ | ✗ | web has no dark mode, so no split needed |
| Dark mode | ✓ | ✓ | ✗ | web = single privacy island only |
| Neutral ramp | ✓ | ✓ | ◐ | web 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 / body | ✗ | ✗ | ✓ | Fraunces 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 pattern | ◐ | ◐ | ◐ | tab bar / bottom nav / glass header — native each |
| Icon set | ◐ | ◐ | ◐ | one semantic set, native renderers by design — SF Symbols / Material / SVG |
| Motion | ✓ | ✓ | ◐ | ease-out/in shared; web adds float |
· 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.)
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.
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.)
| Moment | Duration | Easing |
|---|---|---|
| Micro-interaction (tap, toggle) | 100–150ms | ease-out |
| Popover / sheet enter | 200–300ms | ease-out |
| Sheet / overlay exit | 150–200ms | ease-in |
| Card surfaced (geofence) | 300ms | spring (gentle) |
| Web float (device mockups) | 4s loop | ease-in-out |
| Progress / scan | — | linear (only here) |
cubic-bezier(0.16, 1, 0.3, 1) — fast start, gentle landingcubic-bezier(0.4, 0, 1, 1) — slow start, fast offcubic-bezier(0.34, 1.4, 0.64, 1) — slight overshootAnimate only transform and opacity (GPU). prefers-reduced-motion kills the float and smooth scroll — verify with your OS toggle.
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.
| Pair | Context | Ratio | Verdict |
|---|---|---|---|
| #151515 on #FFFFFF | primary text, light | ~17:1 | AAA |
| #6B7278 on #FFFFFF | secondary text, light | ~4.8:1 | AA |
| #047568 on #FFFFFF | accent text/link, light | ~4.9:1 | AA |
| #5BC7B8 on #0F1111 | accent mark, dark | ~9:1 | AAA |
| #FFFFFF on #05A08E | white on accent fill, dark | ~3.27:1 | AA large/bold only |
| #FFFFFF on #5BC7B8 | white on mark teal (wrong) | ~2.04:1 | FAIL — don't |
| #D63B6E on #FFFFFF | error text, light (semibold) | ~4.5:1 | AA at ≥ semibold |
| #F6F6F6 on #0F1111 | primary text, dark | ~16:1 | AAA |
| #C9CED3 on #0F1111 | secondary text, dark | ~11:1 | AAA |
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.
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.
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.
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.
#1BAA98 + a Teal-500→700 gradient; the apps use flat #047568.#1D1D1F / #515154 / #8E8E93); the apps use #151515 / #6B7278 / #9BA1A8.prefers-color-scheme users see a bright site at night — low-cost to add later using the tokens already defined in this guide.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.
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.
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.