Site
Pricing
Pricing cards, billing interval selection, plan comparisons, discounts, and credit cost tables.
$ npx context-ds add pricingUsage
Use it for
A pricing page or section. Plans come in as props: PricingShelves (or PricingGrid) around PricingCards, PricingToggle above, PricingPlanFit in the second row, PricingBanner under it; CreditCostsTable lists what each API costs.
Avoid
- More than one filled call to action in the grid
- Colored or gradient plan cards
- Bold prices
- Hiding the overage rate
Dependencies: tooltip, use-tab-glide, lucide-react
Example
examples/pricing.tsx
"use client";import * as React from "react";import { ArrowRight } from "lucide-react";import { SectionHeaderRule, SectionHeading, SiteSection } from "@/components/site/section";import { CreditCostsTable, PricingBanner, PricingCard, PricingPlanFit, PricingShelves, PricingToggle, type BillingPeriod, type PricingFeature } from "@/components/site/pricing";import { Button } from "@/components/ui/button";// Context.dev's self-serve catalog (the branch's lib/pricing.ts) plus Enterprise. Yearly is ten months' price.type Plan = { name: string; description: string; monthly?: number; credits?: number; overage?: string; /** The short form the card shows; the full text is its label. */ concurrency?: string; features: PricingFeature[]; cta: string; featured?: boolean;};const SCRAPE_HINT = "1 credit per page at the base rate";const INCLUDED_HINT = "JavaScript rendering, anti-bot bypass, and premium proxies are part of the base scrape price, with no add-on fees";const MONITOR_HINT = "Watch pages, sitemaps, or extracted data for changes. Each run costs 1 credit for page or sitemap targets, or 10 for extract targets";const LOGO_HINT = "Separate quota via Logo Link, not deducted from your credits";const plans: Plan[] = [ { name: "Free Tier", description: "For exploring the APIs and testing your first ideas.", monthly: 0, credits: 1_000, overage: "No overage", concurrency: "1 concurrent request", features: [ { text: "Scrape 1,000 pages", hint: SCRAPE_HINT }, { text: "JS rendering & proxies included", hint: INCLUDED_HINT }, { text: "10 monitors", hint: MONITOR_HINT }, "1 concurrent batch", "Email support", { text: "Pull 10K logos (one-time quota)", hint: LOGO_HINT }, ], cta: "Get started", }, { name: "Developer", description: "For side projects, MVPs, and lightweight apps.", monthly: 19, credits: 7_500, overage: "$2.20 per 1,000 credits overage", concurrency: "10 concurrent requests", features: [ { text: "Scrape 7,500 pages", hint: SCRAPE_HINT }, { text: "JS rendering & proxies included", hint: INCLUDED_HINT }, "750 Answers in Fast mode", { text: "100 monitors", hint: MONITOR_HINT }, "2 concurrent batches", "Email support", "Usage alerts", { text: "Pull 100K logos", hint: LOGO_HINT }, ], cta: "Start building", }, { name: "Pro", description: "For production apps and AI agents powered by web data.", monthly: 99, credits: 125_000, overage: "$1.80 per 1,000 credits overage", concurrency: "100 concurrent requests", features: [ { text: "Scrape 125,000 pages", hint: SCRAPE_HINT }, { text: "JS rendering & proxies included", hint: INCLUDED_HINT }, "12,500 Answers in Fast mode", { text: "1,000 monitors", hint: MONITOR_HINT }, "5 concurrent batches", "Email + Slack support", "Spend controls", { text: "Pull 2.5M logos", hint: LOGO_HINT }, ], cta: "Start Pro", featured: true, }, { name: "Growth", description: "For growing products that need more data, more often.", monthly: 299, credits: 500_000, overage: "$1.40 per 1,000 credits overage", concurrency: "250 concurrent requests", features: [ { text: "Scrape 500,000 pages", hint: SCRAPE_HINT }, { text: "JS rendering & proxies included", hint: INCLUDED_HINT }, "50,000 Answers in Fast mode", { text: "5,000 monitors", hint: MONITOR_HINT }, "5 concurrent batches", "Priority support", { text: "Pull 5M logos", hint: LOGO_HINT }, ], cta: "Start Growth", }, { name: "Scale", description: "For large datasets, bulk scraping, and continuous monitoring.", monthly: 499, credits: 1_000_000, overage: "$1.00 per 1,000 credits overage", concurrency: "500 concurrent requests", features: [ { text: "Scrape 1,000,000 pages", hint: SCRAPE_HINT }, { text: "JS rendering & proxies included", hint: INCLUDED_HINT }, "100,000 Answers in Fast mode", { text: "10,000 monitors", hint: MONITOR_HINT }, "20 concurrent batches", "SSO / SAML", "Priority + Slack support", "Custom usage alerts", { text: "Pull 10M logos", hint: LOGO_HINT }, ], cta: "Start Scale", }, { name: "Enterprise", description: "For teams with custom volume, security, and compliance requirements.", features: [ "Custom rate and monitor limits", "SSO, SAML and SCIM provisioning", "99.9% uptime SLA", "Annual invoicing, MSA and DPA", "Dedicated Slack channel", "Custom support and implementation help", ], cta: "Book demo", },];const money = (value: number) => `$${value.toLocaleString("en-US")}`;// Each plan's best-value range: where its overage stops being cheaper than the next plan up (the branch's// lib/pricing-breakevens.ts, rounded to 50K pages).const planFit = [ { name: "Developer", range: "up to 50K pages", price: "$19" }, { name: "Pro", range: "50K–250K pages", price: "$99" }, { name: "Growth", range: "250K–650K pages", price: "$299" }, { name: "Scale", range: "650K–2M pages", price: "$499" }, { name: "Enterprise", range: "beyond 2M pages", price: "Custom" },];const DOCS = "https://docs.context.dev";const creditCosts = [ { title: "Scrape", href: `${DOCS}/scrape`, cost: "From 1 credit per call" }, { title: "Scrape with JSON extraction", href: `${DOCS}/scrape/json`, cost: "5 credits per page" }, { title: "Crawl", href: `${DOCS}/crawl`, cost: "1 credit per page" }, { title: "Search", href: `${DOCS}/search`, cost: "From 1 credit per 10 results" }, { title: "Answers", href: `${DOCS}/answers`, cost: "From 10 credits per call" }, { title: "Brand", href: `${DOCS}/brand`, cost: "10 credits per call" }, { title: "Styleguide", href: `${DOCS}/styleguide`, cost: "10 credits per call" }, { title: "Parse", href: `${DOCS}/parse`, cost: "From 1 credit per call" },];export function Site() { const [billing, setBilling] = React.useState<BillingPeriod>("monthly"); const cards = plans.map((plan) => { const custom = plan.monthly === undefined; const yearly = billing === "yearly" && !custom && plan.monthly! > 0; const annual = (plan.monthly ?? 0) * 10; return ( <PricingCard key={plan.name} name={plan.name} description={plan.description} price={custom ? "Custom" : money(yearly ? Math.round(annual / 12) : plan.monthly!)} period={custom ? undefined : "/month"} note={yearly ? `${money(annual)}/yr` : undefined} noteHighlight={yearly ? `Save ${money(plan.monthly! * 12 - annual)}` : undefined} featured={plan.featured} cta={ <Button asChild variant={plan.featured ? "pill" : "pill-outline"} className="py-2.5"> <a href={custom ? "https://cal.com/context-dev/30min" : "/signup"}>{plan.cta}</a> </Button> } quota={ custom ? ["2M+ credits per month", "Volume discounts"] : [`${plan.credits!.toLocaleString("en-US")} credits per month`, plan.overage!, { text: plan.concurrency!.replace(/ requests?$/, ""), label: plan.concurrency }] } features={plan.features} /> ); }); return (<SiteSection> <div className="flex w-full flex-col gap-16"> <div className="flex flex-col"> <div className="mb-8 flex justify-center pt-6"> <PricingToggle value={billing} onValueChange={setBilling} /> </div> <h2 className="sr-only">Plans</h2> <PricingShelves first={cards.slice(0, 4)} second={cards.slice(4)} panel={<PricingPlanFit rows={planFit} footnote="Ranges assume overage enabled, billed in 1,000-credit blocks. 1 credit per scrape at the base rate." />} /> <PricingBanner className="mt-10" action={ <Button asChild variant="pill-outline" className="gap-2 py-2.5"> <a href="https://context.dev/startups"> Apply for the discount <ArrowRight aria-hidden="true" /> </a> </Button> } > Eligible startups and nonprofits get 30% off an annual paid plan for 12 months. </PricingBanner> </div> <div className="flex flex-col"> <SectionHeading title="One balance." accent="Every API." description="A standard scrape uses 1 credit. Here’s what each API costs." /> <SectionHeaderRule /> <CreditCostsTable rows={creditCosts} /> </div> </div> </SiteSection>);}Source
components/site/pricing.tsx
"use client";import * as React from "react";import { ArrowUpRight } from "lucide-react";import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";import { useTabGlide } from "@/hooks/use-tab-glide";import { cn } from "@/lib/utils";// The public pricing page: plan cards side by side in one ringed frame, split by hairlines (a 1px gap over a hairline// fill, so the rules run between rows as well as columns). Each card has the same five parts (header, price, call to// action, the quota box, the feature box), and on wide screens the cards share their rows through subgrid, so every// part lines up across the row. Figures in quota and feature lines are picked out in brand blue. Only the most popular// plan's button is filled (black), so the grid has one main action. `PricingShelves` is the pricing page's two-row// layout: the largest plan and Enterprise on the right of the second row, with the plan-fit panel filling the left./** * The site's glass ring: a wrapper 8px wider than its card on every side, in the soft brand blue. A wrapper rather than * a spread shadow, so the ring's outer edge shares the card's radius; the negative margin keeps it from adding size. */const GLASS_RING = "-m-2 rounded-card bg-ring-tint p-2";/** Figures in a line (numbers, prices, percentages) that the brand picks out in blue. */const FIGURE = /(\$?\d[\d,.]*(?:K|M|%)?)/;/** A line of text with its figures in brand blue. Pass `pattern` (one capture group) to pick out something else. */function PricingHighlight({ text, pattern = FIGURE, className }: { text: string; pattern?: RegExp; className?: string }) { return ( <> {text.split(new RegExp(pattern, "g")).map((part, i) => i % 2 === 1 ? ( <span key={i} className={cn("text-fg-brand", className)}> {part} </span> ) : ( part ), )} </> );}/** The small tinted pill for a pricing badge, such as what yearly billing saves. */function PriceNotePill({ className, ...props }: React.ComponentProps<"span">) { return <span data-slot="price-note-pill" className={cn("inline-flex h-5 items-center rounded-pill bg-brand/10 px-2 text-caption leading-snug font-medium whitespace-nowrap text-fg-brand", className)} {...props} />;}type BillingPeriod = "monthly" | "yearly";/** * Monthly or yearly. One white pill with a brand-blue hairline glides under the chosen option on the tabs' spring (it * jumps with reduced motion); until it has been placed the chosen button draws the pill itself, so the first paint * looks the same. */function PricingToggle({ value, onValueChange, yearlyNote = "2 months free", labels = { monthly: "Monthly", yearly: "Yearly" }, className,}: { value: BillingPeriod; onValueChange: (value: BillingPeriod) => void; yearlyNote?: React.ReactNode; labels?: Record<BillingPeriod, React.ReactNode>; className?: string;}) { const pillRef = React.useRef<HTMLSpanElement>(null); const placed = useTabGlide(pillRef, value); const surface = "bg-surface shadow-[inset_0_0_0_var(--ds-space-px)_var(--ds-color-brand)]"; return ( <div data-slot="pricing-toggle" className={cn("relative isolate inline-flex items-center rounded-pill bg-surface p-1 ring-1 ring-hairline", className)}> <span ref={pillRef} aria-hidden="true" className={cn("pointer-events-none absolute top-0 left-0 -z-10 rounded-pill", surface, !placed && "invisible")} /> {(["monthly", "yearly"] as const).map((period) => { const selected = value === period; return ( <button key={period} data-tab-id={period} type="button" aria-pressed={selected} onClick={() => onValueChange(period)} className={cn( "inline-flex items-center gap-2 rounded-pill px-4 py-1.5 text-body-sm leading-snug font-medium outline-none transition-[color] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-focus-ring motion-reduce:transition-none", selected ? "text-fg" : "text-fg-muted hover:text-fg", selected && !placed && surface, )} > {labels[period]} {period === "yearly" && yearlyNote && <PriceNotePill>{yearlyNote}</PriceNotePill>} </button> ); })} </div> );}/** * A quota or feature line. `hint` puts a tooltip on it; `label` is the full text for assistive tech when the line shows a * short form (the brand writes "100 concurrent" for "100 concurrent requests"), which also keeps it on one line. */type PricingFeature = string | { text: string; hint?: React.ReactNode; label?: string };type PricingCardProps = { name: string; description?: string; /** The amount as shown, e.g. "$99", or "Custom". */ price: React.ReactNode; /** After the amount, e.g. "/month"; leave out for a custom price. */ period?: React.ReactNode; /** The line above the price, e.g. "$990/yr". */ note?: React.ReactNode; /** The blue part of that line, e.g. "Save $198". */ noteHighlight?: React.ReactNode; /** The struck-through old price beside the amount, for a discount. */ originalPrice?: React.ReactNode; /** The call to action, usually a full-width pill Button (filled `pill` on the featured plan, `pill-outline` elsewhere). */ cta?: React.ReactNode; /** The quota box: credits, overage and rate limit lines. */ quota?: PricingFeature[]; features?: PricingFeature[]; /** The most popular plan. Marks the card for assistive tech; give it the filled button. */ featured?: boolean; /** A small hairline label beside the name, e.g. "Recommended". */ badge?: React.ReactNode; className?: string;};function FeatureLine({ feature }: { feature: PricingFeature }) { const { text, hint, label } = typeof feature === "string" ? { text: feature, hint: undefined, label: undefined } : feature; return ( <p aria-label={label} className={cn("text-body-sm text-fg-nav max-sm:text-center", label && "whitespace-nowrap")}> {hint ? ( <Tooltip> <TooltipTrigger asChild> <span tabIndex={0} className="cursor-help border-b border-dashed border-line-strong outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"> <PricingHighlight text={text} /> </span> </TooltipTrigger> <TooltipContent wrap className="max-w-72">{hint}</TooltipContent> </Tooltip> ) : ( <PricingHighlight text={text} /> )} </p> );}/** A rounded box of lines split by rules, both in the lightest hairline (black at 6%). */function PricingCardBox({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="pricing-card-box" className={cn("mt-2 rounded-card p-4 ring-1 ring-wash-pill ring-inset [&>*+*]:mt-3 [&>*+*]:border-t [&>*+*]:border-wash-pill [&>*+*]:pt-3", className)} {...props} />;}function PricingCard({ name, description, price, period, note, noteHighlight, originalPrice, cta, quota, features, featured = false, badge, className }: PricingCardProps) { return ( <div data-slot="pricing-card" data-featured={featured || undefined} aria-label={featured ? `${name}, most popular` : undefined} role={featured ? "group" : undefined} className={cn("relative flex w-full flex-col bg-surface p-2 leading-snug lg:row-span-5 lg:grid lg:grid-rows-subgrid", className)}> <div className="flex items-start gap-3 p-2 max-sm:justify-center max-sm:text-center"> <div className="min-w-0"> <div className="flex items-center gap-2 max-sm:justify-center"> <h3 className="text-h5 leading-snug font-normal tracking-normal text-fg">{name}</h3> {badge && <span className="inline-flex h-5 items-center rounded-control border border-line px-2 text-caption font-medium text-fg">{badge}</span>} </div> {description && <p className="mt-1 text-body-sm text-fg-muted">{description}</p>} </div> </div> {/* The price sits just above the button, with the note above it, so the price and button stay put when billing switches. */} <div className="relative flex min-h-30 flex-col justify-end px-2 pt-6 pb-3 text-center"> {(note || noteHighlight) && ( <div className="mb-2.5 flex h-5 items-center justify-center"> <span className="inline-flex items-center gap-1.5 text-caption whitespace-nowrap text-fg-nav"> {note} {noteHighlight && <span className={cn("font-medium text-fg-brand", note && "before:mr-1.5 before:text-fg-nav before:content-['·']")}>{noteHighlight}</span>} </span> </div> )} <div className="flex flex-wrap items-baseline justify-center gap-x-1"> <span className="font-data text-h2 leading-none font-medium text-fg">{price}</span> {originalPrice && <span className="text-body-sm font-medium text-fg-faint line-through">{originalPrice}</span>} {period && <span className="text-caption text-fg-muted">{period}</span>} </div> </div> <div className="p-2 [&>*]:w-full">{cta}</div> {quota && quota.length > 0 ? ( <PricingCardBox> {quota.map((line) => ( <FeatureLine key={typeof line === "string" ? line : line.text} feature={line} /> ))} </PricingCardBox> ) : ( <div aria-hidden="true" /> )} {features && features.length > 0 && ( <PricingCardBox> {features.map((line) => ( <FeatureLine key={typeof line === "string" ? line : line.text} feature={line} /> ))} </PricingCardBox> )} </div> );}const COLUMNS: Record<number, string> = { 1: "sm:grid-cols-1 lg:grid-cols-1", 2: "sm:grid-cols-2 lg:grid-cols-2", 3: "sm:grid-cols-2 lg:grid-cols-3", 4: "sm:grid-cols-2 lg:grid-cols-4", 5: "sm:grid-cols-2 lg:grid-cols-5", 6: "sm:grid-cols-2 lg:grid-cols-6",};/** The ringed frame the cards sit in. Cards wrap to a new row after `columns`, each row still aligned by subgrid. */function PricingGrid({ columns = 4, className, children, ...props }: React.ComponentProps<"div"> & { columns?: 1 | 2 | 3 | 4 | 5 | 6 }) { return ( <div data-slot="pricing-grid" className={cn(GLASS_RING, className)} {...props}> <div className="overflow-hidden rounded-card bg-hairline ring-1 ring-hairline"> <div className={cn("grid grid-cols-1 gap-px", COLUMNS[columns])}>{children}</div> </div> </div> );}const COL_START: Record<number, string> = { 1: "lg:col-start-1", 2: "lg:col-start-2", 3: "lg:col-start-3", 4: "lg:col-start-4", 5: "lg:col-start-5", 6: "lg:col-start-6" };const COL_SPAN: Record<number, string> = { 1: "lg:col-span-1", 2: "lg:col-span-2", 3: "lg:col-span-3", 4: "lg:col-span-4", 5: "lg:col-span-5" };function placed(node: React.ReactNode, className: string) { return React.isValidElement<{ className?: string }>(node) ? React.cloneElement(node, { className: cn(node.props.className, className) }) : node;}/** * The pricing page's two-row grid in one ringed frame. Row one holds `first` (Free and every self-serve plan but the * largest); a short white band follows; row two puts `second` (the largest plan and Enterprise) on the right and `panel` * (usually PricingPlanFit) across the columns they leave free. Cards in each row line up through the shared subgrid rows, * and the panel comes last in the DOM, so it stacks last on phones. Pass PricingCards (or elements that take * `className`) in `second`, since each is placed by class. */function PricingShelves({ first, second, panel, className }: { first: React.ReactNode[]; second: React.ReactNode[]; panel?: React.ReactNode; className?: string }) { const columns = Math.max(1, Math.min(first.length, 6)); const panelSpan = Math.max(1, columns - second.length); return ( <div data-slot="pricing-shelves" className={cn(GLASS_RING, className)}> <div className="overflow-hidden rounded-card bg-hairline ring-1 ring-hairline"> <div className={cn("grid grid-cols-1 gap-px lg:grid-rows-[repeat(11,auto)]", COLUMNS[columns])}> {first} <div aria-hidden="true" className="col-span-full h-6 bg-surface lg:row-start-6 lg:h-8" /> {React.Children.map(second, (card, i) => placed(card, cn("lg:row-start-7", COL_START[panelSpan + 1 + i])))} {panel && placed(panel, cn("sm:col-span-2 lg:col-start-1 lg:row-span-5 lg:row-start-7", COL_SPAN[panelSpan]))} </div> </div> </div> );}type PlanFitRow = { name: React.ReactNode; range: React.ReactNode; price: React.ReactNode };/** * "Which plan fits your page volume?": each plan's best-value range in a ringed white card, the price in brand blue, for * the free columns of PricingShelves' second row. `patternSrc` is the faint dot pattern in its top right corner (the * brand's logo-edge.svg), fading out downward. */function PricingPlanFit({ title = ( <> Which plan fits <span className="text-fg-brand">your page volume</span>? </> ), rows, footnote, patternSrc, className,}: { title?: React.ReactNode; rows: PlanFitRow[]; footnote?: React.ReactNode; patternSrc?: string; className?: string;}) { return ( <div data-slot="pricing-plan-fit" className={cn("relative flex w-full flex-col bg-surface p-6 leading-snug lg:p-8", className)}> <div className={cn(GLASS_RING, "relative flex flex-1 flex-col")}> <div className="relative isolate flex flex-1 flex-col justify-center gap-6 overflow-hidden rounded-card bg-surface px-6 py-8 ring-1 ring-wash-pill ring-inset lg:px-10 lg:py-9"> {patternSrc && ( <div aria-hidden="true" className="pointer-events-none absolute top-0 right-0 -z-10 h-2/5 w-full bg-size-[auto_100%] bg-right-top bg-no-repeat opacity-35 [mask-image:linear-gradient(to_bottom,var(--ds-color-black)_30%,transparent)]" style={{ backgroundImage: `url(${patternSrc})` }} /> )} <h3 className="max-w-104 text-h3 leading-heading font-normal tracking-normal text-balance text-fg">{title}</h3> <ul className="flex flex-col [&>li+li]:mt-3 [&>li+li]:border-t [&>li+li]:border-wash-pill [&>li+li]:pt-3"> {rows.map((row, i) => ( <li key={i} className="flex items-baseline gap-4"> <span className="w-22 shrink-0 text-body-sm text-fg sm:w-30">{row.name}</span> <span className="flex-1 text-body-sm text-fg-muted">{row.range}</span> <span className="w-14 shrink-0 text-right text-body-sm text-fg-brand sm:w-18">{row.price}</span> </li> ))} </ul> {footnote && <p className="text-caption text-fg-muted">{footnote}</p>} </div> </div> </div> );}/** A ringed white strip under the plans: a line on the left, an action (an outline pill) on the right. */function PricingBanner({ action, className, children }: { action?: React.ReactNode; className?: string; children: React.ReactNode }) { return ( <div data-slot="pricing-banner" className={cn(GLASS_RING, className)}> <div className="flex flex-col gap-4 rounded-card bg-surface px-6 py-5 ring-1 ring-hairline sm:flex-row sm:items-center sm:justify-between sm:px-8"> <p className="text-body leading-snug text-fg-nav">{children}</p> {action && <div className="shrink-0">{action}</div>} </div> </div> );}type CreditCostRow = { id?: string; title: React.ReactNode; href?: string; cost: React.ReactNode };/** * What each API costs in credits: one line per API in a ringed white table, the name linking to its docs (with a small * arrow) and the cost in brand blue. Open it with a SectionHeading and SectionHeaderRule, as the pricing page does. */function CreditCostsTable({ rows, caption = "API credit costs per request", labels = { name: "API", cost: "Credits" }, className,}: { rows: CreditCostRow[]; caption?: string; labels?: { name: React.ReactNode; cost: React.ReactNode }; className?: string;}) { return ( <div data-slot="credit-costs-table" className={cn(GLASS_RING, className)}> <table className="w-full overflow-hidden rounded-card bg-surface text-left text-body leading-snug ring-1 ring-hairline"> <caption className="sr-only">{caption}</caption> <thead> <tr className="border-b border-wash-pill"> <th scope="col" className="px-6 py-4 font-normal text-fg-muted md:px-8"> {labels.name} </th> <th scope="col" className="px-6 py-4 text-right font-normal text-fg-muted md:px-8"> {labels.cost} </th> </tr> </thead> <tbody className="divide-y divide-wash-pill"> {rows.map((row, i) => ( <tr key={row.id ?? i}> <th scope="row" className="px-6 py-3.5 font-normal md:px-8"> {row.href ? ( <a href={row.href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 rounded-sm text-fg outline-none hover:text-fg-brand focus-visible:ring-2 focus-visible:ring-focus-ring"> {row.title} <ArrowUpRight aria-hidden="true" className="size-3.5 shrink-0 text-fg-faint" /> </a> ) : ( <span className="text-fg">{row.title}</span> )} </th> <td className="px-6 py-3.5 text-right text-fg-brand tabular-nums md:px-8">{row.cost}</td> </tr> ))} </tbody> </table> </div> );}/** The dashed band the pricing page leads into its next section with: dashed vertical hairlines every 12px. Set a height. */function PricingHairlines({ className, ...props }: React.ComponentProps<"div">) { return ( <div aria-hidden="true" data-slot="pricing-hairlines" className={cn("relative w-full overflow-hidden bg-canvas", className)} {...props}> {/* Vertical lines from a repeating gradient, cut into 4-on, 4-off dashes by a repeating mask. */} <span className="absolute inset-0 bg-[repeating-linear-gradient(to_right,var(--ds-color-border-strong)_0_var(--ds-space-px),transparent_var(--ds-space-px)_var(--ds-space-3))] [mask-image:repeating-linear-gradient(to_bottom,var(--ds-color-black)_0_var(--ds-space-1),transparent_var(--ds-space-1)_var(--ds-space-2))]" /> </div> );}export { PricingCard, PricingCardBox, PricingGrid, PricingShelves, PricingPlanFit, PricingBanner, CreditCostsTable, PricingToggle, PricingHighlight, PriceNotePill, PricingHairlines, type PricingCardProps, type PricingFeature, type PlanFitRow, type CreditCostRow, type BillingPeriod,};Reference: src/components/pricing/PricingConcepts.tsx; src/components/pricing/MarketingPricingCard.tsx; src/components/pricing/marketing-pricing-card-styles.ts; src/components/pricing/ApiCreditCostsTable.tsx; src/components/pricing/PricingPageContent.tsx; src/components/pricing/DashedHairlines.tsx.