---
title: "Cards"
description: "All cards: 24px radius, 24px inset, --surface on --canvas, 1px --border, no resting shadow. Specs for 11 React components: BalanceCard, KpiCard, AccountCard, PaymentMethodCard and 7 more."
canonical: "https://lopifi.com/docs/catalog/cards"
---

# Cards

> All cards: 24px radius, 24px inset, --surface on --canvas, 1px --border, no resting shadow. Specs for 11 React components: BalanceCard, KpiCard, AccountCard, PaymentMethodCard and 7 more.

Source: https://lopifi.com/docs/catalog/cards
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Cards

All cards: 24px radius, 24px inset, `--surface` on `--canvas`, 1px `--border`, **no resting shadow**.

---

## 14. `BalanceCard`

> The hero: one trusted number, and the reason a user opens the product.

**Layer 1:** radius 24 · inset 24 · min-width 280 · height auto

**Layer 2:**

```
┌─ 24 ─────────────────────────────────┐
│  [eyebrow]  ·  [account mark]        │  ← row, space-between
│   8                                  │
│  [figure]                            │
│   8                                  │
│  [delta]  ·  [as-of]                 │
│  16                                  │
├─ full-bleed sparkline ───────────────┤  ← optional, bleeds to 3 edges
└──────────────────────────────────────┘
```

**Layer 3:** eyebrow (1 line, truncate) · figure (never truncates) · delta (1 line) · media (`aspect-[4/1]`, full-bleed) · actions (max 2)

**Layer 4:**

| Slot | Token | Weight | Color |
|---|---|---|---|
| eyebrow | `label` 13/16 | 500 | `--text-secondary` |
| figure | `figure-hero` 40/48 | 500 | `--text-primary` |
| delta | `caption` 12/16 | 400 | semantic |

**3 sizes, 2 weights ✓**

**Available vs. current.** A bank balance has two values and they differ. Show **available** as the figure, and current as a secondary line when they diverge. Mixing them up is the costliest copy error in consumer banking UI.

**Polarity:** required, no default. `standard` for a cash or asset balance, `inverted` for a card or credit line, where up means more owed. (API change: it previously fell back to `standard`.)

**Motion:** none at rest. Value flash when live. Sparkline draws once. Interactive variant gets press `scale(0.97)` and a 1px hover lift.

**Responsive:** figure steps 40 → 28 below a 280px container. Sparkline drops below 240px.

**Edge cases:** negative balance (overdraft — parens *and* a `--negative` figure, plus an explicit "Overdrawn" pill) · redacted · stale (dim to 0.7 + `StaleIndicator`) · very long account name.

---

## 15. `KpiCard`

Label · figure · delta · optional sparkline. The dashboard atom.

- **Baselines align across a `KpiRow`.** Use `grid` with `auto-rows-fr` and a shared figure row, or two cards with different label lengths put their figures at different heights. It's the most visible alignment failure on a dashboard.
- **Display family forbidden** here. Figures align across the row.
- `polarity` is required, with no default, to force the decision.
- The comparison basis is always shown: `vs. Aug`, never a bare `+5.3%`.

**Loading:** skeleton with the figure block at the exact figure width, or the card jumps when data lands.

---

## 16. `AccountCard`
Bank account: institution mark, masked number, type, available and current balances, sync status. Tapping opens the transaction list filtered to it.

Shows **disconnected** and **reauth-required** states prominently, because a stale connection quietly showing an old balance is a real financial hazard. Stale accounts dim and offer a "Reconnect" action.

---

## 17. `PaymentMethodCard`

> The physical object. It gets two of the four sanctioned delight moments: the flip and the tilt.

**Geometry, from ISO/IEC 7810 ID-1:**
```
aspect-ratio: 1.586          (85.60 × 53.98 mm)
border-radius: 3.7% / 5.9%     (3.18mm — 3.7% of width, 5.9% of height)
```
Real proportions matter. A 16:9 card with a 24px radius reads as a rectangle, not a card.

**Networks:** Visa, Mastercard, American Express, Discover, JCB, UnionPay and Diners Club. The
mark is [`CardNetworkMark`](https://lopifi.com/md/docs/catalog/primitives#180-cardnetworkmark) unless a licensed one is passed
as `networkLogo`.

**Face:** flat, a single tint, network mark top-right opposite the chip, masked PAN in `tnum` with `ss02` (so a `0` isn't an `O`), holder name in `label` uppercase, expiry `MM/YY`. Each network has a dark default tint close to the color a reader expects of it, and `tint` overrides it with the issuer's. Visa, Amex and Diners Club print their mark in the face's ink; the others print in color. The masked number follows the network's grouping: `•••• •••••• •0005` for Amex, `•••• •••••• 1667` for a 14-digit Diners Club.

**Amex prints its four-digit code on the front**, above the number on the right. An Amex card reveals the code in place and doesn't turn over, because flipping to an empty reverse teaches the holder to look in the wrong place. The other six flip to a three-digit code on the back.

**No code, no control.** A card on file keeps no security code after authorization. Without `cvv` the reveal button isn't rendered, since a flip to `•••` reveals nothing.

**Motion:**
- **Flip to reveal CVV** — `rotateY(0→180deg)`, `spring.playful`, real 3D with `preserve-3d` + `backface-visibility: hidden`. Not a crossfade. [Spec](https://lopifi.com/md/docs/motion/catalog#card-flip).
- **Pointer tilt** — ±6° from pointer position via `spring.track`, plus a specular sheen at `opacity 0→0.12`. Desktop only, gated behind `@media (hover: hover) and (pointer: fine)`. Springs home on leave.
- Reduced motion: instant swap, no tilt.

The tilt is allowed **only because the card is decorative**. The same effect on a `KpiCard` fails review.

**Security:** CVV and full PAN are never in the DOM until revealed, never in analytics, never in a screenshot-able state without an explicit user action.

---

## 18. `RunwayCard`
Months of runway, current cash, monthly burn, projected zero date. A burn line with a projection segment rendered **dashed**, because a projection is not a fact.

`polarity="inverted"` on burn. Runway under 6 months triggers `--warning`; under 3, `--negative`. The thresholds are configurable because they're a business judgment.

## 19. `InvoiceCard`
Number, customer, amount, due date, aging state (current / 1-30 / 31-60 / 61-90 / 90+). Overdue is `--negative` **with the day count**, never color alone. Partial payments show paid vs. outstanding. A paid invoice — `paidInFull`, or derived from `paid` ≥ `total` (or nothing outstanding) in the invoice's currency — shows "Settled" and is **never overdue**, however far past its due date.

## 20. `SubscriptionCard`
Vendor mark, amount, cadence, next charge, annualized cost. **Annualized is the useful number.** `$49/mo` is `$588/yr`, and that's the figure a spend review acts on.

## 21. `AnomalyCard`
A flagged deviation, with the comparison that triggered it and one clear action (approve / flag / create rule). Never a bare alert. An anomaly without its baseline is noise.

**API change:** `polarity` is now required, no default (it previously defaulted to `inverted`). Spend anomalies pass `inverted`; a revenue or cash anomaly passes `standard`.

## 22. `GoalCard`
Target vs. actual with a progress rule (a rule line, not a rounded progress bar, to match the statement grammar).

## 23. `StatCard`
The minimal unit: label + value. 16px inset, no delta, no media. For dense grids where a `KpiCard` is too much.

---

## 181. `CardStatement`

> The issuer's statement summary: what's owed, by when, and how the figure was reached.

```tsx
<CardStatement
  issuer="Fernhill Bank Business Card"
  network="visa"
  last4="4242"
  period={{ start: new Date(2026, 8, 1), end: new Date(2026, 8, 30) }}
  previousBalance={money(384_210)}
  paymentsAndCredits={money(-50_000)}
  purchases={money(162_480)}
  fees={money(0)}
  interest={money(6_812)}
  newBalance={money(503_502)}
  minimumPayment={money(15_100)}
  dueDate={new Date(2026, 9, 25)}
  creditLimit={money(1_000_000)}
  availableCredit={money(496_498)}
  aprs={[{ label: "Purchases", rate: 24.99 }]}
  lateFee={money(4_100)}
  penaltyApr={29.99}
/>
```

**Layer 2:**
```
┌──────────────────────────────────────────────┐
│ [mark]  Issuer  •••• 4242          [Overdue] │  header: issuer, period, currency
├──────────────────────────────────────────────┤
│ New balance   Minimum payment   Due date     │  the three figures a holder acts on
│ $5,035.02     $151.00           25 Oct 2026  │
│ ↑ +$1,192.92 vs previous statement           │
├──────────────────────────────────────────────┤
│ Previous balance                  $3,842.10  │
│ Payments and credits               ($500.00) │
│ Purchases                         $1,624.80  │
│ Fees charged                              —  │
│ Interest charged                     $68.12  │
│                                  ──────────  │
│ New balance                       $5,035.02  │
│                                  ══════════  │
├──────────────────────────────────────────────┤
│ Credit limit · Available credit · APRs       │
├──────────────────────────────────────────────┤
│ Late payment warning.                        │
└──────────────────────────────────────────────┘
```

**The walk ties or says so.** Previous balance, payments and credits, purchases, fees and
interest are footed in minor units and compared with the new balance the issuer printed. A
non-zero difference adds a Difference row, turns the border `--negative` and states the amount.
A holder paying from a figure that doesn't add up should find out before paying.

**Movements are signed, once.** A payment reduces what's owed, so it's passed negative and
renders `($500.00)` with the paren column reserved. There's no "−" beside the label, which would
make it a double negative. The component never negates.

**Polarity is inverted.** The change against the previous statement is a `Delta` with
`polarity="inverted"`: a balance that rose is more owed. A credit balance (negative new balance)
is labelled "Credit balance", not printed as a negative debt.

**States:**
- **Past due:** `pastDue` present and non-zero. An `Overdue` pill in the header, the due date in
  `--negative`, a line saying the past-due amount is included in the minimum, and the late-payment
  warning in `--negative` instead of the routine secondary text.
- **Over limit:** derived from new balance vs. credit limit, with the amount over stated.
  `availableCredit` is the issuer's figure, never recomputed, and `null` renders n/a.
- **Paid in full:** derived when payments and credits cleared the balance brought forward. It's a
  fact the figures prove, so it's stated in one secondary line.

**Late payment warning** prints only when `lateFee` is given, built from the props (due date,
late fee, penalty APR) and never from invented terms. APRs print at two decimals.

**Every figure states its basis:** issuer, masked account, statement period and currency in the
header; the due date is a date, not "in 12 days".

**3 sizes, 2 weights:** caption (meta and labels), body / `figure-sm` (the walk), `figure-md`
(the three figures acted on).

**Motion:** none. A statement is a document.

