OctoWiki
Memory Appendix

Email Design System

Conventions, palette tokens, state-to-color mappings, and email-client constraints for transactional emails (vouchers, topups, eSIMs)

Source memory file: feedback_email_design_system.md · Category: Feedback / working-preference This is a verbatim dump of Claude's persistent memory for the Octopus project. Rendered inside a code block so nothing is altered.

---
name: Email Design System
description: Conventions, palette tokens, state-to-color mappings, and email-client constraints for transactional emails (vouchers, topups, eSIMs)
type: feedback
originSessionId: 9948a69b-8c8f-4bf7-b0f7-01d081ed8b47
---
# Email Design System

Transactional emails live in `views/email/` (Jet templates) with a preview folder at `views/email/preview/` (static HTML for design review, never sent). When extending or wiring templates, follow the conventions below.

---

## Palette tokens (locked to grasshopper brand.css)

Source of truth: `frontend/grasshopper/public/css/brand.css`.

| Token | Light | Dark | Used for |
|---|---|---|---|
| body bg | `#F2F3F6` | `#13141A` | Page background |
| `card-bg` | `#FFFFFF` | `#22252F` | The email container card (level 1) |
| `card-elevated` | `#FFFFFF` | `#2C3040` | Nested cards inside the container — recharge ticket, eSIM ticket, attachment callout (level 2) |
| `surface-bg` | `#F8F9FB` | `#1E2027` | Inset detail panels inside an elevated card — status rows, plan chips (level 3 / recessed) |
| `border-c` | `#C9CDD8` | `#2C3040` | Card borders |
| `divider-c` | `#EDEEF0` | `#2C3040` | Internal row dividers |
| `text-ink` | `#1A1C23` | `#E0E2E8` | Primary content |
| `text-slate` | `#3C4152` | `#C9CDD8` | Body copy |
| `text-mist` | `#7B8499` | `#9BA3B5` | Eyebrow labels, helper text, footer links |

**Surface levels are not just visual sugar — they encode hierarchy.** Container > elevated nested cards > inset detail panels. In dark mode the levels reverse so the inset is the *darkest* (recessed feel); pick the right one when adding a new section.

Brand colors used unchanged across light/dark: `#FF8426` (orange CTA), `#3BA8D9` (blue links + eSIM accent), `#12A870` / `#34D399` (success), `#E03030` / `#F87171` (failure), `#E8960F` / `#FCD34D` (warning), `#7B8499` (cancellation neutral).

---

## State → visual mapping

Each terminal state has a fixed visual treatment so users can recognize it at a glance.

| State | Pill (header right) | Card top gradient | Icon | Notice block | Tone |
|---|---|---|---|---|---|
| Delivered/Success | green text on `rgba(18,168,112,0.18)` | `#059669``#34D399` (green) | `✓` | green or none | celebratory |
| Failed | red `#F87171` on `rgba(224,48,48,0.18)` | `#DC2626``#F87171` (red) | `×` | `notice-red` (`#FEF2F2` / `#2A0808`) | apologetic |
| Cancelled | neutral `#C9CDD8` on `rgba(155,163,181,0.18)` | `#454D5F``#7B8499` (slate) | `⊘` (⊘) | `notice-slate` (`#F8F9FB` / `#1E2027`) | informational |
| Partially Delivered | amber `#FCD34D` on `rgba(232,150,15,0.18)` | slate header (most worked) | mixed | `notice-amber` (`#FFFBEB` / `#2A1F00`) | warning, not error |
| eSIM (any state) | blue accent | `#006090``#3BA8D9` (blue) | `✆` (sim card) | reuses state-appropriate | product-specific |

Rationale: failed and cancelled both end in "no delivery + refund," but the *tone* differs. Failed = "we tried and couldn't" (apologize, suggest retry/contact). Cancelled = "the order was cancelled" (neutral, no apology). Partial uses amber not red because most of the order succeeded — red would over-alarm.

---

## Voucher delivery permutations (6 cases)

Vouchers vary by which fields the vendor returns. Each permutation has a distinct UI:

| Code | PIN | Claim URL | Layout |
|---|---|---|---|
| ✓ | ✗ | ✗ | Big code block, no CTA |
| ✓ | ✓ | ✗ | Code block + PIN/expiry pills side-by-side |
| ✗ | ✓ | ✓ | Big spaced PIN + "Open Claim Page" CTA + URL fallback |
| ✗ | ✗ | ✓ | Hero CTA card (dark slate, orange button) + URL fallback |
| ✗ | ✓ | ✗ | Spaced-out PIN, no CTA |
| ✓ | ✓ | ✓ | "Recommended" CTA → divider "or use manually" → code + PIN |

**Rule:** the orange "Redeem" CTA appears *only when* a claim URL exists. URL fallback is shown as monospace text under the CTA in case email clients break buttons.

Single voucher orders (qty=1) inline these details. Bulk orders (qty up to ~1000) use a flat XLSX attachment + an "X codes attached" callout — never inline the codes.

---

## Content rules — what stays out

- **No vendor names anywhere.** Only product info that the client UI shows. The recharge card eyebrow shows `CountryCode` ("UAE"), not the operator ("Du UAE"). The voucher card shows the product ("Amazon US Gift Card"), not the issuer (Runa, TRS).
- **No personal name greeting.** No name field exists on Order/Recharge/EsimOrder, so emails open with "Your voucher is ready" not "Hi Vinay".
- **Order ref always shown** in the support line — `Email support@octopuscards.io with order ref ORD-XXX` — in mono, ink-strong color. Same convention in every terminal-state email.
- **Mobile numbers always masked** (`+971 50 *** 4287`) in the recipient pill.

---

## Email-client constraints (why the markup looks the way it does)

| Pattern | Reason |
|---|---|
| Table-based layout, no flexbox/grid | Outlook desktop and many corporate clients use Word's rendering engine — modern CSS layout breaks |
| Inline styles for everything visible by default | Gmail strips `<style>` blocks in some forwarding scenarios; inline survives |
| `<style>` block only for `@media` queries (responsive + dark mode) and accordion CSS | These can't be expressed inline |
| 600px container, breaks to 100% under 600px via `@media` | Standard email width; mobile clients squeeze to viewport |
| `<details>`/`<summary>` accordions | Apple Mail / iOS / Gmail web/iOS / Yahoo render as collapsible. Outlook desktop ignores `<summary>` and shows everything expanded — fine fallback |
| **No tabs.** | Tabs need JS (stripped) or CSS `:checked`/`:target` hacks (broken in Outlook entirely). Don't try. |
| Octopus logo as **PNG** at `https://octopuscards.io/octopus-cards-full.png` (600×206, 41KB) | The SVGs in the repo are 700KB raster-traced; Gmail clips emails over 102KB and Outlook strips SVG `<img>` |
| QR code container forced `#FFFFFF` even in dark mode | Inverted/dark QR backgrounds fail to scan |
| Excel attachments handled by mailer service, not inline | The email just shows a callout pointing to the file |

---

## Dark mode triggers

Real email clients use `@media (prefers-color-scheme: dark)` (Apple Mail, iOS Mail, Outlook iOS) and `[data-ogsc]` attribute (Outlook.com). Both are wired in every preview.

Gmail webmail and Outlook desktop do **not** support `prefers-color-scheme` — Gmail does its own auto-inversion (we accept), Outlook desktop stays in light mode.

**Preview-only toggle:** the floating top-right button uses `html.force-dark` / `html.force-light` classes that override the `@media` rules via `html:not(.force-light)` qualifier. This is preview scaffolding (button + JS + class-based overrides). When wiring to Jet, strip:
- `<button class="theme-toggle">` and the `<script>` block
- `html.force-dark` / `html.force-light` selectors
- `html:not(.force-light)` qualifier on the `@media` rules — make them plain again

Keep the `@media` and `[data-ogsc]` rules as-is.

---

## File coverage matrix (preview = production target)

```
voucher.html              — single voucher delivered (DELIVERED, qty=1)
voucher_bulk.html         — bulk voucher delivered with XLSX (DELIVERED, qty>1)
voucher_failed.html       — voucher order FAILED (refund)
voucher_cancelled.html    — voucher order CANCELLED (refund)
voucher_partial.html      — voucher order PARTIALLY_DELIVERED (some delivered, some refunded)

topup.html                — recharge DELIVERED/RECHARGED
topup_failed.html         — recharge FAILED — both fixable + non-fixable variants stacked
topup_cancelled.html      — recharge CANCELLED

esim.html                 — eSIM DELIVERED with QR + LPA + ICCID
esim_failed.html          — eSIM order FAILED (refund)
esim_cancelled.html       — eSIM order CANCELLED (refund)

voucher_permutations.html — design ref showing all 6 voucher field permutations stacked
```

Production templates will be `.html.jet` + `.txt.jet` pairs in `views/email/`. Existing `order_delivery.html.jet` and `esim_delivery.html.jet` predate this design; replacing them is part of the wiring task.

---

## Why: a few non-obvious choices

- **Wordmark text first, then PNG logo** — text wordmark using brand colors is bulletproof (no broken-image risk in any client). When the user wanted the actual logo we converted SVG→PNG via `rsvg-convert -w 600`.
- **3-tier surface in topup/esim, 2-tier in voucher** — voucher has no nested cards (just product card sitting in container), so it doesn't need `card-elevated`. Don't introduce levels you don't use.
- **Voucher email "How to use" is open by default**, Details and Terms collapsed — most users want to know how to redeem first.
- **Topup failed has TWO variants in one preview file**`FailureIsUserFixable` boolean drives different UX (retry CTA vs. refund-and-done). The Jet template will have a conditional; the preview shows both stacked for review.
- **Partial uses amber, not red** — red implies "everything broke." Amber says "most worked, here's what didn't."