design catalog · transactional email · v1

Every email we send

Ten templates, rendered from the real code in convex/email/templates.ts, all on the branded shell today. They used to split into two visual worlds, which is the reason this page exists: two went through the warm-clay branded shell, and seven fell through to an unbranded fallback that shared no colour, type, or button with the brand.

The page runs in that order: what ships today, then the two directions it was judged against, then the whole set re-envisioned in the chosen one — Direction A, with the full logo and a warm #FDFAF8 ground.

Branded shell · was 2 of 9

Purchase confirmation and renewal reminder called renderBrandedEmailShell(). Warm ground, 600px card, brand palette, serif display, pill CTA.

Unbranded fallback · was 7 of 9

Every invite/membership email plus the config test called renderSimpleEmailHtml(): #f9fafb ground, #e5e7eb borders, Inter/Arial, and a #111827 button. No brand token appears anywhere in it.

Current · as shipped now

Generated from convex/email/templates.ts by tools/dump-email-previews.ts, re-run after any template change.

purchase confirmation · account

You purchased SearchSync on tangible.one

purchase confirmation · guest

You purchased SearchSync on tangible.one

renewal reminder · dormant — Stripe sends these today

Your SearchSync, Tangible Blocks license expires in 14 days

invite sent

You are invited to join Team Tangible on Tangible

invite reminder

Reminder: join Team Tangible on Tangible

invite accepted

ghen@teamtangible.com joined Team Tangible

invite revoked

Your invite to Team Tangible was revoked

member removed

Team Tangible access removed

role changed

Your role in Team Tangible has changed

config test · staff only

Tangible email configuration test

Before · what shipped until 22 Aug 2026

These are the good ones, and still the reason the shell reads as generic: a white card on near-white, with a near-black CTA and no brandmark unless a logoUrl is passed — and no call site passes one.

purchase confirmation · account

Your SearchSync license & download

purchase confirmation · guest

Your SearchSync license & download

renewal reminder · dormant — Stripe sends these today

Your SearchSync, Tangible Blocks license expires in 14 days

Before · the unbranded fallback (now retired)

Seven templates, one generic shell. These are the first emails a new teammate ever gets from us — the invite lands before any receipt does.

invite sent

You are invited to join Team Tangible on Tangible

invite reminder

Reminder: join Team Tangible on Tangible

invite accepted

ghen@teamtangible.com joined Team Tangible

invite revoked

Your invite to Team Tangible was revoked

member removed

Team Tangible access removed

role changed

Your role in Team Tangible has changed

config test · staff only

Tangible email configuration test

Redesign directions · proposals

Both use the purchase confirmation as the specimen so they're comparable with the panels above, and both are real email HTML — tables and inline styles, no @font-face (clients strip it), no SVG (Outlook drops it). The brandmark is built from a 3×3 table of coloured cells, which is the one way to get the actual mark into an inbox intact.

Content parity is deliberate. Both directions carry everything the shipped template carries — order line, license panel, download CTA, the 30-day link note, the claim block, and the footer. Only the treatment differs. Each is shown in both variants, because they diverge on the thing that matters most: a guest has no key yet (it's minted on claim), so the license panel states the condition and the claim block carries the sign-in path rather than a dashboard link.

Direction A · warm letter

Warm ground (#FDFAF8, the same surface colour the dashboard uses — full cream read too intense at page scale). Letterhead carries the full logo — wordmark and mark, per Julia — not a mark plus "Tangible" set in a serif. Serif display at a size you'd actually call display. Indigo pill replaces the near-black one. Key in a cream-soft chip with a tobacco hairline.

Mechanism note. The full logo includes letterforms, and letterforms can't be built from table cells — so this is a hosted 3× transparent PNG drawn at 26px, passed as the shell's existing logoUrl (which no call site passes today). The trade-off against the table-cell mark it replaces: a blocked image degrades to styled alt text rather than to the real mark. Production must serve it from a durable host — R2 or api.tangible.one — not the design catalog it points at here.

A · account variant

Key present · claim block → dashboard

A · guest variant

No key yet · claim block → sign in with the purchase email

Direction B · ledger

Treats a receipt as a record, not a letter: ink masthead with the mark reversed out, cream body, label/value rows. Optimised for the person who comes back six months later hunting the key — which prose-first layouts bury. Note how the guest variant turns the License row into a condition rather than a blank.

B · account variant

Key present · claim block → dashboard

B · guest variant

No key yet · claim block → sign in with the purchase email

Shell v2 · shipped — rendered from the real code

All nine templates through the chosen direction — the 7/2 split gone. These are no longer a proposal: the shell now lives in convex/email/brandedShell.ts with its parts in convex/email/parts.ts, all seven stragglers are repointed, renderSimpleEmailHtml is retired, and these frames are dumped from that code. The earlier hand-built previews were generated by tools/design-email-previews.mjs from one shell() function, on purpose: hand-authoring ten pages is exactly how the shipped set drifted apart in the first place, and the artifact would stop being evidence of anything. That generator is also the reference for the real change in convex/email/brandedShell.ts — its signature deliberately matches renderBrandedEmailShell().

Copy is preserved, not rewritten. Every fact these carry is a fact the shipped template carries. Where wording changed it changed to fit the layout (facts moved into label/value rows), never to add a claim. Two judgement calls worth challenging: the bad-news emails (invite revoked, access removed) get no cheerful eyebrow and no pill CTA — inventing an action where the only real next step is a human felt worse than a dead end; and invite accepted goes to existing members, so its CTA is member management rather than anything the reader must do.

purchase confirmation · account

Key present · rows + key chip · claim → dashboard

purchase confirmation · guest

No key yet · dashed condition panel · claim → sign in

renewal reminder

Tobacco eyebrow — a deadline, not good news

invite sent

The first email a teammate ever gets · inviter message in a cream panel

invite reminder

Expiry is the point, so it is the only row

invite accepted

Goes to existing members · mint panel, no competing CTA

invite revoked

No pill CTA — the real next step is a person

member removed

Same restraint; never dress up losing access

role changed

Rows carry the account and the new role

config test · staff only

Branded anyway — its whole job is proving the pipe works

Still missing · language and coverage

Fixing the split surfaced a bigger gap. The platform emits 35 outbox codes; ten of them have an email. Several of the silent ones are the moments where money or access changes — exactly where silence is most expensive. This section is an inventory, not a design: nothing below is built.

Missing emails · money or access changes, customer hears nothing

Outbox codeWhat happenedWhy silence hurts
SUBSCRIPTION_PAST_DUEA payment failedBuilt — see below. Dormant until dunningSender is flipped.
SUBSCRIPTION_UNPAIDDunning exhaustedBuilt — the `final` severity of the same email.
LICENSE_KEY_EXPIREDA key stopped workingUpdates silently stop. First contact is a support ticket.
LICENSE_KEY_REVOKEDA key was pulledSame, but deliberate — so it needs an explanation, not a discovery.
SUBSCRIPTION_CANCEL_SCHEDULEDCancellation bookedNo confirmation of when access actually ends — the sunsetting cohort's exact confusion.
SUBSCRIPTION_TRIAL_WILL_ENDTrial endingEmits a CRM fact only. Nothing reaches the person about to be charged.
ORDER_REFUNDEDMoney returnedA refund with no email reads as a mistake or a fraud alert.
UPGRADE_APPLIEDPlan changed mid-termProration is the single most disputed line on any invoice; explain it once, cheaply.
ACTIVATION_ACTIVATEDA new site used the keyThe licensing equivalent of "new sign-in from a new device" — the only way a customer notices key sharing.
ACCOUNT_PRIMARY_RECIPIENT_CHANGEDBilling mail redirectedChanging who receives billing email is a security-relevant act; it should notify both addresses.
CATALOG_PRICE_CHANGEDA price movedAdvance notice before a subscription renews at a new price is a legal requirement in several markets.

Design language now present

  • Four tones — positive (olive), neutral (indigo), attention (tobacco), negative (danger #C22F32). The last one is new: email previously had no way to say "this went wrong", so revoked-access mail was styled like a receipt.
  • Four panel tones — next-step, keepsake, done, problem.
  • The no-CTA convention for news whose only real next step is a person.
  • Julia's danger/success ramps from tailwind.config.js, finally reachable from email.

Design language still absent

  • Escalation. Dunning is not one email but three, and day 1 must not look like the final notice. There is no severity ladder — only a single "negative".
  • Dark mode. Apple Mail and Outlook.com force-invert; a white card on warm can come out muddy. No prefers-color-scheme handling exists.
  • Narrow screens. The card is a fixed 600px with 44px padding — fine on desktop, tight at 320px. No mobile treatment.
  • Colour is the only tone signal. A colour-blind reader gets no difference between "attention" and "negative"; neither carries an icon or a word.
  • Semantics. Headings are <div>s, so screen readers get no document outline.
  • One-off surfaces with no shared language yet: an invoice/receipt table, a diff ("your plan changed from → to"), and a security notice.

The honest summary: the branding gap is now closed and the coverage gap is not. Of the two, coverage is worth more — a beautifully branded set of nine emails still leaves a customer whose payment just failed hearing nothing at all. Recommended order: payment failed → trial ending → licence expired/revoked → cancellation confirmed, then the rest.

New · payment failed (the first coverage gap closed)

The highest-value missing email, now built. Two severities, not one — and they come straight from the fact stream rather than an attempt counter: SUBSCRIPTION_PAST_DUE means Stripe will retry the card, SUBSCRIPTION_UNPAID means it has stopped. Day one must not look like the final notice, which is the escalation gap this page listed as missing.

Dormant by default. Gated on a new dunningSender setting that defaults to stripe, exactly like renewal reminders. Stripe's own dunning emails may already be switched on, and two "your payment failed" messages for one event reads as a system in disarray at precisely the moment the customer is deciding whether to re-enter a card. Confirm Stripe's are OFF before flipping it.

past_due · attention

Nothing lost yet · keepsake panel reassures · retry date when known

unpaid · negative

Access-ends date stated · problem panel · sites keep working

Two deliberate differences from the revoked-access emails. This one keeps its pill CTA in both severities — unlike bad news whose only next step is a person, a declined card is something the customer can fix alone, so withholding the button would be the unhelpful choice. And it never states a date it wasn't given: no amount is available (neither the fact nor the subscriptions row carries one) and no retry date exists for past_due, so those rows are omitted rather than guessed.

Copy: Julia's document, applied

Cross-referenced against Tangible.One Transactional Emails (Julia, 22 Aug 2026). Her document carries worked copy for the two purchase variants; the other seven were pasted current output rather than copy decisions, so those remain mine and are still unreviewed.

Findings & build notes

Painted 11:41 AM EST all tangible.one artifacts