Component library
Back to catalog

Step reveal rail

An editorial process rail: numbered steps side by side under a progress rail, where the chosen step widens to uncover its illustration and the rest close to clean columns of text. Keyboard, tap and pointer all choose a step; narrow rails become a list that reveals the chosen step's picture.

sectionstepsprocesshoverdark

Open preview in a new tab

Use this component

Three ways to get Step reveal rail 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 4 files 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 Step reveal rail from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/step-reveal-rail/

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 4 files 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: "Step reveal rail"
Library page: https://astro.baysixmedia.com/components/step-reveal-rail/
Files: 4 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/StepRevealRail.astro =====
---
/**
 * Step reveal rail — an editorial process rail.
 *
 * Numbered steps side by side in one panel, divided by hairlines, with a
 * progress rail across the top. One step is open: it widens far enough to
 * uncover its picture while the others close to columns of text. Choosing a
 * step — by pointer, tap or keyboard — opens it; on a device with hover,
 * pointing previews a step and leaving returns to the chosen one.
 *
 * The reveal is a CLIP, not a slide. Each step lays its text column and its
 * picture out at a fixed offset from its own left edge, so nothing inside a
 * step moves while the row redistributes: the open step's right edge sweeps
 * past the picture and uncovers it, and the type never reflows mid-animation.
 * Closed steps show their number, their whole title and the start of their
 * text, ending on a clean ellipsis rather than a word cut in half.
 *
 * No script. The chosen step is a native radio in each step's header —
 * arrow keys move between steps, Tab leaves the group — and `:has()` does the
 * rest. Every word is in the document in every state; only the pictures of
 * closed steps are covered. Below the row's width the rail becomes a list:
 * every step's words stay visible and the chosen step also shows its picture.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour, size and duration is a `--srr-*` custom property on the root.
 */

export interface StepRevealRailStep {
	/**
	 * The marker for the step. Left out, it is the position in the list,
	 * zero-padded to two digits — "01", "02". Set it for lettered or
	 * differently-formatted sequences.
	 */
	number?: string;
	/** The step's name. The only required field. */
	title: string;
	/** A sentence or two under it. */
	body?: string;
	/** The illustration. Cropped to fill, so the subject wants to be central. */
	image?: string;
	/**
	 * Alternative text. Left empty the picture is marked decorative, which is
	 * right when it illustrates the words rather than adding to them.
	 */
	imageAlt?: string;
	/** Makes the title a link. */
	href?: string;
}

interface Props {
	/** The steps, in order. */
	steps?: StepRevealRailStep[];
	/**
	 * Which step is open to begin with. Out-of-range values clamp to the first
	 * step rather than leaving the rail with nothing open.
	 */
	restIndex?: number;
	/** Heading level for each step's title. */
	headingLevel?: "h2" | "h3" | "h4" | "p";
	/** The panel behind the steps. */
	railBackground?: string;
	/** Each step's own fill. */
	cardBackground?: string;
	/** The mat the picture sits on. */
	mat?: string;
	/** The progress rail, the open step's number and the focus ring. */
	accent?: string;
	/** Titles. */
	ink?: string;
	/** Body copy and closed-step numbers. */
	mutedInk?: string;
	/** Corner radius, shared by the panel and the pictures. */
	radius?: string;
	/** Width the rail stops growing at. */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. Also names the step control, so two rails on a page stay independent. */
	id?: string;
}

/* No demo content: the steps are a prop, and with none the rail renders nothing. The catalog preview passes its own. */
const {
	steps = [],
	restIndex = 0,
	headingLevel = "h3",
	railBackground = "#161514",
	cardBackground = "#161514",
	mat = "#23211e",
	accent = "#e3b35f",
	ink = "#f4f0e8",
	mutedInk = "#b9b2a6",
	radius = "18px",
	maxWidth = "1180px",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

/* A step with no title has nothing to be, so it is dropped. No steps left, no rail. */
const items = (steps ?? []).filter((step) => step?.title);
const rest = Number.isInteger(restIndex) && restIndex >= 0 && restIndex < items.length ? restIndex : 0;

/* Only schemes that navigate: the component is copied into projects where a step's href may come from content. */
const SAFE_SCHEME = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safe = (value?: string) => (value && SAFE_SCHEME.test(value.trim()) ? value.trim() : undefined);

/*
 * How much wider the open step is than a closed one. Fewer steps can afford
 * a wider open step; more steps keep the closed columns readable.
 */
const count = items.length;
const GROW = count <= 3 ? 2.1 : 2;
/* Five or more steps do not fit a readable row at any width: they are always the list. */
const dense = count >= 5;

/* The radio group's name: from the id when given, otherwise unique per render. */
const group = `srr-${id ?? Math.random().toString(36).slice(2, 10)}`;

const style = [
	`--srr-count:${count}`,
	`--srr-grow:${GROW}`,
	`--srr-rail-bg:${railBackground}`,
	`--srr-card-bg:${cardBackground}`,
	`--srr-mat:${mat}`,
	`--srr-accent:${accent}`,
	`--srr-ink:${ink}`,
	`--srr-muted:${mutedInk}`,
	`--srr-radius:${radius}`,
	`--srr-max:${maxWidth}`,
].join(";");
---

{count > 0 && (
<div class:list={["step-reveal-rail", className]} id={id} style={style} data-step-reveal-rail>
	<ol class="srr-rail" data-dense={dense ? "" : undefined} role="list">
		{
			items.map((step, index) => {
				const href = safe(step.href);
				const src = safe(step.image);
				const marker = step.number ?? String(index + 1).padStart(2, "0");
				const inputId = `${group}-${index}`;
				return (
					<li class="srr-step" data-bare={src ? undefined : ""}>
						<input class="srr-input" type="radio" name={group} id={inputId} checked={index === rest} />
						<div class="srr-clip">
							<div class="srr-inner">
								<div class="srr-text">
									<label class="srr-pick" for={inputId}>
										<span class="srr-number">{marker}</span>
										<span class="srr-sr">: {step.title}</span>
									</label>
									<div class="srr-copy">
										<Heading class="srr-title">
											{href ? (
												<a class="srr-link" href={href}>
													{step.title}
												</a>
											) : (
												step.title
											)}
										</Heading>
										{step.body && <p class="srr-body">{step.body}</p>}
									</div>
								</div>
								{src && (
									<div class="srr-figure">
										<div class="srr-mat">
											<img class="srr-image" src={src} alt={step.imageAlt ?? ""} width="900" height="1200" loading="lazy" decoding="async" />
										</div>
									</div>
								)}
							</div>
						</div>
					</li>
				);
			})
		}
	</ol>
</div>
)}

<style>
	.step-reveal-rail {
		--srr-pad: clamp(18px, 2.2cqw, 30px);
		--srr-gap-inner: 18px;
		--srr-number-size: clamp(1.75rem, 1.3rem + 1.4cqw, 2.5rem);
		--srr-title-size: clamp(1.125rem, 1rem + 0.45cqw, 1.3125rem);
		--srr-body-size: 0.9375rem;
		--srr-duration: 380ms;
		--srr-ease: cubic-bezier(0.22, 1, 0.36, 1);

		/* The query container: the layout follows the rail's own width, not the window's. */
		container: srr / inline-size;
		box-sizing: border-box;
		inline-size: 100%;
		max-inline-size: var(--srr-max);
		margin-inline: auto;
		color: var(--srr-ink);
		font-family: inherit;
	}

	.step-reveal-rail *,
	.step-reveal-rail *::before,
	.step-reveal-rail *::after {
		box-sizing: border-box;
	}

	/* Scoped reset: a host's bare element rules (`h3 { margin }`, `ol { padding }`) must not move the rail. */
	.step-reveal-rail :global(:where(h1, h2, h3, h4, h5, h6, p, ol, li, a, span, label)) {
		margin: 0;
		padding: 0;
		border: 0;
		background: none;
		font: inherit;
		color: inherit;
		letter-spacing: inherit;
		text-align: inherit;
		text-transform: none;
		list-style: none;
	}

	/* ---- the list (narrow rails, and five-plus steps below their row width) ---- */

	.srr-rail {
		display: flex;
		flex-direction: column;
		border-radius: var(--srr-radius);
		background: var(--srr-rail-bg);
		padding: 6px clamp(16px, 4cqw, 28px);
	}

	.srr-step {
		position: relative;
		background: var(--srr-card-bg);
		border-block-start: 1px solid rgb(255 255 255 / 0.1);
	}

	.srr-step:first-child {
		border-block-start: 0;
	}

	/* The rail segment: a hairline for steps ahead, accent for the chosen step and those before it. */
	.srr-step::before {
		content: "";
		position: absolute;
		inset-block-start: -1px;
		inset-inline: 0;
		block-size: 1px;
		background: transparent;
		transition:
			background-color var(--srr-duration) var(--srr-ease),
			block-size var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked)::before {
		block-size: 2px;
		background: var(--srr-accent);
	}

	.srr-input {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		margin: 0;
		opacity: 0;
		pointer-events: none;
	}

	.srr-clip {
		position: relative;
	}

	.srr-inner {
		display: grid;
		grid-template-columns: auto minmax(0, 1fr);
		column-gap: clamp(16px, 4cqw, 28px);
		padding-block: clamp(20px, 5cqw, 28px);
	}

	.srr-text {
		display: contents;
	}

	/* The whole step is the control: the label's box is stretched over it; links sit above. */
	.srr-pick {
		display: block;
		cursor: pointer;
	}

	.srr-pick::after {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 0;
	}

	.srr-number {
		display: block;
		min-inline-size: 2ch;
		font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
		font-size: var(--srr-number-size);
		font-weight: 400;
		line-height: 1;
		font-variant-numeric: tabular-nums lining-nums;
		letter-spacing: -0.02em;
		color: color-mix(in srgb, var(--srr-ink) 72%, var(--srr-card-bg));
		transition: color var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked) .srr-number {
		color: var(--srr-accent);
	}

	.srr-sr {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.srr-copy {
		display: flex;
		flex-direction: column;
		gap: 10px;
		min-inline-size: 0;
		padding-block-start: 4px;
	}

	.srr-title {
		position: relative;
		z-index: 1;
		inline-size: fit-content;
		max-inline-size: 100%;
		font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
		font-size: var(--srr-title-size);
		font-weight: 600;
		line-height: 1.22;
		letter-spacing: -0.005em;
		color: var(--srr-ink);
		overflow-wrap: break-word;
		hyphens: manual;
		text-wrap: balance;
	}

	/* The padding makes a one-line title link a 24px-tall target; the negative margin keeps the layout where it was. */
	.srr-link {
		display: inline-block;
		padding-block: 0.12em;
		margin-block: -0.12em;
		color: inherit;
		text-decoration: none;
	}

	.srr-link:hover {
		text-decoration: underline;
		text-decoration-thickness: 1px;
		text-underline-offset: 0.18em;
	}

	.srr-link:focus-visible {
		outline: 2px solid var(--srr-accent);
		outline-offset: 4px;
		border-radius: 2px;
	}

	.srr-body {
		max-inline-size: 58ch;
		font-size: var(--srr-body-size);
		font-weight: 400;
		line-height: 1.55;
		color: var(--srr-muted);
		overflow-wrap: break-word;
		hyphens: manual;
		text-wrap: pretty;
	}

	/* The chosen step's picture opens beneath its words; the others stay closed. */
	.srr-figure {
		grid-column: 2;
		display: grid;
		grid-template-rows: 0fr;
		transition: grid-template-rows var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked) .srr-figure {
		grid-template-rows: 1fr;
	}

	.srr-mat {
		position: relative;
		min-block-size: 0;
		overflow: hidden;
		border-radius: calc(var(--srr-radius) - 6px);
		background: var(--srr-mat);
	}

	.srr-step:has(.srr-input:checked) .srr-mat {
		margin-block-start: 18px;
		aspect-ratio: 4 / 3;
	}

	.srr-image {
		position: absolute;
		inset: 0;
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
		object-position: center;
	}

	/* Keyboard focus on the step's radio: a ring round the whole step. */
	.srr-step:has(.srr-input:focus-visible) {
		outline: 2px solid var(--srr-accent);
		outline-offset: 2px;
		border-radius: 6px;
	}

	.srr-step:not(:has(.srr-input:checked)):hover .srr-number {
		color: var(--srr-ink);
	}

	/* ---- the row ------------------------------------------------------------ */

	/* A wide list (five or six steps on a desktop, or 720–879px): the chosen step's picture sits beside its words. */
	@container srr (min-width: 720px) {
		.srr-inner {
			grid-template-columns: auto minmax(0, 1fr) minmax(0, 38%);
		}

		.srr-figure {
			grid-column: 3;
			grid-row: 1;
			align-self: start;
		}

		.srr-step:has(.srr-input:checked) .srr-mat {
			margin-block-start: 0;
		}
	}

	/* The row: up to four steps, from a rail width of 880px. Five or six steps stay a list. */
	@container srr (min-width: 880px) {
		.srr-rail:not([data-dense]) {
			flex-direction: row;
			padding: 0;
			overflow: hidden;
		}

		.srr-rail:not([data-dense]) > .srr-step {
			--srr-track: calc(100cqw - (var(--srr-count) - 1) * 1px);
			--srr-closed-w: calc(var(--srr-track) / (var(--srr-grow) + var(--srr-count) - 1));
			--srr-open-w: calc(var(--srr-closed-w) * var(--srr-grow));
			--srr-text-w: calc(var(--srr-closed-w) - 2 * var(--srr-pad));
			--srr-fig-w: max(0px, calc(var(--srr-open-w) - var(--srr-closed-w) - var(--srr-pad)));

			flex: 1 1 0;
			min-inline-size: 0;
			border-block-start: 0;
			border-inline-start: 1px solid rgb(255 255 255 / 0.1);
			transition: flex-grow var(--srr-duration) var(--srr-ease);
		}

		.srr-rail:not([data-dense]) > .srr-step:first-child {
			border-inline-start: 0;
		}

		/* Across the top: the progress rail. Hairline ahead, accent up to and including the chosen step. */
		.srr-rail:not([data-dense]) > .srr-step::before {
			inset-block-start: 0;
			block-size: 3px;
			background: rgb(255 255 255 / 0.08);
		}

		.srr-rail:not([data-dense]) > .srr-step:has(~ .srr-step .srr-input:checked)::before {
			background: color-mix(in srgb, var(--srr-accent) 45%, transparent);
		}

		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked)::before {
			block-size: 3px;
			background: var(--srr-accent);
		}

		/* Open: the chosen step — or, on a device with hover, the one being pointed at. */
		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked) {
			flex-grow: var(--srr-grow);
		}

		.srr-rail:not([data-dense]) > .srr-step .srr-clip {
			block-size: 100%;
			overflow: hidden;
		}

		.srr-rail:not([data-dense]) .srr-inner {
			display: flex;
			align-items: stretch;
			gap: var(--srr-pad);
			block-size: 100%;
			padding: calc(var(--srr-pad) + 14px) 0 var(--srr-pad) var(--srr-pad);
		}

		.srr-rail:not([data-dense]) .srr-text {
			display: flex;
			flex-direction: column;
			justify-content: flex-start;
			gap: clamp(28px, 4cqw, 56px);
			flex: 0 0 auto;
			inline-size: var(--srr-text-w);
			min-block-size: calc(var(--srr-fig-w) * 4 / 3);
		}

		/* A step with no picture has nothing to uncover: its words take the step's whole width. */
		.srr-rail:not([data-dense]) > .srr-step[data-bare] .srr-text {
			flex: 1 1 auto;
			inline-size: auto;
			min-inline-size: var(--srr-text-w);
			padding-inline-end: var(--srr-pad);
		}

		.srr-rail:not([data-dense]) .srr-copy {
			padding: 0;
		}

		.srr-rail:not([data-dense]) .srr-figure {
			display: block;
			flex: 0 0 auto;
			inline-size: var(--srr-fig-w);
			align-self: center;
		}

		.srr-rail:not([data-dense]) .srr-mat,
		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked) .srr-mat {
			margin: 0;
			aspect-ratio: 3 / 4;
		}

		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:focus-visible) {
			outline-offset: -3px;
			border-radius: 0;
		}
	}

	/* Hover previews a step on devices that have hover; leaving returns to the chosen one. Row layout only. */
	@media (hover: hover) {
		@container srr (min-width: 880px) {
			.srr-rail:not([data-dense]):has(> .srr-step:hover) > .srr-step:has(.srr-input:checked):not(:hover) {
				flex-grow: 1;
			}

			.srr-rail:not([data-dense]) > .srr-step:hover {
				flex-grow: var(--srr-grow);
			}
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.step-reveal-rail {
			--srr-duration: 0.01ms;
		}
	}

	@media (forced-colors: active) {
		.srr-rail,
		.srr-step,
		.srr-mat {
			border: 1px solid CanvasText;
		}

		.srr-step:has(.srr-input:checked)::before {
			background: Highlight;
		}

		.srr-step:has(.srr-input:focus-visible),
		.srr-link:focus-visible {
			outline-color: Highlight;
		}
	}
</style>

===== END src/components/StepRevealRail.astro =====

===== FILE (supporting): src/components/StepRevealRail.md =====
# Step reveal rail

An editorial process rail: numbered steps side by side in one panel, divided
by hairlines, with a progress rail across the top. The chosen step widens far
enough to uncover its picture; the others close to clean columns of text.
Choose a step by pointer, tap or keyboard; on a device with hover, pointing
previews a step and leaving returns to the chosen one.

Catalog page: `/components/step-reveal-rail`

## Purpose

For a "how it works", "what to expect" or project-workflow section with three
or four steps (five or six render as the list layout), where each step has a picture worth showing but not six
pictures' worth of room. The reveal buys the space: one picture at a time,
and the words for every step all the time.

Use something else if every step's picture has to be visible at once — that
is a grid.

## Files required

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

- No imports, no npm packages, no icon library, no CSS reset.
- **No script.** The chosen step is a native radio in each step's header and
  `:has()` does the rest. Nothing to boot, tear down, or strand.
- Every colour, size and duration is a `--srr-*` custom property on the root.
- No webfont: body type is inherited from the page; titles and numbers use
  the system serif stack (`ui-serif, Georgia, …`), so nothing is fetched.

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

The catalog demo passes three illustrations from `public/step-reveal-rail/`
as props; the component has no default pictures.

## Basic usage

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

<StepRevealRail
  steps={[
    { title: "Book an inspection", body: "A technician looks before anyone quotes.", image: "/steps/inspect.jpg", imageAlt: "" },
    { title: "Agree the price", body: "Nothing starts until you sign the option you chose.", image: "/steps/price.jpg", imageAlt: "" },
    { title: "Repair and sign-off", body: "A second look confirms the result.", image: "/steps/repair.jpg", imageAlt: "" },
  ]}
/>
```

## Props

| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `steps` | `StepRevealRailStep[]?` | `[]` | The steps, in order. None: nothing renders. A step without a `title` is dropped. |
| `restIndex` | `number?` | `0` | Which step is open to begin with. Out of range clamps to the first. |
| `headingLevel` | `"h2" \| "h3" \| "h4" \| "p"?` | `"h3"` | Element for each title. |
| `railBackground`, `cardBackground` | `string?` | `#161514` | The panel and each step. |
| `mat` | `string?` | `#23211e` | Behind each picture. |
| `accent` | `string?` | `#e3b35f` | Progress rail, the open step's number, focus ring. |
| `ink`, `mutedInk` | `string?` | `#f4f0e8`, `#b9b2a6` | Titles; body copy and closed numbers. |
| `radius` | `string?` | `18px` | Panel and picture corners. |
| `maxWidth` | `string?` | `1180px` | Where the rail stops growing. |
| `id`, `class` | `string?` | none | Root id (also names the step control) and extra class. |

### A step

```ts
interface StepRevealRailStep {
  number?: string;   // marker; defaults to "01", "02"…
  title: string;     // required
  body?: string;
  image?: string;    // cropped to fill; no image: the step's words take its whole width
  imageAlt?: string; // "" marks it decorative
  href?: string;     // makes the title a link
}
```

## How the reveal works

A CLIP, not a slide. Each step lays its text column and picture out at a
fixed offset from its own left edge, so nothing moves while the row
redistributes: the open step's right edge sweeps past the picture and
uncovers it, and the type never reflows mid-animation. Closed steps show
their number, title and full text — only the picture is covered. The open
step is 2.1× a closed one with three steps and 2× with four. Titles are never
hyphenated.

## Layouts

| Rail width | Layout |
| :--- | :--- |
| ≥ 880, up to four steps | the row with the reveal |
| 720–879, or five or six steps from 720 | a list: number beside words for every step; the chosen step's picture opens beside its words |
| < 720 | the same list; the chosen step's picture opens beneath its words |

The layout follows the rail's own width through container queries, so it
behaves the same in a narrow column on a wide page. Tapping a step works in
both layouts.

## Keyboard

The steps are a radio group: Tab reaches the chosen step, arrow keys move
between steps (opening each), and Tab again leaves the group — one tab stop
per rail however many steps it has. Title links, where a step has an `href`,
are ordinary tab stops after it. The whole step shows a 2px accent focus
ring.

## Accessibility notes

- Every word of every step is in the document and the accessibility tree in
  every state; only closed steps' pictures are covered visually.
- Each radio's accessible name is "01: Step title".
- Pictures are decorative by default (`imageAlt: ""`).
- Under `forced-colors: active` the panel, steps and mats take real borders
  and the chosen step's rail segment and the focus ring use `Highlight`.

## Motion

One transition: the columns resize over 380ms on
`cubic-bezier(0.22, 1, 0.36, 1)`, and the list layout's picture opens on the
same curve. Under `prefers-reduced-motion: reduce` both are instant; the
behaviour is unchanged.

===== END src/components/StepRevealRail.md =====

===== FILE (required): public/step-reveal-rail/scene-1.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1200" width="900" height="1200" role="img">
  <defs>
    <linearGradient id="s1sky" x1="0" y1="0" x2="0.25" y2="1">
      <stop offset="0" stop-color="#080d18"/><stop offset="0.5" stop-color="#14263c"/>
      <stop offset="1" stop-color="#0b1720"/></linearGradient>
    <radialGradient id="s1glow" cx="0.5" cy="0.44" r="0.46">
      <stop offset="0" stop-color="#2f8f5a" stop-opacity="0.34"/>
      <stop offset="1" stop-color="#2f8f5a" stop-opacity="0"/></radialGradient>
    <linearGradient id="s1link" x1="0" y1="0" x2="1" y2="0">
      <stop offset="0" stop-color="#39ff14" stop-opacity="0.15"/>
      <stop offset="0.5" stop-color="#39ff14" stop-opacity="0.75"/>
      <stop offset="1" stop-color="#39ff14" stop-opacity="0.15"/></linearGradient>
  </defs>
  <rect width="900" height="1200" fill="url(#s1sky)"/>
  <rect width="900" height="1200" fill="url(#s1glow)"/>
  <g stroke="#7f8ea3" stroke-opacity="0.12" stroke-width="2">
    <path d="M0 340h900M0 600h900M0 860h900M230 240v720M450 240v720M670 240v720"/>
  </g>
  <g stroke="url(#s1link)" stroke-width="5" stroke-linecap="round" fill="none">
    <path d="M230 470 450 600"/><path d="M450 600 670 470"/><path d="M450 600 450 830"/>
  </g>
  <g fill="#0c1826" stroke="#4d6076" stroke-width="4">
    <rect x="170" y="410" width="120" height="120" rx="30"/>
    <rect x="610" y="410" width="120" height="120" rx="30"/>
    <rect x="390" y="770" width="120" height="120" rx="30"/>
  </g>
  <rect x="380" y="530" width="140" height="140" rx="38" fill="#12331b" stroke="#39ff14" stroke-width="5"/>
  <g fill="#39ff14">
    <circle cx="450" cy="600" r="16"/>
    <rect x="212" y="452" width="36" height="8" rx="4" opacity="0.55"/>
    <rect x="652" y="452" width="36" height="8" rx="4" opacity="0.55"/>
    <rect x="432" y="812" width="36" height="8" rx="4" opacity="0.55"/>
  </g>
  <g fill="#f4b740" opacity="0.7">
    <circle cx="742" cy="286" r="7"/><circle cx="168" cy="932" r="5"/><circle cx="806" cy="742" r="5"/>
  </g>
</svg>

===== END public/step-reveal-rail/scene-1.svg =====

===== FILE (required): public/step-reveal-rail/scene-2.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1200" width="900" height="1200" role="img">
  <defs>
    <linearGradient id="s2sky" x1="0.1" y1="0" x2="0.9" y2="1">
      <stop offset="0" stop-color="#0d0a18"/><stop offset="0.55" stop-color="#241a3a"/>
      <stop offset="1" stop-color="#0f1622"/></linearGradient>
    <radialGradient id="s2glow" cx="0.62" cy="0.38" r="0.55">
      <stop offset="0" stop-color="#7c5cff" stop-opacity="0.38"/>
      <stop offset="1" stop-color="#7c5cff" stop-opacity="0"/></radialGradient>
    <linearGradient id="s2face" x1="0" y1="0" x2="0.6" y2="1">
      <stop offset="0" stop-color="#1b2740"/><stop offset="1" stop-color="#101827"/></linearGradient>
  </defs>
  <rect width="900" height="1200" fill="url(#s2sky)"/>
  <rect width="900" height="1200" fill="url(#s2glow)"/>
  <g stroke="#8b7fb8" stroke-opacity="0.14" stroke-width="2">
    <path d="M0 420h900M0 690h900M300 260v760M600 260v760"/>
  </g>
  <g fill="url(#s2face)" stroke="#42537a" stroke-width="4">
    <rect x="150" y="700" width="600" height="120" rx="26"/>
    <rect x="210" y="546" width="480" height="120" rx="26"/>
    <rect x="270" y="392" width="360" height="120" rx="26"/>
  </g>
  <g fill="#39ff14" opacity="0.85">
    <rect x="186" y="748" width="120" height="10" rx="5"/>
    <rect x="246" y="594" width="90" height="10" rx="5"/>
    <rect x="306" y="440" width="60" height="10" rx="5"/>
  </g>
  <g fill="#6d7f9e" opacity="0.6">
    <rect x="186" y="774" width="250" height="8" rx="4"/>
    <rect x="246" y="620" width="190" height="8" rx="4"/>
    <rect x="306" y="466" width="130" height="8" rx="4"/>
  </g>
  <g stroke="#39ff14" stroke-width="5" stroke-linecap="round" fill="none" opacity="0.7">
    <path d="M690 606h60v154h-60"/><path d="M630 452h120v154h-60"/>
  </g>
  <g fill="#39ff14"><circle cx="750" cy="452" r="14"/><circle cx="690" cy="760" r="14"/></g>
  <g fill="#f4b740" opacity="0.65">
    <circle cx="160" cy="330" r="6"/><circle cx="792" cy="898" r="5"/>
  </g>
</svg>

===== END public/step-reveal-rail/scene-2.svg =====

===== FILE (required): public/step-reveal-rail/scene-3.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 1200" width="900" height="1200" role="img">
  <defs>
    <linearGradient id="s3sky" x1="0" y1="0" x2="0.15" y2="1">
      <stop offset="0" stop-color="#060b16"/><stop offset="0.48" stop-color="#0d2b3a"/>
      <stop offset="1" stop-color="#103028"/></linearGradient>
    <radialGradient id="s3dawn" cx="0.5" cy="0.62" r="0.48">
      <stop offset="0" stop-color="#39ff14" stop-opacity="0.42"/>
      <stop offset="0.6" stop-color="#1e8f3a" stop-opacity="0.16"/>
      <stop offset="1" stop-color="#39ff14" stop-opacity="0"/></radialGradient>
    <linearGradient id="s3beam" x1="0" y1="1" x2="0" y2="0">
      <stop offset="0" stop-color="#39ff14" stop-opacity="0.5"/>
      <stop offset="1" stop-color="#39ff14" stop-opacity="0"/></linearGradient>
  </defs>
  <rect width="900" height="1200" fill="url(#s3sky)"/>
  <g fill="#cfe4ff" opacity="0.5">
    <circle cx="140" cy="180" r="4"/><circle cx="310" cy="112" r="3"/><circle cx="520" cy="196" r="4"/>
    <circle cx="700" cy="120" r="3"/><circle cx="820" cy="248" r="4"/><circle cx="230" cy="300" r="3"/>
    <circle cx="612" cy="318" r="3"/><circle cx="404" cy="252" r="3"/>
  </g>
  <rect width="900" height="1200" fill="url(#s3dawn)"/>
  <path d="M330 900 450 300 570 900z" fill="url(#s3beam)"/>
  <path d="M0 900h900v300H0z" fill="#081a18"/>
  <path d="M0 900c120-26 190 10 300-6s180-40 300-16 190 30 300 22v300H0z" fill="#0b2420"/>
  <g fill="#0a1a26" stroke="#2b4a52" stroke-width="4">
    <rect x="96" y="742" width="104" height="160" rx="10"/>
    <rect x="224" y="796" width="80" height="106" rx="10"/>
    <rect x="628" y="770" width="92" height="132" rx="10"/>
    <rect x="744" y="822" width="72" height="80" rx="10"/>
  </g>
  <g fill="#f4b740" opacity="0.75">
    <rect x="120" y="772" width="18" height="18" rx="4"/><rect x="158" y="772" width="18" height="18" rx="4"/>
    <rect x="120" y="816" width="18" height="18" rx="4"/><rect x="652" y="800" width="18" height="18" rx="4"/>
    <rect x="690" y="800" width="18" height="18" rx="4"/><rect x="652" y="844" width="18" height="18" rx="4"/>
  </g>
  <path d="M450 320c46 66 70 140 70 214 0 74-24 148-70 214-46-66-70-140-70-214 0-74 24-148 70-214z"
        fill="#12331b" stroke="#39ff14" stroke-width="6"/>
  <circle cx="450" cy="520" r="34" fill="#39ff14"/>
  <path d="M450 748c-30 36-46 74-46 112h92c0-38-16-76-46-112z" fill="#f4b740" opacity="0.8"/>
</svg>

===== END public/step-reveal-rail/scene-3.svg =====

--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
Verified 2026-09-25 at 1440, 1280, 1024, 900, 885, 880, 879, 834, 480, 390, 360, 320, 280px.
Passed: full-page context, keyboard, no javascript, reduced motion, two instances, no overflow, declarations corroborated, lifecycle.
Library source fingerprint: sha256:bc12123fb56666fadf6dacc1b4b3d53906e50bfbe245e2ede193df90914f2e7d — MATCHES the files below.

--- 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
“Step reveal rail” from my Astro Component Library:
https://astro.baysixmedia.com/components/step-reveal-rail/

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
4 required + 1 supporting
Verified
2026-09-25
Client navigation
Not applicable — ships no script
Source fingerprint
Matches the verified source
Production ready
Yes

Production-ready component

Every required check passed against the code below, and the recorded fingerprint still matches it.

Verified 2026-09-25 at 1440, 1280, 1024, 900, 885, 880, 879, 834, 480, 390, 360, 320, 280px

  • Full-page contextpass
  • Keyboardpass
  • No JavaScriptpass
  • Reduced motionpass
  • Two instancespass
  • No overflowpass
  • Declarations corroboratedpass
  • Lifecycle — declared to ship no scriptnot-applicable
11 verification notes
  • Full re-verification 2026-09-25 (Step 27 redesign): the component was rebuilt as an editorial process rail — hairline-divided steps under a progress rail, native radios for choosing a step, full text in closed steps, and a list layout below 880px (and for five or six steps at any width). The 2026-09-18 measurements against the original reference no longer apply and are not carried forward.
  • Every check was run against the production build (`npm run build`, dist/ served statically) on the integration-test fixture route, both instances: three steps with no links, and four steps with a resting step of 2 and links on every step.
  • Responsive: 1440, 1280, 1024, 900, 885, 880, 879, 834, 480, 390, 360, 320 and 280. At every width scrollWidth equals clientWidth, zero duplicate ids, and no number, title or body outside its step or scrolling sideways. The row/list switch is exact: row at 880, list at 879. Also swept at 320/390/768/1280/1440 on 3-, 4- and 6-step rails with long titles and bodies and with no pictures.
  • Keyboard: each rail is one radio group — Tab reaches the chosen step, arrow keys move between steps and open each (measured 287/604/288 after two ArrowRight presses at 1440), Tab leaves the group, and title links follow as ordinary stops. The whole step carries a 2px accent focus ring (read back from computed style). Each radio's accessible name is '01: Step title'.
  • Without JavaScript the rail is unchanged: zero script elements in the component, and with script execution disabled both rails render, because the behaviour is :checked and :has().
  • Reduced motion: the duration token computes to 1e-05s under emulation and choosing a step still opens it (ArrowRight gives 287/604/288 immediately).
  • Two instances are independent: each rail's radios share a name unique to that rail (from id when given, otherwise a random per-render name); choosing step 2 in the first rail left the second rail on its own step (before [0,2], after [1,2]). Zero duplicate ids on the page.
  • Forced colours (emulated): the panel and each mat take a solid 1px CanvasText border and the steps keep their inline divider; the chosen step's rail segment and the focus ring use Highlight.
  • Declarations corroborated: static (no script anywhere), hasMotion (the flex-grow and picture-reveal transitions), hasFocusableContent (a radio group in every rail), acceptsIdProp (id sets the root id and names the radio group), multi-instance supported (above). Zero console messages on the fixture route and the preview.
  • Full page context: rendered between a light services grid and a dark contact band at 1440 and 390 with no overflow or spacing collisions.
  • Chromium only. No WebKit or Gecko build was available in this session, and no claim is made about either; the features that matter are :has(), container queries and a transition on flex-grow.

Open the verification fixture

src/components/library/StepRevealRail.astro

This component is not one file on its own

  • assetsCopy these across too — they are referenced by URL, so nothing in the source above points at them: public/step-reveal-rail/scene-1.svg, public/step-reveal-rail/scene-2.svg, public/step-reveal-rail/scene-3.svg. Leave them behind and those URLs resolve to nothing on the other end. What each component does without them is in its README.
---
/**
 * Step reveal rail — an editorial process rail.
 *
 * Numbered steps side by side in one panel, divided by hairlines, with a
 * progress rail across the top. One step is open: it widens far enough to
 * uncover its picture while the others close to columns of text. Choosing a
 * step — by pointer, tap or keyboard — opens it; on a device with hover,
 * pointing previews a step and leaving returns to the chosen one.
 *
 * The reveal is a CLIP, not a slide. Each step lays its text column and its
 * picture out at a fixed offset from its own left edge, so nothing inside a
 * step moves while the row redistributes: the open step's right edge sweeps
 * past the picture and uncovers it, and the type never reflows mid-animation.
 * Closed steps show their number, their whole title and the start of their
 * text, ending on a clean ellipsis rather than a word cut in half.
 *
 * No script. The chosen step is a native radio in each step's header —
 * arrow keys move between steps, Tab leaves the group — and `:has()` does the
 * rest. Every word is in the document in every state; only the pictures of
 * closed steps are covered. Below the row's width the rail becomes a list:
 * every step's words stay visible and the chosen step also shows its picture.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour, size and duration is a `--srr-*` custom property on the root.
 */

export interface StepRevealRailStep {
	/**
	 * The marker for the step. Left out, it is the position in the list,
	 * zero-padded to two digits — "01", "02". Set it for lettered or
	 * differently-formatted sequences.
	 */
	number?: string;
	/** The step's name. The only required field. */
	title: string;
	/** A sentence or two under it. */
	body?: string;
	/** The illustration. Cropped to fill, so the subject wants to be central. */
	image?: string;
	/**
	 * Alternative text. Left empty the picture is marked decorative, which is
	 * right when it illustrates the words rather than adding to them.
	 */
	imageAlt?: string;
	/** Makes the title a link. */
	href?: string;
}

interface Props {
	/** The steps, in order. */
	steps?: StepRevealRailStep[];
	/**
	 * Which step is open to begin with. Out-of-range values clamp to the first
	 * step rather than leaving the rail with nothing open.
	 */
	restIndex?: number;
	/** Heading level for each step's title. */
	headingLevel?: "h2" | "h3" | "h4" | "p";
	/** The panel behind the steps. */
	railBackground?: string;
	/** Each step's own fill. */
	cardBackground?: string;
	/** The mat the picture sits on. */
	mat?: string;
	/** The progress rail, the open step's number and the focus ring. */
	accent?: string;
	/** Titles. */
	ink?: string;
	/** Body copy and closed-step numbers. */
	mutedInk?: string;
	/** Corner radius, shared by the panel and the pictures. */
	radius?: string;
	/** Width the rail stops growing at. */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. Also names the step control, so two rails on a page stay independent. */
	id?: string;
}

/* No demo content: the steps are a prop, and with none the rail renders nothing. The catalog preview passes its own. */
const {
	steps = [],
	restIndex = 0,
	headingLevel = "h3",
	railBackground = "#161514",
	cardBackground = "#161514",
	mat = "#23211e",
	accent = "#e3b35f",
	ink = "#f4f0e8",
	mutedInk = "#b9b2a6",
	radius = "18px",
	maxWidth = "1180px",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

/* A step with no title has nothing to be, so it is dropped. No steps left, no rail. */
const items = (steps ?? []).filter((step) => step?.title);
const rest = Number.isInteger(restIndex) && restIndex >= 0 && restIndex < items.length ? restIndex : 0;

/* Only schemes that navigate: the component is copied into projects where a step's href may come from content. */
const SAFE_SCHEME = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safe = (value?: string) => (value && SAFE_SCHEME.test(value.trim()) ? value.trim() : undefined);

/*
 * How much wider the open step is than a closed one. Fewer steps can afford
 * a wider open step; more steps keep the closed columns readable.
 */
const count = items.length;
const GROW = count <= 3 ? 2.1 : 2;
/* Five or more steps do not fit a readable row at any width: they are always the list. */
const dense = count >= 5;

/* The radio group's name: from the id when given, otherwise unique per render. */
const group = `srr-${id ?? Math.random().toString(36).slice(2, 10)}`;

const style = [
	`--srr-count:${count}`,
	`--srr-grow:${GROW}`,
	`--srr-rail-bg:${railBackground}`,
	`--srr-card-bg:${cardBackground}`,
	`--srr-mat:${mat}`,
	`--srr-accent:${accent}`,
	`--srr-ink:${ink}`,
	`--srr-muted:${mutedInk}`,
	`--srr-radius:${radius}`,
	`--srr-max:${maxWidth}`,
].join(";");
---

{count > 0 && (
<div class:list={["step-reveal-rail", className]} id={id} style={style} data-step-reveal-rail>
	<ol class="srr-rail" data-dense={dense ? "" : undefined} role="list">
		{
			items.map((step, index) => {
				const href = safe(step.href);
				const src = safe(step.image);
				const marker = step.number ?? String(index + 1).padStart(2, "0");
				const inputId = `${group}-${index}`;
				return (
					<li class="srr-step" data-bare={src ? undefined : ""}>
						<input class="srr-input" type="radio" name={group} id={inputId} checked={index === rest} />
						<div class="srr-clip">
							<div class="srr-inner">
								<div class="srr-text">
									<label class="srr-pick" for={inputId}>
										<span class="srr-number">{marker}</span>
										<span class="srr-sr">: {step.title}</span>
									</label>
									<div class="srr-copy">
										<Heading class="srr-title">
											{href ? (
												<a class="srr-link" href={href}>
													{step.title}
												</a>
											) : (
												step.title
											)}
										</Heading>
										{step.body && <p class="srr-body">{step.body}</p>}
									</div>
								</div>
								{src && (
									<div class="srr-figure">
										<div class="srr-mat">
											<img class="srr-image" src={src} alt={step.imageAlt ?? ""} width="900" height="1200" loading="lazy" decoding="async" />
										</div>
									</div>
								)}
							</div>
						</div>
					</li>
				);
			})
		}
	</ol>
</div>
)}

<style>
	.step-reveal-rail {
		--srr-pad: clamp(18px, 2.2cqw, 30px);
		--srr-gap-inner: 18px;
		--srr-number-size: clamp(1.75rem, 1.3rem + 1.4cqw, 2.5rem);
		--srr-title-size: clamp(1.125rem, 1rem + 0.45cqw, 1.3125rem);
		--srr-body-size: 0.9375rem;
		--srr-duration: 380ms;
		--srr-ease: cubic-bezier(0.22, 1, 0.36, 1);

		/* The query container: the layout follows the rail's own width, not the window's. */
		container: srr / inline-size;
		box-sizing: border-box;
		inline-size: 100%;
		max-inline-size: var(--srr-max);
		margin-inline: auto;
		color: var(--srr-ink);
		font-family: inherit;
	}

	.step-reveal-rail *,
	.step-reveal-rail *::before,
	.step-reveal-rail *::after {
		box-sizing: border-box;
	}

	/* Scoped reset: a host's bare element rules (`h3 { margin }`, `ol { padding }`) must not move the rail. */
	.step-reveal-rail :global(:where(h1, h2, h3, h4, h5, h6, p, ol, li, a, span, label)) {
		margin: 0;
		padding: 0;
		border: 0;
		background: none;
		font: inherit;
		color: inherit;
		letter-spacing: inherit;
		text-align: inherit;
		text-transform: none;
		list-style: none;
	}

	/* ---- the list (narrow rails, and five-plus steps below their row width) ---- */

	.srr-rail {
		display: flex;
		flex-direction: column;
		border-radius: var(--srr-radius);
		background: var(--srr-rail-bg);
		padding: 6px clamp(16px, 4cqw, 28px);
	}

	.srr-step {
		position: relative;
		background: var(--srr-card-bg);
		border-block-start: 1px solid rgb(255 255 255 / 0.1);
	}

	.srr-step:first-child {
		border-block-start: 0;
	}

	/* The rail segment: a hairline for steps ahead, accent for the chosen step and those before it. */
	.srr-step::before {
		content: "";
		position: absolute;
		inset-block-start: -1px;
		inset-inline: 0;
		block-size: 1px;
		background: transparent;
		transition:
			background-color var(--srr-duration) var(--srr-ease),
			block-size var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked)::before {
		block-size: 2px;
		background: var(--srr-accent);
	}

	.srr-input {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		margin: 0;
		opacity: 0;
		pointer-events: none;
	}

	.srr-clip {
		position: relative;
	}

	.srr-inner {
		display: grid;
		grid-template-columns: auto minmax(0, 1fr);
		column-gap: clamp(16px, 4cqw, 28px);
		padding-block: clamp(20px, 5cqw, 28px);
	}

	.srr-text {
		display: contents;
	}

	/* The whole step is the control: the label's box is stretched over it; links sit above. */
	.srr-pick {
		display: block;
		cursor: pointer;
	}

	.srr-pick::after {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 0;
	}

	.srr-number {
		display: block;
		min-inline-size: 2ch;
		font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
		font-size: var(--srr-number-size);
		font-weight: 400;
		line-height: 1;
		font-variant-numeric: tabular-nums lining-nums;
		letter-spacing: -0.02em;
		color: color-mix(in srgb, var(--srr-ink) 72%, var(--srr-card-bg));
		transition: color var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked) .srr-number {
		color: var(--srr-accent);
	}

	.srr-sr {
		position: absolute;
		inline-size: 1px;
		block-size: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}

	.srr-copy {
		display: flex;
		flex-direction: column;
		gap: 10px;
		min-inline-size: 0;
		padding-block-start: 4px;
	}

	.srr-title {
		position: relative;
		z-index: 1;
		inline-size: fit-content;
		max-inline-size: 100%;
		font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
		font-size: var(--srr-title-size);
		font-weight: 600;
		line-height: 1.22;
		letter-spacing: -0.005em;
		color: var(--srr-ink);
		overflow-wrap: break-word;
		hyphens: manual;
		text-wrap: balance;
	}

	/* The padding makes a one-line title link a 24px-tall target; the negative margin keeps the layout where it was. */
	.srr-link {
		display: inline-block;
		padding-block: 0.12em;
		margin-block: -0.12em;
		color: inherit;
		text-decoration: none;
	}

	.srr-link:hover {
		text-decoration: underline;
		text-decoration-thickness: 1px;
		text-underline-offset: 0.18em;
	}

	.srr-link:focus-visible {
		outline: 2px solid var(--srr-accent);
		outline-offset: 4px;
		border-radius: 2px;
	}

	.srr-body {
		max-inline-size: 58ch;
		font-size: var(--srr-body-size);
		font-weight: 400;
		line-height: 1.55;
		color: var(--srr-muted);
		overflow-wrap: break-word;
		hyphens: manual;
		text-wrap: pretty;
	}

	/* The chosen step's picture opens beneath its words; the others stay closed. */
	.srr-figure {
		grid-column: 2;
		display: grid;
		grid-template-rows: 0fr;
		transition: grid-template-rows var(--srr-duration) var(--srr-ease);
	}

	.srr-step:has(.srr-input:checked) .srr-figure {
		grid-template-rows: 1fr;
	}

	.srr-mat {
		position: relative;
		min-block-size: 0;
		overflow: hidden;
		border-radius: calc(var(--srr-radius) - 6px);
		background: var(--srr-mat);
	}

	.srr-step:has(.srr-input:checked) .srr-mat {
		margin-block-start: 18px;
		aspect-ratio: 4 / 3;
	}

	.srr-image {
		position: absolute;
		inset: 0;
		inline-size: 100%;
		block-size: 100%;
		object-fit: cover;
		object-position: center;
	}

	/* Keyboard focus on the step's radio: a ring round the whole step. */
	.srr-step:has(.srr-input:focus-visible) {
		outline: 2px solid var(--srr-accent);
		outline-offset: 2px;
		border-radius: 6px;
	}

	.srr-step:not(:has(.srr-input:checked)):hover .srr-number {
		color: var(--srr-ink);
	}

	/* ---- the row ------------------------------------------------------------ */

	/* A wide list (five or six steps on a desktop, or 720–879px): the chosen step's picture sits beside its words. */
	@container srr (min-width: 720px) {
		.srr-inner {
			grid-template-columns: auto minmax(0, 1fr) minmax(0, 38%);
		}

		.srr-figure {
			grid-column: 3;
			grid-row: 1;
			align-self: start;
		}

		.srr-step:has(.srr-input:checked) .srr-mat {
			margin-block-start: 0;
		}
	}

	/* The row: up to four steps, from a rail width of 880px. Five or six steps stay a list. */
	@container srr (min-width: 880px) {
		.srr-rail:not([data-dense]) {
			flex-direction: row;
			padding: 0;
			overflow: hidden;
		}

		.srr-rail:not([data-dense]) > .srr-step {
			--srr-track: calc(100cqw - (var(--srr-count) - 1) * 1px);
			--srr-closed-w: calc(var(--srr-track) / (var(--srr-grow) + var(--srr-count) - 1));
			--srr-open-w: calc(var(--srr-closed-w) * var(--srr-grow));
			--srr-text-w: calc(var(--srr-closed-w) - 2 * var(--srr-pad));
			--srr-fig-w: max(0px, calc(var(--srr-open-w) - var(--srr-closed-w) - var(--srr-pad)));

			flex: 1 1 0;
			min-inline-size: 0;
			border-block-start: 0;
			border-inline-start: 1px solid rgb(255 255 255 / 0.1);
			transition: flex-grow var(--srr-duration) var(--srr-ease);
		}

		.srr-rail:not([data-dense]) > .srr-step:first-child {
			border-inline-start: 0;
		}

		/* Across the top: the progress rail. Hairline ahead, accent up to and including the chosen step. */
		.srr-rail:not([data-dense]) > .srr-step::before {
			inset-block-start: 0;
			block-size: 3px;
			background: rgb(255 255 255 / 0.08);
		}

		.srr-rail:not([data-dense]) > .srr-step:has(~ .srr-step .srr-input:checked)::before {
			background: color-mix(in srgb, var(--srr-accent) 45%, transparent);
		}

		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked)::before {
			block-size: 3px;
			background: var(--srr-accent);
		}

		/* Open: the chosen step — or, on a device with hover, the one being pointed at. */
		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked) {
			flex-grow: var(--srr-grow);
		}

		.srr-rail:not([data-dense]) > .srr-step .srr-clip {
			block-size: 100%;
			overflow: hidden;
		}

		.srr-rail:not([data-dense]) .srr-inner {
			display: flex;
			align-items: stretch;
			gap: var(--srr-pad);
			block-size: 100%;
			padding: calc(var(--srr-pad) + 14px) 0 var(--srr-pad) var(--srr-pad);
		}

		.srr-rail:not([data-dense]) .srr-text {
			display: flex;
			flex-direction: column;
			justify-content: flex-start;
			gap: clamp(28px, 4cqw, 56px);
			flex: 0 0 auto;
			inline-size: var(--srr-text-w);
			min-block-size: calc(var(--srr-fig-w) * 4 / 3);
		}

		/* A step with no picture has nothing to uncover: its words take the step's whole width. */
		.srr-rail:not([data-dense]) > .srr-step[data-bare] .srr-text {
			flex: 1 1 auto;
			inline-size: auto;
			min-inline-size: var(--srr-text-w);
			padding-inline-end: var(--srr-pad);
		}

		.srr-rail:not([data-dense]) .srr-copy {
			padding: 0;
		}

		.srr-rail:not([data-dense]) .srr-figure {
			display: block;
			flex: 0 0 auto;
			inline-size: var(--srr-fig-w);
			align-self: center;
		}

		.srr-rail:not([data-dense]) .srr-mat,
		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:checked) .srr-mat {
			margin: 0;
			aspect-ratio: 3 / 4;
		}

		.srr-rail:not([data-dense]) > .srr-step:has(.srr-input:focus-visible) {
			outline-offset: -3px;
			border-radius: 0;
		}
	}

	/* Hover previews a step on devices that have hover; leaving returns to the chosen one. Row layout only. */
	@media (hover: hover) {
		@container srr (min-width: 880px) {
			.srr-rail:not([data-dense]):has(> .srr-step:hover) > .srr-step:has(.srr-input:checked):not(:hover) {
				flex-grow: 1;
			}

			.srr-rail:not([data-dense]) > .srr-step:hover {
				flex-grow: var(--srr-grow);
			}
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.step-reveal-rail {
			--srr-duration: 0.01ms;
		}
	}

	@media (forced-colors: active) {
		.srr-rail,
		.srr-step,
		.srr-mat {
			border: 1px solid CanvasText;
		}

		.srr-step:has(.srr-input:checked)::before {
			background: Highlight;
		}

		.srr-step:has(.srr-input:focus-visible),
		.srr-link:focus-visible {
			outline-color: Highlight;
		}
	}
</style>