# 💳 Billing Components


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## 🎯 Overview

<table>
<colgroup>
<col style="width: 34%" />
<col style="width: 37%" />
<col style="width: 27%" />
</colgroup>
<thead>
<tr>
<th>Category</th>
<th>Component</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>🛒 Checkout</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#checkoutcard"><code>CheckoutCard</code></a></td>
<td>Post-login checkout with monthly/yearly toggle and POST form</td>
</tr>
<tr>
<td>⏳ Trial</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#trialbanner"><code>TrialBanner</code></a></td>
<td>Dashboard banner showing trial status (info/warning tone)</td>
</tr>
<tr>
<td>📊 Status</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#billingstatuscard"><code>BillingStatusCard</code></a></td>
<td>Subscription status card with configurable messaging</td>
</tr>
</tbody>
</table>

------------------------------------------------------------------------

## 🏗️ Architecture

    ┌─────────────────────────────────────────────────────────┐
    │                 Post-Login Dashboard                     │
    ├─────────────────────────────────────────────────────────┤
    │  TrialBanner (info or warning tone)                     │
    │  ├─ Days remaining + optional CTA                       │
    ├─────────────────────────────────────────────────────────┤
    │  BillingStatusCard                                      │
    │  ├─ Plan label + status chip + period end + manage CTA  │
    ├─────────────────────────────────────────────────────────┤
    │  CheckoutCard                                           │
    │  ├─ Monthly/yearly toggle + savings chip                │
    │  ├─ POST form with hidden fields + CTA                  │
    └─────────────────────────────────────────────────────────┘

------------------------------------------------------------------------

## 📋 When to Use Which Component

<table>
<colgroup>
<col style="width: 34%" />
<col style="width: 37%" />
<col style="width: 27%" />
</colgroup>
<thead>
<tr>
<th>Scenario</th>
<th>Component</th>
<th>Module</th>
</tr>
</thead>
<tbody>
<tr>
<td>Public marketing page with multiple pricing tiers</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/web_pages.html#pricingsection"><code>PricingSection</code></a></td>
<td><code>web_pages</code></td>
</tr>
<tr>
<td>Post-login checkout for a single plan</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#checkoutcard"><code>CheckoutCard</code></a></td>
<td><code>billing</code></td>
</tr>
<tr>
<td>Dashboard banner showing trial countdown</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#trialbanner"><code>TrialBanner</code></a></td>
<td><code>billing</code></td>
</tr>
<tr>
<td>Dashboard card showing subscription state</td>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#billingstatuscard"><code>BillingStatusCard</code></a></td>
<td><code>billing</code></td>
</tr>
</tbody>
</table>

[`PricingSection`](https://abhisheksreesaila.github.io/fh-matui/web_pages.html#pricingsection)
is for **pre-login marketing**. The billing components are for
**post-login transactional UI**. All billing components are
presentation-only and backend-agnostic — no payment provider
integration, no hardcoded product names.

<details class="code-fold">
<summary>Code</summary>

``` python
import socket
import time
import subprocess
from fastcore.utils import partial
from fasthtml.jupyter import FastHTML, JupyUvi, HTMX

def kill_process_on_port(port):
    """Kill any process using the specified port on Windows"""
    try:
        result = subprocess.run(
            f'netstat -ano | findstr :{port}',
            shell=True, capture_output=True, text=True
        )
        if result.stdout:
            lines = result.stdout.strip().split('\n')
            for line in lines:
                if 'LISTENING' in line:
                    pid = line.strip().split()[-1]
                    subprocess.run(f'taskkill /PID {pid} /F', shell=True, capture_output=True)
                    print(f"Killed process {pid} on port {port}")
                    time.sleep(0.5)
                    return True
        return False
    except Exception as e:
        print(f"Could not kill process on port {port}: {e}")
        return False

def find_available_port(start_port=5000, max_attempts=10):
    for port in range(start_port, start_port + max_attempts):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(('', port))
                return port
            except OSError:
                continue
    raise RuntimeError(f"Could not find available port in range {start_port}-{start_port+max_attempts}")

if 'server' in globals():
    try:
        server.stop()
        time.sleep(0.5)
    except:
        pass

preferred_port = 7030
kill_process_on_port(preferred_port)
port = find_available_port(preferred_port)

app = FastHTML(hdrs=MatTheme.blue.headers(title="Billing Components", mode="dark"))
rt = app.route

try:
    server = JupyUvi(app, port=port)
    preview = partial(HTMX, app=app, port=port)
    print(f"Server running on port {port}")
except Exception as e:
    print(f"Failed to start server: {e}")
    raise
```

</details>

<script>
document.body.addEventListener('htmx:configRequest', (event) => {
    if(event.detail.path.includes('://')) return;
    htmx.config.selfRequestsOnly=false;
    event.detail.path = `${location.protocol}//${location.hostname}:7030${event.detail.path}`;
});
</script>

    Server running on port 7030

------------------------------------------------------------------------

## 🛒 CheckoutCard

<table>
<colgroup>
<col style="width: 57%" />
<col style="width: 42%" />
</colgroup>
<thead>
<tr>
<th>Component</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#checkoutcard"><code>CheckoutCard</code></a></td>
<td>Single-plan checkout card with monthly/yearly toggle and POST
form</td>
</tr>
</tbody>
</table>

**Features:** Instance-safe JS IDs (uuid), configurable copy, hidden
form fields, savings chip, trial callout

------------------------------------------------------------------------

<a
href="https://github.com/abhisheksreesaila/fh-matui/blob/master/fh_matui/billing.py#L73"
target="_blank" style="float:right; font-size:smaller">source</a>

### CheckoutCard

``` python

def CheckoutCard(
    monthly_price:float, # Monthly price (required)
    yearly_price:float, # Yearly price (required)
    title:str='Choose Your Plan', # Card title
    subtitle:str='', # Card subtitle
    currency:str='$', # Currency symbol
    trial_days:int=0, # Trial period in days (0 = none)
    cta_text:str='Subscribe', # Submit button text
    form_action:str='/checkout', # POST form action URL
    plan_id:str='', # Hidden field: plan identifier
    hidden_fields:dict=None, # Additional hidden fields {name: value}
    default_period:str='monthly', # Default toggle: 'monthly' or 'yearly'
    fine_print:str='', # Small text below CTA
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

```

*Single-plan checkout card with monthly/yearly toggle and POST form.*

Designed for post-login checkout flows. Submits via POST form with
hidden fields (plan_id, billing_period, plus any custom fields). All
JS/CSS IDs are namespaced per instance using a uuid, so multiple
CheckoutCards can coexist on one page.

Args: monthly_price: Monthly price as float (e.g., 9.99) yearly_price:
Yearly price as float (e.g., 99.99) title: Card heading subtitle: Card
subheading currency: Currency symbol prepended to prices trial_days: If
\> 0, shows a trial callout above the form cta_text: Text for the submit
button form_action: URL the form POSTs to plan_id: Value for the hidden
plan_id field hidden_fields: Dict of additional hidden form fields
{name: value} default_period: Which period is selected on load
(‘monthly’ or ‘yearly’) fine_print: Small disclaimer text below the
button cls: Additional CSS classes for the card

Example: CheckoutCard( monthly_price=9.99, yearly_price=99.99,
plan_id=“pro”, trial_days=14, fine_print=“Cancel anytime. No questions
asked.”, )

<details class="code-fold">
<summary>Code</summary>

``` python
# Example: CheckoutCard with trial period and custom hidden fields
def ex_checkout_card():
    return Div(
        CheckoutCard(
            monthly_price=9.99,
            yearly_price=99.99,
            plan_id="pro",
            trial_days=14,
            hidden_fields={"coupon": "WELCOME10"},
            fine_print="Cancel anytime. No questions asked.",
        ),
        cls="responsive padding",
    )

preview(ex_checkout_card())
```

</details>

<iframe src="http://localhost:7030/_RSj8j4zgR2a2x2-hW4Nt3w" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

<details class="code-fold">
<summary>Code</summary>

``` python
# Example: CheckoutCard defaulting to yearly billing
def ex_checkout_yearly():
    return Div(
        CheckoutCard(
            monthly_price=29.99,
            yearly_price=249.99,
            title="Enterprise Plan",
            subtitle="For growing teams",
            default_period="yearly",
            cta_text="Start Free Trial",
            form_action="/billing/subscribe",
            plan_id="enterprise",
        ),
        cls="responsive padding",
    )

preview(ex_checkout_yearly())
```

</details>

<iframe src="http://localhost:7030/_Up9V7GyeTtW7e4_XRXP-XQ" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

------------------------------------------------------------------------

## ⏳ TrialBanner

<table>
<colgroup>
<col style="width: 57%" />
<col style="width: 42%" />
</colgroup>
<thead>
<tr>
<th>Component</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#trialbanner"><code>TrialBanner</code></a></td>
<td>Thin, dismissible top-of-page bar showing trial countdown</td>
</tr>
</tbody>
</table>

**Features:** Auto-derives info/warning tone, sits above the navbar,
close button, configurable copy, optional CTA

**Positioning:** Place the banner **before** the navbar in the DOM. In a
[`TopLayout`](https://abhisheksreesaila.github.io/fh-matui/app_pages.html#toplayout)
route, return it as the first tuple element:
`(TrialBanner(...), *TopLayout(content, nav_bar=...))`

------------------------------------------------------------------------

<a
href="https://github.com/abhisheksreesaila/fh-matui/blob/master/fh_matui/billing.py#L225"
target="_blank" style="float:right; font-size:smaller">source</a>

### TrialBanner

``` python

def TrialBanner(
    days_remaining:int, # Days left in trial
    end_date_text:str='', # e.g., 'Trial ends Mar 15'
    status:str='trialing', # Status key (for custom styling hooks)
    cta_text:str='', # Optional CTA link text
    cta_href:str='', # CTA link URL
    message:str='', # Override default message
    dismissible:bool=True, # Show close button
    banner_id:str='', # Custom element ID (auto-generated if empty)
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

```

*Thin, dismissible trial banner — sits above the app layout.*

Renders a compact full-width bar with icon, message, optional CTA, and
close button. Place it **before** the Layout in the DOM so it appears
above everything. When dismissed, it removes itself from the page.

Automatically switches to warning tone (error-container) when 3 or fewer
days remain, otherwise uses info tone (primary-container).

Positioning with Layout::

    @rt("/dashboard")
    def get(req):
        content = dashboard_content()
        if 'HX-Request' in req.headers: return content
        return (
            TrialBanner(days_remaining=5, cta_text="Upgrade", cta_href="/checkout"),
            Layout(content, sidebar_links=my_sidebar_links()),
        )

Args: days_remaining: Number of days left in the trial end_date_text:
Optional text (shown after a separator) status: Status string (available
as a data attribute hook) cta_text: If non-empty, renders an inline CTA
link cta_href: URL for the CTA link message: Custom message (overrides
the default) dismissible: Whether to show the close button (default
True) banner_id: Element ID for the banner (auto-generated if empty)
cls: Additional CSS classes

Example: TrialBanner(days_remaining=14, cta_text=‘Upgrade Now’,
cta_href=‘/checkout’) TrialBanner(days_remaining=2, end_date_text=‘Trial
ends Feb 23’)

<details class="code-fold">
<summary>Code</summary>

``` python
# Example: TrialBanner in info tone (14 days remaining)
preview(TrialBanner(
    days_remaining=14,
    end_date_text="Trial ends March 15, 2026",
    cta_text="Upgrade Now",
    cta_href="/checkout",
))
```

</details>

<iframe src="http://localhost:7030/_Y2T0wKcJQYuhXddOg7eg3A" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

<details class="code-fold">
<summary>Code</summary>

``` python
# Example: TrialBanner in warning tone (2 days remaining, non-dismissible)
preview(TrialBanner(
    days_remaining=2,
    end_date_text="Trial ends February 23, 2026",
    cta_text="Upgrade Now",
    cta_href="/checkout",
    dismissible=False,
))
```

</details>

<iframe src="http://localhost:7030/_m8cE0k0DQ1CNF3WCXfddAQ" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

------------------------------------------------------------------------

## 📊 BillingStatusCard

<table>
<colgroup>
<col style="width: 57%" />
<col style="width: 42%" />
</colgroup>
<thead>
<tr>
<th>Component</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a
href="https://abhisheksreesaila.github.io/fh-matui/billing.html#billingstatuscard"><code>BillingStatusCard</code></a></td>
<td>Subscription status card with status-specific messaging</td>
</tr>
</tbody>
</table>

**Features:** Status chip with semantic colors, configurable messages,
optional manage CTA

------------------------------------------------------------------------

<a
href="https://github.com/abhisheksreesaila/fh-matui/blob/master/fh_matui/billing.py#L340"
target="_blank" style="float:right; font-size:smaller">source</a>

### BillingStatusCard

``` python

def BillingStatusCard(
    status:str, # 'active', 'trialing', 'past_due', 'canceled', 'checkout_pending'
    plan_label:str='', # e.g., 'Pro Plan'
    current_period_end:str='', # e.g., 'March 15, 2026'
    manage_url:str='', # URL for billing management portal
    manage_text:str='Manage Billing', # CTA button text
    messages:dict=None, # Override status -> message mapping
    cls:str='', # Additional CSS classes
    kwargs:VAR_KEYWORD
):

```

*Subscription status card with status-specific messaging.*

Displays the current subscription status with a semantic color chip,
plan label, period end date, and an optional “Manage Billing” CTA. All
copy is configurable via the messages dict override.

Supported statuses: active, trialing, past_due, canceled,
checkout_pending. Unknown statuses render with a neutral chip and the
status string as message.

Args: status: Subscription status string plan_label: Name of the current
plan current_period_end: Human-readable period end date manage_url: If
non-empty, renders a manage billing button manage_text: Text for the
manage billing button messages: Dict overriding default status messages
cls: Additional CSS classes

Example: BillingStatusCard(status=‘active’, plan_label=‘Pro Plan’,
current_period_end=‘March 15, 2026’, manage_url=‘/billing/portal’)

<details class="code-fold">
<summary>Code</summary>

``` python
# Example: BillingStatusCard for each status
def ex_billing_status_all():
    statuses = [
        ("active", "Pro Plan", "March 15, 2026"),
        ("trialing", "Pro Plan", "March 1, 2026"),
        ("past_due", "Pro Plan", "February 15, 2026"),
        ("canceled", "Pro Plan", ""),
        ("checkout_pending", "", ""),
    ]
    cards = [
        BillingStatusCard(
            status=s,
            plan_label=label,
            current_period_end=end,
            manage_url="/billing/portal" if s in ("active", "past_due") else "",
        )
        for s, label, end in statuses
    ]
    return Div(*cards, cls="responsive padding column medium-space")

preview(ex_billing_status_all())
```

</details>

<iframe src="http://localhost:7030/_3BdJPOhdQTmdY3k4RdleUg" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

------------------------------------------------------------------------

## 🌐 Live App Preview

Full billing dashboard inside a
[`Layout`](https://abhisheksreesaila.github.io/fh-matui/components.html#layout)
shell (sidebar + NavBar) with the
[`TrialBanner`](https://abhisheksreesaila.github.io/fh-matui/billing.html#trialbanner)
sitting above the entire layout. After running the server cell and this
cell, open the port URL:

- `/billing` — Dashboard with TrialBanner + BillingStatusCard +
  CheckoutCard
- `/billing/checkout` — Centered CheckoutCard with trial callout
- `/billing/status` — All 5 subscription status cards + warning
  TrialBanner

<details class="code-fold">
<summary>Code</summary>

``` python
# --- Navigation items ---
def billing_nav_items():
    """Top NavBar links — hx-boost on Layout handles HTMX automatically."""
    return [
        A("Dashboard", href="/billing"),
        A("Checkout", href="/billing/checkout"),
        A("Status", href="/billing/status"),
    ]

def billing_sidebar_items():
    """Sidebar links with icons — hx-boost on Layout handles HTMX automatically."""
    return [
        A(Icon("dashboard"), Span("Dashboard"), href="/billing", cls="nav-link"),
        A(Icon("shopping_cart"), Span("Checkout"), href="/billing/checkout", cls="nav-link"),
        A(Icon("receipt_long"), Span("Status"), href="/billing/status", cls="nav-link"),
    ]

# Highlights the active sidebar link after each HTMX navigation
active_nav_script = Script("""
function updateActiveNav() {
    document.querySelectorAll('.nav-link').forEach(link => {
        link.classList.toggle('active', link.getAttribute('href') === window.location.pathname);
    });
}
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', updateActiveNav);
} else {
    updateActiveNav();
}
document.body.addEventListener('htmx:afterSettle', updateActiveNav);
""")


# --- App shell: TrialBanner sits above the full Layout ---
def get_billing_layout(content, days_remaining=5):
    banner = TrialBanner(
        days_remaining=days_remaining,
        end_date_text="Trial ends February 26, 2026",
        cta_text="Upgrade Now",
        cta_href="/billing/checkout",
    )
    return Div(
        banner,
        Layout(
            content,
            sidebar_links=billing_sidebar_items(),
            nav_bar=NavBar(*billing_nav_items(), brand=H3("BillingDemo"), sticky=True),
        ),
        active_nav_script,
    )


# --- Page content builders ---
def billing_dashboard_content():
    """Main billing dashboard: status card + checkout side-by-side."""
    return Div(
        Grid(
            GridCell(
                BillingStatusCard(
                    status="trialing",
                    plan_label="Pro Plan",
                    current_period_end="February 26, 2026",
                    manage_url="/billing/status",
                ),
                span="s12 m6",
            ),
            GridCell(
                CheckoutCard(
                    monthly_price=19.99,
                    yearly_price=199.99,
                    plan_id="pro",
                    trial_days=5,
                    cta_text="Subscribe Now",
                    form_action="/billing/checkout",
                ),
                span="s12 m6",
            ),
        ),
        cls="medium-space",
    )

def billing_checkout_content():
    """Dedicated checkout page with a single centered CheckoutCard."""
    return Grid(
        GridCell(span="s0 m2 l3"),
        GridCell(
            H3("Complete Your Purchase"),
            CheckoutCard(
                monthly_price=9.99,
                yearly_price=99.99,
                plan_id="pro",
                trial_days=14,
                hidden_fields={"coupon": "WELCOME10"},
                fine_print="Cancel anytime. No questions asked.",
                form_action="/billing/checkout",
            ),
            span="s12 m8 l6",
        ),
        GridCell(span="s0 m2 l3"),
    )

def billing_status_content():
    """All five subscription states for visual QA."""
    statuses = [
        ("active",           "Pro Plan", "March 15, 2026"),
        ("trialing",         "Pro Plan", "March 1, 2026"),
        ("past_due",         "Pro Plan", "February 15, 2026"),
        ("canceled",         "Pro Plan", ""),
        ("checkout_pending", "",         ""),
    ]
    cards = [
        BillingStatusCard(
            status=s, plan_label=label, current_period_end=end,
            manage_url="/billing" if s in ("active", "past_due") else "",
        )
        for s, label, end in statuses
    ]
    return Div(
        H3("All Subscription States"),
        Div(*cards, cls="medium-space"),
        cls="medium-space",
    )


# --- Routes ---
@rt("/billing")
def get(req):
    content = billing_dashboard_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content)

@rt("/billing/checkout")
def get(req):
    content = billing_checkout_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content)

@rt("/billing/status")
def get(req):
    content = billing_status_content()
    if "HX-Request" in req.headers: return content
    return get_billing_layout(content, days_remaining=2)  # warning tone on status page


# In-notebook preview
preview(get_billing_layout(billing_dashboard_content()))
```

</details>

<iframe src="http://localhost:7030/_aXv8JhojSmuq34OozvnCQQ" style="width: 100%; height: auto; border: none;" onload="{
        let frame = this;
        window.addEventListener('message', function(e) {
            if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
            if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
        }, false);
    }" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe> 

------------------------------------------------------------------------

## 🔍 Mobile & Theme Verification

All billing components use BeerCSS semantic classes
(`primary-container`, `error-container`, `surface-container`, etc.) so
they automatically adapt to light/dark mode.

### Manual Checks

1.  **Mobile (320–480px):** All cards stack full-width, toggle buttons
    remain tappable, form is usable
2.  **Light mode:** Remove `dark` class from `<body>` — verify contrast
    and readability
3.  **Dark mode:** Add `dark` class to `<body>` — verify all semantic
    colors adapt
4.  **Form payload:** Open browser dev tools → Network tab → submit form
    → verify `plan_id`, `billing_period`, and custom hidden fields in
    POST body
