Resolved: light

Cards & surfaces

The elevated-surface family — bordered containers that group related content and lift it off the page canvas. Card is the foundation (the portal's one canonical surface); CompactCard, ActionCard, LockableToolCard and LockableToolRow are higher-level compositions built on top of it. Every surface draws from the theme-aware ramp (bg-surface over bg-background, with bg-surface-2 / -3 for nested layers) plus a hairline border and the --fx-elev-card shadow — so flip the theme in the top bar to confirm they re-skin. NB: in DARK the elevation shadow resolves to `none`; depth comes from the surface ramp + border instead, which is why every demo here is framed on the darker bg-background canvas.

<Card>
The base surface: rounded-lg, border, bg-surface, --fx-elev-card shadow. Props: padding (none/sm/md/lg), interactive (hover border), accentBorder (indigo/green/amber/red left bar).
<CompactCard>
A padding="none" Card with an optional SectionHeader band and a divide-y body — the portal's grouped-list / "Go to" pattern. Fill it with ListRow / CompactListItem children.
<ActionCard>
A single focused primary-action card: accent IconChip + label + short desc + either a whole-card href (chevron) or an explicit cta node. For 1–2 hero actions, not dense lists.
<LockableToolCard>
A tool/destination card that is NEVER hidden when gated: unlocked → primary CTA to href; locked → disabled CTA, lock reason, optional Learn More. Carries a status pill + notification count.
<LockableToolRow>
The dense ROW form of LockableToolCard, for grouped tool lists inside a CompactCard. Unlocked → whole-row link with chevron; locked → static muted row with an inline lock reason.
surface ramp
bg-background (canvas) → bg-surface (the card) → bg-surface-2 / bg-surface-3 (nested chips, list-row icons). Elevation = ramp step + hairline border + --fx-elev-card (none in dark).
tsx
import { Card } from "@/components/ui";

<Card padding="md" accentBorder="indigo" interactive>
  <h3 className="font-medium">Card title</h3>
  <p className="text-sm text-muted">Grouped content, lifted off the canvas.</p>
</Card>

Card — padding scale

The four padding keys, none · sm · md · lg, mapping to '' · p-4 · p-5 · p-6. md is the default. Padding only changes the internal inset — the border, radius (rounded-lg) and --fx-elev-card shadow are identical across all four. Each card sits on the bg-background canvas so its edge and shadow read.

Card body — padding="none"
padding="none"
Card body — padding="sm"
padding="sm"
Card body — padding="md"
padding="md"
Card body — padding="lg"
padding="lg"
padding="none"
padding="none" — no inset (""). For cards that own their own internal layout: a CompactCard divide-y body, a media tile, a table-in-a-card.
padding="sm"
padding="sm" — p-4 (16px). Dense cards, sidebars, secondary surfaces.
padding="md"
padding="md" — p-5 (20px). The DEFAULT. The everyday content card.
padding="lg"
padding="lg" — p-6 (24px). Roomy hero / feature cards that need breathing space.

Card — interactive

The interactive boolean adds transition-colors hover:border-[var(--color-border-strong)] — the border darkens on hover to signal the whole card is clickable (typically wrapped in a next/link). It changes nothing else; pair it with a real link/role for keyboard + AT affordance. Hover the right-hand card to see the border respond.

Static card

Border stays put on hover — informational, not clickable.

interactive={false} (default) — static surface
Interactive card

Hover me — the border steps to border-strong.

interactive — border darkens on hover
interactive
boolean (default false). Adds transition-colors + hover:border-[var(--color-border-strong)]. Purely visual — wrap the Card in a next/link (or add role/tabIndex) for the actual affordance.
no shadow lift
Interactive deliberately does NOT raise the shadow (the dark theme has no card shadow at all); the border step is the single, theme-safe hover cue.

Card — accentBorder tones

An optional 2px colored left bar (accentBorder) tints the card to a semantic tone: indigo (brand) · green (good) · amber (warn) · red (bad). It's drawn as a left border so it survives any padding/overflow and re-skins with the theme. Use it sparingly to flag a card's state — and never lean on the bar alone: each demo also names its state in words.

Featured

Left bar tinted indigo — state named in text, not color alone.

Featured
accentBorder="indigo"
Complete

Left bar tinted green — state named in text, not color alone.

Complete
accentBorder="green"
Action needed

Left bar tinted amber — state named in text, not color alone.

Action needed
accentBorder="amber"
Blocked

Left bar tinted red — state named in text, not color alone.

Blocked
accentBorder="red"
accentBorder="indigo"
accentBorder="indigo" — border-l-2 border-l-accent (brand beryl). Default emphasis / “this is the one”.
accentBorder="green"
accentBorder="green" — border-l-2 border-l-[var(--color-good)]. Success / complete / healthy.
accentBorder="amber"
accentBorder="amber" — border-l-2 border-l-[var(--color-warn)]. Warning / pending / needs attention.
accentBorder="red"
accentBorder="red" — border-l-2 border-l-[var(--color-bad)]. Error / blocked / urgent.

CompactCard — header + list body

The grouped-list surface: a padding="none" Card wrapping an optional header band (a SectionHeader when you pass a string, or any node) over a divide-y body. The card supplies the header rule and the row dividers — fill the body with ListRow / CompactListItem children, which carry their own px-4 inset. This is the portal's canonical "Go to" / navigation block.

header="Go to" (string → SectionHeader) + headerHint

Your tasks

View all
Confirm your address
Unlocks your local reps
To do
Complete your profile
Helps us match you to a team
Done
header as a custom node (SectionHeader with icon)
headerless (header omitted) — just a divide-y body
headerDensity="compact" — tighter header band (py-2)
header
ReactNode. A string renders a standard SectionHeader; pass a node for a custom header (e.g. SectionHeader with an icon/hint). Omit for a headerless card.
headerHint
ReactNode. Right-aligned hint/action, used only when header is a string (forwarded to SectionHeader's hint).
headerDensity
default | compact (default "default"). Header band vertical padding: default = px-4 py-3, compact = px-4 py-2. Affects only the header, not the body rows.
children
The divide-y body rows — typically ListRow / CompactListItem. Each child is its own separated row; the dividers + header rule are supplied by the card (--color-rule).
surface
Renders Card padding="none" with overflow-hidden so the rows clip to the rounded corners. Same bg-surface + border + --fx-elev-card as a plain Card.

ActionCard — focused primary action

A single, focused call-to-action card: an accent IconChip, a label headline, a short supporting desc, and one action. Either the whole card links (href — the card becomes interactive and shows a trailing arrow) OR it carries an explicit cta node at the bottom (a Button); the two are mutually exclusive, and href wins. Reserve it for the one or two hero actions on a surface — use ListRow for dense lists.

href — whole-card link (interactive, arrow affordance)
Verify your account

Confirm your email to unlock the full portal.

cta — explicit Button action at the bottom
icon
LucideIcon (required). Rendered as an h-5 w-5 glyph inside an h-9 w-9 bg-accent-soft / text-accent chip at the card's top-left.
label
ReactNode (required). The headline — text-sm font-semibold, the card's primary line.
desc
ReactNode (optional). A short supporting line under the label — text-xs text-muted.
href
string (optional). Makes the WHOLE card a next/link (interactive Card + group-hover arrow). Mutually exclusive with cta — href wins when both are set.
cta
ReactNode (optional). An explicit action node (a Button, or a Link-wrapped Button) pinned at the bottom. Used only when href is NOT set, for actions that aren't plain whole-card navigation.

LockableToolCard — gated tool (card)

A tool / destination card for an organizer tools grid that is NEVER hidden when gated. Unlocked: an indigo IconChip, an optional status pill + notification count, a description, and a primary CTA linking to href. Locked: the chip + CTA go disabled/gray, a Lock reason line explains why, and an optional Learn More link points at how to unlock — so the user always sees the capability exists and how to earn it. Composes IconChip + StatusPill + NotificationBadge.

Active

Send a broadcast

Message everyone on your team at once.

unlocked — status pill, count, primary CTA → href

Member directory

See contact details for everyone on your team.

Available once you're confirmed as a team lead.

locked — disabled CTA, lock reason, Learn More
icon
LucideIcon (required). Rendered inside an IconChip — tone indigo when unlocked, gray + lock overlay when locked.
title / desc
ReactNode. Card title (text-sm font-semibold) and a short supporting description (text-xs text-muted).
href / ctaLabel
Unlocked CTA: a primary Button-styled next/link to href (defaults to "#"). ctaLabel defaults to "Open".
status
{ label, tone?, dot?, pulse? } (optional). Renders a small StatusPill in the header. tone/dot/pulse forward to StatusPill.
count
number (default 0). Renders a NotificationBadge beside the title when > 0.
locked
boolean (default false). Gray IconChip + lock overlay, a disabled secondary Button as the CTA, and (with lockReason) a Lock-icon reason line. The card aria-disabled, never hidden.
lockReason / learnMoreHref
Shown only when locked: lockReason is the why (one short line), learnMoreHref renders a small accent “Learn More” link for how to unlock.

LockableToolRow — gated tool (row)

The dense ROW form of LockableToolCard, built to live inside a CompactCard's divide-y body for a grouped tools list. Unlocked, the whole row is a next/link (sm IconChip + title + optional count + one-line subtitle + chevron, hover background). Locked, it becomes a static, muted, non-navigable row (role=link + aria-disabled): no chevron, no count, with the lock surfaced as an inline reason that replaces the subtitle and is mirrored to the title tooltip + an sr-only “Locked” cue.

Organizer tools

2 available
Send a broadcast
Message your whole team at once
Schedule an event
Create a march, meeting or canvass
Member directoryLocked: Unlocks when you're a confirmed team lead
Unlocks when you're a confirmed team lead
Export rosterLocked: Requires the data-access agreement
Requires the data-access agreement
a CompactCard of tool rows — unlocked & locked together
unlocked row — whole-row link, count badge, chevron
Member directoryLocked: Unlocks when you're a confirmed team lead
Unlocks when you're a confirmed team lead
locked row — static, muted, inline lock reason, no chevron
icon
LucideIcon (required). Rendered in a size="sm" IconChip — indigo when unlocked, gray + lock overlay when locked.
title / subtitle
ReactNode. Primary line (text-sm font-medium, truncated) and an optional ONE-LINE subtitle (text-xs muted) — not a paragraph. When locked, lockReason replaces the subtitle.
href
string. Unlocked + href → the whole row is a next/link with hover:bg-surface-2 and a trailing chevron. Omit (or lock) and the row is a static element.
count
number (default 0). NotificationBadge by the title when > 0 — but only on an unlocked, navigable row (a locked row never advertises a count).
locked
boolean (default false). Renders a static, muted, role=link aria-disabled row: no link, no chevron, no count, gray lock-overlay chip.
lockReason
ReactNode, shown only when locked. Replaces the subtitle as an inline Lock-icon line, is mirrored to the row's title tooltip when it's a plain string, and feeds an sr-only “Locked: …” cue for AT.

Elevation & the surface ramp

How depth is built — and why it differs by theme. All of these cards use a single elevation recipe: a step up the surface ramp, a hairline border, and the --fx-elev-card shadow. The shadow only exists in LIGHT; in DARK it resolves to `none`, so depth comes entirely from the ramp + border. That's the whole reason every demo above is framed on the bg-background canvas — on a card-colored tile the separation would vanish in dark.

bg-background (--color-bg, the page canvas)
bg-surface (--color-surface, the Card)
bg-surface-2 (--color-surface-2, nested)
bg-surface-3 (--color-surface-3, deepest)
the ramp — background → surface → surface-2 → surface-3
rounded-lg
Card radius — --radius-lg = 8px (= --radius-card). The standard corner for every surface in this section.
border-[--color-border]
The hairline edge every Card carries (--color-border). interactive steps it to --color-border-strong on hover; the accentBorder bar overrides only the left edge.
shadow --fx-elev-card
LIGHT: `0 1px 2px …, 0 2px 8px -2px …` — a soft single-level card lift. DARK: `none` — no shadow; depth = ramp + border only.
--fx-elev-overlay / -modal
The higher elevation tiers (popovers, modals). Cards never use these — a Card is the ground-floor surface.
surface ramp
background (canvas) → surface (card) → surface-2 → surface-3 (progressively nested fills, e.g. ListRow's icon chip is bg-surface-2). Each step is one shade apart in both themes.