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.