Skip to content

Color

Every color in Tomoda comes from frontend/constants/Themes.ts. The system is token-based: components reference semantic names (primary, surface, text) and the active theme decides what hex to render.

Two themes ship today: dark (default — the brand's primary mode) and light. Both are first-class — every component is built to work in either.

Surface ladder

The "tonal layering" depth model. Each step up the ladder adds about 5-8% in luminance. To create depth, place a higher-tier surface on top of a lower-tier one — no shadow required.

Dark

background #0e0e0f App root, scroll background
surface #18181a Cards, headers
surfaceContainerLow #1c1c1f Grouping containers
surfaceContainerHigh #252528 Interactive layers, inputs
surfaceContainerHighest #2e2e32 Popovers, focused inputs

Light

background / surface #FBFAF6 App root, cards — warm paper
surfaceContainerLow #f4f4f5 Grouping containers
surfaceContainerHigh #e4e4e7 Interactive layers, inputs
surfaceContainerHighest #d4d4d8 Popovers, focused inputs

Brand accents

Primary — duotone

The primary token is duotone across themes. Dark mode is gold-forward: primary is ember gold. Light mode is ink-forward: primary is the brand ink, so filled CTAs read as a confident dark chip on warm paper. Gold does not disappear in light mode, it moves to the Cozy Campfire accent family and the gold gradient.

Text and icons on a primary surface come from onPrimary (never a hardcoded literal): dark ink on the gold dark-mode primary, light paper on the ink light-mode primary. The gold gradient stays gold in both themes, so labels on it use onPrimaryGradient (dark ink in both).

Glyphs and text painted on a saturated accent fill (small category badges, colored pills like the notification kind badge) use onColor (white in both themes), so the contrast against the semantic fill doesn't flip with the app theme.

primary (dark) #D4955A Ember gold
primaryContainer (dark) #673d00 Gradient end, deep bronze
primary (light) #0F0F10 Ink-forward light primary
primaryContainer (light) #fce3c5 Warm cream tint
onPrimary (dark) #0F0F10 Dark ink on the gold primary
onPrimary (light) #F7F5F0 Light paper on the ink primary
onPrimaryGradient #0F0F10 Dark ink on the gold gradient, both themes

Primary CTAs use a 135° linear gradient from primary to primaryContainer, giving the warm gold a metallic, brushed quality. Don't stop at flat fills. The gradient is gold in both themes; its labels use onPrimaryGradient.

Secondary — indigo

Used for non-primary interactive accents (links in certain contexts, secondary highlights). Picked deliberately as the cool complement to the warm gold.

secondary (dark) #818CF8
secondary (light) #4F46E5

Status colors

For success, warning, error states. Sized and saturated for clear differentiation in both modes.

success (dark) #34D399
warning (dark) #FBBF24
error (dark) #F87171
success (light) #059669
warning (light) #D97706
error (light) #DC2626

Online / active-now

online is the presence green used for active-now and online-now indicators (chat header status, friend dots, cluster popup dots, location "operational" state). Dark keeps a bright green for near-black chrome; light drops to a deeper green so it stays legible on warm paper.

online (dark) #4CAF50 Active-now / online-now
online (light) #2E7D32

Notification / unread

notification is the dedicated unread/alert accent: unread-count badges (ui/NotificationBadge, used on the nav dock and header bells) and per-row unread dots (chat list, notifications list). A warm ember-red that pops on both grounds with white (onColor) text. It is deliberately separate from primary (which inverts gold↔ink between themes, so a primary-filled badge goes flat black in light mode) and from error (destructive / failure), so "you have something new" never reads as either the brand accent or an error state.

notification (dark) #C44A2C White text 4.8:1
notification (light) #B23A22 White text 6.0:1

Dock

dock is the frosted background of the bottom app-nav bar (components/layout/AppNav.tsx). Dark keeps a near-black glass; light uses the taupe surfaceContainerHigh tone at 0.82 alpha so the bar reads as a distinct elevated surface over the warm-paper background instead of blending into it.

dock (dark) rgba(10,10,11,0.75)
dock (light) rgba(229,225,216,0.82) Taupe elevated bar

Text

Three tiers per mode. Use the lowest-emphasis tier that still reads — every step up the contrast ladder is an attention cost.

text (dark) #FAFAFA Primary content
textSecondary (dark) #A1A1AA Labels, helper text
textMuted (dark) #52525B Disabled, metadata
text (light) #18181B Primary content
textSecondary (light) #71717A Labels, helper text
textMuted (light) #A1A1AA Disabled, metadata

Cozy Campfire — accent palette

A warm, autumn-toned set used for tag pills, earned-state highlights, decorative accents, and any moment where we want to gently introduce variety without leaving the brand temperature. Never use for chrome (backgrounds, app shell). Both dark- and light-mode variants are tuned for legibility against their respective base surfaces.

Dark mode

oxblood#660F09Deep ember scar — rare, high-emphasis
ember#C44A2CGlowing coal — earned / live / "now"
rust#B0623AIron oxide — secondary warm tag
amber#E89545Autumn flame — progress, attention
cedar#895129Cinnamon wood — warm filled surfaces
bronze#AD7A32Aged metal — info-warm
ochre#C19A4AAutumn leaf — landmarks, attractions
mustard#8A6E29Dried grass — quieter ochre
chestnut#5C3B22Dark warm wood — borders, depth
pinecone#3D2B1EDeepest warm brown — edges, ground

Light mode

Same palette, darkened for contrast against white surfaces. The semantic meaning of each token stays the same across modes.

oxblood#4A0A05
ember#9A3820
rust#864628
amber#B3691A
cedar#6E3E1E
bronze#7C551E
ochre#8F6E22
mustard#614B16
chestnut#3F2614
pinecone#2A1B0F

Borders

Borders are intentionally subtle. The border and outlineVariant tokens are both rendered at 15% opacity — solid lines are forbidden by the no-line rule.

  • Dark: rgba(255, 255, 255, 0.15)
  • Light: rgba(0, 0, 0, 0.10)

Map markers

Marker colors are mode-independent and live in MARKER_COLORS (frontend/components/ui/categoryIcons.ts), not in the theme. The marker layer (web components/map/engine/markers, native components/map/native/MarkerOverlay) renders the same on both light and dark maps, so its palette must not flip with the app theme. Web and native import the same map to stay in lock-step.

The palette covers event/location/moment tiers plus the ink and status accents painted on them:

  • eventDefault / eventSponsored / calendarEvent — event pin tiers.
  • locationBase / locationActive / locationHot — location activity tiers.
  • momentColor / momentMuted — moment pins.
  • markerInk (#0e0e0f) — dark ink for text and borders on the gold/orange marker tiles and badges.
  • globallyActive (#7c3aed) — the sharing-worldwide globe badge.
  • ringEvents / ringLocations / ringMoments — geo-cluster ring segments (one source for both renderers).

Map tiles

components/map/engine/styles.ts ships the per-mode STYLES JSON array for Google Maps (mobile), selected by map theme in MapView.native.tsx. On web, Leaflet styles are driven by the same theme tokens (custom CSS overrides on top of OpenStreetMap base tiles).

Adding a color

  1. Add the token to frontend/constants/Themes.ts in both the dark and light blocks. Every token must have both-mode parity.
  2. If it's a brand-level addition (a new accent), add a swatch entry to the appropriate section of this page in the same PR.
  3. If it's a semantic state (e.g. info), update Components to show which components use it.
  4. Run task docs:build to verify swatches render correctly.