Component library
Back to catalog

Split stat feature section

A two-column feature section: an eyebrow, a large uppercase heading and a row of hairline-divided figures on the left, a grid of ringed-icon cards on the right.

sectionfeaturesstatsgriddarkstatic

Open preview in a new tab

Use this component

Three ways to get Split stat feature section into one of your own projects. The first two carry the component's files with them, so nothing has to fetch this page.

Start here

Placement prompt

The whole thing: what to do, where it goes, what must not change, and every one of the component's 1 file appended underneath with the path each belongs at. Paste it into Claude Code inside the destination project — it fetches nothing and needs nothing else.

Read it first — the files are added underneath when you copy
Add Split stat feature section from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/split-stat-feature-section/

Place it in:
[ENTER PAGE, FILE, OR ROUTE]

Position:
[ENTER WHERE IT SHOULD APPEAR]

Every file you need is appended below, each fenced with the path it belongs at. Write them there, relative to the project root. Do not import anything from the deployed library at runtime, and do not fetch the library page — the files below are the source of truth.

Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.

Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.

Install kit

The 1 file this component needs and nothing else, each fenced with the path it belongs at, followed by what the library's own verification covered and what it did not. No instructions attached — for placing it yourself, or handing it to something that has its own.

Read it first
Install kit: "Split stat feature section"
Library page: https://astro.baysixmedia.com/components/split-stat-feature-section/
Files: 1 required, 1 supporting

Write each block below to the path named in its header, relative to the project root. A block marked (required) is one the component does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.

===== FILE (required): src/components/SplitStatFeatureSection.astro =====
---
/**
 * Split stat feature section
 *
 * A two-column section: a headline block on the left carrying an eyebrow, a
 * large uppercase heading, a short paragraph and a row of figures divided by
 * hairlines; a grid of feature cards on the right, each with a ringed icon,
 * an uppercase title and a line or two of body.
 *
 * Static: no script at all, and nothing focusable — it is a block of content,
 * not a control. There is nothing for a client-side router to tear down and
 * nothing a motion preference changes.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour, size and duration is a `--fsl-*` custom property on the root.
 */

export interface FeatureStat {
	/** The figure, already formatted — "18K+", "99.9%", "4.8". */
	value: string;
	/** The word under it. Set in small caps-height uppercase. */
	label: string;
}

export interface FeatureCard {
	/** The uppercase title. */
	title: string;
	/** One to three lines under it. */
	body: string;
	/** Which glyph sits in the ring. See GLYPHS below to extend the set. */
	icon?: "cloud" | "monitor" | "spark" | "shield" | "chart" | "lock";
}

interface Props {
	/** The small pill above the heading. `""` hides it. */
	eyebrow?: string;
	/** The heading. Rendered uppercase by the stylesheet, not by the string. */
	heading?: string;
	/** One or two sentences under the heading. `""` hides it. */
	body?: string;
	/** The figures. Three is the measured shape; more wrap. */
	stats?: FeatureStat[];
	/** The cards. Four is the measured shape; the grid takes any number. */
	features?: FeatureCard[];
	/** Heading level for the section title. Cards use the next one down. */
	headingLevel?: "h1" | "h2" | "h3";
	/** The section surface, painted edge to edge. */
	background?: string;
	/** Every text colour and every hairline derives from this. */
	ink?: string;
	/**
	 * The card fill. Separate from `ink` because the reference does not derive
	 * it: dark cards are white at 6 per cent, light ones are white at 55, so
	 * one is a lift off the surface and the other is a panel on top of it.
	 */
	cardBackground?: string;
	/** The card hairline. */
	cardBorderColor?: string;
	/**
	 * Content width before the section padding. `"none"` is the reference,
	 * which does not cap — on a very wide display the two columns simply keep
	 * growing. Most pages will want a number here.
	 */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. */
	id?: string;
}

const {
	eyebrow = "",
	heading = "",
	body = "",
	stats = [],
	features = [],
	headingLevel = "h2",
	background = "#111111",
	ink = "#f4f4f4",
	cardBackground = "rgb(255 255 255 / 0.06)",
	cardBorderColor = "rgb(255 255 255 / 0.12)",
	maxWidth = "none",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const CardHeading = headingLevel === "h1" ? "h2" : headingLevel === "h2" ? "h3" : "h4";

/*
 * A figure needs both its value and its label, and a card needs its title; a
 * partial record is dropped rather than drawn half-empty. With no heading, no
 * figures and no cards there is nothing to show, so nothing renders.
 */
const figures = stats.filter((stat) => stat && stat.value && stat.label);
const cards = features.filter((card) => card && card.title);
const hasContent = Boolean(heading) || figures.length > 0 || cards.length > 0;

/*
 * A <section> is only a landmark when it has an accessible name, so the
 * heading supplies one. The id has to be unique per instance because two of
 * these can sit on one page; it derives from the caller's `id` when there is
 * one and is generated when there is not.
 */
const uid = id ?? `fsl-${Math.random().toString(36).slice(2, 9)}`;
const headingId = `${uid}-heading`;

/*
 * Inlined rather than fetched, so the section makes no request of its own.
 * Drawn on a 24 grid at stroke 1.7, matching the reference's weight. Generic
 * UI glyphs, not anyone's trademark.
 */
const GLYPHS: Record<string, string[]> = {
	cloud: ["M17.3 19H7a4.5 4.5 0 0 1-.8-8.9 6 6 0 0 1 11.5 1.1 4 4 0 0 1-.4 7.8z"],
	monitor: [
		"M3 5.5A1.5 1.5 0 0 1 4.5 4h15A1.5 1.5 0 0 1 21 5.5v9a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 14.5z",
		"M12 16v4",
		"M8 20h8",
	],
	spark: ["M13 2.5 5 13.5h6l-1 8 8-11.5h-6z"],
	shield: ["M12 3.2 19 6v5.6c0 4.2-2.9 7.6-7 8.4-4.1-.8-7-4.2-7-8.4V6z", "m9.2 11.8 2 2 3.6-3.8"],
	chart: ["M3 20h18", "M7.5 20v-5.5", "M12 20V7", "M16.5 20v-9"],
	lock: [
		"M6.5 10.5h11a1 1 0 0 1 1 1V19a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7.5a1 1 0 0 1 1-1z",
		"M9 10.5V8a3 3 0 0 1 6 0v2.5",
	],
};

const style = [
	`--fsl-bg:${background}`,
	`--fsl-ink:${ink}`,
	`--fsl-card-bg:${cardBackground}`,
	`--fsl-card-border:${cardBorderColor}`,
	`--fsl-max:${maxWidth}`,
].join(";");
---

{hasContent && (
<section
	class:list={["split-stat-feature", className]}
	id={id}
	style={style}
	aria-labelledby={heading ? headingId : undefined}
	data-split-stat-feature
>
	<div class="fsl-inner">
		<header class="fsl-head">
			{eyebrow && (
				<p class="fsl-eyebrow"><span class="fsl-pill">{eyebrow}</span></p>
			)}

			{heading && <Heading class="fsl-title" id={headingId}>{heading}</Heading>}

			{body && <p class="fsl-body">{body}</p>}

			{figures.length > 0 && (
				<ul class="fsl-stats">
					{figures.map((stat) => (
						<li class="fsl-stat">
							<span class="fsl-stat-value">{stat.value}</span>
							<span class="fsl-stat-label">{stat.label}</span>
						</li>
					))}
				</ul>
			)}
		</header>

		{cards.length > 0 && (
			<ul class="fsl-cards">
				{cards.map((card) => (
					<li>
						<article class="fsl-card">
							<div class="fsl-ring">
								<svg
									class="fsl-icon"
									viewBox="0 0 24 24"
									fill="none"
									stroke="currentColor"
									stroke-width="1.7"
									stroke-linecap="round"
									stroke-linejoin="round"
									aria-hidden="true"
								>
									{(GLYPHS[card.icon ?? "cloud"] ?? GLYPHS.cloud).map((d) => (
										<path d={d}></path>
									))}
								</svg>
							</div>
							<div class="fsl-card-text">
								<CardHeading class="fsl-card-title">{card.title}</CardHeading>
								{card.body && <p class="fsl-card-body">{card.body}</p>}
							</div>
						</article>
					</li>
				))}
			</ul>
		)}
	</div>
</section>
)}

<style>
	.split-stat-feature {
		/*
		 * Four steps, and they are the reference's own: 1024, 768 and 431 are
		 * where it changes padding, column count and type size. Written as
		 * min-width queries below so the boundaries land on the same pixel.
		 */
		--fsl-pad: 56px;
		--fsl-gap: 56px;
		--fsl-title-size: 48px;
		/* The figure row has its own two steps, and they are not the section's:
		   above 768 the figures are large and hairline-divided, below it they
		   are smaller, closer together and undivided. */
		--fsl-stat-gap: 56px;
		--fsl-stat-size: 36px;
		--fsl-stat-min: 0px;
		--fsl-divider-w: 1px;

		/*
		 * Every tone in the section is one of these alphas over the ink. The
		 * reference uses the same ratios in both its themes, which is why two
		 * colour props are enough to reskin the whole thing.
		 */
		--fsl-a-pill-bg: 0.08;
		--fsl-a-pill-edge: 0.14;
		--fsl-a-pill-text: 0.5;
		--fsl-a-body: 0.62;
		--fsl-a-stat-label: 0.55;
		--fsl-a-divider: 0.14;
		--fsl-a-ring: 0.22;
		--fsl-a-card-body: 0.55;

		--fsl-card-radius: 22px;
		--fsl-card-gap: 12px;

		box-sizing: border-box;
		display: block;
		inline-size: 100%;
		padding: var(--fsl-pad);
		background: var(--fsl-bg);
		color: var(--fsl-ink);
		/* A system stack on purpose: a component in this library fetches
		   nothing, so it can never be waiting on a webfont. */
		font-family:
			system-ui,
			-apple-system,
			"Segoe UI",
			Roboto,
			Helvetica,
			Arial,
			sans-serif;
	}

	.split-stat-feature *,
	.split-stat-feature *::before,
	.split-stat-feature *::after {
		box-sizing: border-box;
	}

	/*
	 * A scoped reset, and it earns its place. Everything below sets type on
	 * classes, which beats inheritance — but it does not beat a host rule
	 * aimed at the bare element, and a marketing page that styles `h2`, `ul`
	 * or `p` globally is the normal case rather than the exception.
	 *
	 * `:where()` keeps this at the specificity of the root alone, so the rules
	 * underneath still win on source order while a host's element selector
	 * loses. The `:global()` is load-bearing: Astro cannot put its scope
	 * attribute inside `:where()`, so without it the compiler emits a second
	 * attribute beside it and the whole thing outranks the class rules.
	 */
	.split-stat-feature :global(:where(h1, h2, h3, h4, p, ul, li, span, article, header)) {
		margin: 0;
		padding: 0;
		border: 0;
		font: inherit;
		color: inherit;
		list-style: none;
	}

	.fsl-inner {
		display: grid;
		grid-template-columns: 1fr 1fr;
		/* Centred against each other: the two columns are rarely the same
		   height, and the reference balances them rather than topping them. */
		align-items: center;
		gap: var(--fsl-gap);
		max-inline-size: var(--fsl-max);
		margin-inline: auto;
	}

	/* ---- the headline column ------------------------------------------- */

	.fsl-head {
		display: flex;
		flex-direction: column;
		gap: 22px;
	}

	.fsl-eyebrow {
		display: flex;
	}

	.fsl-pill {
		display: inline-block;
		padding: 5px 12px;
		border: 1px solid color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-edge) * 100%), transparent);
		border-radius: 999px;
		background: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-bg) * 100%), transparent);
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-text) * 100%), transparent);
		font-size: 12px;
		letter-spacing: 0.1em;
		text-transform: uppercase;
	}

	.fsl-title {
		/*
		 * Uppercased here rather than in the string, so the prop stays
		 * readable prose and a screen reader is not handed shouted text.
		 * Line height is 0.95 at every step, which is the reference's ratio.
		 */
		font-size: var(--fsl-title-size);
		line-height: 0.95;
		font-weight: 400;
		letter-spacing: -0.03em;
		text-transform: uppercase;
		text-wrap: balance;
		color: var(--fsl-ink);
	}

	.fsl-body {
		max-inline-size: 460px;
		font-size: 16px;
		line-height: 1.7;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-body) * 100%), transparent);
	}

	.fsl-stats {
		display: flex;
		flex-wrap: wrap;
		gap: 20px var(--fsl-stat-gap);
		/*
		 * Load-bearing, and it is the fix for a defect the reference has.
		 *
		 * Each divider is drawn half a gap to the LEFT of its own item, so an
		 * item that begins a line puts its divider at a negative offset —
		 * outside this box, where `overflow: hidden` removes it. Written the
		 * obvious way, as `border-inline-end` on every item but the last, the
		 * hairline survives a wrap and ends a line with nothing after it. The
		 * reference does exactly that, and it shows: from about 950px down to
		 * 768 its figure row wraps and leaves a hairline hanging off the end
		 * of the first line. Measured at 900, 850, 800, 780, 769 and 768.
		 */
		overflow: hidden;
	}

	.fsl-stat {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 6px;
		min-inline-size: var(--fsl-stat-min);
	}

	.fsl-stat::before {
		content: "";
		position: absolute;
		inset-block: 0;
		inset-inline-start: calc(var(--fsl-stat-gap) / -2);
		border-inline-start: var(--fsl-divider-w) solid
			color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-divider) * 100%), transparent);
	}

	.fsl-stat-value {
		font-size: var(--fsl-stat-size);
		line-height: 1;
		font-weight: 900;
		letter-spacing: -0.04em;
		/* Tabular, so a figure that changes does not shift the row. */
		font-variant-numeric: tabular-nums;
		color: var(--fsl-ink);
	}

	.fsl-stat-label {
		font-size: 11px;
		line-height: 1.4;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-stat-label) * 100%), transparent);
	}

	/* ---- the card grid -------------------------------------------------- */

	.fsl-cards {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--fsl-card-gap);
	}

	.fsl-card {
		display: flex;
		flex-direction: column;
		gap: 14px;
		/* Full height, so two cards in a row match even when one body wraps
		   to a third line. The reference's row does this by grid default; an
		   explicit height keeps it true when the grid is a single column. */
		block-size: 100%;
		padding: 24px;
		border: 1px solid var(--fsl-card-border);
		border-radius: var(--fsl-card-radius);
		background: var(--fsl-card-bg);
		/* Lift, then a one-pixel highlight along the top edge. */
		box-shadow:
			0 8px 28px color-mix(in srgb, var(--fsl-ink) 6%, transparent),
			inset 0 1px 0 rgb(255 255 255 / 0.06);
	}

	.fsl-ring {
		flex: 0 0 auto;
		display: grid;
		place-items: center;
		inline-size: 40px;
		block-size: 40px;
		border: 1.33px solid color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-ring) * 100%), transparent);
		border-radius: 999px;
	}

	.fsl-icon {
		inline-size: 18px;
		block-size: 18px;
		color: var(--fsl-ink);
	}

	.fsl-card-text {
		display: flex;
		flex-direction: column;
		gap: 6px;
	}

	.fsl-card-title {
		font-size: 16px;
		line-height: 1.2;
		font-weight: 900;
		letter-spacing: -0.03em;
		text-transform: uppercase;
		color: var(--fsl-ink);
	}

	.fsl-card-body {
		font-size: 14px;
		line-height: 1.55;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-card-body) * 100%), transparent);
	}

	/* ---- responsive ----------------------------------------------------- */

	/*
	 * Mobile first, so each boundary lands on the same pixel as the
	 * reference's. The reference changes at 1024, 768 and 431; written as
	 * `max-width` these would each miss a viewport reporting a fractional
	 * width just under the round number.
	 *
	 * Base = the smallest step: 16px padding, one column, one card per row.
	 */
	.split-stat-feature {
		--fsl-pad: 16px;
		--fsl-gap: 32px;
		--fsl-title-size: 28px;
		/* Below the section's own 768 step the figures shrink, close up and
		   lose their dividers entirely — the reference's treatment, measured
		   at 767 and below. The minimum keeps three short figures in an even
		   row the way the reference's are, without clipping a longer label. */
		--fsl-stat-gap: 20px;
		--fsl-stat-size: 28px;
		--fsl-stat-min: 88px;
		--fsl-divider-w: 0;
	}

	.fsl-inner {
		grid-template-columns: minmax(0, 1fr);
	}

	.fsl-cards {
		grid-template-columns: minmax(0, 1fr);
	}

	@media (min-width: 431px) {
		.split-stat-feature {
			--fsl-pad: 24px;
			--fsl-gap: 40px;
			--fsl-title-size: 34px;
		}

		.fsl-cards {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}
	}

	@media (min-width: 768px) {
		.split-stat-feature {
			--fsl-pad: 40px;
			--fsl-gap: 56px;
			--fsl-title-size: 42px;
			--fsl-stat-gap: 56px;
			--fsl-stat-size: 36px;
			--fsl-stat-min: 0px;
			--fsl-divider-w: 1px;
		}

		.fsl-inner {
			grid-template-columns: 1fr 1fr;
		}
	}

	@media (min-width: 1024px) {
		.split-stat-feature {
			--fsl-pad: 56px;
			--fsl-title-size: 48px;
		}
	}

	/* ---- preferences ---------------------------------------------------- */

	@media (forced-colors: active) {
		/*
		 * The cards are a translucent fill and a translucent hairline, the
		 * pill and the icon ring likewise, and none of that survives here.
		 * Each one takes a real border instead.
		 */
		.fsl-card,
		.fsl-pill,
		.fsl-ring {
			border-color: CanvasText;
		}

		.fsl-card {
			box-shadow: none;
		}

		.fsl-stat::before {
			border-inline-start-color: CanvasText;
		}
	}
</style>

===== END src/components/SplitStatFeatureSection.astro =====

===== FILE (supporting): src/components/SplitStatFeatureSection.md =====
# Split stat feature section

A two-column section: an eyebrow, a large uppercase heading, a short
paragraph and a row of hairline-divided figures on the left; a grid of
ringed-icon cards on the right.

Catalog page: `/components/split-stat-feature-section`

## Purpose

The "what this actually does" block on a marketing page — the one that comes
after the hero and has to carry both the claim and the evidence. The left
column argues, the right column enumerates, and the figures sit between them
so the claim has something under it.

Use something else if you have eight features. Four cards is the shape; six
still reads, and past that the grid stops being scannable and becomes a
table.

## Files required

**`SplitStatFeatureSection.astro`** — one file, the whole component.

- No imports at all, not even a type.
- No npm packages, no icon library, no React, no Tailwind, no CSS reset.
- No global stylesheet. Every colour and size is declared on the component's
  own root element as a `--fsl-*` custom property.
- No webfonts and **no images**. Every icon is inline SVG, so the section
  makes **no network request of its own** — nothing to copy into `public/`.
- The only assumption about the host is that Astro compiles `.astro` files.

`SplitStatFeatureSection.md` — this file. Documentation, not required to run.

## Basic usage

```astro
---
import SplitStatFeatureSection from "../components/SplitStatFeatureSection.astro";
---

<SplitStatFeatureSection
  eyebrow="Workflow"
  heading="Start small. Ship often. Scale when it matters."
  body="Everything a small team needs to get work out the door."
  stats={[
    { value: "18K+", label: "Teams onboard" },
    { value: "99.9%", label: "Uptime" },
  ]}
  features={[
    { title: "Shared drafts", body: "Everyone edits the same version.", icon: "cloud" },
    { title: "Locked by default", body: "Permissions start closed.", icon: "shield" },
  ]}
/>
```

No client directive, no wrapper, no init call — the component ships **no
script at all**.

It is a full-bleed band: it paints its own background edge to edge and
applies its own padding, so it drops straight into the page flow.

## Props

Every prop is optional, and no content is built in: with no `heading`, no
`stats` and no `features` the component renders nothing at all. The catalog
demo passes its copy, figures and cards as props.

| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `""` | The pill above the heading. Hidden when empty; never renders the section on its own. |
| `heading` | `string?` | `""` | The heading. **Pass sentence case** — the stylesheet uppercases it. Also names the section landmark; hidden when empty. |
| `body` | `string?` | `""` | One or two sentences under it. Hidden when empty. |
| `stats` | `FeatureStat[]?` | `[]` | The figures. Real numbers only: the default is none, and the row is dropped. A figure missing its value or label is dropped. |
| `features` | `FeatureCard[]?` | `[]` | The cards. A card without a `title` is dropped; one without a `body` shows just its title. |
| `headingLevel` | `"h1" \| "h2" \| "h3"?` | `"h2"` | The section heading's element. Cards use the next level down. |
| `background` | `string?` | `#111111` | The surface it paints, edge to edge. |
| `ink` | `string?` | `#f4f4f4` | Every text colour and hairline derives from this. |
| `cardBackground` | `string?` | `rgb(255 255 255 / 0.06)` | The card fill. |
| `cardBorderColor` | `string?` | `rgb(255 255 255 / 0.12)` | The card hairline. |
| `maxWidth` | `string?` | `"none"` | Content width before the padding. See below. |
| `class` | `string?` | — | Extra class on the root. |
| `id` | `string?` | — | Element id. |

### One figure

| Field | Type | What it does |
| :--- | :--- | :--- |
| `value` | `string` | Already formatted — `"18K+"`, `"99.9%"`. Set in tabular figures. |
| `label` | `string` | The word under it. Uppercased by the stylesheet. |

### One card

| Field | Type | What it does |
| :--- | :--- | :--- |
| `title` | `string` | Uppercased by the stylesheet. |
| `body` | `string` | One to three lines. |
| `icon` | `"cloud" \| "monitor" \| "spark" \| "shield" \| "chart" \| "lock"?` | Which glyph sits in the ring. |

```ts
import type { FeatureStat, FeatureCard } from "../components/SplitStatFeatureSection.astro";
```

### About `maxWidth`

It defaults to `"none"`, which is what the reference does — the two columns
keep growing on a very wide display. That is faithful but rarely what you
want past about 1600px:

```astro
<SplitStatFeatureSection maxWidth="1440px" />
```

### A light section

Every tone derives from `ink` except the card fill, so the light version is
four props:

```astro
<SplitStatFeatureSection
  background="#ececec"
  ink="#111111"
  cardBackground="rgb(255 255 255 / 0.55)"
  cardBorderColor="rgb(17 17 17 / 0.12)"
/>
```

Those are the reference's own light values, read off its theme toggle. There
is no `prefers-color-scheme` rule in the file, on purpose: no component in
this library ships one, because a component that picks its own theme fights
the page it lands in.

### Adding an icon

The glyphs live in a `GLYPHS` map near the top of the file. Once the
component is in your project it is your file — add paths drawn on a 24 grid
at stroke 1.7 and widen the `icon` union to match.

## Behaviour

None. It is a block of content: no script, no state, nothing focusable,
nothing that animates. The only thing that changes is the layout, and only
with the viewport.

## Responsive

Four steps, and they are the reference's own pixels:

| Width | Padding | Columns | Cards | Heading |
| :--- | :--- | :--- | :--- | :--- |
| ≥ 1024 | 56px | two | 2-up | 48px |
| 768–1023 | 40px | two | 2-up | 42px |
| 431–767 | 24px | one | 2-up | 34px |
| ≤ 430 | 16px | one | 1-up | 28px |

Line height is 0.95 at every step.

The figure row has its own step at 768, separate from the section's: above
it the figures are 36px and divided by hairlines, below it they are 28px,
closer together and undivided.

## Accessibility

- It renders a `<section>` **named by its own heading** via
  `aria-labelledby`, so it is a real landmark rather than a generic region.
  The id is generated per instance, so two of these on one page do not
  collide.
- `headingLevel` sets the section heading and the cards follow one level
  down, so the outline stays correct instead of every section being an `h2`.
- **The heading is uppercased by CSS, not by the string.** Pass sentence
  case. Shouted text in the markup is read as shouted text by some screen
  readers, and it is unreadable in the prop.
- Icons are `aria-hidden`. They decorate the card title, which carries the
  meaning.
- Figures are set in tabular numerals, so a number that updates does not
  shift the row.
- Nothing here is focusable, so there is no tab order to get wrong and no
  focus ring to draw.
- Under `forced-colors: active` the cards, the pill, the icon rings and the
  figure dividers each take a real border, since all four are otherwise a
  translucent fill or hairline that does not survive.

## Reduced motion

Nothing moves, so there is nothing to reduce. The component declares
`hasMotion: false` and the readiness record marks the reduced-motion check
*not applicable* rather than claiming a pass it did not earn.

## Without JavaScript

Identical. The component ships no script, so there is nothing to degrade.

## Content length

- **Eyebrow** — one or two words.
- **Heading** — six to ten words. It is set very large and very tight; past
  about twelve words it stops being a heading.
- **Body** — one or two sentences, capped at 460px so it never runs the full
  column width.
- **Figures** — two to four. Values of four or five characters.
- **Cards** — four is the measured shape. Bodies of one to three lines; the
  cards in a row match heights, so a long one sets the row.

## Sizing

Full width, painting its own background edge to edge, with content capped at
`maxWidth` and centred. The two columns are equal halves above 768px and
stack below it, centred against each other vertically so a short left column
sits level with a tall card grid.

Retune from CSS without touching the file:

```css
.my-page .split-stat-feature {
  --fsl-pad: 40px;
  --fsl-card-radius: 12px;
  --fsl-stat-gap: 40px;
}
```

## Differences from the reference

Rebuilt from a live page, measured rather than copied. Every value taken
matches: 56/40/24/16 padding at 1024/768/431, two equal columns at 56px gap,
a 22px card radius with 24px padding and a 12px grid gap, a 40px icon ring
around an 18px glyph, and type at 48/16/36/11/16/14 with the same tracking
and the same 0.95 heading ratio. What differs:

- **The copy, the figures and the icons are original.** Nothing of the
  reference's wording or artwork is reproduced.
- **A wrapped figure row no longer leaves a hairline hanging.** The
  reference draws its dividers as `border-right` on every figure but the
  last, which survives a line break: from about 950px down to 768 its row
  wraps and a hairline ends the first line with nothing after it — measured
  at 900, 850, 800, 780, 769 and 768. Here each divider is drawn half a gap
  to its own left and clipped by the row, so an item that begins a line has
  no divider at all.
- **The heading is a real landmark name.** The reference's section has no
  accessible name.
- **Figures have a minimum width below 768px** so three short ones line up
  evenly, as the reference's do, without clipping a longer label. The
  reference fixes the width instead, which only works for its own copy.
- **Sub-pixel:** the reference's hairlines report 0.667px because its page is
  read at a fractional device pixel ratio; these are authored 1px, as are
  the reference's. The divider being positioned rather than in flow also
  makes each figure run 0.7px narrower than the reference's.
- The reference sets its type in Manrope at weight 900. This uses the system
  stack, so the platform's heaviest face stands in and the headline reads
  slightly differently. That is deliberate: a component in this library
  fetches nothing.

===== END src/components/SplitStatFeatureSection.md =====

--- VERIFIED IN THE COMPONENT LIBRARY ---
NOT production ready in the library.
Verified 2026-09-21 at 1600, 1440, 1280, 1024, 1023, 900, 834, 768, 767, 640, 480, 431, 430, 390, 360, 320px.
Passed: full-page context, keyboard, no javascript, reduced motion, two instances, no overflow, declarations corroborated, lifecycle.
Library source has CHANGED since verification. Treat every result above as expired.

--- NOT PROVEN ---
- ClientRouter: not applicable — this component ships no script.

--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.

--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.

Component reference

A fragment for a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving branding and content open. It does not carry the code.

Read it first
“Split stat feature section” from my Astro Component Library:
https://astro.baysixmedia.com/components/split-stat-feature-section/

Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.

The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.

Component readiness

Judged on recorded evidence, not on how it looks in the frame above. A description of a component is not a result.

Interactivity
Static
Profile
core only
Installation files
1 required + 1 supporting
Verified
2026-09-21
Client navigation
Not applicable — ships no script
Source fingerprint
Source has CHANGED since verification
Production ready
Not yet

Not production ready

Something required failed, is missing, or contradicts a declared fact.

Blocking

  • Component changed after verification — re-run verification and record the new fingerprint.

Verified 2026-09-21 at 1600, 1440, 1280, 1024, 1023, 900, 834, 768, 767, 640, 480, 431, 430, 390, 360, 320px

  • Full-page contextpass
  • Keyboard — declared to contain nothing focusablenot-applicable
  • No JavaScriptpass
  • Reduced motion — declared to have no motionnot-applicable
  • Two instancespass
  • No overflowpass
  • Declarations corroboratedpass
  • Lifecycle — declared to ship no scriptnot-applicable
26 verification notes
  • Every check below was run against the dev server and then re-run against the production build served by `astro preview`. Both agreed.
  • Rebuilt from a live page by measurement, not by copying. Read at 1440px and compared box by box; every value taken matches. Section padding 56px over #111111; two equal columns at a 56px gap, centred against each other rather than topped; header stack at 22px; the eyebrow a 999px pill at 5px/12px padding, 12px type at 0.1em tracking over ink at 8 per cent inside a hairline at 14; the heading 48px on a 0.95 line at -0.03em, uppercased by the stylesheet; the paragraph 16px on 1.7 at 62 per cent, capped at 460px; figures 36px weight 900 at -0.04em over 11px labels at 0.08em and 55 per cent; cards 312px wide at 24px padding, a 22px radius, a 14px stack, a 12px grid gap, ink at 6 per cent inside a hairline at 12, and a 40px ring at 22 per cent around an 18px glyph on a 24 grid at stroke 1.7.
  • The four responsive steps are the reference's own pixels, found by bisection rather than guessed: 1024, 768 and 431. Padding runs 56/40/24/16, the heading 48/42/34/28 on a 0.95 line throughout, the section gap 56/56/40/32, the two columns collapse to one below 768, and the card grid goes 2-up to 1-up below 431. Verified on both sides of every boundary - 1024 against 1023, 768 against 767, 431 against 430 - and each one flips on the same pixel as the reference.
  • Written mobile-first, and that is not a style preference: as `max-width` queries the boundaries would each miss a viewport reporting a fractional width just under the round number, which is how the same mistake was caught on another component in this library. The base block is the smallest step and every query adds to it.
  • The figure row has a second, separate step at 768 that the section's own padding step hides. Above it the figures are 36px and divided by hairlines with 28px of air on each side; below it they are 28px, the gap closes to 20px and the dividers disappear entirely. Measured on the reference at 1440, 1023, 767, 430, 390 and 320.
  • A real defect in the reference, found by measuring rather than by looking, and fixed here. Its dividers are `border-right` on every figure but the last, which survives a line break - so when the figure row wraps, a hairline ends the first line with nothing after it. It wraps on the reference from about 950px down to 768px, which is the whole of its two-column tablet range; confirmed at 900, 850, 800, 780, 769 and 768. Here each divider is drawn half a gap to its own left as a pseudo-element and the row clips its own overflow, so a figure that begins a line puts its divider outside the box and it is simply not painted. Verified across sixteen widths: the divider count tracks the number of same-line neighbours and never once ends a line.
  • The section is a real landmark. It renders a `<section>` named by its own heading through `aria-labelledby`, where the reference's is an unnamed generic region. The id is generated per instance rather than fixed, so two of these on one page do not collide - checked on the fixture, which renders both and reports zero duplicate ids and both names resolving to their own heading.
  • Heading levels are settable and the cards follow. `headingLevel` sets the section heading and the card titles render one level down from it; the fixture's second instance is an h3 with h4 cards, so the relationship is observed rather than asserted.
  • The heading and the card titles are uppercased by the stylesheet, not by the strings. The props carry sentence case, which keeps them readable in a template and keeps shouted text out of the accessibility tree.
  • Static, and checked rather than assumed: zero `<script>` elements, zero transitions and zero animations across every descendant of both fixture instances. The reference is the same - its cards declare `transition: all` with no duration, which is nothing at all.
  • Nothing in the section is focusable, so `keyboard` is recorded not-applicable rather than passed. That verdict is licensed by `hasFocusableContent: false` and corroborated against the rendered fixture, which reports zero focusable descendants. A pass would claim a tab order was exercised; there is none to exercise.
  • `reducedMotion` is recorded not-applicable on the same basis: `hasMotion: false`, corroborated by zero elements with a transition duration or an animation name under reduced-motion emulation, and an identical rendered height with the preference set and unset.
  • Lifecycle and client navigation are not-applicable. The component registers no listeners, holds no state and writes nothing to the document, so there is nothing for a router to strand. A pass would imply a teardown was observed working; there is no teardown.
  • Without JavaScript the section is identical - same height to the tenth of a pixel, all three figures, all four cards, the eyebrow present and the landmark still named. Nothing degrades because nothing was ever doing anything.
  • Forced colours: the cards, the eyebrow pill, the icon rings and the figure dividers are all a translucent fill or hairline, and none of that survives there, so each takes a real border and the card shadow is dropped. Observed under emulation rather than declared from the stylesheet - all four resolve to CanvasText and the shadow reports `none`.
  • Two instances on one page are independent and differ in STATE rather than only in identity: the first is the dark default at full width with an eyebrow, three figures and four cards; the second runs the reference's light palette through the colour props, caps its width at 1100px, drops the eyebrow entirely, carries two figures and six cards, and sits one heading level lower. Zero duplicate ids, no page overflow.
  • Declarations corroborated against the rendered fixture: zero focusable descendants against hasFocusableContent: false, zero transitions and zero animations against hasMotion: false, zero script elements against interactivity: static, both supplied ids reaching their roots against acceptsIdProp, and two independently-configured instances against multiInstanceSupport.
  • No overflow anywhere. Checked at sixteen widths from 1600 down to 320 on both the preview and the fixture route: no descendant escapes the section box on either side, and `scrollWidth === clientWidth` on the document at every one.
  • Install kit confirmed from the component's own page: one required file carried inline with its destination path, plus the README as supporting. There are no assets to declare - all six icons are inline SVG, so the section renders with nothing copied into `public/` and issues no network request of its own.
  • Zero console messages and zero page errors across the preview route, the fixture route and the component page.
  • The copy, the figures and the icons are original to this library. The reference's wording is not reproduced, and the glyphs were drawn here on the same 24 grid at the same 1.7 stroke rather than taken from it.
  • `maxWidth` defaults to `none`, which is the reference's behaviour rather than a good default: it caps nothing, so at 1600px the two columns measure 716px each and keep growing beyond that. Faithful, stated, and overridable - the fixture's second instance sets 1100px to prove the prop works.
  • Two divergences in the figure row are stated rather than absorbed. Below 768px each figure takes an 88px minimum so three short ones line up evenly the way the reference's do; the reference fixes the width instead, which happens to fit its own labels and would clip a longer one. And because the divider is positioned rather than in flow, each figure run measures about 0.7px narrower per divider than the reference's, whose border occupies layout space.
  • The reference sets its type in Manrope at weight 900 and this uses the system stack, so the heaviest available face stands in and the headline reads slightly differently. Deliberate: a component in this library fetches nothing, so it can never be waiting on a webfont.
  • A measurement caveat worth stating rather than hiding: the reference's hairlines report 0.667px because its page is read at a fractional device pixel ratio. Both it and this are authored at 1px.
  • Chromium only. No WebKit or Gecko build was available in this session, and no claim is made about either. The place that gap matters most is the `overflow: hidden` clip that removes a wrapped row's leading divider, since a engine that computed the pseudo-element's position differently would paint it.

Open the verification fixture

src/components/library/SplitStatFeatureSection.astro

one fileNo imports, no companion files, no packages. Copy SplitStatFeatureSection.astro into any Astro project's components folder and render it.

---
/**
 * Split stat feature section
 *
 * A two-column section: a headline block on the left carrying an eyebrow, a
 * large uppercase heading, a short paragraph and a row of figures divided by
 * hairlines; a grid of feature cards on the right, each with a ringed icon,
 * an uppercase title and a line or two of body.
 *
 * Static: no script at all, and nothing focusable — it is a block of content,
 * not a control. There is nothing for a client-side router to tear down and
 * nothing a motion preference changes.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour, size and duration is a `--fsl-*` custom property on the root.
 */

export interface FeatureStat {
	/** The figure, already formatted — "18K+", "99.9%", "4.8". */
	value: string;
	/** The word under it. Set in small caps-height uppercase. */
	label: string;
}

export interface FeatureCard {
	/** The uppercase title. */
	title: string;
	/** One to three lines under it. */
	body: string;
	/** Which glyph sits in the ring. See GLYPHS below to extend the set. */
	icon?: "cloud" | "monitor" | "spark" | "shield" | "chart" | "lock";
}

interface Props {
	/** The small pill above the heading. `""` hides it. */
	eyebrow?: string;
	/** The heading. Rendered uppercase by the stylesheet, not by the string. */
	heading?: string;
	/** One or two sentences under the heading. `""` hides it. */
	body?: string;
	/** The figures. Three is the measured shape; more wrap. */
	stats?: FeatureStat[];
	/** The cards. Four is the measured shape; the grid takes any number. */
	features?: FeatureCard[];
	/** Heading level for the section title. Cards use the next one down. */
	headingLevel?: "h1" | "h2" | "h3";
	/** The section surface, painted edge to edge. */
	background?: string;
	/** Every text colour and every hairline derives from this. */
	ink?: string;
	/**
	 * The card fill. Separate from `ink` because the reference does not derive
	 * it: dark cards are white at 6 per cent, light ones are white at 55, so
	 * one is a lift off the surface and the other is a panel on top of it.
	 */
	cardBackground?: string;
	/** The card hairline. */
	cardBorderColor?: string;
	/**
	 * Content width before the section padding. `"none"` is the reference,
	 * which does not cap — on a very wide display the two columns simply keep
	 * growing. Most pages will want a number here.
	 */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. */
	id?: string;
}

const {
	eyebrow = "",
	heading = "",
	body = "",
	stats = [],
	features = [],
	headingLevel = "h2",
	background = "#111111",
	ink = "#f4f4f4",
	cardBackground = "rgb(255 255 255 / 0.06)",
	cardBorderColor = "rgb(255 255 255 / 0.12)",
	maxWidth = "none",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;
const CardHeading = headingLevel === "h1" ? "h2" : headingLevel === "h2" ? "h3" : "h4";

/*
 * A figure needs both its value and its label, and a card needs its title; a
 * partial record is dropped rather than drawn half-empty. With no heading, no
 * figures and no cards there is nothing to show, so nothing renders.
 */
const figures = stats.filter((stat) => stat && stat.value && stat.label);
const cards = features.filter((card) => card && card.title);
const hasContent = Boolean(heading) || figures.length > 0 || cards.length > 0;

/*
 * A <section> is only a landmark when it has an accessible name, so the
 * heading supplies one. The id has to be unique per instance because two of
 * these can sit on one page; it derives from the caller's `id` when there is
 * one and is generated when there is not.
 */
const uid = id ?? `fsl-${Math.random().toString(36).slice(2, 9)}`;
const headingId = `${uid}-heading`;

/*
 * Inlined rather than fetched, so the section makes no request of its own.
 * Drawn on a 24 grid at stroke 1.7, matching the reference's weight. Generic
 * UI glyphs, not anyone's trademark.
 */
const GLYPHS: Record<string, string[]> = {
	cloud: ["M17.3 19H7a4.5 4.5 0 0 1-.8-8.9 6 6 0 0 1 11.5 1.1 4 4 0 0 1-.4 7.8z"],
	monitor: [
		"M3 5.5A1.5 1.5 0 0 1 4.5 4h15A1.5 1.5 0 0 1 21 5.5v9a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 14.5z",
		"M12 16v4",
		"M8 20h8",
	],
	spark: ["M13 2.5 5 13.5h6l-1 8 8-11.5h-6z"],
	shield: ["M12 3.2 19 6v5.6c0 4.2-2.9 7.6-7 8.4-4.1-.8-7-4.2-7-8.4V6z", "m9.2 11.8 2 2 3.6-3.8"],
	chart: ["M3 20h18", "M7.5 20v-5.5", "M12 20V7", "M16.5 20v-9"],
	lock: [
		"M6.5 10.5h11a1 1 0 0 1 1 1V19a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7.5a1 1 0 0 1 1-1z",
		"M9 10.5V8a3 3 0 0 1 6 0v2.5",
	],
};

const style = [
	`--fsl-bg:${background}`,
	`--fsl-ink:${ink}`,
	`--fsl-card-bg:${cardBackground}`,
	`--fsl-card-border:${cardBorderColor}`,
	`--fsl-max:${maxWidth}`,
].join(";");
---

{hasContent && (
<section
	class:list={["split-stat-feature", className]}
	id={id}
	style={style}
	aria-labelledby={heading ? headingId : undefined}
	data-split-stat-feature
>
	<div class="fsl-inner">
		<header class="fsl-head">
			{eyebrow && (
				<p class="fsl-eyebrow"><span class="fsl-pill">{eyebrow}</span></p>
			)}

			{heading && <Heading class="fsl-title" id={headingId}>{heading}</Heading>}

			{body && <p class="fsl-body">{body}</p>}

			{figures.length > 0 && (
				<ul class="fsl-stats">
					{figures.map((stat) => (
						<li class="fsl-stat">
							<span class="fsl-stat-value">{stat.value}</span>
							<span class="fsl-stat-label">{stat.label}</span>
						</li>
					))}
				</ul>
			)}
		</header>

		{cards.length > 0 && (
			<ul class="fsl-cards">
				{cards.map((card) => (
					<li>
						<article class="fsl-card">
							<div class="fsl-ring">
								<svg
									class="fsl-icon"
									viewBox="0 0 24 24"
									fill="none"
									stroke="currentColor"
									stroke-width="1.7"
									stroke-linecap="round"
									stroke-linejoin="round"
									aria-hidden="true"
								>
									{(GLYPHS[card.icon ?? "cloud"] ?? GLYPHS.cloud).map((d) => (
										<path d={d}></path>
									))}
								</svg>
							</div>
							<div class="fsl-card-text">
								<CardHeading class="fsl-card-title">{card.title}</CardHeading>
								{card.body && <p class="fsl-card-body">{card.body}</p>}
							</div>
						</article>
					</li>
				))}
			</ul>
		)}
	</div>
</section>
)}

<style>
	.split-stat-feature {
		/*
		 * Four steps, and they are the reference's own: 1024, 768 and 431 are
		 * where it changes padding, column count and type size. Written as
		 * min-width queries below so the boundaries land on the same pixel.
		 */
		--fsl-pad: 56px;
		--fsl-gap: 56px;
		--fsl-title-size: 48px;
		/* The figure row has its own two steps, and they are not the section's:
		   above 768 the figures are large and hairline-divided, below it they
		   are smaller, closer together and undivided. */
		--fsl-stat-gap: 56px;
		--fsl-stat-size: 36px;
		--fsl-stat-min: 0px;
		--fsl-divider-w: 1px;

		/*
		 * Every tone in the section is one of these alphas over the ink. The
		 * reference uses the same ratios in both its themes, which is why two
		 * colour props are enough to reskin the whole thing.
		 */
		--fsl-a-pill-bg: 0.08;
		--fsl-a-pill-edge: 0.14;
		--fsl-a-pill-text: 0.5;
		--fsl-a-body: 0.62;
		--fsl-a-stat-label: 0.55;
		--fsl-a-divider: 0.14;
		--fsl-a-ring: 0.22;
		--fsl-a-card-body: 0.55;

		--fsl-card-radius: 22px;
		--fsl-card-gap: 12px;

		box-sizing: border-box;
		display: block;
		inline-size: 100%;
		padding: var(--fsl-pad);
		background: var(--fsl-bg);
		color: var(--fsl-ink);
		/* A system stack on purpose: a component in this library fetches
		   nothing, so it can never be waiting on a webfont. */
		font-family:
			system-ui,
			-apple-system,
			"Segoe UI",
			Roboto,
			Helvetica,
			Arial,
			sans-serif;
	}

	.split-stat-feature *,
	.split-stat-feature *::before,
	.split-stat-feature *::after {
		box-sizing: border-box;
	}

	/*
	 * A scoped reset, and it earns its place. Everything below sets type on
	 * classes, which beats inheritance — but it does not beat a host rule
	 * aimed at the bare element, and a marketing page that styles `h2`, `ul`
	 * or `p` globally is the normal case rather than the exception.
	 *
	 * `:where()` keeps this at the specificity of the root alone, so the rules
	 * underneath still win on source order while a host's element selector
	 * loses. The `:global()` is load-bearing: Astro cannot put its scope
	 * attribute inside `:where()`, so without it the compiler emits a second
	 * attribute beside it and the whole thing outranks the class rules.
	 */
	.split-stat-feature :global(:where(h1, h2, h3, h4, p, ul, li, span, article, header)) {
		margin: 0;
		padding: 0;
		border: 0;
		font: inherit;
		color: inherit;
		list-style: none;
	}

	.fsl-inner {
		display: grid;
		grid-template-columns: 1fr 1fr;
		/* Centred against each other: the two columns are rarely the same
		   height, and the reference balances them rather than topping them. */
		align-items: center;
		gap: var(--fsl-gap);
		max-inline-size: var(--fsl-max);
		margin-inline: auto;
	}

	/* ---- the headline column ------------------------------------------- */

	.fsl-head {
		display: flex;
		flex-direction: column;
		gap: 22px;
	}

	.fsl-eyebrow {
		display: flex;
	}

	.fsl-pill {
		display: inline-block;
		padding: 5px 12px;
		border: 1px solid color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-edge) * 100%), transparent);
		border-radius: 999px;
		background: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-bg) * 100%), transparent);
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-pill-text) * 100%), transparent);
		font-size: 12px;
		letter-spacing: 0.1em;
		text-transform: uppercase;
	}

	.fsl-title {
		/*
		 * Uppercased here rather than in the string, so the prop stays
		 * readable prose and a screen reader is not handed shouted text.
		 * Line height is 0.95 at every step, which is the reference's ratio.
		 */
		font-size: var(--fsl-title-size);
		line-height: 0.95;
		font-weight: 400;
		letter-spacing: -0.03em;
		text-transform: uppercase;
		text-wrap: balance;
		color: var(--fsl-ink);
	}

	.fsl-body {
		max-inline-size: 460px;
		font-size: 16px;
		line-height: 1.7;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-body) * 100%), transparent);
	}

	.fsl-stats {
		display: flex;
		flex-wrap: wrap;
		gap: 20px var(--fsl-stat-gap);
		/*
		 * Load-bearing, and it is the fix for a defect the reference has.
		 *
		 * Each divider is drawn half a gap to the LEFT of its own item, so an
		 * item that begins a line puts its divider at a negative offset —
		 * outside this box, where `overflow: hidden` removes it. Written the
		 * obvious way, as `border-inline-end` on every item but the last, the
		 * hairline survives a wrap and ends a line with nothing after it. The
		 * reference does exactly that, and it shows: from about 950px down to
		 * 768 its figure row wraps and leaves a hairline hanging off the end
		 * of the first line. Measured at 900, 850, 800, 780, 769 and 768.
		 */
		overflow: hidden;
	}

	.fsl-stat {
		position: relative;
		display: flex;
		flex-direction: column;
		gap: 6px;
		min-inline-size: var(--fsl-stat-min);
	}

	.fsl-stat::before {
		content: "";
		position: absolute;
		inset-block: 0;
		inset-inline-start: calc(var(--fsl-stat-gap) / -2);
		border-inline-start: var(--fsl-divider-w) solid
			color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-divider) * 100%), transparent);
	}

	.fsl-stat-value {
		font-size: var(--fsl-stat-size);
		line-height: 1;
		font-weight: 900;
		letter-spacing: -0.04em;
		/* Tabular, so a figure that changes does not shift the row. */
		font-variant-numeric: tabular-nums;
		color: var(--fsl-ink);
	}

	.fsl-stat-label {
		font-size: 11px;
		line-height: 1.4;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-stat-label) * 100%), transparent);
	}

	/* ---- the card grid -------------------------------------------------- */

	.fsl-cards {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--fsl-card-gap);
	}

	.fsl-card {
		display: flex;
		flex-direction: column;
		gap: 14px;
		/* Full height, so two cards in a row match even when one body wraps
		   to a third line. The reference's row does this by grid default; an
		   explicit height keeps it true when the grid is a single column. */
		block-size: 100%;
		padding: 24px;
		border: 1px solid var(--fsl-card-border);
		border-radius: var(--fsl-card-radius);
		background: var(--fsl-card-bg);
		/* Lift, then a one-pixel highlight along the top edge. */
		box-shadow:
			0 8px 28px color-mix(in srgb, var(--fsl-ink) 6%, transparent),
			inset 0 1px 0 rgb(255 255 255 / 0.06);
	}

	.fsl-ring {
		flex: 0 0 auto;
		display: grid;
		place-items: center;
		inline-size: 40px;
		block-size: 40px;
		border: 1.33px solid color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-ring) * 100%), transparent);
		border-radius: 999px;
	}

	.fsl-icon {
		inline-size: 18px;
		block-size: 18px;
		color: var(--fsl-ink);
	}

	.fsl-card-text {
		display: flex;
		flex-direction: column;
		gap: 6px;
	}

	.fsl-card-title {
		font-size: 16px;
		line-height: 1.2;
		font-weight: 900;
		letter-spacing: -0.03em;
		text-transform: uppercase;
		color: var(--fsl-ink);
	}

	.fsl-card-body {
		font-size: 14px;
		line-height: 1.55;
		color: color-mix(in srgb, var(--fsl-ink) calc(var(--fsl-a-card-body) * 100%), transparent);
	}

	/* ---- responsive ----------------------------------------------------- */

	/*
	 * Mobile first, so each boundary lands on the same pixel as the
	 * reference's. The reference changes at 1024, 768 and 431; written as
	 * `max-width` these would each miss a viewport reporting a fractional
	 * width just under the round number.
	 *
	 * Base = the smallest step: 16px padding, one column, one card per row.
	 */
	.split-stat-feature {
		--fsl-pad: 16px;
		--fsl-gap: 32px;
		--fsl-title-size: 28px;
		/* Below the section's own 768 step the figures shrink, close up and
		   lose their dividers entirely — the reference's treatment, measured
		   at 767 and below. The minimum keeps three short figures in an even
		   row the way the reference's are, without clipping a longer label. */
		--fsl-stat-gap: 20px;
		--fsl-stat-size: 28px;
		--fsl-stat-min: 88px;
		--fsl-divider-w: 0;
	}

	.fsl-inner {
		grid-template-columns: minmax(0, 1fr);
	}

	.fsl-cards {
		grid-template-columns: minmax(0, 1fr);
	}

	@media (min-width: 431px) {
		.split-stat-feature {
			--fsl-pad: 24px;
			--fsl-gap: 40px;
			--fsl-title-size: 34px;
		}

		.fsl-cards {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}
	}

	@media (min-width: 768px) {
		.split-stat-feature {
			--fsl-pad: 40px;
			--fsl-gap: 56px;
			--fsl-title-size: 42px;
			--fsl-stat-gap: 56px;
			--fsl-stat-size: 36px;
			--fsl-stat-min: 0px;
			--fsl-divider-w: 1px;
		}

		.fsl-inner {
			grid-template-columns: 1fr 1fr;
		}
	}

	@media (min-width: 1024px) {
		.split-stat-feature {
			--fsl-pad: 56px;
			--fsl-title-size: 48px;
		}
	}

	/* ---- preferences ---------------------------------------------------- */

	@media (forced-colors: active) {
		/*
		 * The cards are a translucent fill and a translucent hairline, the
		 * pill and the icon ring likewise, and none of that survives here.
		 * Each one takes a real border instead.
		 */
		.fsl-card,
		.fsl-pill,
		.fsl-ring {
			border-color: CanvasText;
		}

		.fsl-card {
			box-shadow: none;
		}

		.fsl-stat::before {
			border-inline-start-color: CanvasText;
		}
	}
</style>