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: "Entry offer pair"
Library page: https://astro.baysixmedia.com/components/entry-offer-pair/
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/EntryOfferPair.astro =====
---
/**
* EntryOfferPair (candidate, Step 36)
*
* A pricing section for a business that sells ONE main engagement and ONE
* smaller first step that leads into it. Not a tier table: the two offers are
* drawn at deliberately different weights (≈ 1 : 2) and joined by a drawn
* connector that carries the relationship between them.
*
* - The entry offer sits on the section ground, unboxed: name, one line,
* its price, any terms as plain lines under one rule, a filled ink action.
* - The primary offer is the one panel on the page. At full width it is two
* columns: the offer (name, one line, then price, terms and a brand-colour
* action directly under it) beside an ordered rail of how it runs on a
* faint second plane, so the price is read against the journey it pays
* for. With 5–6 steps the panel turns over: the offer across the top
* (lead | deal), the steps beneath as a segmented rail in three columns.
* - The relationship line ("the first fee comes off the full job") sits on a
* rule that leaves the entry column straight under its action and ends in
* an arrow at the panel's edge; on narrow widths it turns vertical and
* points down.
*
* Truth rules:
* - No default names, prices, terms, steps or claims. An offer renders only
* with a name. With neither offer there is no section at all (not even
* the heading). With one, it renders alone and nothing is said about the
* other.
* - A price line renders only when a price or price note is supplied; an
* offer without one simply has no price line. Values must be strings.
* - The relationship renders only when supplied AND both offers render.
* - Links only through safeHref (http(s), mailto, tel, #, relative).
*
* Responsive (container queries on the section's own width):
* - < 640px: stacked — entry, the connector pointing down, the panel; inside
* the panel the offer then the rail; actions full width.
* - 640–959px: still stacked; the entry splits into lead | deal; the panel
* splits into offer | rail (or lead | deal when it has no steps).
* - ≥ 960px: entry column 1fr, panel 2fr; the connector runs across the gap.
*
* No script. Motion: only the buttons' colour eases on hover; none under
* reduced motion. One family throughout (display and body hooks).
*
* Hooks read (never declared): --eop-bg, --eop-panel-bg, --eop-text,
* --eop-text-muted, --eop-brand, --eop-brand-text, --eop-border,
* --eop-family, --eop-display, --eop-grid-max, --eop-grid-gutter,
* --eop-heading-size.
*/
export interface OfferAction {
label: string;
href: string;
}
export interface OfferStep {
title: string;
detail?: string;
}
export interface Offer {
/** The offer's name, as the business calls it. Required for the offer to render. */
name: string;
/** Optional short line above the name that states the offer's place ("Start here"). */
kicker?: string;
/** One sentence on what it is. */
summary?: string;
/** The price exactly as the business states it ("$129", "8–12% of build cost"). */
price?: string;
/** What the price covers or how it is set ("per visit", "fixed in writing before work starts"). */
priceNote?: string;
/** Eligibility, terms and commitment — one quiet line each. */
terms?: string[];
cta?: OfferAction | null;
}
export interface PrimaryOffer extends Offer {
/** How the engagement runs, in order (up to 6). */
steps?: OfferStep[];
/** Optional caption over the steps ("How a repair runs"). */
stepsHeading?: string;
}
export interface EntryOfferPairProps {
heading: string;
headingLevel?: "h2" | "h3";
eyebrow?: string;
intro?: string;
/** The main engagement: the panel. */
primary?: PrimaryOffer | null;
/** The smaller first step that leads into it. */
entry?: Offer | null;
/** How the entry leads into the primary ("The visit fee is credited toward the repair"). Only when supplied. */
relationship?: string;
/** Interface labels; English defaults. */
labels?: { steps?: string };
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends EntryOfferPairProps {}
const {
heading,
headingLevel = "h2",
eyebrow = "",
intro = "",
primary: primaryProp = null,
entry: entryProp = null,
relationship = "",
labels = {},
fontStylesheet = "https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&display=swap",
class: className,
id,
} = Astro.props as Props;
const L = { steps: "How it runs", ...labels };
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: unknown) => (typeof href === "string" && SAFE.test(href.trim()) ? href.trim() : "");
const text = (v: unknown) => (typeof v === "string" ? v.trim() : "");
const clean = (o: Offer | PrimaryOffer | null | undefined) => {
const name = text(o?.name);
if (!o || !name) return null;
const href = safeHref(o.cta?.href);
const label = text(o.cta?.label);
/* Steps are records ({ title, detail? }), as the contract declares; a bare string is still read as a title so hand-written props degrade safely. */
const steps = ((o as PrimaryOffer).steps ?? ([] as unknown[]))
.map((s: unknown) => (typeof s === "string" ? { title: s.trim(), detail: "" } : { title: text((s as OfferStep)?.title), detail: text((s as OfferStep)?.detail) }))
.filter((s) => s.title)
.slice(0, 6);
return {
name,
kicker: text(o.kicker),
summary: text(o.summary),
price: text(o.price),
priceNote: text(o.priceNote),
terms: (Array.isArray(o.terms) ? o.terms : []).map(text).filter(Boolean),
cta: href && label ? { href, label } : null,
steps,
stepsHeading: text((o as PrimaryOffer).stepsHeading),
};
};
const primary = clean(primaryProp);
const entry = clean(entryProp);
const show = Boolean(primary || entry);
const both = Boolean(primary && entry);
const bridge = both ? text(relationship) : "";
const Heading = headingLevel;
const Sub = headingLevel === "h2" ? "h3" : "h4";
const uid = `${(id ?? "eop").replace(/[^\w-]/g, "") || "eop"}-${Math.random().toString(36).slice(2, 7)}`;
const headingText = text(heading);
---
{fontStylesheet && show && <link rel="stylesheet" href={fontStylesheet} />}
{
show && (
<section
class:list={["eop", { "eop--both": both, "eop--bridge": Boolean(bridge), "eop--solo-entry": !primary }, className]}
id={id}
aria-labelledby={headingText ? `${uid}-h` : undefined}
>
<div class="eop-inner">
{(headingText || eyebrow || intro) && (
<header class="eop-head">
{eyebrow && <p class="eop-eyebrow">{eyebrow}</p>}
{headingText && (
<Heading class="eop-title" id={`${uid}-h`}>
{headingText}
</Heading>
)}
{intro && <p class="eop-intro">{intro}</p>}
</header>
)}
<div class="eop-pair">
{entry && (
<article class="eop-entry" aria-labelledby={`${uid}-e`}>
<div class="eop-lead">
{entry.kicker && <p class="eop-kicker">{entry.kicker}</p>}
<Sub class="eop-name" id={`${uid}-e`}>
{entry.name}
</Sub>
{entry.summary && <p class="eop-summary">{entry.summary}</p>}
</div>
{(entry.price || entry.priceNote || entry.terms.length > 0 || entry.cta) && (
<div class="eop-deal">
{(entry.price || entry.priceNote) && (
<p class:list={["eop-price", { "eop-price--note-only": !entry.price }]}>
{entry.price && <span class:list={["eop-amount", { "eop-amount--whole": entry.price.length <= 14, "eop-amount--long": entry.price.length > 14 }]}>{entry.price}</span>}
{entry.priceNote && <span class="eop-price-note">{entry.priceNote}</span>}
</p>
)}
{entry.terms.length > 0 && (
<ul class="eop-terms" role="list">
{entry.terms.map((t) => (
<li>{t}</li>
))}
</ul>
)}
{entry.cta && (
<a class="eop-btn eop-btn--entry" href={entry.cta.href}>
{entry.cta.label}
</a>
)}
</div>
)}
</article>
)}
{bridge && (
<p class="eop-bridge">
<span class="eop-bridge-text">{bridge}</span>
</p>
)}
{primary && (
<article class:list={["eop-primary", { "eop-primary--steps": primary.steps.length > 0 && primary.steps.length <= 4, "eop-primary--long": primary.steps.length > 4 }]} aria-labelledby={`${uid}-p`}>
<div class="eop-card">
<div class="eop-offer">
<div class="eop-lead">
{primary.kicker && <p class="eop-kicker">{primary.kicker}</p>}
<Sub class="eop-name eop-name--primary" id={`${uid}-p`}>
{primary.name}
</Sub>
{primary.summary && <p class="eop-summary">{primary.summary}</p>}
</div>
{(primary.price || primary.priceNote || primary.terms.length > 0 || primary.cta) && (
<div class="eop-deal">
{(primary.price || primary.priceNote) && (
<p class:list={["eop-price eop-price--primary", { "eop-price--note-only": !primary.price }]}>
{primary.price && <span class:list={["eop-amount", { "eop-amount--whole": primary.price.length <= 14, "eop-amount--long": primary.price.length > 14 }]}>{primary.price}</span>}
{primary.priceNote && <span class="eop-price-note">{primary.priceNote}</span>}
</p>
)}
{primary.terms.length > 0 && (
<ul class="eop-terms" role="list">
{primary.terms.map((t) => (
<li>{t}</li>
))}
</ul>
)}
{primary.cta && (
<a class="eop-btn" href={primary.cta.href}>
{primary.cta.label}
</a>
)}
</div>
)}
</div>
{primary.steps.length > 0 && (
<div class="eop-journey">
{primary.stepsHeading && (
<p class="eop-journey-title" id={`${uid}-s`}>
{primary.stepsHeading}
</p>
)}
<ol
class="eop-steps"
aria-labelledby={primary.stepsHeading ? `${uid}-s` : undefined}
aria-label={primary.stepsHeading ? undefined : L.steps}
>
{primary.steps.map((s) => (
<li class="eop-step">
<span class="eop-step-title">{s.title}</span>
{s.detail && <span class="eop-step-detail">{s.detail}</span>}
</li>
))}
</ol>
</div>
)}
</div>
</article>
)}
</div>
</div>
</section>
)
}
<style>
.eop {
--eop-surface: var(--eop-bg, #eef0eb);
--eop-panel: var(--eop-panel-bg, #ffffff);
--eop-ink: var(--eop-text, #182026);
--eop-soft: var(--eop-text-muted, #56606a);
--eop-accent: var(--eop-brand, #1f5c4d);
--eop-accent-ink: var(--eop-brand-text, #ffffff);
--eop-line: var(--eop-border, #d2d7cf);
--eop-font: var(--eop-family, "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serif);
--eop-font-display: var(--eop-display, "Schibsted Grotesk", ui-sans-serif, system-ui, sans-serif);
--eop-gap: 24px;
container-type: inline-size;
background: var(--eop-surface);
color: var(--eop-ink);
font-family: var(--eop-font);
font-size: 16px;
line-height: 1.55;
}
.eop *,
.eop *::before,
.eop *::after {
box-sizing: border-box;
}
.eop-inner {
max-width: var(--eop-grid-max, 1232px);
margin: 0 auto;
padding: 72px 20px;
}
/* ---- header ---- */
.eop-head {
max-width: 44rem;
margin-bottom: 48px;
}
.eop-eyebrow {
margin: 0 0 14px;
color: var(--eop-accent);
font: 600 15px/1.4 var(--eop-font);
}
.eop-title {
margin: 0;
font-family: var(--eop-font-display);
font-size: var(--eop-heading-size, clamp(32px, 4vw, 48px));
font-weight: 600;
line-height: 1.06;
letter-spacing: -0.025em;
text-wrap: balance;
overflow-wrap: anywhere;
}
.eop-intro {
max-width: 38rem;
margin: 18px 0 0;
color: var(--eop-soft);
font-size: 18px;
line-height: 1.55;
}
/* ---- the pair ---- */
.eop-pair {
display: grid;
grid-template-columns: minmax(0, 1fr);
row-gap: var(--eop-gap);
}
.eop--bridge .eop-pair {
row-gap: 0;
}
.eop-kicker {
margin: 0 0 10px;
color: var(--eop-accent);
font-size: 15px;
font-weight: 600;
line-height: 1.4;
}
.eop-name {
margin: 0;
font-family: var(--eop-font-display);
font-size: 26px;
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.015em;
overflow-wrap: anywhere;
}
.eop-name--primary {
font-size: clamp(28px, 3vw, 34px);
line-height: 1.1;
letter-spacing: -0.02em;
}
.eop-summary {
max-width: 34rem;
margin: 12px 0 0;
color: var(--eop-soft);
font-size: 16px;
line-height: 1.55;
}
.eop-deal {
display: flex;
flex-direction: column;
margin-top: 28px;
}
/* Price, then its note, each on its own line. */
.eop-price {
display: flex;
flex-direction: column;
gap: 4px;
margin: 0;
padding-bottom: 18px;
}
.eop-amount {
font-family: var(--eop-font-display);
font-size: 34px;
font-weight: 600;
line-height: 1.05;
letter-spacing: -0.02em;
font-variant-numeric: lining-nums;
overflow-wrap: anywhere;
}
.eop-price--primary .eop-amount {
font-size: clamp(36px, 6cqi, 50px);
letter-spacing: -0.03em;
}
/* A worded price ("8–12% of build cost") steps down a size so it reads as one line or two, not four. */
.eop-amount--long {
font-size: 26px;
line-height: 1.15;
}
.eop-price--primary .eop-amount--long {
font-size: clamp(28px, 4.4cqi, 36px);
letter-spacing: -0.02em;
}
/* A short price is never split at its range dash. */
.eop-amount--whole {
white-space: nowrap;
}
.eop-price-note {
color: var(--eop-soft);
font-size: 15px;
line-height: 1.45;
}
/* No figure, only how the price is set: that line becomes the price line. */
.eop-price--note-only .eop-price-note {
color: var(--eop-ink);
font-family: var(--eop-font-display);
font-size: 20px;
font-weight: 500;
line-height: 1.3;
letter-spacing: -0.01em;
}
/* Terms: separate, quiet, ruled lines. */
/* Terms: the conditions of the price, set as plain lines in the ink, under one rule. */
.eop-terms {
display: grid;
gap: 6px;
margin: 0;
padding: 16px 0 0;
list-style: none;
border-top: 1px solid var(--eop-ink);
}
.eop-terms li {
color: var(--eop-ink);
font-size: 15px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.eop-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
align-self: stretch;
min-height: 50px;
margin-top: 24px;
padding: 12px 22px;
border: 1px solid var(--eop-accent);
border-radius: 6px;
background: var(--eop-accent);
color: var(--eop-accent-ink);
font: 600 16px/1.3 var(--eop-font);
text-align: center;
text-decoration: none;
transition: background-color 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
/* The first step is a real, filled action in the ink; the main offer keeps the brand colour. */
.eop-btn--entry {
border-color: var(--eop-ink);
background: var(--eop-ink);
color: var(--eop-surface);
}
.eop-btn:hover {
background: color-mix(in srgb, var(--eop-accent) 84%, var(--eop-ink));
}
.eop-btn--entry:hover {
border-color: var(--eop-accent);
background: var(--eop-accent);
color: var(--eop-accent-ink);
}
.eop-btn:focus-visible {
outline: 3px solid var(--eop-accent);
outline-offset: 3px;
}
/* ---- entry: unboxed, on the ground ---- */
.eop-entry {
display: flex;
flex-direction: column;
padding-top: 22px;
border-top: 2px solid var(--eop-ink);
}
/* ---- the connector: a rule that carries the relationship ---- */
.eop-bridge {
position: relative;
margin: 0;
padding: 22px 0 34px 30px;
color: var(--eop-ink);
font-family: var(--eop-font-display);
font-size: 19px;
font-weight: 500;
line-height: 1.4;
letter-spacing: -0.01em;
}
.eop-bridge-text {
display: block;
max-width: 30rem;
overflow-wrap: anywhere;
}
/* vertical rule down to the panel */
.eop-bridge::before {
content: "";
position: absolute;
top: 0;
bottom: 2px;
left: 8px;
border-left: 2px solid var(--eop-accent);
}
/* arrowhead, pointing down at the panel */
.eop-bridge::after {
content: "";
position: absolute;
bottom: 3px;
left: 4px;
width: 10px;
height: 10px;
border-right: 2px solid var(--eop-accent);
border-bottom: 2px solid var(--eop-accent);
transform: rotate(45deg);
}
/* ---- primary: the one panel ---- */
.eop-primary {
container: eop-card / inline-size;
border: 1px solid var(--eop-line);
border-radius: 10px;
background: var(--eop-panel);
}
.eop-card {
display: grid;
grid-template-columns: minmax(0, 1fr);
height: 100%;
}
.eop-offer {
display: flex;
flex-direction: column;
padding: 28px 22px;
}
/* The rail sits on a faint second plane of the panel, so two columns of different length read as two surfaces. */
.eop-journey {
padding: 26px 22px 30px;
border-top: 1px solid var(--eop-line);
border-radius: 0 0 9px 9px;
background: color-mix(in srgb, var(--eop-surface) 45%, var(--eop-panel));
}
.eop-journey-title {
margin: 0 0 20px;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
}
.eop-steps {
margin: 0;
padding: 0;
list-style: none;
counter-reset: eop-step;
}
.eop-step {
position: relative;
display: flex;
flex-direction: column;
gap: 3px;
min-height: 32px;
padding: 4px 0 22px 48px;
counter-increment: eop-step;
}
.eop-step:last-child {
padding-bottom: 0;
}
/* the numeral: this is a real sequence */
.eop-step::before {
content: counter(eop-step);
position: absolute;
top: 0;
left: 0;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 1.5px solid var(--eop-accent);
border-radius: 50%;
background: var(--eop-panel);
color: var(--eop-accent);
font: 600 14px/1 var(--eop-font);
font-variant-numeric: tabular-nums;
}
/* the rail between numerals */
.eop-step:not(:last-child)::after {
content: "";
position: absolute;
top: 36px;
bottom: 4px;
left: 15.25px;
border-left: 1.5px solid var(--eop-line);
}
.eop-step-title {
font-size: 16px;
font-weight: 600;
line-height: 1.4;
overflow-wrap: anywhere;
}
.eop-step-detail {
color: var(--eop-soft);
font-size: 14.5px;
line-height: 1.5;
overflow-wrap: anywhere;
}
/* ---- one offer alone: keep it to a comfortable measure ---- */
.eop--solo-entry .eop-entry {
max-width: 34rem;
}
/* ---- ≥ 640: tablet — offers still stacked, each split in two ---- */
@container (min-width: 640px) {
.eop-inner {
padding: 88px 40px;
}
.eop-btn {
align-self: flex-start;
}
.eop-entry {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 40px;
}
.eop-entry .eop-deal {
margin-top: 4px;
}
.eop--solo-entry .eop-entry {
max-width: none;
}
.eop-bridge {
padding-left: 36px;
margin-left: 12px;
}
.eop-offer {
padding: 36px 36px;
}
.eop-journey {
padding: 32px 36px 36px;
}
}
/* The panel's own width decides its split: offer | rail, or lead | deal. */
@container eop-card (min-width: 600px) {
.eop-primary--steps .eop-card {
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
}
.eop-primary--steps .eop-journey {
border-top: 0;
border-left: 1px solid var(--eop-line);
border-radius: 0 9px 9px 0;
padding: 40px 36px 40px;
}
/*
* Five or six steps: a side rail would outrun the offer column, so the panel turns over —
* the offer across the top (lead | deal), the steps under it as a segmented rail in three columns.
*/
.eop-primary--long .eop-offer {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 40px;
}
.eop-primary--long .eop-deal {
margin-top: 4px;
}
.eop-primary--long .eop-journey {
padding: 32px 36px 36px;
}
.eop-primary--long .eop-steps {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 24px;
row-gap: 28px;
}
.eop-primary--long .eop-step {
padding: 46px 0 0;
}
.eop-primary--long .eop-step:not(:last-child)::after,
.eop-primary--long .eop-step::after {
content: "";
position: absolute;
top: 15.25px;
bottom: auto;
left: 40px;
right: 0;
border-left: 0;
border-top: 1.5px solid var(--eop-line);
}
/* The price follows the copy directly: the offer column reads top-down, never hollow. */
.eop-primary--steps .eop-deal {
margin-top: 32px;
}
.eop-primary:not(.eop-primary--steps):not(.eop-primary--long) .eop-offer {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
column-gap: 40px;
height: 100%;
}
/* Without a rail the deal settles at the panel's foot, opposite the name. */
.eop-primary:not(.eop-primary--steps):not(.eop-primary--long) .eop-deal {
align-self: end;
margin-top: 4px;
}
}
/* ---- ≥ 960: side by side, 1 : 2, the connector across the gap ---- */
@container (min-width: 960px) {
.eop {
--eop-gap: 56px;
}
.eop-inner {
padding: 104px var(--eop-grid-gutter, 48px);
}
.eop-head {
margin-bottom: 56px;
}
.eop--both .eop-pair {
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
grid-template-rows: auto 1fr;
column-gap: var(--eop-gap);
row-gap: 0;
}
.eop--both .eop-entry {
display: flex;
grid-column: 1;
grid-row: 1 / span 2;
}
.eop--both.eop--bridge .eop-entry {
grid-row: 1;
}
.eop--both .eop-entry .eop-deal {
margin-top: 28px;
}
.eop--both .eop-primary {
grid-column: 2;
grid-row: 1 / span 2;
}
/* Straight under the entry's action: the rule leaves the first step and lands on the panel. */
.eop--both .eop-bridge {
grid-column: 1;
grid-row: 2;
align-self: start;
margin: 40px 0 0;
padding: 20px 0 0;
}
/* the rule runs out of the column and across the gap */
.eop--both .eop-bridge::before {
top: 0;
bottom: auto;
left: 0;
right: calc(-1 * var(--eop-gap) + 3px);
border-left: 0;
border-top: 2px solid var(--eop-accent);
}
/* arrowhead, pointing right at the panel's edge */
.eop--both .eop-bridge::after {
top: -5px;
bottom: auto;
left: auto;
right: calc(-1 * var(--eop-gap) + 3px);
transform: rotate(-45deg);
}
.eop-offer {
padding: 40px 40px 40px;
}
.eop--solo-entry .eop-entry {
max-width: 46rem;
}
}
@media (prefers-reduced-motion: reduce) {
.eop-btn {
transition: none;
}
}
@media (forced-colors: active) {
.eop-btn {
border-color: ButtonText;
}
.eop-bridge::before,
.eop-bridge::after,
.eop-step::before {
border-color: CanvasText;
}
.eop-btn:focus-visible {
outline-color: Highlight;
}
}
</style>
===== END src/components/EntryOfferPair.astro =====
===== FILE (supporting): src/components/EntryOfferPair.md =====
# Entry offer pair
A pricing section for one main engagement and the smaller first step that leads into it: the first step unboxed on the left, the engagement as a panel at twice its width, joined by a drawn connector carrying how one leads into the other. The main offer's price sits beside a numbered rail of how it runs. Prices, terms, steps and the connector render only when supplied.
## Integration
- A section heading (h2 by default); offer names are one level down.
- No offer names, no section (not even the heading). One offer alone renders alone, with no connector.
- Prices, price notes, terms, steps and the relationship line render only when supplied; a primary offer without a price simply has no price line. Strings only.
- INPUT_SCHEMA has no pricing path: the contract binds price, priceNote, terms and relationship to facts.differentiators as the closest verified-statement path. The builder must pass the business's own figures and terms verbatim; nothing auto-fills a price.
- The relationship line renders only when both offers render. Both actions go through safeHref and render only with a label and a safe href.
- Container queries: stacked below 640px (connector vertical); offers split internally from 640px; 1fr : 2fr side by side with the connector across the gap from 960px. Reads --eop-* hooks including panel-bg, grid-max, grid-gutter and heading-size.
- No script. Loads Schibsted Grotesk by default — pass fontStylesheet="" when the site loads its fonts.
## Provenance
Staged as candidate `entry-offer-pair` (Step 28) and admitted through the candidate pipeline. Source type: visual-reference. References (direction only): https://www.parsleyhealth.com/. Quality history: quality/candidates/records/entry-offer-pair.json.
===== END src/components/EntryOfferPair.md =====
--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
No usable source fingerprint, so these results cannot be tied to 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.