Chapter 22 · Operator · Plans & billing

Billing. Money in, value out.

The operator-side surface for plans, usage, invoices, and payment methods. Six components compose the kit’s “everything billing” workflow: plan tier cards, usage meters, invoice rows, payment method displays, the billing history table, and a drop-in page wrapper.

22.1 Plan card

A plan-tier card composed of name + price/period + description + feature list + CTA. Three intensities cover the common 3-tier pricing layout: intent-muted for the low-end tier, intent-accent for the highlighted recommended tier (accent border + tinted background), and intent-ink for the high-end enterprise tier (ink background + paper text). An optional pill in the top-right corner marks the recommended or current plan. Below 480px the grid stacks; the card itself just flexes to container width.

Three tiers — Free, Pro, Enterprise

.plan-card

Three side-by-side cards demonstrating the canonical pricing layout. The Pro card carries intent-accent and a “Recommended” pill; the Enterprise card uses intent-ink for the high-end inversion. Feature bullets render with a small var(--success)-tinted check glyph; the ink variant flips the check to var(--paper) for contrast. CTA pins to the bottom of each card so unequal feature counts don’t misalign the action row.

Free
$0
billed monthly

Everything you need to evaluate MagicBlocks on a small team.

  • Up to 3 seats
  • 1,000 records
  • Community support
  • Standard integrations
Recommended
Pro
$29
per seat / month

Everything in Free, plus the power-user surface area most teams need.

  • Unlimited seats
  • Unlimited records
  • Priority support
  • Custom domains
  • SSO & SCIM
  • Audit log access
Enterprise
Custom
billed annually

For organisations that need procurement, compliance, and dedicated support.

  • Everything in Pro
  • Dedicated success manager
  • SOC 2 + HIPAA
  • 99.99% SLA
  • Custom contracts
<div class="plan-card-grid">
  <article class="plan-card intent-muted">
    <div class="plan-card-head">
      <div class="plan-card-name">Free</div>
      <div class="plan-card-price">$0</div>
      <div class="plan-card-period">billed monthly</div>
    </div>
    <p class="plan-card-desc">Everything you need…</p>
    <ul class="plan-card-features">
      <li><svg class="plan-card-feature-check">…</svg><span>Up to 3 seats</span></li>
      …
    </ul>
    <div class="plan-card-cta">
      <button type="button" class="btn btn-ghost">Get started</button>
    </div>
  </article>
  <article class="plan-card intent-accent">
    <span class="plan-card-pill">Recommended</span>
    …
  </article>
  <article class="plan-card intent-ink">…</article>
</div>
.plan-card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s-4);
  align-items: stretch;
}

.plan-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  padding: var(--s-5);
  border-radius: var(--r-md);
  border: 1px solid var(--hair);
  background: var(--bg-paper);
  min-height: 360px;
}

.plan-card.intent-muted {
  background: var(--bg-paper);
  border-color: var(--hair);
}

.plan-card.intent-accent {
  background: color-mix(in oklab, var(--accent) 6%, var(--bg-paper));
  border-color: var(--accent);
  border-width: 1.5px;
}

.plan-card.intent-ink {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}

.plan-card-pill {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--paper);
  font: 600 10.5px/1 var(--f-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.plan-card-head {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.plan-card-name {
  font: 600 16px/1.25 var(--f-display);
  color: var(--fg);
}

.plan-card-price {
  font: 700 32px/1.05 var(--f-display);
  color: var(--fg);
  font-variant-numeric: tabular-nums;
}

.plan-card-period {
  font: 500 12px/1.3 var(--f-mono);
  color: var(--fg-soft);
}

.plan-card-desc {
  margin: 0;
  font: 400 14px/1.5 var(--f-body);
  color: var(--fg-soft);
}

.plan-card-features {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.plan-card-features li {
  display: flex;
  align-items: flex-start;
  gap: var(--s-2);
  font: 400 14px/1.45 var(--f-body);
  color: var(--fg);
}

.plan-card-feature-check {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 2px;
  color: var(--success);
}

.plan-card-cta {
  margin-top: auto;
  display: flex;
}

.plan-card-cta > * {
  flex: 1;
}

.plan-card.intent-ink .plan-card-name,
.plan-card.intent-ink .plan-card-price {
  color: var(--paper);
}

.plan-card.intent-ink .plan-card-period,
.plan-card.intent-ink .plan-card-desc {
  color: color-mix(in oklab, var(--paper) 70%, transparent);
}

.plan-card.intent-ink .plan-card-features li {
  color: color-mix(in oklab, var(--paper) 90%, transparent);
}

/* …additional rules trimmed for brevity — see _shared.css */
import { PlanCard } from "@magicblocksai/ui";

<div className="plan-card-grid">
  <PlanCard
    name="Free"
    price="$0"
    period="billed monthly"
    description="Everything you need to evaluate MagicBlocks on a small team."
    features={[
      "Up to 3 seats",
      "1,000 records",
      "Community support",
      "Standard integrations",
    ]}
    cta={<button type="button" className="btn btn-ghost">Get started</button>}
  />
  <PlanCard
    name="Pro"
    price="$29"
    period="per seat / month"
    description="Everything in Free, plus the power-user surface area most teams need."
    features={[
      "Unlimited seats",
      "Unlimited records",
      "Priority support",
      "Custom domains",
      "SSO & SCIM",
      "Audit log access",
    ]}
    cta={<button type="button" className="btn btn-primary">Start free trial</button>}
    intent="accent"
    pill="Recommended"
  />
  <PlanCard
    name="Enterprise"
    price="Custom"
    period="billed annually"
    description="For organisations that need procurement, compliance, and dedicated support."
    features={[
      "Everything in Pro",
      "Dedicated success manager",
      "SOC 2 + HIPAA",
      "99.99% SLA",
      "Custom contracts",
    ]}
    cta={<button type="button" className="btn btn-ink">Contact sales</button>}
    intent="ink"
  />
</div>

22.2 Usage meter

A labeled progress bar with current/limit numbers, an overage indicator, and an optional projection line. Pure presentational — the visual state derives from current / limit: at or above 100% renders .is-over with an var(--error) tint, at or above 80% renders .is-approaching with a var(--warning) tint, and below 80% uses the default var(--accent) fill. The bar fill animates from 0 to its target width on mount via transition: width var(--dur-3); prefers-reduced-motion: reduce suppresses the transition.

Three meters — healthy, approaching, over

.usage-meter

Three meters stacked in a .usage-meter-stack wrapper. API calls sits at ~31% of the limit (healthy, default accent fill). Storage sits at 88% (approaching, warning tint). Seats sits at 130% (over, error tint with an .usage-meter-overage label reporting the delta).

API calls this month 31,247 / 100,000 calls
Storage 8.8 / 10.0 GB
Seats 26 / 20 seats
Over by 6 seats
<div class="usage-meter-stack">
  <div class="usage-meter">
    <div class="usage-meter-head">
      <span class="usage-meter-label">API calls this month</span>
      <span class="usage-meter-numbers">31,247 / 100,000 calls</span>
    </div>
    <div class="usage-meter-bar">
      <span class="usage-meter-fill" style="width:31.247%"></span>
    </div>
  </div>
  <div class="usage-meter is-approaching">
    <div class="usage-meter-head">
      <span class="usage-meter-label">Storage</span>
      <span class="usage-meter-numbers">8.8 / 10.0 GB</span>
    </div>
    <div class="usage-meter-bar">
      <span class="usage-meter-fill" style="width:88%"></span>
    </div>
  </div>
  <div class="usage-meter is-over">
    <div class="usage-meter-head">
      <span class="usage-meter-label">Seats</span>
      <span class="usage-meter-numbers">26 / 20 seats</span>
    </div>
    <div class="usage-meter-bar">
      <span class="usage-meter-fill" style="width:100%"></span>
    </div>
    <span class="usage-meter-overage">
      <svg class="usage-meter-overage-icon">…</svg>Over by 6 seats
    </span>
  </div>
</div>
.usage-meter-stack {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  max-width: 480px;
}

.usage-meter {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}

.usage-meter-head {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
}

.usage-meter-label {
  font: 500 14px/1.3 var(--f-body);
  color: var(--fg);
}

.usage-meter-numbers {
  font: 500 13px/1.3 var(--f-mono);
  color: var(--fg-soft);
  font-variant-numeric: tabular-nums;
}

.usage-meter-bar {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--bg-sunk);
  overflow: hidden;
}

.usage-meter-fill {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  width: 0;
  background: var(--accent);
  border-radius: 999px;
  transition: width var(--dur-3) var(--ease);
}

.usage-meter.is-approaching .usage-meter-fill {
  background: var(--warning);
}

.usage-meter.is-over .usage-meter-fill {
  background: var(--error);
}

.usage-meter-projection {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  border-left: 1px dashed var(--fg-dim);
  pointer-events: none;
}

.usage-meter-overage {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: 500 12px/1.3 var(--f-body);
  color: var(--error-text);
}

.usage-meter-overage-icon {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  color: var(--error-text);
}

@media (max-width: 480px) {
  .usage-meter-head {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--s-1);
  }
}

@media (prefers-reduced-motion: reduce) {
  .usage-meter-fill {
    transition: none;
  }
}
import { UsageMeter } from "@magicblocksai/ui";

<div className="usage-meter-stack">
  <UsageMeter
    label="API calls this month"
    current={31247}
    limit={100000}
    unit="calls"
  />
  <UsageMeter
    label="Storage"
    current={8.8}
    limit={10}
    unit="GB"
    format={(n) => n.toFixed(1)}
  />
  <UsageMeter
    label="Seats"
    current={26}
    limit={20}
    unit="seats"
  />
</div>

22.3 Invoice row

A single invoice row primitive — date + number + amount + status pill + optional download icon. Used inside <BillingHistoryTable> (section 24.5) or composed standalone inside a .invoice-row-list wrapper for custom table-shaped layouts. The status pill colour-codes four states: paid (success tint), open (accent tint), void (sunken neutral), uncollectible (error tint). Below 480px the grid reflows to two rows; the download icon, when present, spans both rows on the right edge.

Four rows — paid, paid, open, void

.invoice-row

Four invoice rows stacked inside a .invoice-row-list wrapper. The first two are paid (success pill + download icon), the third is still open (accent pill, no download — pending), and the fourth is void (sunken pill, no actions). The wrapper provides the hairline border + rounded corners; the per-row bottom hairlines align to the radius via overflow: hidden on the wrapper.

14 May 2026 INV-2026-0142 $292.00 Paid
14 Apr 2026 INV-2026-0118 $292.00 Paid
14 Mar 2026 INV-2026-0094 $32.00 Open
14 Feb 2026 INV-2026-0070 $292.00 Void
<div class="invoice-row-list">
  <div class="invoice-row">
    <span class="invoice-row-date">14 May 2026</span>
    <span class="invoice-row-number">INV-2026-0142</span>
    <span class="invoice-row-amount">$292.00</span>
    <span class="invoice-row-status is-paid">Paid</span>
    <a class="invoice-row-download" href="/invoices/INV-2026-0142.pdf"
       aria-label="Download invoice INV-2026-0142 as PDF">
      <svg>…</svg>
    </a>
  </div>
  <div class="invoice-row">
    <span class="invoice-row-date">14 Mar 2026</span>
    <span class="invoice-row-number">INV-2026-0094</span>
    <span class="invoice-row-amount">$32.00</span>
    <span class="invoice-row-status is-open">Open</span>
  </div>
  <div class="invoice-row">
    <span class="invoice-row-date">14 Feb 2026</span>
    <span class="invoice-row-number">INV-2026-0070</span>
    <span class="invoice-row-amount">$292.00</span>
    <span class="invoice-row-status is-void">Void</span>
  </div>
</div>
.invoice-row-list {
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--bg-paper);
}

.invoice-row {
  display: grid;
  grid-template-columns: auto 1fr auto auto auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--hair);
  transition: background var(--dur-2) var(--ease);
}

.invoice-row:last-child {
  border-bottom: 0;
}

.invoice-row.is-interactive {
  cursor: pointer;
}

.invoice-row.is-interactive:hover {
  background: var(--bg-paper);
}

.invoice-row-date {
  font: 500 13px/1.3 var(--f-mono);
  color: var(--fg-soft);
  white-space: nowrap;
}

.invoice-row-number {
  font: 400 14px/1.3 var(--f-body);
  color: var(--fg);
}

.invoice-row-amount {
  font: 600 14px/1.3 var(--f-body);
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  text-align: right;
  min-width: 80px;
}

.invoice-row-status {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  font: 600 11px/1.4 var(--f-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.invoice-row-status.is-paid {
  background: var(--success-soft);
  color: var(--success-text);
}

.invoice-row-status.is-open {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.invoice-row-status.is-void {
  background: var(--bg-sunk);
  color: var(--fg-dim);
}

.invoice-row-status.is-uncollectible {
  background: var(--error-soft);
  color: var(--error-text);
}

.invoice-row-download {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  color: var(--fg-soft);
  background: transparent;
  text-decoration: none;
  transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}

.invoice-row-download:hover {
  background: var(--bg-sunk);
  color: var(--fg);
}

.invoice-row-download svg {
  width: 16px;
  height: 16px;
}

/* …additional rules trimmed for brevity — see _shared.css */
import { InvoiceRow } from "@magicblocksai/ui";

<div className="invoice-row-list">
  <InvoiceRow
    date="14 May 2026"
    number="INV-2026-0142"
    amount="$292.00"
    status="paid"
    pdfHref="/invoices/INV-2026-0142.pdf"
  />
  <InvoiceRow
    date="14 Apr 2026"
    number="INV-2026-0118"
    amount="$292.00"
    status="paid"
    pdfHref="/invoices/INV-2026-0118.pdf"
  />
  <InvoiceRow
    date="14 Mar 2026"
    number="INV-2026-0094"
    amount="$32.00"
    status="open"
  />
  <InvoiceRow
    date="14 Feb 2026"
    number="INV-2026-0070"
    amount="$292.00"
    status="void"
  />
</div>

22.4 Payment method card

A card-on-file display — brand mark + last 4 digits + expiry + cardholder name + Edit and Remove action buttons. An optional .payment-method-card-pill in the top-right of the head marks the default card. The brand glyph is an inline SVG inside a fixed-size .payment-method-card-brand wrapper (48×32); its currentColor strokes pick up the wrapper’s text colour so dark-mode flips automatically. Below 480px the grid stacks (single column) and the action buttons stretch full-width via flex: 1.

Two cards — Visa (primary) and Mastercard

.payment-method-card

Two cards side-by-side inside a .payment-method-grid wrapper. The Visa card carries the “Primary” pill and both action buttons (Edit + Remove); the Mastercard has Edit only (no destructive action on the fallback card). Edit uses .btn-ghost for a quiet hover; Remove uses .btn-danger-outline to signal the destructive intent without screaming.

Primary
•••• 4242 Expires 12/27 Jay Stockwell
•••• 8888 Expires 09/26 Jay Stockwell
<div class="payment-method-grid">
  <div class="payment-method-card">
    <div class="payment-method-card-head">
      <span class="payment-method-card-brand"><svg>…</svg></span>
      <span class="payment-method-card-pill">Primary</span>
    </div>
    <div class="payment-method-card-meta">
      <span class="payment-method-card-last4">•••• 4242</span>
      <span class="payment-method-card-expiry">Expires 12/27</span>
      <span class="payment-method-card-name">Jay Stockwell</span>
    </div>
    <div class="payment-method-card-actions">
      <button type="button" class="btn btn-ghost btn-sm">Edit</button>
      <button type="button" class="btn btn-danger-outline btn-sm">Remove</button>
    </div>
  </div>
  <div class="payment-method-card">
    <div class="payment-method-card-head">
      <span class="payment-method-card-brand"><svg>…</svg></span>
    </div>
    <div class="payment-method-card-meta">
      <span class="payment-method-card-last4">•••• 8888</span>
      <span class="payment-method-card-expiry">Expires 09/26</span>
      <span class="payment-method-card-name">Jay Stockwell</span>
    </div>
    <div class="payment-method-card-actions">
      <button type="button" class="btn btn-ghost btn-sm">Edit</button>
    </div>
  </div>
</div>
.payment-method-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-4);
}

.payment-method-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  background: var(--bg-paper);
}

.payment-method-card-head {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
}

.payment-method-card-brand {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 32px;
  color: var(--fg);
  flex: 0 0 auto;
}

.payment-method-card-brand svg {
  width: 100%;
  height: 100%;
}

.payment-method-card-pill {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent-text);
  font: 600 11px/1.4 var(--f-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.payment-method-card-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.payment-method-card-last4 {
  font: 600 18px/1.3 var(--f-mono);
  color: var(--fg);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05em;
}

.payment-method-card-expiry {
  font: 400 13px/1.3 var(--f-mono);
  color: var(--fg-dim);
}

.payment-method-card-name {
  font: 400 13px/1.3 var(--f-body);
  color: var(--fg-soft);
}

.payment-method-card-actions {
  display: flex;
  flex-direction: row;
  gap: var(--s-2);
  justify-content: flex-end;
  margin-top: auto;
}

@media (max-width: 480px) {
  .payment-method-grid {
    grid-template-columns: 1fr;
  }
  .payment-method-card-actions {
    flex-direction: row;
    width: 100%;
  }
  .payment-method-card-actions .btn {
    flex: 1;
  }
}
import { PaymentMethodCard } from "@magicblocksai/ui";

<div className="payment-method-grid">
  <PaymentMethodCard
    brand="visa"
    last4="4242"
    expiry="12/27"
    name="Jay Stockwell"
    isPrimary
    onEdit={() => openCardEditor("pm_visa_4242")}
    onRemove={() => removeCard("pm_visa_4242")}
  />
  <PaymentMethodCard
    brand="mastercard"
    last4="8888"
    expiry="09/26"
    name="Jay Stockwell"
    onEdit={() => openCardEditor("pm_mc_8888")}
  />
</div>

22.5 Billing history table

A composed table wrapping .invoice-row-list (chapter 24.3) with a top control row — filter chips on the left + a sort toggle on the right — and a non-semantic header row aligned to the per-row grid. The chip group reuses the kit’s existing .chip / .chip-button / .chip-active family from chapter 07; each chip carries a .chip-count showing the number of matching invoices. The list chrome (border + radius + overflow hidden) stays as the single source of truth from chapter 24.3 — this section only layers controls + header + empty-state on top. Below 480px the control row stacks and the chip group becomes horizontally scrollable.

Eight invoices — mixed statuses, sorted newest first

.billing-history-table

Eight invoices covering all four status states: 4 paid (success pill + download icon), 2 open (accent pill — no download yet), 1 void (sunken pill, no actions), and 1 uncollectible (error pill, no actions). The filter chip group shows the per-status counts inline (All 8 / Paid 4 / Open 2 / Void 1); the active chip carries aria-pressed="true" + the accent fill. The Date column header doubles as the sort toggle — clicking it flips the order between newest-first and oldest-first.

Date Invoice Amount Status
14 May 2026 INV-2026-0142 $292.00 Paid
14 Apr 2026 INV-2026-0118 $292.00 Paid
14 Mar 2026 INV-2026-0094 $32.00 Open
14 Feb 2026 INV-2026-0070 $292.00 Paid
14 Jan 2026 INV-2026-0046 $292.00 Paid
14 Dec 2025 INV-2025-0922 $292.00 Uncollectible
14 Nov 2025 INV-2025-0898 $48.00 Open
14 Oct 2025 INV-2025-0874 $292.00 Void
<div class="billing-history-table">
  <div class="billing-history-table-controls">
    <div class="billing-history-table-filters chip-row" role="group" aria-label="Filter invoices by status">
      <button type="button" class="chip chip-button chip-active" aria-pressed="true">
        <span>All</span><span class="chip-count">8</span>
      </button>
      <button type="button" class="chip chip-button" aria-pressed="false">
        <span>Paid</span><span class="chip-count">4</span>
      </button>
      <button type="button" class="chip chip-button" aria-pressed="false">
        <span>Open</span><span class="chip-count">2</span>
      </button>
      <button type="button" class="chip chip-button" aria-pressed="false">
        <span>Void</span><span class="chip-count">1</span>
      </button>
    </div>
    <button type="button" class="billing-history-table-sort">
      <span>Date</span>
      <span class="billing-history-table-sort-dir" aria-hidden="true">↓</span>
      <span class="billing-history-table-sort-label">Newest first</span>
    </button>
  </div>
  <div class="invoice-row-list billing-history-table-list">
    <div class="billing-history-table-header" role="row">
      <span role="columnheader">Date</span>
      <span role="columnheader">Invoice</span>
      <span role="columnheader">Amount</span>
      <span role="columnheader">Status</span>
      <span role="columnheader" aria-label="Actions"></span>
    </div>
    <div class="invoice-row">…</div>
    …
  </div>
</div>
.billing-history-table {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.billing-history-table-controls {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  gap: var(--s-3);
}

.billing-history-table-filters {
  /* Inherits `.chip-row` for inline-flex + wrap + gap. The class is here so
     this control gets its own selector hook for the mobile reflow. */
  flex-wrap: wrap;
}

.billing-history-table-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  appearance: none;
  background: var(--bg-paper);
  color: var(--fg);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  padding: 4px 12px;
  font: 500 12.5px/1.3 var(--f-body);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}

.billing-history-table-sort:hover {
  background: var(--bg-sunk);
  border-color: color-mix(in oklab, var(--fg) 25%, transparent);
}

.billing-history-table-sort:focus-visible {
  outline: 0;
  box-shadow: var(--sh-focus);
}

.billing-history-table-sort-dir {
  font: 600 14px/1 var(--f-mono);
  color: var(--accent);
}

.billing-history-table-sort-label {
  color: var(--fg-soft);
  font: 400 12px/1.3 var(--f-body);
}

.billing-history-table-list {
  /* `.invoice-row-list` provides the border + radius + overflow + paper
     background; we just need a hook for the header row below. */
}

.billing-history-table-header {
  display: grid;
  grid-template-columns: auto 1fr auto auto auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--bg-sunk);
  border-bottom: 1px solid var(--hair);
  font: 600 11px/1.4 var(--f-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-soft);
}

.billing-history-table-empty {
  padding: var(--s-6) var(--s-4);
  text-align: center;
  color: var(--fg-soft);
  font: 400 14px/1.5 var(--f-body);
  background: var(--bg-paper);
}

/* …additional rules trimmed for brevity — see _shared.css */
import { BillingHistoryTable } from "@magicblocksai/ui";

<BillingHistoryTable
  invoices={[
    { id: "i1", date: "14 May 2026", number: "INV-2026-0142", amount: "$292.00", status: "paid",          pdfHref: "/invoices/INV-2026-0142.pdf", sortKey: 1747180800 },
    { id: "i2", date: "14 Apr 2026", number: "INV-2026-0118", amount: "$292.00", status: "paid",          pdfHref: "/invoices/INV-2026-0118.pdf", sortKey: 1744588800 },
    { id: "i3", date: "14 Mar 2026", number: "INV-2026-0094", amount: "$32.00",  status: "open",          sortKey: 1742083200 },
    { id: "i4", date: "14 Feb 2026", number: "INV-2026-0070", amount: "$292.00", status: "paid",          pdfHref: "/invoices/INV-2026-0070.pdf", sortKey: 1739491200 },
    { id: "i5", date: "14 Jan 2026", number: "INV-2026-0046", amount: "$292.00", status: "paid",          pdfHref: "/invoices/INV-2026-0046.pdf", sortKey: 1736812800 },
    { id: "i6", date: "14 Dec 2025", number: "INV-2025-0922", amount: "$292.00", status: "uncollectible", sortKey: 1734134400 },
    { id: "i7", date: "14 Nov 2025", number: "INV-2025-0898", amount: "$48.00",  status: "open",          sortKey: 1731542400 },
    { id: "i8", date: "14 Oct 2025", number: "INV-2025-0874", amount: "$292.00", status: "void",          sortKey: 1728864000 },
  ]}
  onRowClick={(id) => openInvoice(id)}
/>

22.6 Billing page

A page-shaped wrapper composing the chapter’s five primitives into the canonical billing-page layout: PlansUsagePayment methodsBilling history. Each section gets a small uppercase label above its content; the wrapper itself is a flex column with var(--s-8) between sections. Stateless glue — the composed children (<BillingHistoryTable> in particular) own any interactive state they need. The composed primitives carry their own mobile reflow, so the wrapper has nothing extra to add for narrow viewports. The consumer’s page-shell wrapper handles max-width; here the demo is shown unconstrained inside the .demo-stage.

Realistic billing page — 3 plans, 2 meters, 1 card, 4 invoices

.billing-page

A realistic mid-tier customer view: the Pro plan is recommended (accent intent + pill), API calls are healthy (~25%), storage is approaching the limit (88%, warning tint), one Visa-on-file is marked primary, and four recent invoices mix paid + open. The page wrapper just stacks the four sections; everything inside each section delegates to the primitive from 24.1–24.5.

Free
$0
billed monthly

Everything you need to evaluate MagicBlocks on a small team.

  • Up to 3 seats
  • 1,000 records
  • Community support
Recommended
Pro
$29
per seat / month

Everything in Free, plus the power-user surface area most teams need.

  • Unlimited seats
  • Unlimited records
  • Priority support
  • SSO & SCIM
Enterprise
Custom
billed annually

For organisations that need procurement, compliance, and dedicated support.

  • Everything in Pro
  • SOC 2 + HIPAA
  • 99.99% SLA
API calls this month 24,500 / 100,000 calls
Storage 8.8 / 10.0 GB
Primary
•••• 4242 Expires 12/27 Jay Stockwell
Date Invoice Amount Status
14 May 2026 INV-2026-0142 $292.00 Paid
14 Apr 2026 INV-2026-0118 $292.00 Paid
14 Mar 2026 INV-2026-0094 $32.00 Open
14 Feb 2026 INV-2026-0070 $292.00 Paid
<div class="billing-page">
  <section class="billing-page-section">
    <h2 class="billing-page-section-label">Plan</h2>
    <div class="plan-card-grid">
      <article class="plan-card intent-muted">…</article>
      <article class="plan-card intent-accent">…</article>
      <article class="plan-card intent-ink">…</article>
    </div>
  </section>
  <section class="billing-page-section">
    <h2 class="billing-page-section-label">Usage</h2>
    <div class="usage-meter-stack">…</div>
  </section>
  <section class="billing-page-section">
    <h2 class="billing-page-section-label">Payment methods</h2>
    <div class="payment-method-grid">…</div>
  </section>
  <section class="billing-page-section">
    <h2 class="billing-page-section-label">Billing history</h2>
    <div class="billing-history-table">…</div>
  </section>
</div>
.billing-page {
  display: flex;
  flex-direction: column;
  gap: var(--s-8);
}

.billing-page-section {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

.billing-page-section-label {
  margin: 0;
  font: 600 16px/1.3 var(--f-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-soft);
}
import { BillingPage } from "@magicblocksai/ui";

<BillingPage
  plans={[
    {
      name: "Free",
      price: "$0",
      period: "billed monthly",
      description: "Everything you need to evaluate MagicBlocks on a small team.",
      features: ["Up to 3 seats", "1,000 records", "Community support"],
      cta: <button type="button" className="btn btn-ghost">Get started</button>,
    },
    {
      name: "Pro",
      price: "$29",
      period: "per seat / month",
      description: "Everything in Free, plus the power-user surface area most teams need.",
      features: ["Unlimited seats", "Unlimited records", "Priority support", "SSO & SCIM"],
      cta: <button type="button" className="btn btn-primary">Start free trial</button>,
      intent: "accent",
      pill: "Recommended",
    },
    {
      name: "Enterprise",
      price: "Custom",
      period: "billed annually",
      description: "For organisations that need procurement, compliance, and dedicated support.",
      features: ["Everything in Pro", "SOC 2 + HIPAA", "99.99% SLA"],
      cta: <button type="button" className="btn btn-ink">Contact sales</button>,
      intent: "ink",
    },
  ]}
  usage={[
    { label: "API calls this month", current: 24500, limit: 100000, unit: "calls" },
    { label: "Storage", current: 8.8, limit: 10, unit: "GB", format: (n) => n.toFixed(1) },
  ]}
  paymentMethods={[
    {
      brand: "visa",
      last4: "4242",
      expiry: "12/27",
      name: "Jay Stockwell",
      isPrimary: true,
      onEdit: () => openCardEditor("pm_visa_4242"),
      onRemove: () => removeCard("pm_visa_4242"),
    },
  ]}
  invoices={[
    { id: "i1", date: "14 May 2026", number: "INV-2026-0142", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0142.pdf", sortKey: 1747180800 },
    { id: "i2", date: "14 Apr 2026", number: "INV-2026-0118", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0118.pdf", sortKey: 1744588800 },
    { id: "i3", date: "14 Mar 2026", number: "INV-2026-0094", amount: "$32.00",  status: "open", sortKey: 1742083200 },
    { id: "i4", date: "14 Feb 2026", number: "INV-2026-0070", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0070.pdf", sortKey: 1739491200 },
  ]}
/>