Header — start here
Install as site header
The one to use for a header. It carries every file inline with its destination path, and tells the destination agent to inventory the existing routes and CSS before replacing anything — the parts that go wrong when a header meets a real layout.
Read it first
Install "Closing statement footer" from my Astro Component Library as this project's site header.
This is a header, not a page section. It replaces something that already exists, over routes that already exist, under CSS that already exists. Work through the steps in order and report as you go.
--- WHAT THIS HEADER IS ---
Component: Closing statement footer
Library page: https://astro.baysixmedia.com/components/closing-statement-footer/
Navigation mode: static — a flat list of links, with no panels to open
Position mode: normal
Desktop dropdowns: no
Separate mobile menu: no
Locks body scroll while open: no
Astro client navigation: supported — it re-initialises on astro:page-load and detaches its listeners before each swap
Required files: 1
Dependencies: none — no npm packages, no CSS framework, no UI framework
Assets: none
Prop interface, verbatim from the component:
interface Props {
businessName?: string;
logo?: { src: string; alt?: string; width?: number; height?: number } | null;
homeHref?: string;
/** The closing statement. Omitted: the top is a slim contact row instead. */
closingHeading?: string;
closingIntro?: string;
headingLevel?: "h2" | "h3";
cta?: ClosingStatementFooterLink | null;
phone?: { number: string; label?: string } | null;
email?: string;
/** One or two sentences under the brand, from verified facts. */
description?: string;
address?: string | readonly string[];
addressHref?: string;
hours?: string | readonly string[];
primaryLinks?: readonly ClosingStatementFooterLink[];
serviceLinks?: readonly ClosingStatementFooterLink[];
legalLinks?: readonly ClosingStatementFooterLink[];
social?: readonly ClosingStatementFooterLink[];
labels?: { pages?: string; services?: string; call?: string; email?: string };
copyright?: string;
/** The wide name at the foot; on by default. */
signOff?: boolean;
fontStylesheet?: string;
class?: string;
id?: string;
}
Route data is passed through props. The prop interface above is the shape to map the destination's routes into.
Known integration notes for this header:
- Goes at the end of the layout, after <slot />, outside <main>. It is a <footer>: at the top level that is the contentinfo landmark.
- closingHeading opens it with a statement beside the action, number and email; omit it on pages that already ended on a call to action and the top becomes one slim contact row.
- Address, social links, pages and services each render only when supplied; an empty column is never drawn. tel: and mailto: are built from supplied values; the copyright is computed.
- The sign-off (the business name, wide, one tone off the ground) is sized from the name's length so it always fits; signOff=false removes it.
- Loads Geist from Google Fonts by default. Pass fontStylesheet="" on a site that loads its own faces.
--- EXPECTED BEHAVIOUR ---
Desktop: every link is reachable in one tab pass, with no panels to open.
Mobile: the same markup reflows; there is no separate menu to open.
Accessibility: a single landmark <nav> with an accessible name, aria-current on the link for the current route, visible focus on every control, 44x44px minimum targets, full prefers-reduced-motion support, and a working no-JavaScript fallback. None of this may be removed while adapting the design.
--- STEPS ---
1. Inspect this project before editing anything. Find its Astro version, whether it uses <ClientRouter /> or view transitions, its global stylesheet, and its existing header. Report what you found before changing a file.
2. Find the real shared layout — the one every page actually renders through — and the current header inside it. Do not assume src/layouts/Layout.astro.
3. Inventory every current navigation route: read the existing header, src/pages/, and any route or nav-data file. List every label and href before touching anything.
4. Map those real routes into this component's navigation structure. Use the destination's own routes and labels. Do not ship the demo tree, and do not invent routes to fill out a panel that looks empty.
5. Preserve the destination's branding and approved copy: its brand name, wordmark, logo mark, button labels and tone. Adapt the component's colour and type props to the destination's tokens.
6. Identify global CSS in this project that targets header, nav, a, button, ul or body, and anything that sets overflow or transform on an ancestor of the header. Those are what break an installed header. Report the collisions and how you resolved each one.
7. Build the replacement alongside the existing header first. Remove the old one only once the new one renders and works on every route.
8. Remove the old header's JavaScript and CSS once it is gone — menu toggles, scroll listeners, body-scroll locks, media-query handlers and their stylesheets. Leaving them causes double body locks and phantom listeners.
9. Make sure exactly one header renders. Check for a second one in a nested layout, in an individual page, or left behind in the old markup.
10. Wire up positioning. It sits in the normal flow and scrolls away with the page. Nothing needs reserving for it.
11. No layout height needs reserving for this one — say so explicitly rather than adding padding that would leave a gap.
12. Preserve the skip link. If the project has one it must still be the first focusable element and must still land on the main content; if a normal header would cover the target, add scroll-margin-top to it. If the project has no skip link, add one.
13. Leave analytics, meta tags, SEO, structured data and unrelated scripts in the layout exactly as they are. Removing a header is not a reason to touch anything else in <head>.
14. Make it work on a normal page load and, if this project uses client-side routing, across client navigation too: state reset between pages, no listeners accumulating, no body scroll lock surviving a navigation.
15. Visit every real route in this project and confirm the header renders, the correct link is marked current, and every href resolves. No 404s.
16. Test the whole site at 1440, 1280, 1024, 834, 390 and 320px: no horizontal overflow, no clipped panels, keyboard operation throughout, and the header never covering focused content.
--- FILES ---
Write each block below to the path in its header. Only the file marked (required) is needed to render; the supporting files are documentation and worked examples.
===== FILE (required): src/components/ClosingStatementFooter.astro =====
---
/**
* ClosingStatementFooter (candidate, shell campaign — original, role `footer`)
*
* The page's close and the site's footer as one composition on a deep ground. At the top, the closing:
* a large statement on the left and, on the right, a contact index — the ways to reach the business as
* equal ruled rows in order of weight: the one action (its label set large, an arrow in the accent),
* the number, the email. A hairline, then the reference layer: the brand with a supplied description,
* address and hours (each only when supplied), the page links and the service links as quiet columns.
* Then the legal line. At the very foot the business name is set as a sign-off fitted exactly to the
* grid width (SVG textLength, spacing only), one tone off the ground — the last thing on the page.
*
* Without a closing statement (a page that already ended on a call to action) the top becomes a slim
* contact row, so the same button is never asked for twice in a row.
*
* Truth rules. Every fact, link and action renders only when passed; the address only when supplied;
* social links only when supplied (by name, never icons standing in for proof). tel:/mailto: are built
* from supplied values; the copyright is "© {year} {name}" unless overridden. Empty columns are never
* drawn.
*
* Reads, never declares: --csf-bg, --csf-text, --csf-text-muted, --csf-brand, --csf-brand-text,
* --csf-border, --csf-family, --csf-display, --csf-grid-max, --csf-grid-gutter, --csf-heading-size.
*/
export interface ClosingStatementFooterLink {
label: string;
href: string;
}
interface Props {
businessName?: string;
logo?: { src: string; alt?: string; width?: number; height?: number } | null;
homeHref?: string;
/** The closing statement. Omitted: the top is a slim contact row instead. */
closingHeading?: string;
closingIntro?: string;
headingLevel?: "h2" | "h3";
cta?: ClosingStatementFooterLink | null;
phone?: { number: string; label?: string } | null;
email?: string;
/** One or two sentences under the brand, from verified facts. */
description?: string;
address?: string | readonly string[];
addressHref?: string;
hours?: string | readonly string[];
primaryLinks?: readonly ClosingStatementFooterLink[];
serviceLinks?: readonly ClosingStatementFooterLink[];
legalLinks?: readonly ClosingStatementFooterLink[];
social?: readonly ClosingStatementFooterLink[];
labels?: { pages?: string; services?: string; call?: string; email?: string };
copyright?: string;
/** The wide name at the foot; on by default. */
signOff?: boolean;
fontStylesheet?: string;
class?: string;
id?: string;
}
const {
businessName = "",
logo = null,
homeHref = "/",
closingHeading = "",
closingIntro = "",
headingLevel = "h2",
cta = null,
phone = null,
email = "",
description = "",
address,
addressHref,
hours,
primaryLinks = [],
serviceLinks = [],
legalLinks = [],
social = [],
labels = {},
copyright,
signOff = true,
fontStylesheet = "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap",
class: className,
id,
} = Astro.props;
const safeHref = (href: string | undefined) => {
const h = (href ?? "").trim();
return /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(h) ? h : null;
};
const clean = (list: readonly ClosingStatementFooterLink[]) => list.map((l) => ({ label: (l?.label ?? "").trim(), href: safeHref(l?.href) })).filter((l): l is { label: string; href: string } => Boolean(l.label && l.href));
const pages = clean(primaryLinks);
const services = clean(serviceLinks);
const legal = clean(legalLinks);
const socials = clean(social).filter((l) => /^https?:\/\//i.test(l.href));
const action = cta && cta.label?.trim() && safeHref(cta.href) ? { label: cta.label.trim(), href: safeHref(cta.href)! } : null;
const digits = (phone?.number ?? "").replace(/[^\d+]/g, "");
const call = phone && phone.number?.trim() && digits.replace(/\D/g, "").length >= 7 ? { number: phone.number.trim(), label: (phone.label ?? labels.call ?? "Call").trim(), href: `tel:${digits}` } : null;
const mail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()) ? email.trim() : "";
const lines = (v: string | readonly string[] | undefined) => (Array.isArray(v) ? v : v ? [v] : []).map((x) => String(x).trim()).filter(Boolean);
const addressLines = lines(address);
const hourLines = lines(hours);
const about = description.trim();
const mapHref = addressHref && /^https:\/\//i.test(addressHref.trim()) ? addressHref.trim() : null;
const name = businessName.trim();
const statement = closingHeading.trim();
const hasContact = Boolean(action || call || mail);
const year = new Date().getFullYear();
const rights = copyright === undefined ? (name ? `© ${year} ${name}` : "") : copyright;
const Heading = headingLevel;
/*
* The sign-off is SVG text fitted exactly to the grid width (textLength, spacing only — glyphs are never
* stretched). Its size is estimated from the name's length so the spacing adjustment stays small.
*/
const SIGN_W = 1000;
const signFont = name ? Math.round(Math.min(260, SIGN_W / (Math.max(name.length, 4) * 0.5))) : 0;
/* Tall enough for descenders: the baseline sits at 80% of the box. */
const signH = Math.round(signFont * 1.02);
const home = safeHref(homeHref);
const brandFacts = Boolean(about || addressLines.length || hourLines.length);
---
{(name || hasContact || pages.length > 0) && (
<footer class:list={["csf", className]} id={id} data-closing={statement ? "statement" : "row"} data-brand={brandFacts ? "full" : "name"}>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<div class="csf-inner">
{(statement || hasContact) && (
<div class="csf-close">
{statement && (
<div class="csf-statement">
<Heading class="csf-heading">{statement}</Heading>
{closingIntro.trim() && <p class="csf-intro">{closingIntro.trim()}</p>}
</div>
)}
{hasContact && (
<ul class="csf-reach" role="list">
{action && (
<li>
<a class="csf-row csf-row--action" href={action.href}>
<span class="csf-row-value">{action.label}</span>
<svg class="csf-arrow" aria-hidden="true" viewBox="0 0 24 24" width="24" height="24"><path d="M4 12h15M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.8" /></svg>
</a>
</li>
)}
{call && (
<li>
<a class="csf-row csf-row--call" href={call.href}>
<span class="csf-row-key">{call.label}</span>
<span class="csf-row-value">{call.number}</span>
</a>
</li>
)}
{mail && (
<li>
<a class="csf-row" href={`mailto:${mail}`}>
<span class="csf-row-key">{labels.email ?? "Email"}</span>
<span class="csf-row-value">{mail}</span>
</a>
</li>
)}
</ul>
)}
</div>
)}
<div class="csf-reference">
{(name || logo?.src || brandFacts) && (
<div class="csf-brand">
{(name || logo?.src) && (home ? (
<a class="csf-brand-link" href={home}>
{logo?.src ? <img class="csf-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="csf-wordmark">{name}</span>}
</a>
) : (
<span class="csf-brand-link">{logo?.src ? <img class="csf-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="csf-wordmark">{name}</span>}</span>
))}
{about && <p class="csf-about">{about}</p>}
{(addressLines.length > 0 || hourLines.length > 0) && (
<div class="csf-facts">
{addressLines.length > 0 && (
<address class="csf-address">
{mapHref ? <a href={mapHref}>{addressLines.map((l) => <span>{l}</span>)}</a> : addressLines.map((l) => <span>{l}</span>)}
</address>
)}
{hourLines.length > 0 && <p class="csf-hours">{hourLines.map((l) => <span>{l}</span>)}</p>}
</div>
)}
</div>
)}
{pages.length > 0 && (
<nav class="csf-group" aria-label={labels.pages ?? "Pages"}>
<p class="csf-group-title" aria-hidden="true">{labels.pages ?? "Pages"}</p>
<ul role="list">
{pages.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
</ul>
</nav>
)}
{services.length > 0 && (
<nav class="csf-group" aria-label={labels.services ?? "Services"}>
<p class="csf-group-title" aria-hidden="true">{labels.services ?? "Services"}</p>
<ul role="list">
{services.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
</ul>
</nav>
)}
</div>
{(rights || legal.length > 0 || socials.length > 0) && (
<div class="csf-legal">
{rights && <p class="csf-rights">{rights}</p>}
{(legal.length > 0 || socials.length > 0) && (
<ul class="csf-legal-links" role="list">
{legal.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
{socials.map((l) => <li><a href={l.href} rel="me noopener">{l.label}</a></li>)}
</ul>
)}
</div>
)}
{signOff && name && (
<svg class="csf-sign" aria-hidden="true" focusable="false" viewBox={`0 0 ${SIGN_W} ${signH}`} preserveAspectRatio="xMinYMax meet">
<text x="0" y={Math.round(signFont * 0.8)} textLength={SIGN_W} lengthAdjust="spacing" font-size={signFont}>{name}</text>
</svg>
)}
</div>
</footer>
)}
<style>
.csf {
--csf-ground: var(--csf-bg, #111416);
--csf-ink: var(--csf-text, #f3f1ec);
--csf-soft: var(--csf-text-muted, rgb(243 241 236 / 0.66));
--csf-accent: var(--csf-brand, #d9c7a3);
--csf-accent-ink: var(--csf-brand-text, #111416);
--csf-line: var(--csf-border, rgb(243 241 236 / 0.16));
--csf-font: var(--csf-family, "Geist", ui-sans-serif, system-ui, sans-serif);
--csf-font-display: var(--csf-display, "Geist", ui-sans-serif, system-ui, sans-serif);
--csf-pad: 20px;
container-type: inline-size;
overflow: hidden;
background: var(--csf-ground);
color: var(--csf-ink);
font-family: var(--csf-font);
}
.csf-inner {
box-sizing: border-box;
max-width: var(--csf-grid-max, 1232px);
margin: 0 auto;
padding: 56px var(--csf-pad) 20px;
}
.csf a {
color: inherit;
}
.csf a:focus-visible {
outline: 2px solid var(--csf-accent);
outline-offset: 3px;
border-radius: 2px;
}
/* The close: statement | contact index */
.csf-close {
display: grid;
gap: 32px;
padding-bottom: 44px;
border-bottom: 1px solid var(--csf-line);
}
.csf[data-closing="row"] .csf-close {
padding-bottom: 28px;
}
.csf[data-closing="statement"] .csf-close {
padding-bottom: 24px;
}
.csf-heading {
margin: 0;
max-width: 18ch;
font-family: var(--csf-font-display);
font-size: var(--csf-heading-size, clamp(34px, 7.4cqi, 60px));
font-weight: 600;
line-height: 1.04;
letter-spacing: -0.035em;
text-wrap: balance;
}
.csf-intro {
margin: 18px 0 0;
max-width: 44ch;
font-size: 17px;
line-height: 1.55;
color: var(--csf-soft);
}
.csf-reach {
display: grid;
align-content: end;
margin: 0;
padding: 0;
list-style: none;
}
.csf-row {
display: grid;
grid-template-columns: 5em minmax(0, 1fr);
align-items: baseline;
gap: 12px;
min-height: 56px;
padding-block: 14px;
border-top: 1px solid var(--csf-line);
text-decoration: none;
}
.csf-row-key {
font-size: 13px;
color: var(--csf-soft);
}
.csf-row-value {
font-size: 18px;
font-weight: 500;
overflow-wrap: anywhere;
}
/* A number never breaks across lines. */
.csf-row--call .csf-row-value {
white-space: nowrap;
overflow-wrap: normal;
font-family: var(--csf-font-display);
font-size: 26px;
font-weight: 600;
letter-spacing: -0.015em;
font-variant-numeric: tabular-nums;
}
/* The action is the index's first row: its label large, an arrow in the accent. */
.csf-row--action {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
border-top: 2px solid var(--csf-ink);
}
.csf-row--action .csf-row-value {
font-family: var(--csf-font-display);
font-size: clamp(22px, 3.2cqi, 30px);
font-weight: 600;
letter-spacing: -0.02em;
}
.csf-arrow {
color: var(--csf-accent);
transition: transform 180ms ease;
}
.csf-row--action:hover .csf-arrow {
transform: translateX(4px);
}
.csf-row:hover .csf-row-value {
text-decoration: underline;
text-underline-offset: 5px;
text-decoration-thickness: 1px;
}
.csf-row--action:hover .csf-row-value {
text-decoration-color: var(--csf-accent);
}
/* The reference layer */
.csf-reference {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 32px 24px;
padding-block: 40px 44px;
}
.csf-brand {
grid-column: 1 / -1;
display: grid;
gap: 14px;
align-content: start;
}
.csf-brand-link {
display: inline-flex;
align-items: center;
min-height: 44px;
justify-self: start;
text-decoration: none;
}
.csf-wordmark {
font-family: var(--csf-font-display);
font-size: 20px;
font-weight: 650;
letter-spacing: -0.02em;
overflow-wrap: anywhere;
}
.csf-logo {
display: block;
height: 36px;
width: auto;
max-width: 220px;
object-fit: contain;
}
.csf-about {
margin: 0;
max-width: 38ch;
font-size: 15px;
line-height: 1.55;
color: var(--csf-soft);
}
.csf-facts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px 24px;
font-size: 14px;
line-height: 1.55;
color: var(--csf-soft);
}
.csf-address,
.csf-hours {
display: grid;
margin: 0;
font-style: normal;
}
.csf-address a {
display: grid;
text-decoration: none;
}
.csf-address a:hover {
color: var(--csf-ink);
}
.csf-group-title {
margin: 0 0 12px;
font-size: 13px;
color: var(--csf-soft);
}
.csf-group ul {
display: grid;
gap: 2px;
margin: 0;
padding: 0;
list-style: none;
}
.csf-group a {
display: inline-flex;
align-items: center;
min-height: 36px;
font-size: 15px;
text-decoration: none;
}
.csf-group a:hover {
text-decoration: underline;
text-underline-offset: 4px;
}
.csf-legal {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 24px;
padding-top: 20px;
border-top: 1px solid var(--csf-line);
font-size: 13px;
color: var(--csf-soft);
}
.csf-rights {
margin: 0;
}
.csf-legal-links {
display: flex;
flex-wrap: wrap;
gap: 0 20px;
margin: 0;
padding: 0;
list-style: none;
}
.csf-legal-links a {
display: inline-flex;
align-items: center;
min-height: 36px;
text-decoration: none;
}
.csf-legal-links a:hover {
color: var(--csf-ink);
text-decoration: underline;
text-underline-offset: 4px;
}
/* The sign-off: the name fitted to the grid width, one tone off the ground, sitting on the footer's foot. */
/* One solid tone a step off the ground (mixed, not transparent): quiet, but crisp. */
.csf-sign {
display: block;
width: 100%;
height: auto;
margin-top: 28px;
color: color-mix(in srgb, var(--csf-ink) 17%, var(--csf-ground));
user-select: none;
}
.csf-sign text {
font-family: var(--csf-font-display);
font-weight: 700;
letter-spacing: -0.02em;
fill: currentColor;
}
@container (min-width: 760px) {
.csf-close {
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
column-gap: 56px;
align-items: end;
}
.csf[data-closing="statement"] .csf-close {
padding-bottom: 44px;
}
/* Beside the statement the index closes on its own rule; stacked, the section's rule closes it. */
.csf[data-closing="statement"] .csf-reach {
border-bottom: 1px solid var(--csf-line);
}
.csf-row--call .csf-row-value {
font-size: 22px;
}
.csf[data-closing="row"] .csf-close {
grid-template-columns: 1fr;
}
.csf[data-closing="row"] .csf-reach {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
column-gap: 32px;
border-bottom: 0;
}
.csf-reference {
grid-template-columns: minmax(0, 5fr) repeat(2, minmax(0, 3fr));
}
.csf[data-brand="name"] .csf-reference {
grid-template-columns: minmax(0, 4fr) repeat(2, minmax(0, 4fr));
}
.csf-brand {
grid-column: auto;
}
}
@container (min-width: 960px) {
/* A container query cannot style its own container: the gutter is set on its children. */
.csf > * {
--csf-pad: var(--csf-grid-gutter, 48px);
}
.csf-inner {
padding-top: 88px;
}
.csf-close,
.csf[data-closing="statement"] .csf-close {
padding-bottom: 64px;
}
.csf-reference {
padding-block: 48px 56px;
}
.csf-sign {
margin-top: 40px;
}
.csf-row--call .csf-row-value {
font-size: 26px;
}
}
@media (prefers-reduced-motion: reduce) {
.csf-arrow {
transition: none;
}
.csf-row--action:hover .csf-arrow {
transform: none;
}
}
@media (forced-colors: active) {
.csf-row--action {
border-top-color: CanvasText;
}
.csf-sign {
color: GrayText;
}
}
</style>
===== END src/components/ClosingStatementFooter.astro =====
===== FILE (supporting): src/components/ClosingStatementFooter.md =====
# Closing statement footer
A dark footer that closes the page: a large statement beside the ways to reach the business (action, number, email), a quiet layer with the brand, address (only if supplied), pages and services, a legal line, and the business name as a wide sign-off sized to fit. Without a statement the top is one slim contact row.
## Integration
- Goes at the end of the layout, after <slot />, outside <main>. It is a <footer>: at the top level that is the contentinfo landmark.
- closingHeading opens it with a statement beside the action, number and email; omit it on pages that already ended on a call to action and the top becomes one slim contact row.
- Address, social links, pages and services each render only when supplied; an empty column is never drawn. tel: and mailto: are built from supplied values; the copyright is computed.
- The sign-off (the business name, wide, one tone off the ground) is sized from the name's length so it always fits; signOff=false removes it.
- Loads Geist from Google Fonts by default. Pass fontStylesheet="" on a site that loads its own faces.
## Provenance
Staged as candidate `closing-statement-footer` (Step 28) and admitted through the candidate pipeline. Source type: original. References (direction only): https://www.flexport.com/, https://linear.app/, https://forteraglobal.com/. Quality history: quality/candidates/records/closing-statement-footer.json.
===== END src/components/ClosingStatementFooter.md =====
--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
No verification run has been recorded.
Passed: integration route, keyboard, no javascript, reduced motion, two instances, no overflow.
This record pre-dates source fingerprinting, so it is not tied to a specific revision.
--- 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.
--- FINISH ---
Run this project's own check and build commands. Report: the layout file you changed, the routes you mapped and what you mapped them to, the CSS collisions you found, what you deleted from the old header, and anything you could not verify. Do not commit and do not deploy.