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: "Markets served"
Library page: https://astro.baysixmedia.com/components/markets-served/
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/MarketsServed.astro =====
---
/**
* MarketsServed (candidate, Step 38.2 — owner-directed original, role `coverage`)
*
* Where a business works, set as a typographic statement. A header — eyebrow over heading on one
* side, a sentence over one optional action on the other, the two columns sharing a top and a bottom
* line — sits over one heavy rule; below it the supplied markets run across the full measure as
* display type, one after another like a line of set text, separated by a light accent stroke. The
* statement is the section's subject: no map, no pins, no cards, no photograph, no ruled index rows.
* An optional quiet note sits under a hairline at the end.
*
* Scale follows the list so the block always reads as finished: 1–3 markets are set very large,
* 4–7 large, 8–12 medium, 13+ small. `presentation="summary"` (a homepage mention of a footprint
* that has its own page) sets everything one step smaller and tighter; "feature" is the default.
*
* Truth rules. It shows only the markets passed in `markets`, in the order given and exactly as
* written — never sorted, merged, grouped, abbreviated, counted or ranked (no numerals). A market is
* a place the business says it serves: nothing here draws or implies an office, terminal, depot,
* yard, lane, route, radius, schedule, capacity or coverage guarantee. No markets, no section.
* Heading, intro, note and action render only when supplied; the action only with a safe href.
*
* Responsive (container queries on the section's own width): below 760px one column in reading
* order — heading, sentence, rule, markets, the action, then the note. From 760px the header sits
* above the rule (stacked, then split heading | copy from 960px). Each market is one unit at every
* width: lines break between markets, separators sit only between markets on the same line, and only
* a name longer than the whole line wraps inside itself, with tighter leading than between markets.
*
* Reads, never declares: --msv-bg, --msv-text, --msv-text-muted, --msv-brand, --msv-brand-text,
* --msv-border, --msv-family, --msv-display, --msv-grid-max, --msv-grid-gutter, --msv-heading-size.
* No script.
*/
export interface MarketsServedAction {
label: string;
href: string;
}
interface Props {
/** Section heading. */
heading: string;
headingLevel?: "h2" | "h3";
/** Short label above the heading. */
eyebrow?: string;
/** One or two sentences beside the heading. */
intro?: string;
/** The markets, regions, cities or areas the business serves — exactly as supplied, in order. */
markets?: readonly string[];
/** One quiet factual line under the statement (e.g. that the list is as supplied). */
note?: string;
/** One optional action. */
cta?: MarketsServedAction | null;
/** "feature" (default) for a page about the footprint; "summary" for a shorter mention elsewhere. */
presentation?: "feature" | "summary";
fontStylesheet?: string;
class?: string;
id?: string;
}
const {
heading,
headingLevel = "h2",
eyebrow = "",
intro = "",
markets = [],
note = "",
cta = null,
presentation = "feature",
fontStylesheet = "https://fonts.googleapis.com/css2?family=Archivo:ital,wdth,wght@0,62..125,300..800;1,62..125,300..800&display=swap",
class: className,
id,
} = Astro.props;
const list = markets.map((m) => (typeof m === "string" ? m.trim() : "")).filter(Boolean);
const safeHref = (href: string | undefined) => {
const h = (href ?? "").trim();
return /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(h) ? h : null;
};
const action = cta && cta.label?.trim() && safeHref(cta.href) ? { label: cta.label.trim(), href: safeHref(cta.href)! } : null;
const steps = ["xl", "lg", "md", "sm"] as const;
const base = list.length <= 3 ? 0 : list.length <= 7 ? 1 : list.length <= 12 ? 2 : 3;
const scale = steps[Math.min(3, base + (presentation === "summary" ? 1 : 0))];
const Heading = headingLevel;
const headingId = `${id ?? "msv"}-heading`;
---
{list.length > 0 && heading?.trim() && (
<section class:list={["msv", className]} id={id} data-presentation={presentation} aria-labelledby={headingId}>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<div class="msv-inner">
<header class="msv-head">
<div class="msv-title">
{eyebrow.trim() && <p class="msv-eyebrow">{eyebrow.trim()}</p>}
<Heading class="msv-heading" id={headingId}>{heading.trim()}</Heading>
</div>
{(intro.trim() || action) && (
<div class="msv-copy">
{intro.trim() && <p class="msv-intro">{intro.trim()}</p>}
{action && <a class="msv-action" href={action.href}>{action.label}</a>}
</div>
)}
</header>
<div class="msv-statement">
<ul class="msv-list" data-scale={scale} role="list">
{list.map((m) => <li class="msv-market"><span class="msv-name">{m}</span></li>)}
</ul>
</div>
{note.trim() && <p class="msv-note">{note.trim()}</p>}
</div>
</section>
)}
<style>
.msv {
--msv-surface: var(--msv-bg, #f4f4f1);
--msv-ink: var(--msv-text, #101214);
--msv-soft: var(--msv-text-muted, #3b3f46);
--msv-accent: var(--msv-brand, #2d43c4);
--msv-accent-ink: var(--msv-brand-text, #ffffff);
--msv-line: var(--msv-border, #d3d4cf);
--msv-font: var(--msv-family, "Archivo", ui-sans-serif, system-ui, sans-serif);
--msv-font-display: var(--msv-display, "Archivo", ui-sans-serif, system-ui, sans-serif);
container-type: inline-size;
background: var(--msv-surface);
color: var(--msv-ink);
font-family: var(--msv-font);
}
/*
* Below 760px the section reads top to bottom as one column — heading, sentence, the markets, the
* action, then the note — so the visitor sees where the business works before being asked to act.
* The header's boxes step aside (display: contents) and `order` sets that sequence.
*/
.msv-inner {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: var(--msv-grid-max, 1232px);
margin: 0 auto;
padding: 56px 20px 60px;
}
.msv[data-presentation="summary"] .msv-inner {
padding-block: 44px 48px;
}
.msv-head,
.msv-copy {
display: contents;
}
.msv-title { order: 1; }
.msv-intro { order: 2; }
.msv-statement { order: 3; }
.msv-action { order: 4; }
.msv-note { order: 5; }
.msv-title {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
.msv-eyebrow {
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.3;
color: var(--msv-accent);
}
.msv-heading {
margin: auto 0 0;
font-family: var(--msv-font-display);
font-size: var(--msv-heading-size, clamp(36px, 6cqi, 64px));
font-weight: 680;
font-stretch: 86%;
line-height: 1;
letter-spacing: -0.02em;
text-wrap: balance;
}
.msv[data-presentation="summary"] .msv-heading {
font-size: var(--msv-heading-size, clamp(30px, 3.8cqi, 48px));
}
.msv-intro {
margin: 18px 0 0;
max-width: 44ch;
font-size: 17px;
line-height: 1.55;
color: var(--msv-soft);
text-wrap: pretty;
}
.msv-action {
display: inline-flex;
align-items: center;
min-height: 48px;
margin-top: 28px;
padding: 0 24px;
background: var(--msv-ink);
color: var(--msv-surface);
font-size: 16px;
font-weight: 600;
text-decoration: none;
transition: background-color 160ms ease, color 160ms ease;
}
.msv-action:hover {
background: var(--msv-accent);
color: var(--msv-accent-ink);
}
.msv-action:focus-visible {
outline: 3px solid var(--msv-accent);
outline-offset: 3px;
}
/*
* The statement: the markets as set type across the full measure, under the one heavy rule.
* Each market carries its separator in front of it; the list starts one separator-width to the left
* and the statement clips that margin, so the separator of whichever market begins a line is cut
* away. Strokes only ever sit between two markets on the same line — none hangs at a line end.
*/
.msv-statement {
align-self: stretch;
margin-top: 28px;
padding-top: 32px;
border-top: 3px solid var(--msv-ink);
overflow-x: clip;
}
.msv-list {
--msv-sep: 0.8em;
margin: 0 0 0 calc(-1 * var(--msv-sep));
padding: 0;
list-style: none;
font-family: var(--msv-font-display);
font-weight: 470;
font-stretch: 92%;
letter-spacing: -0.025em;
line-height: 1.12;
text-wrap: pretty;
}
/* Each market is one unit: lines break between markets, and only a name longer than the line wraps inside itself. */
.msv-market {
display: inline-flex;
align-items: baseline;
max-width: 100%;
margin-block: 0.07em;
vertical-align: baseline;
}
.msv-market::before {
content: "/" / "";
flex: none;
width: var(--msv-sep);
padding-right: 0.02em;
text-align: center;
font-weight: 300;
font-stretch: 100%;
color: var(--msv-accent);
}
/* A name that must wrap keeps tighter leading than the space between markets, so it still reads as one place. */
.msv-name {
min-width: 0;
line-height: 0.98;
overflow-wrap: anywhere;
}
.msv-list[data-scale="xl"] { font-size: 44px; }
.msv-list[data-scale="lg"] { font-size: 32px; }
.msv-list[data-scale="md"] { font-size: 26px; }
.msv-list[data-scale="sm"] { font-size: 22px; }
.msv-note {
align-self: stretch;
margin: 28px 0 0;
padding-top: 16px;
border-top: 1px solid var(--msv-line);
font-size: 15px;
line-height: 1.5;
color: var(--msv-soft);
}
@container (min-width: 760px) {
/* From 760px: the header sits above the rule in reading order (stacked until 960px), note last. */
.msv-inner {
display: block;
padding: 88px 40px 80px;
}
.msv[data-presentation="summary"] .msv-inner {
padding-block: 64px 60px;
}
.msv-title,
.msv-intro,
.msv-statement,
.msv-note,
.msv-action {
order: 0;
}
.msv-head {
display: grid;
row-gap: 22px;
padding-bottom: 36px;
}
.msv-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.msv-intro {
margin: 0;
max-width: 52ch;
}
.msv-action {
margin-top: 0;
}
.msv-statement {
margin-top: 0;
padding-top: 40px;
}
/* Market type grows with the section but stays clearly under the heading at tablet widths. */
.msv-list[data-scale="xl"] { font-size: clamp(52px, calc(10cqi - 24px), 120px); }
.msv-list[data-scale="lg"] { font-size: clamp(36px, calc(7.2cqi - 36px), 68px); }
.msv-list[data-scale="md"] { font-size: clamp(30px, calc(5.6cqi - 26px), 56px); }
.msv-list[data-scale="sm"] { font-size: clamp(26px, calc(4.2cqi - 18px), 44px); }
.msv-note {
margin-top: 32px;
}
}
@container (min-width: 960px) {
/* Header beside header — eyebrow | sentence on the top line, heading | action on the bottom line. */
.msv-head {
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
column-gap: 56px;
align-items: stretch;
}
.msv-copy {
gap: 18px;
}
.msv-intro {
max-width: 44ch;
}
.msv-action {
margin-top: auto;
}
}
@container (min-width: 1100px) {
.msv-inner {
padding-inline: var(--msv-grid-gutter, 48px);
}
}
@media (prefers-reduced-motion: reduce) {
.msv-action {
transition: none;
}
}
@media (forced-colors: active) {
.msv-statement {
border-top-color: CanvasText;
}
.msv-action {
border: 2px solid ButtonText;
}
.msv-market::before {
color: CanvasText;
}
}
</style>
===== END src/components/MarketsServed.astro =====
===== FILE (supporting): src/components/MarketsServed.md =====
# Markets served
Where a business works, set as one typeset statement: a split header (heading; sentence and an optional action) over a heavy rule, then the supplied markets, regions or areas running across the full width as display type sized by count. Lists only what is supplied, in order; no map, chips, rows, numbers or claims. A summary presentation for a mention on another page.
## Integration
- A section heading (h2 by default).
- No markets, no section. Markets render in the order and wording supplied; the component adds no map, pin, lane, route, count or claim.
- Type scales with the count: 2–3 very large, 4–7 large, 8–12 medium. `presentation="summary"` sets it one step smaller for a mention on a page that is not about the footprint.
- Header splits heading | sentence from 760px of the section's own width (container queries); stacked below.
- The action renders only from `cta` with a safe href.
- Loads Archivo (variable width) from Google Fonts by default. Pass fontStylesheet="" on a site that loads its own faces (via --msv-family / --msv-display).
## Provenance
Staged as candidate `markets-served` (Step 28) and admitted through the candidate pipeline. Source type: original. References (direction only): none. Quality history: quality/candidates/records/markets-served.json.
===== END src/components/MarketsServed.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.