Brand
§5.2d — logo v1, locked 2026-08-31
Lockup
Icon plus wordmark. Three variants, each tied to a surface — pick the variant, never restyle one.
Icon
The mark alone. Used below the lockup's minimum size, and as the app icon.
Clear space & minimum size
Clear space is the icon's width on every side. The lockup stops at 24px tall — below that, the icon goes in alone.
Platform icons
What the browser tab, the Android launcher and the iOS home screen actually get. Served straight from public/brand — no generated stand-ins left.

Raster lockups
Flat PNGs for anything that cannot render SVG — OG cards, email, slide decks.


Never: rotate the mark, add a gradient or shadow, outline it, recolour it, redraw the bite, put the forest lockup on a dark surface (use inverse), or typeset “nomma” in another font beside the icon. The bite always faces top-right — it is the brand. Colours are forest, paper and ink only; the mark never uses moss or honey, so it cannot be mistaken for a status.
Colour
§5.2 — locked 2026-08-28
Tokens
Defined once in globals.css. No hex appears anywhere else.
paper #FAF7F0
App background. Warm off-white, never pure white
sand #EBE1CD
Cards, secondary surfaces, input fills
forest #1E4634
Primary actions, brand, links, active states
moss #7E9F87
Fills only — squircles, illustration. Not text
ink #161B18
All text, panda black. Never #000000
honey #B98A3C
Warnings & price-hike fills ONLY
moss-deep #4F6455
moss as text — renews-soon lines
honey-deep #785E2E
honey as text — price-hike lines
Contrast audit
Every text and glyph pairing the components actually use, measured live against the flattened background. Every one passes.
| Pairing | Ratio | AA |
|---|---|---|
| ink on paper — body | 16.3 | pass |
| forest on paper — links, buttons | 9.9 | pass |
| ink on sand — card body | 13.4 | pass |
| forest on sand — secondary button | 8.2 | pass |
| paper on forest — primary button | 9.9 | pass |
| paper on ink — toast | 16.3 | pass |
| ink-muted on sand — secondary row line | 4.9 | pass |
| ink-muted on paper — captions | 5.3 | pass |
| moss-deep on sand — renews in 4 days | 4.9 | pass |
| honey-deep on sand — price hike | 4.7 | pass |
| ink-muted on honey tint — alert body | 5.1 | pass |
| ink-muted on moss tint — alert body | 5.0 | pass |
| ink glyph on honey squircle | 5.6 | pass |
| ink glyph on moss squircle | 6.0 | pass |
| paper glyph on forest squircle | 9.9 | pass |
| moss icon on ink toast | 6.0 | pass |
| Superseded 2026-08-29 — kept for comparison | ||
| ink/40 — was the secondary line | 2.4 | fail · needs 4.5 |
| moss — was renews-soon text | 2.2 | fail · needs 4.5 |
| honey — was price-hike text | 2.4 | fail · needs 4.5 |
| paper glyph on honey | 2.9 | fail · needs 3 |
| paper glyph on moss | 2.7 | fail · needs 3 |
Resolved 2026-08-29 (Open Decision #8). Five charter-prescribed pairings sat between 2.2:1 and 2.9:1 — the range where text is hard to read on a phone in daylight — against the 4.5:1 that §5.3 requires. The fix keeps every colour doing the same job and only deepens it: one muted level (ink-muted) for all secondary text and placeholders, plusmoss-deep and honey-deep for the two coloured text lines. moss and honey stay exactly as locked and remain correct for fills. Squircle glyphs went from paper to ink, which is the same swap in reverse.
Type
§5.2 — Fraunces (display) + Satoshi (body/UI), both self-hosted
Display — Fraunces
The cozy carrier. Once per screen, never twice.
€182.40
Nothing tracked yet
Body & UI — Satoshi
Everything else, at three weights.
Upcoming renewals
Netflix Standard
We never touch your bank. Add what you pay for and Nomma keeps the maths, the dates and the reminders straight.
Spotify Premium Duo
Renews 12 Sep · Yearly plan
Per month
Tabular numerals
Mandatory on money and dates. Left column is tabular, right is proportional — watch the decimal points.
tabular
proportional
Both families are served from our own origin as .woff2 with font-display: swap and a metric-matched fallback (§5.2c). No request reaches Google or Fontshare at runtime.
Spacing, radius, elevation, motion
§5.2 — 4px scale · radius family 12/16/24 · one shadow · one easing
Spacing — 4px base
Every gap is a multiple of 4.
Radius
Pebble-soft, matching the mascot roundness.
Elevation
One card shadow. Overlays get a second, stronger token — dialogs and toasts only.
Motion
One easing: cubic-bezier(0.22, 1, 0.36, 1). Three sanctioned moments — the total counting up, a subscription landing, and Bao. Everything honours prefers-reduced-motion.
settle — 420ms
rise — 320ms
Signature element — the monthly total
§5.2 · §5.2c — largest thing in the app by ~3x. If Fraunces looks timid, it is too small.
per month
€2,679.24 a year
Counts up once, on load, easing on the one project easing curve. Under prefers-reduced-motion the final figure renders immediately with no interpolation. Bao never appears beside it (§5.2b) — the number is the hero here.
Inputs
§5.2 sand fills · §5.2c no hard borders · §5.3 errors say what to do
Text — every state
Use the name on your bank statement.
Enter a name so you can find this later.
Locked while the import finishes.
Adornments
Currency prefix and cycle suffix, both tabular.
Price must be more than 0.
Select and textarea
Only you ever see this.
Toggle
Inline pending
A field that is validating rather than a whole-page spinner.
Error copy names what happened and what to do, in sentence case, with no apology (§5.3). The v1 palette has no danger colour, so an invalid field is signalled by an inset ink rule plus ink-weighted message text — see Open Decisions #7.
Cards & surfaces
§5.2c — no hard borders. Elevation is one soft shadow. Hairlines only inside grouped cards.
Base card
Next renewal
Netflix Standard
€13.49 charges on 2 Sep
Grouped card with hairline dividers
The only sanctioned use of a rule.
Service squircles
42px, radius 14, inset shading. Initial marks stand in until the catalogue is seeded.
Badges
List rows
§5.2c — style A on the dashboard, style B in the all-subscriptions view. Shared internals.
Style A — dashboard
Each row is its own soft card. Radius 18, 10px gaps. The dashboard shows the next four or five renewals only.
Coming up
Netflix
Renews in 4 days
€13.49
/mo
Spotify
Duo · Renews in 12 days
€14.99
/mo
iCloud+
200 GB · Renews in 19 days
€2.99
/mo
Adobe Photography
Up from €131.88
€142.90
/yr
Style B — all subscriptions
One card, hairline dividers, dense enough to scale past ten.
Netflix
Renews in 4 days
€13.49
/mo
Spotify
Duo · Renews in 12 days
€14.99
/mo
iCloud+
200 GB · Renews in 19 days
€2.99
/mo
Adobe Photography
Up from €131.88
€142.90
/yr
Gym — Elixia
Paused · Paused since Jun
€49.90
/mo
Row states, side by side
Spotify
Duo · Renews in 12 days
€14.99
/mo
Netflix
Renews in 4 days
€13.49
/mo
iCloud+
Renews today
€2.99
/mo
Adobe Photography
Up from €131.88
€142.90
/yr
Gym — Elixia
Paused · Paused since Jun
€49.90
/mo
Amazon Prime Video Channels — Paramount+
Renews in 6 days
€7.99
/mo
Service catalog
§M1 · §5.2e — 54 services, v1
Brand marks
Each service's own mark in its own colour, self-hosted from public/logos. Never a logo API (§8).
Letter marks
12 of 54 services have no freely distributable mark — their owners have asked icon sets to stop carrying them. These fall back to the designed letter mark, which is also what every custom subscription gets.
Mixed list — the real test
Brand marks and letter marks in one dashboard list. If this reads as unfinished rather than deliberate, that is the thing to fix.
Netflix
Renews in 4 days
€13.49
/mo
Spotify
Individual · Renews in 19 days
€11.99
/mo
Disney+
Standard · Renews in 25 days
€9.99
/mo
Gym membership
Renews in 30 days
€39.00
/mo
Brand marks are third-party trademarks used to identify the service they belong to — ordinary nominative use, and the reason §5.2e exempts them from the palette rather than recolouring them. Nomma's own six tokens still govern everything around them. The marks come from Simple Icons (CC0) and are written into public/logos at build time by npm run build:catalog; nothing is fetched at runtime.
Alerts
§5.2c — one signal only: tinted card plus matching icon squircle. Never border + dot + colour.
Netflix went up €2.00 a month
Switching Spotify to yearly saves €24.00
We could not read that screenshot
Not allowed
Three signals for one message.
Border, dot and colour at once
Pick one.
Sign in
§M1 — magic link, no passwords
States
One form, five states. Nothing here submits — the view is rendered without its action.
Check your inbox
We sent a sign-in link to you@example.com. It works once and expires in an hour.
Wrong address, or nothing arrived? Try again
Magic link is the only method for now (founder decision 2026-08-31). Google and Apple stay on the M1 list: neither provider is configured in the Supabase project yet, and Apple needs the €99/year developer account that §M4 defers to submission time — see Open Decision #9. When they land they go above the email field, with a divider, and this section grows a row of provider buttons.
Add & dashboard
§M1 — quick-add in ≤3 taps
Step one — find the service
Live: this filters the real 54-service catalog. Try “net”, “pri”, or something that is not there.
Not in the list?
Add it yourselfStep two — details
A plan fills in price, currency and cycle, and says where the figure came from. With no plan seeded the price is empty and asks, rather than guessing.
Dashboard
Monthly total is the largest thing on the page by ~3x and everything defers to it (§5.2). Bao never appears here.
per month
€773.52 a year
Coming up
Netflix
Renews in 2 days
€13.49
/mo
Spotify
Individual · Renews in 11 days
€11.99
/mo
Disney+
Renews in 19 days
€9.99
/mo
Gym membership
Renews in 26 days
€39.00
/mo
Editing
The same fields, pre-filled from the row. A stored price always wins over the plan's list price — someone on a legacy rate or a student discount must not have it overwritten.
All subscriptions — list style B
One card, hairline dividers, grouped by status. Dense on purpose: this is the page that has to survive twenty of them, which is what lets the dashboard stay airy.
Netflix
Renews in 2 days
€16.99
/mo
Spotify
Individual · Renews in 11 days
€12.99
/mo
Disney+
Standard · Renews in 19 days
€11.99
/mo
iCloud+
200 GB · Renews in 24 days
€2.99
/mo
Gym membership
Paused
€39.00
/mo
Settings
Currency and region are both consequential — one denominates the hero total, the other decides which plan prices the add flow offers — so the copy says so rather than leaving them looking cosmetic.
Feedback
One box. No rating, no categories, no NPS — those measure people; this one listens to them.
Got it — thank you
A person reads every one of these during the beta.
Renewal reminders
Push is per-browser, and the copy says so rather than implying an account-wide switch. The prompt only ever follows a tap — a permission prompt on page load is the most-blocked pattern on the web.
Get a notification before a subscription renews. Push is per-device, so you turn it on wherever you want it.
Get a notification before a subscription renews. Push is per-device, so you turn it on wherever you want it.
Reminders are on for this device. Add Nomma to another phone or laptop and you will be asked there too.
Notifications are blocked
Reminders are not available here
That did not work
Get a notification before a subscription renews. Push is per-device, so you turn it on wherever you want it.
The total sums only the profile's own currency. Nomma holds no exchange rate, and inventing one would put a wrong number in the largest type in the app — so anything in another currency is counted separately and said out loud. npm run check:money covers the arithmetic: weekly is 52/12 rather than ×4, monthly × 12 always equals yearly, and the total is summed at full precision and rounded once.
Empty states
§5.3 — every list has a designed empty state that invites the first action
Dashboard — nothing tracked yet
Nothing tracked yet
Add the first thing you pay for every month. It takes about five seconds.
Search — no match in the catalogue
No match for “Nebula”
It is not in the catalogue yet. Add it by hand and it will work exactly the same.
Import — nothing found in the screenshot
No subscriptions in that screenshot
Screenshots of Settings → Subscriptions or a bank transaction list work best.
Loading, success, error
§5.3 — every async action has all three, and errors say what happened and what to do
Loading — list
Loading — hero
The total never flashes a wrong number; it holds a placeholder instead.
Processing — import
Klarna-style: dot, label, one line. Bao munches while it works (§5.1, §5.2b).
Reading your screenshot
About 10 seconds. Nothing is saved until you confirm.
- Image received
- Finding subscriptions
- Ready for you to confirm
Success
Found 9 subscriptions
Worth €182.40 a month. Check them before we save anything.
Error
What happened, then what to do. No apology, no vague wording.
That import did not go through
You have used all 5 imports today
The limit resets at midnight. You can still add subscriptions by hand.
Toasts
Confirm and leave. Never more than three, never the only copy of anything important.
Static — all tones
Live
Fires the real thing, bottom of the screen, four seconds.
Dialogs
Native <dialog>: platform focus trap, Esc to close. Bottom sheet under 640px.
Static — panels as they render
Live
Open one to check the sheet behaviour and the backdrop on a phone.
Bao
§5.2b — charm at the edges, calm at the centre. Never on the dashboard next to real data.
Rejected 2026-08-29 — full rework pending. This drawing is not a design and is not being iterated on. It stays only so the states above keep their real footprint while the surrounding UI is reviewed. The rework happens after M1, against real screens, alongside the second design round (§5.4.5), and ships as Lottie for wave and munching. What is still worth judging here: the placement rule and the size Bao occupies — not the panda.
wave
Onboarding
munching
Import in progress
celebrating
Savings found / import success
sleeping
Empty states
puzzled
Errors, 404