Internal

NommaDesign lab

Every token, component and state in one place. This is where design gets approved — on a real phone, before any feature uses it.

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.

primary — on paper
Nomma
primary — on sand
Nomma
inverse — on forest
Nomma
mono ink — one-colour contexts
Nomma

Icon

The mark alone. Used below the lockup's minimum size, and as the app icon.

forest
paper on forest
ink

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.

minimum — 24px
below minimum — icon only

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.

favicon.ico — 32px
manifest — 192 & maskable
apple touch — 180
app store — 1024

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.

Aa

paper #FAF7F0

App background. Warm off-white, never pure white

Aa

sand #EBE1CD

Cards, secondary surfaces, input fills

Aa

forest #1E4634

Primary actions, brand, links, active states

Aa

moss #7E9F87

Fills only — squircles, illustration. Not text

Aa

ink #161B18

All text, panda black. Never #000000

Aa

honey #B98A3C

Warnings & price-hike fills ONLY

Aa

moss-deep #4F6455

moss as text — renews-soon lines

Aa

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.

PairingRatioAA
ink on paper — body16.3pass
forest on paper — links, buttons9.9pass
ink on sand — card body13.4pass
forest on sand — secondary button8.2pass
paper on forest — primary button9.9pass
paper on ink — toast16.3pass
ink-muted on sand — secondary row line4.9pass
ink-muted on paper — captions5.3pass
moss-deep on sand — renews in 4 days4.9pass
honey-deep on sand — price hike4.7pass
ink-muted on honey tint — alert body5.1pass
ink-muted on moss tint — alert body5.0pass
ink glyph on honey squircle5.6pass
ink glyph on moss squircle6.0pass
paper glyph on forest squircle9.9pass
moss icon on ink toast6.0pass
Superseded 2026-08-29 — kept for comparison
ink/40 — was the secondary line2.4fail · needs 4.5
moss — was renews-soon text2.2fail · needs 4.5
honey — was price-hike text2.4fail · needs 4.5
paper glyph on honey2.9fail · needs 3
paper glyph on moss2.7fail · 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.

hero — clamp(4rem, 18vw, 6rem)

€182.40

display — clamp(1.75rem, 6.5vw, 2.25rem)

Nothing tracked yet

Body & UI — Satoshi

Everything else, at three weights.

title 22 / bold

Upcoming renewals

heading 18 / bold

Netflix Standard

body 16 / regular

We never touch your bank. Add what you pay for and Nomma keeps the maths, the dates and the reminders straight.

label 15 / bold — row names

Spotify Premium Duo

caption 13 / regular — secondary lines

Renews 12 Sep · Yearly plan

micro 11 / bold uppercase — eyebrows

Per month

Tabular numerals

Mandatory on money and dates. Left column is tabular, right is proportional — watch the decimal points.

tabular

€11.99€8.00€149.00

proportional

€11.99€8.00€149.00

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.

4
8
12
16
24
32
48
64

Radius

Pebble-soft, matching the mascot roundness.

pebble-sm
logo
pebble
row
pebble-lg
full

Elevation

One card shadow. Overlays get a second, stronger token — dialogs and toasts only.

shadow-soft — cards
shadow-overlay — dialogs, toasts
not allowed — 1px outline

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.

Buttons

§5.1 pill-shaped controls · §5.3 buttons say what they do

Variants — default state

Sizes

lg (52px) is the primary touch target on mobile.

States

Hover and press are live — tap and hold to see them.

Focus

Tab to these. A 2px forest ring with a 2px gap, on every focusable thing in the app.

Icon buttons

Full width — the mobile default for a primary action

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.

/ month
/ month

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.

Checking the catalogue for Netflix…

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.

CurrencyEUR
Remind me3 days before
Reminder channelPush

Service squircles

42px, radius 14, inset shading. Initial marks stand in until the catalogue is seeded.

Badges

EntertainmentBetaSavingPrice up

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

default

Spotify

Duo · Renews in 12 days

€14.99

/mo

renews within 7 days — moss

Netflix

Renews in 4 days

€13.49

/mo

renews today

iCloud+

Renews today

€2.99

/mo

price hike — honey, the only place it appears

Adobe Photography

Up from €131.88

€142.90

/yr

paused

Gym — Elixia

Paused · Paused since Jun

€49.90

/mo

long name, truncated at 360px

Amazon Prime Video Channels — Paramount+

Renews in 6 days

€7.99

/mo

loading

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

Standard moved from €11.49 to €13.49 on 1 September. That is €24.00 more a year.

Switching Spotify to yearly saves €24.00

You pay €14.99 a month. The yearly plan works out at €13.00.

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.

idle

We'll email you a link that signs you in. No password to remember.

submitting

We'll email you a link that signs you in. No password to remember.

error — address rejected by the server

We'll email you a link that signs you in. No password to remember.

error — rate limited

We'll email you a link that signs you in. No password to remember.

dead link — expired or already used

We'll email you a link that signs you in. No password to remember.

sent

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 yourself

Step 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.

catalog service, with plans

Netflix

Video

Pick yours and the price fills in.

Roughly is fine — you can change it later.

Only you ever see this.

catalog service, no plans seeded — today's real state

Netflix

Video

Roughly is fine — you can change it later.

Only you ever see this.

custom subscription

Use the name on your bank statement so you can find it later.

Roughly is fine — you can change it later.

Only you ever see this.

saving

Netflix

Video

Roughly is fine — you can change it later.

Only you ever see this.

rejected — price is not a number

Netflix

Video

Roughly is fine — you can change it later.

Only you ever see this.

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.

Netflix

Video

Price from help.netflix.com. Not checked by a human yet — change it if it is wrong.

Roughly is fine — you can change it later.

Only you ever see this.

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.

idle

Your monthly total is shown in this. Subscriptions in another currency are listed but not added in — Nomma does not convert.

Decides which plan prices you are offered when adding a service. Only Ireland and the United States have prices so far — anywhere else, Nomma asks you for the price instead.

saved

Saved

Your totals now use this currency.

Your monthly total is shown in this. Subscriptions in another currency are listed but not added in — Nomma does not convert.

Decides which plan prices you are offered when adding a service. Only Ireland and the United States have prices so far — anywhere else, Nomma asks you for the price instead.

Feedback

One box. No rating, no categories, no NPS — those measure people; this one listens to them.

idle

Bugs, confusion, something missing, something you liked. It goes straight to the founder.

sending

Bugs, confusion, something missing, something you liked. It goes straight to the founder.

rejected

Bugs, confusion, something missing, something you liked. It goes straight to the founder.

sent

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.

off — not yet granted

Get a notification before a subscription renews. Push is per-device, so you turn it on wherever you want it.

asking

Get a notification before a subscription renews. Push is per-device, so you turn it on wherever you want it.

on for this device

Reminders are on for this device. Add Nomma to another phone or laptop and you will be asked there too.

blocked by the browser — cannot re-prompt
unsupported — iPhone before install
failed

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

zz

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.

  1. Image received
  2. Finding subscriptions
  3. 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.

?

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

Netflix added.

Spotify removed.

Adding 9 subscriptions…

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

confirm

Pause Netflix?

It stays in your list and stops counting towards your monthly total. You can start it again any time.
destructive

Delete Netflix?

This removes it and its reminders. It cannot be undone.
working

Add 9 subscriptions?

Worth €182.40 a month.

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

zz

sleeping

Empty states

?

puzzled

Errors, 404