In My Head — Visual Design and Theme Architecture
Complete Material 3 light and dark colour schemes derived from the brand source palette for In My Head, with semantic role mapping and accessibility validation.
This document defines how In My Head’s brand source palette becomes a complete, accessible Material 3 colour system.
It complements the finalized brand guidelines doc (name, logo, wordmark, and source palette). The brand guidelines define the literal identity assets and source palette; this document defines how that palette becomes an in-app colour system.
- Status
-
Implemented in the Android MVP; continuing visual verification on device
- Last reviewed
-
August 2026
| Role mapping and delivery boundaries in this document are settled decisions. Exact tonal values are computed and contrast-verified below, but remain provisional until viewed on production screens and physical devices. |
Purpose and scope
The objective is to establish a deliberate, accessible, complete Material 3 colour language for In My Head from the four brand seed colours, before any screens are built.
The review covers:
-
Complete Material 3 light and dark colour schemes, computed from the brand seeds using the actual HCT tonal-palette algorithm (not hand-picked approximations).
-
Semantic mapping of each role to what it means in this app specifically.
-
Contrast validation for every meaningful pair.
-
One explicit decision about a brand colour that stays outside the core role system.
The review does not cover:
-
Logo, wordmark, or name lockup construction — that belongs in the brand guidelines doc.
-
Font-family selection and component layout beyond the role mapping. The finalized Comfortaa wordmark treatment and Baloo 2 Material text family are defined in the brand guidelines and implemented in the app theme.
-
KMP-specific theming plumbing (Android
ColorSchemevs. future iOS/Desktop equivalents). -
Using tertiary or other roles merely to create variety.
Starting point
Brand source palette
| Source role | Value | Chosen purpose |
|---|---|---|
Primary seed |
|
Pink-red. Reserved for the principal action — the record control — matching the near-universal "red = recording" convention used by cameras, voice recorders, and call apps. |
Secondary seed |
|
Blue. Supporting actions and controls that shouldn’t compete with the record affordance. |
Tertiary seed |
|
Teal-green. Accent and "processed / complete" states — a transcript that’s been cleaned up. |
Reserved accent |
|
Yellow. Kept outside the core role system rather than forced into a fourth role — see Reserved accent: yellow. |
These colours are identity seeds, not four literal Material role values. The roles below are computed tonal derivatives of these seeds, chosen for accessibility — not the raw brand hex applied directly.
Identity families
-
Pink-red is the primary identity seed — reserved for the principal action and limited brand emphasis, not spread across ordinary UI.
-
Blue supplies the secondary family — calmer supporting actions and controls.
-
Teal supplies the tertiary family — accent and completion states.
-
Yellow remains a reserved accent, not a core role.
-
Error remains a separate, standard red family and is deliberately not derived from the brand pink-red. Primary is already a saturated red-adjacent hue here; deriving error from the same family would blur "this is the main action" and "something’s wrong" — the one thing a red error state most needs to avoid.
Design principles
Roles are semantic
Colour is assigned according to what a component communicates. Two components may use different colours when they represent different states or categories, but not merely to add variety.
Primary is selective
Primary is reserved for the record control, strong actions, and limited brand emphasis. Ordinary headings, transcript text, timestamps, and passive icons use surface foreground roles, not primary.
Selection uses a container
Selected list items and selected navigation destinations use primaryContainer with onPrimaryContainer. Selection also carries a non-colour cue (shape, position, or typography) — never colour alone.
Surfaces express hierarchy
Surface and surface-container roles distinguish the screen, cards, and grouped regions, with deliberately restrained steps between them. A surface-container level looks similar to base surface because it expresses hierarchy, not a semantic category — secondaryContainer and tertiaryContainer remain distinguishable enough to communicate their separate purpose.
Complete Material 3 role contract
Every role in the stable Material 3 ColorScheme is explicitly defined below for both light and dark schemes — nothing is left to library defaults.
Computed using Google’s HCT tonal-palette algorithm (materialyoucolor, the same math behind Android’s own dynamic colour system), spec version 2021, TonalSpot tone mapping, against the four brand seeds plus a standard (non-brand-derived) error seed.
Light scheme
| Role | Hex | Tone |
|---|---|---|
primary |
|
40 |
onPrimary |
|
100 |
primaryContainer |
|
90 |
onPrimaryContainer |
|
30 |
inversePrimary |
|
80 |
secondary |
|
40 |
onSecondary |
|
100 |
secondaryContainer |
|
90 |
onSecondaryContainer |
|
30 |
tertiary |
|
40 |
onTertiary |
|
100 |
tertiaryContainer |
|
90 |
onTertiaryContainer |
|
30 |
background |
|
98 |
onBackground |
|
10 |
surface |
|
98 |
onSurface |
|
10 |
surfaceVariant |
|
90 |
onSurfaceVariant |
|
30 |
surfaceTint |
|
40 |
surfaceBright |
|
98 |
surfaceDim |
|
87 |
surfaceContainerLowest |
|
100 |
surfaceContainerLow |
|
96 |
surfaceContainer |
|
94 |
surfaceContainerHigh |
|
92 |
surfaceContainerHighest |
|
90 |
inverseSurface |
|
20 |
inverseOnSurface |
|
95 |
error |
|
40 |
onError |
|
100 |
errorContainer |
|
90 |
onErrorContainer |
|
30 |
outline |
|
50 |
outlineVariant |
|
80 |
scrim |
|
0 |
primaryFixed |
|
90 |
primaryFixedDim |
|
80 |
onPrimaryFixed |
|
10 |
onPrimaryFixedVariant |
|
30 |
secondaryFixed |
|
90 |
secondaryFixedDim |
|
80 |
onSecondaryFixed |
|
10 |
onSecondaryFixedVariant |
|
30 |
tertiaryFixed |
|
90 |
tertiaryFixedDim |
|
80 |
onTertiaryFixed |
|
10 |
onTertiaryFixedVariant |
|
30 |
Dark scheme
| Role | Hex | Tone |
|---|---|---|
primary |
|
80 |
onPrimary |
|
20 |
primaryContainer |
|
30 |
onPrimaryContainer |
|
90 |
inversePrimary |
|
40 |
secondary |
|
80 |
onSecondary |
|
20 |
secondaryContainer |
|
30 |
onSecondaryContainer |
|
90 |
tertiary |
|
80 |
onTertiary |
|
20 |
tertiaryContainer |
|
30 |
onTertiaryContainer |
|
90 |
background |
|
6 |
onBackground |
|
90 |
surface |
|
6 |
onSurface |
|
90 |
surfaceVariant |
|
30 |
onSurfaceVariant |
|
80 |
surfaceTint |
|
80 |
surfaceBright |
|
24 |
surfaceDim |
|
6 |
surfaceContainerLowest |
|
4 |
surfaceContainerLow |
|
10 |
surfaceContainer |
|
12 |
surfaceContainerHigh |
|
17 |
surfaceContainerHighest |
|
22 |
inverseSurface |
|
90 |
inverseOnSurface |
|
20 |
error |
|
80 |
onError |
|
20 |
errorContainer |
|
30 |
onErrorContainer |
|
90 |
outline |
|
60 |
outlineVariant |
|
30 |
scrim |
|
0 |
primaryFixed |
|
90 |
primaryFixedDim |
|
80 |
onPrimaryFixed |
|
10 |
onPrimaryFixedVariant |
|
30 |
secondaryFixed |
|
90 |
secondaryFixedDim |
|
80 |
onSecondaryFixed |
|
10 |
onSecondaryFixedVariant |
|
30 |
tertiaryFixed |
|
90 |
tertiaryFixedDim |
|
80 |
onTertiaryFixed |
|
10 |
onTertiaryFixedVariant |
|
30 |
Fixed roles are identical across both schemes by design — they’re for content that must stay visually stable regardless of light/dark mode (e.g. a persistent brand chip).
Literal brand hex vs. in-app tonal derivative
Worth calling out explicitly, since two of the three seeds shift a fair amount from the raw brand swatch once run through the accessibility-safe tone mapping:
-
Primary — raw
#FF0052→ in-app#BE003B(light) /#FFB2B7(dark). The raw swatch is too light/vibrant to safely carry white text at body-text sizes, so the role uses a slightly deeper, equally-recognisable red. The literal#FF0052stays a valid brand constant for the icon, splash, and marketing use — just not for theprimaryrole’s UI surfaces. -
Secondary — raw
#0055DA→ in-app#0054D7. Near-identical; blue happened to already sit close to the accessible tone. -
Tertiary — raw
#00C68D→ in-app#006C4B(light) /#3DDFA4(dark). The raw teal is quite light already, so its accessible "base" role tone darkens noticeably. If you want the original bright teal to actually show up recognisably in the UI, that’stertiaryContainer(#62FDBF) ortertiaryFixedDim, not the basetertiaryrole.
Reserved accent: yellow
#FFD400 is not mapped into the core primary/secondary/tertiary/error system. Yellow has an unusually narrow accessible range — there’s very little room where a yellow fill and readable text on top of it both work at once, so forcing it into a full role (with an onYellow foreground) tends to fight the "clean look" goal rather than serve it.
Instead, treat it as a single reserved, non-text accent for one deliberate moment — e.g. a subtle "processing" pulse while the local LLM cleanup step runs — used sparingly and never as a text-bearing surface.
| Tone | Hex |
|---|---|
T30 (darkest, usable as text on light surfaces) |
|
T80 (usable as a light fill) |
|
T90 (very light, container-like) |
|
This is genuinely optional for MVP — it can be dropped entirely without losing anything structural.
App colour direction
Production mappings
-
Record control (principal action):
primarywithonPrimary. -
Recording-active indicator (pulse/dot while live):
primary— reinforces the record convention rather than introducing a second red. -
Secondary controls (pause, cancel, settings entry):
secondary/onSecondaryfor emphasis,onSurfaceVariantfor low-emphasis icons. -
Transcript-ready / cleanup-complete confirmation:
tertiaryContainerwithonTertiaryContainer. -
Selected list item or nav destination:
primaryContainerwithonPrimaryContainer. -
Ordinary headings, transcript text, filenames:
onSurface. -
Supporting text (timestamps, metadata, durations):
onSurfaceVariant. -
Strong control borders:
outline. -
Quiet dividers:
outlineVariant. -
Destructive action (delete a recording or transcript): error family.
-
Processing/in-progress bespoke highlight: the reserved yellow accent, non-text only, used sparingly.
Accessibility and validation
Every pair below is computed from actual tone values via the same contrast math Material uses internally to guarantee its role-pair targets — not eyeballed.
| Pair | Light | Dark |
|---|---|---|
primary / onPrimary |
6.46:1 |
7.72:1 |
primaryContainer / onPrimaryContainer |
7.24:1 |
7.24:1 |
secondary / onSecondary |
6.46:1 |
7.72:1 |
secondaryContainer / onSecondaryContainer |
7.24:1 |
7.24:1 |
tertiary / onTertiary |
6.46:1 |
7.72:1 |
tertiaryContainer / onTertiaryContainer |
7.24:1 |
7.24:1 |
error / onError |
6.46:1 |
7.72:1 |
errorContainer / onErrorContainer |
7.24:1 |
7.24:1 |
background / onBackground |
16.31:1 |
14.35:1 |
surface / onSurface |
16.31:1 |
14.35:1 |
surfaceVariant / onSurfaceVariant |
7.24:1 |
5.49:1 |
inverseSurface / inverseOnSurface |
11.59:1 |
10.18:1 |
surface / outline (boundary, non-text) |
4.27:1 |
5.85:1 |
WCAG AA requires 4.5:1 for normal text and 3:1 for UI-component boundaries/non-text. Every text-bearing pair here clears AA, and most clear AAA (7:1). outline is a boundary role, not text, so it’s validated against the 3:1 non-text minimum — both schemes clear it comfortably.
Non-colour cues still apply on top of this: selected states, errors, and links must not rely on colour alone (shape, icon, underline, or position as a second signal).
Open decisions
-
Typography scale (display/headline/title/body/label sizes) — separate pass. Typeface itself is chosen (Baloo 2 for UI text, see brand-guidelines.adoc), but mapping it onto Material 3’s type roles isn’t done yet.
-
Whether the dark-theme primary red reads correctly on real hardware (especially AMOLED) — needs physical-device verification, not just simulator review.
-
Whether the yellow accent ships at all in MVP, or gets deferred entirely.
-
Logo, wordmark, and name lockup — concept and typefaces settled, see brand-guidelines.adoc; exact icon variation still being finalized.