The 5 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: "Circle badge hero"
Library page: https://astro.baysixmedia.com/components/circle-badge-hero/
Files: 5 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/CircleBadgeHero.astro =====
---
/**
* CircleBadgeHero
*
* A white top band with an orange eyebrow, a two-size uppercase heading
* (a small line over one huge word), a paragraph and, on the right, three
* overlapping client avatars with a big figure and a caption. Below it a
* full-bleed photo under a navy wash, with a round navy badge overlapping its
* top edge — a ring of text that turns once every 8 seconds around a static
* arrow — and, at the bottom right, an orange block with two round arrow
* buttons beside a white block holding one underlined link.
*
* No script. The ring turns with a CSS animation, off under reduced motion.
*
* Reads --tg-* design tokens from any ancestor, with fallbacks.
*/
export interface CircleBadgeHeroLink {
label: string;
href: string;
}
export interface CircleBadgeHeroImage {
src?: string;
alt?: string;
}
export interface CircleBadgeHeroProps {
eyebrow?: string;
/** The smaller first line of the heading. `""` drops it. */
headingTop?: string;
/** The huge second line of the heading. */
headingMain?: string;
headingLevel?: "h1" | "h2";
text?: string;
/** Up to three round avatars. `[]` drops the row. */
clients?: CircleBadgeHeroImage[];
/** The big figure beside the avatars, e.g. "42k+". `""` drops it. */
statValue?: string;
statLabel?: string;
/** The full-bleed photo. Omitted, or `src: ""`, renders the labelled placeholder. */
image?: CircleBadgeHeroImage;
/** The rotating badge. Its label is written round the ring. `null` drops it. */
badge?: CircleBadgeHeroLink | null;
/**
* The right-hand round arrow is a real link to this. The left arrow is
* always decorative. `null` makes both decorative.
*/
nextLink?: CircleBadgeHeroLink | null;
/** The underlined link in the white block. `null` drops the whole corner card. */
cta?: CircleBadgeHeroLink | null;
/**
* Extra top padding in px, for a header that sits absolutely over the
* hero. The reference's is 104px. Default 0: the hero follows the header.
*/
headerOffset?: number;
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends CircleBadgeHeroProps {}
/* No copy, photo or links of its own: the catalog demo passes them through
the registry's props, so a copied hero never ships an invented business. */
const {
eyebrow = "",
headingTop = "",
headingMain = "",
headingLevel = "h1",
text = "",
clients = [],
statValue = "",
statLabel = "",
image: imageProp,
badge = null,
nextLink = null,
cta = null,
headerOffset = 0,
fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;600&display=swap",
class: className,
id,
} = Astro.props;
/* Only the photo passed; with no alt it is decorative. */
const image = { src: imageProp?.src ?? "", alt: imageProp?.alt ?? "" };
const Heading = headingLevel;
const uid = id ?? `cbh-${Math.random().toString(36).slice(2, 8)}`;
const ringId = `${uid}-ring`;
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
/* A link needs both halves: a label with no destination, or the reverse, is not drawn. */
const link = (l: CircleBadgeHeroLink | null | undefined) => (l && l.label && l.href ? l : null);
const badgeLink = link(badge);
const nextHop = link(nextLink);
const ctaLink = link(cta);
const avatars = clients.slice(0, 3);
const ringText = badgeLink ? `${badgeLink.label} * ${badgeLink.label} * ` : "";
/* Measured slot: 1425 × 783 at desktop (~1.82:1); the file is 2000 × 1100. */
const IMAGE_SPEC = "~1.82:1 · 2000 × 1100 px";
/* A photo alone is not a hero: with no copy, figure or link, render nothing. */
const hasCopy = !!eyebrow || !!headingTop || !!headingMain || !!text;
const hasTop = hasCopy || avatars.length > 0 || !!statValue || !!statLabel;
const hasContent = hasTop || !!badgeLink || !!ctaLink;
---
{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
class:list={["cbh", className]}
id={id}
style={`--cbh-offset: ${Math.max(0, Number(headerOffset) || 0)}px;`}
>
{hasTop && (
<div class="cbh-top">
{hasCopy && (
<div class="cbh-copy">
{eyebrow && <p class="cbh-eyebrow">{eyebrow}</p>}
{(headingTop || headingMain) && (
<Heading class="cbh-heading">
{headingTop && <span class="cbh-heading-top">{headingTop}</span>}
<span class="cbh-heading-main">{headingMain}</span>
</Heading>
)}
{text && <p class="cbh-text">{text}</p>}
</div>
)}
{
(avatars.length > 0 || statValue || statLabel) && (
<div class="cbh-clients">
{(avatars.length > 0 || statValue) && (
<div class="cbh-clients-row">
{avatars.length > 0 && (
<ul class="cbh-avatars" role="list">
{avatars.map((client) => (
<li class="cbh-avatar">
{client.src ? (
<img src={client.src} alt={client.alt ?? ""} width="60" height="60" loading="lazy" decoding="async" />
) : (
<span class="cbh-avatar-ph" aria-hidden="true" />
)}
</li>
))}
</ul>
)}
{statValue && <p class="cbh-stat">{statValue}</p>}
</div>
)}
{statLabel && <p class="cbh-stat-label">{statLabel}</p>}
</div>
)
}
</div>
)}
<div class="cbh-band">
<div class="cbh-media">
{
image.src ? (
<img src={image.src} alt={image.alt} width="2000" height="1100" decoding="async" fetchpriority="high" />
) : (
<div class="cbh-ph" role="img" aria-label="Image placeholder: wide landscape photo">
<div class="cbh-ph-text" aria-hidden="true">
<span class="cbh-ph-label">Image</span>
<span class="cbh-ph-hint">Wide landscape photo</span>
<span class="cbh-ph-spec">{IMAGE_SPEC}</span>
</div>
</div>
)
}
<div class="cbh-wash" aria-hidden="true"></div>
</div>
{
badgeLink && (
<div class="cbh-badge-anchor">
<a class="cbh-badge" href={safeHref(badgeLink.href)} aria-label={badgeLink.label}>
<svg class="cbh-ring" viewBox="0 0 200 200" aria-hidden="true" focusable="false">
<defs>
<path id={ringId} d="M100,100 m-60,0 a60,60 0 1,1 120,0 a60,60 0 1,1 -120,0" />
</defs>
<text class="cbh-ring-text">
<textPath href={`#${ringId}`} textLength="374" lengthAdjust="spacing">
{ringText}
</textPath>
</text>
</svg>
<span class="cbh-badge-core" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
<path d="M7 17 17 7M9 7h8v8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</a>
</div>
)
}
{
ctaLink && (
<div class="cbh-corner">
<div class="cbh-arrows">
<span class="cbh-round cbh-round--off" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
<path d="M19 12H5m6-6-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
{nextHop ? (
<a class="cbh-round" href={safeHref(nextHop.href)} aria-label={nextHop.label}>
<span class="cbh-round-core" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
<path d="M5 12h14m-6-6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</a>
) : (
<span class="cbh-round cbh-round--static" aria-hidden="true">
<span class="cbh-round-core">
<svg viewBox="0 0 24 24" width="24" height="24" focusable="false">
<path d="M5 12h14m-6-6 6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</span>
</span>
)}
</div>
<div class="cbh-cta-wrap">
<a class="cbh-cta" href={safeHref(ctaLink.href)}>{ctaLink.label}</a>
</div>
</div>
)
}
</div>
</section>
)}
<style>
.cbh {
--cbh-navy: var(--tg-navy, #020e28);
--cbh-teal: #0e2d3a;
--cbh-orange: var(--tg-orange, #fd5523);
--cbh-muted: var(--tg-muted, #61697d);
--cbh-display: var(--tg-font-display, Outfit, "Outfit Placeholder", ui-sans-serif, system-ui, sans-serif);
--cbh-body: var(--tg-font-body, "DM Sans", "DM Sans Placeholder", ui-sans-serif, system-ui, sans-serif);
--cbh-ease: cubic-bezier(0.25, 0.1, 0.25, 1);
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
gap: 36px;
padding-top: calc(40px + var(--cbh-offset, 0px));
background: #fff;
color: var(--cbh-navy);
overflow-x: clip;
}
.cbh *,
.cbh *::before,
.cbh *::after {
box-sizing: border-box;
}
/* ---- top band ---- */
.cbh-top {
display: flex;
flex-direction: column;
gap: 30px;
width: 100%;
max-width: 1293px;
margin: 0 auto;
padding: 0 20px;
}
.cbh-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
}
.cbh .cbh-eyebrow {
margin: 0 0 20px;
font: 600 16px/1 var(--cbh-display);
text-transform: uppercase;
color: var(--cbh-orange);
}
.cbh-heading {
display: flex;
flex-direction: column;
gap: 16px;
margin: 0;
font-family: var(--cbh-display);
text-transform: uppercase;
letter-spacing: normal;
color: var(--cbh-navy);
}
.cbh-heading-top {
display: block;
font-size: clamp(40px, 5.2vw, 74px);
font-weight: 700;
line-height: 0.95;
}
.cbh-heading-main {
display: block;
font-size: clamp(50px, 11.16vw, 159px);
font-weight: 800;
line-height: 0.9;
}
.cbh .cbh-text {
max-width: 834px;
margin: 20px 0 0;
font: 400 16px/1.155 var(--cbh-body);
color: var(--cbh-navy);
}
.cbh-clients {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
flex: none;
}
.cbh-clients-row {
display: flex;
align-items: center;
gap: 16px;
}
.cbh .cbh-avatars {
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
.cbh-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
background: #ecf0f1;
}
.cbh-avatar + .cbh-avatar {
margin-left: -14px;
}
.cbh-avatar img,
.cbh-avatar-ph {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cbh-avatar-ph {
background: repeating-linear-gradient(135deg, #e6e9ee 0 6px, #edf0f3 6px 12px);
}
.cbh .cbh-stat {
margin: 0;
font: 600 34px/normal var(--cbh-display);
color: var(--cbh-navy);
}
.cbh .cbh-stat-label {
max-width: 190px;
margin: 0;
font: 600 18px/1.5 var(--cbh-display);
color: var(--cbh-muted);
}
/* ---- photo band ---- */
.cbh-band {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 332px;
}
.cbh-media {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--cbh-navy);
}
.cbh-media img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.cbh-wash {
position: absolute;
inset: 0;
background: rgb(2 14 40 / 0.5);
}
/* Placeholder for `src: ""`. */
.cbh-ph {
display: grid;
place-items: center;
width: 100%;
height: 100%;
padding: 16px;
text-align: center;
background: repeating-linear-gradient(135deg, #1b2740 0 12px, #213050 12px 24px);
color: rgb(255 255 255 / 0.75);
}
.cbh-ph-text {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.cbh-ph-label {
font: 600 12px/1 var(--cbh-body);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.cbh-ph-hint {
max-width: 28ch;
font: 500 14px/1.4 var(--cbh-body);
}
.cbh-ph-spec {
font: 400 12px/1.2 var(--cbh-body);
}
/* ---- rotating badge: hidden on a phone, as measured ---- */
.cbh-badge-anchor {
display: none;
}
.cbh-badge {
position: absolute;
display: grid;
place-items: center;
width: 182px;
height: 182px;
border-radius: 50%;
background: var(--cbh-navy);
color: #fff;
text-decoration: none;
}
.cbh-badge:focus-visible {
outline: 3px solid var(--cbh-orange);
outline-offset: 4px;
}
.cbh-ring {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
animation: cbh-spin 8s linear infinite;
}
.cbh-ring-text {
fill: #fff;
font: 600 19px var(--cbh-display);
}
.cbh-badge-core {
display: grid;
place-items: center;
width: 91px;
height: 91px;
border: 1px solid rgb(255 255 255 / 0.85);
border-radius: 50%;
}
@keyframes cbh-spin {
to {
transform: rotate(360deg);
}
}
/* ---- corner card: orange arrows + white link ---- */
.cbh-corner {
position: relative;
z-index: 1;
display: flex;
align-self: flex-end;
width: 100%;
height: 82px;
}
.cbh-arrows {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex: 0 0 47%;
background: var(--cbh-orange);
}
.cbh-round {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 50px;
background: #fff;
color: var(--cbh-navy);
transition: background-color 0.3s var(--cbh-ease);
}
.cbh-round-core {
display: grid;
place-items: center;
color: var(--cbh-navy);
transition: color 0.3s var(--cbh-ease);
}
.cbh-round--off {
background: rgb(255 255 255 / 0.6);
color: rgb(14 45 58 / 0.6);
}
a.cbh-round:hover {
background: var(--cbh-teal);
}
a.cbh-round:hover .cbh-round-core {
color: #fff;
}
a.cbh-round:focus-visible {
outline: 3px solid #fff;
outline-offset: 3px;
}
.cbh-cta-wrap {
display: flex;
align-items: center;
justify-content: center;
flex: 1 1 auto;
min-width: 0;
padding: 0 12px;
background: #fff;
}
.cbh-cta {
display: inline-flex;
align-items: center;
min-height: 44px;
font: 700 18px/1.1 var(--cbh-display);
color: var(--cbh-navy);
text-align: center;
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
transition: color 0.3s var(--cbh-ease);
}
.cbh-cta:hover {
color: var(--cbh-orange);
}
.cbh-cta:focus-visible {
outline: 2px solid var(--cbh-orange);
outline-offset: 4px;
}
/* ---- tablet ---- */
@media (min-width: 810px) {
.cbh {
gap: 60px;
padding-top: calc(80px + var(--cbh-offset, 0px));
}
.cbh-top {
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 40px;
}
.cbh .cbh-eyebrow {
margin-bottom: 30px;
font-size: 18px;
line-height: 0.7;
}
.cbh-heading {
gap: 40px;
}
.cbh-heading-top {
line-height: 0.705;
}
.cbh-heading-main {
line-height: 0.708;
}
.cbh .cbh-text {
max-width: min(834px, 62vw);
margin-top: 40px;
}
.cbh-avatar {
width: 60px;
height: 60px;
}
.cbh-avatar + .cbh-avatar {
margin-left: -22px;
}
.cbh .cbh-stat-label {
max-width: 170px;
font-size: 20px;
line-height: 30px;
}
.cbh-band {
min-height: 0;
aspect-ratio: 16 / 10;
}
.cbh-badge-anchor {
position: absolute;
inset: 0 0 auto;
z-index: 2;
display: block;
max-width: 1293px;
margin: 0 auto;
padding: 0 20px;
}
.cbh-badge {
top: -78px;
right: clamp(40px, 10vw, 143px);
width: 150px;
height: 150px;
}
.cbh-badge-core {
width: 75px;
height: 75px;
}
.cbh-corner {
width: min(520px, 70%);
height: 120px;
}
.cbh-arrows {
flex-basis: 37%;
}
.cbh-round {
width: 46px;
height: 46px;
}
.cbh-cta {
font-size: 22px;
}
}
/* ---- desktop: the measured 1425 × 1376 frame ---- */
@media (min-width: 1200px) {
.cbh {
padding-top: calc(105px + var(--cbh-offset, 0px));
}
.cbh .cbh-text {
max-width: 834px;
}
.cbh-band {
aspect-ratio: 1425 / 783;
}
.cbh-badge {
right: 143px;
width: 182px;
height: 182px;
}
.cbh-badge-core {
width: 91px;
height: 91px;
}
.cbh-corner {
width: 607px;
height: 152px;
}
.cbh-arrows {
flex: 0 0 226px;
}
.cbh-cta {
font-size: 24px;
line-height: 0.71;
}
}
@media (prefers-reduced-motion: reduce) {
.cbh-ring {
animation: none;
}
}
@media (forced-colors: active) {
.cbh-badge,
.cbh-round {
border: 1px solid ButtonText;
}
.cbh-ring-text {
fill: CanvasText;
}
}
</style>
===== END src/components/CircleBadgeHero.astro =====
===== FILE (supporting): src/components/CircleBadgeHero.md =====
# Circle badge hero
A freight-company hero in two bands. On white: an orange eyebrow, a two-size
uppercase heading (a small line over one huge word), a paragraph, and on the
right three overlapping client avatars beside a big figure and a caption.
Below: a full-bleed photo under a navy wash, a round navy badge overlapping
its top edge with a line of text that turns round it, and a corner card at
the bottom right — an orange block with two round arrows beside a white
block holding one underlined link.
Catalog page: `/components/circle-badge-hero`
## Files required
**`CircleBadgeHero.astro`** — the whole component. No imports, no npm
packages, **no script**. The ring turns with a CSS animation.
`CircleBadgeHero.md` — this file.
`public/circle-badge-hero/` — the catalog demo's photos (see "Images and
credits"). The component has no default photo: copy the folder and pass them,
or pass your own `image` and `clients`.
**The component ships no copy of its own.** Eyebrow, heading, paragraph,
figure, photo, badge and links all default to empty, so a copied hero never
carries the catalog's fictional carrier, its "since 1998" history or its
routes. The catalog demo passes them through the registry's `props`. Each block
is drawn only when its data is present, and with no copy, figure, badge or link
at all the hero renders nothing.
## Basic usage
```astro
---
import CircleBadgeHero from "../components/CircleBadgeHero.astro";
---
<CircleBadgeHero
headingTop="Fast freight"
headingMain="Logistics"
text="One sentence about what you move and for whom."
image={{ src: "/img/your-photo.webp", alt: "" }}
badge={{ label: "Get A Free Quote", href: "/request-quote" }}
nextLink={{ label: "See our services", href: "/services" }}
cta={{ label: "Explore Our Fleet", href: "/services" }}
/>
```
If your header is positioned over the hero (as the reference's is), add its
height so the copy clears it:
```astro
<CircleBadgeHero headerOffset={104} />
```
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `""` | Orange uppercase line. `""` drops it. |
| `headingTop` | `string?` | `""` | Small first heading line. `""` drops it. |
| `headingMain` | `string?` | `""` | The huge line. Keep it one short word — it is sized to the viewport. With both heading lines empty the heading is not drawn. |
| `headingLevel` | `"h1" \| "h2"?` | `"h1"` | |
| `text` | `string?` | `""` | Paragraph under the heading. `""` drops it. |
| `clients` | `{ src?, alt? }[]?` | `[]` | Up to three avatars. Decorative by default (`alt: ""`). `[]` drops them. |
| `statValue` | `string?` | `""` | The figure beside the avatars. |
| `statLabel` | `string?` | `""` | Grey caption under it. No trust claim is shown unless you pass one. |
| `image` | `{ src?, alt? }?` | none | The band photo. Omitted, or `src: ""`, renders a generic labelled placeholder. A `src` without `alt` is decorative. |
| `badge` | `{ label, href } \| null?` | `null` | The rotating badge. The label is written twice round the ring and is the link's accessible name. `null` drops it. |
| `nextLink` | `{ label, href } \| null?` | `null` | The right-hand round arrow's link and accessible name. `null` makes both arrows decorative. |
| `cta` | `{ label, href } \| null?` | `null` | The underlined link in the white block. `null` drops the whole corner card. |
| `headerOffset` | `number?` | `0` | Extra top padding in px for an overlaying header. |
| `fontStylesheet` | `string?` | Outfit + DM Sans | `""` skips the Google Fonts link. |
| `class`, `id` | `string?` | — | On the root `<section>`. `id` also seeds the ring's `textPath` id. |
## Behaviour
- **Rotating badge.** A 182px navy disc; the text ring is an SVG `textPath`
stretched to the circumference with `textLength`, so any label length
fills it evenly. It turns once every **8s** (the reference measured
~46°/s), linear, forever. A static ↗ arrow sits in a 91px outlined circle
in the middle. The centre of the badge sits 13px below the photo's top
edge, 214px in from the content's right edge, as measured.
- **Round arrow hover** (measured): the right arrow's bg goes white →
`#0e2d3a` and its arrow navy → white, 0.3s. The left arrow is the
reference's "disabled" state: white at 60% with a 60% teal arrow.
- **CTA link hover:** navy → orange (see Approximations).
- **No appear animation** — the reference hero has none measured, and it is
in view on load.
- **Reduced motion:** the ring stops. Colour changes stay.
## Layout
| Width | Layout |
| :--- | :--- |
| ≥ 1200 | Copy (834px) and client block side by side; photo band 1425:783; corner card 607×152 (orange 226px); heading 74 / 159px. |
| 810–1199 | Same row, fluid; heading scales with the viewport; photo 16:10; badge 150px; corner card 520×120. |
| < 810 | Stacked; badge hidden (as on the reference phone); photo band 332px tall with a full-width 82px corner card (orange 47%). |
## Accessibility
- One heading holds both lines, so the demo reads as "Fast freight Logistics".
- The badge and the right arrow are real links with accessible names; the
decorative arrow is `aria-hidden`. There is no fake slider: the reference's
arrows change nothing, so they are not presented as carousel controls.
- Focus rings: orange on the badge and CTA, white on the round arrow.
- The rotating ring is continuous motion with no pause control beyond
`prefers-reduced-motion`. It is decorative text with no information the
link name doesn't carry. If you need WCAG 2.2.2 strictness, add a pause
control or set the animation to run a limited number of times.
- Targets are at least 44px.
## Images and credits
All Unsplash License (free commercial use, no attribution required),
optimised to `.webp`:
| File | Size | Unsplash id |
| :--- | :--- | :--- |
| `public/circle-badge-hero/port-trucks.webp` | 2000×1100 | fT4SwA83jH4 |
| `public/circle-badge-hero/client-1.webp` | 120×120 | N8lRH2uxih4 |
| `public/circle-badge-hero/client-2.webp` | 120×120 | pAtA8xe_iVM |
| `public/circle-badge-hero/client-3.webp` | 120×120 | WYE2UhXsU1Y |
## Approximations
- **Top padding** excludes the header: the reference's 209px includes its
104px overlaying nav, so the default is 105px and `headerOffset` adds the
rest back. Phone (40px) and tablet (80px) values are read off screenshots.
- **Tablet (810–1199)** was not captured; its sizes are interpolated.
- **Ring typography** (19px Outfit 600, radius 60 in a 200 viewBox) is
matched by eye to the screenshot; the reference draws it as an SVG whose
internals weren't dumped.
- **CTA link hover colour** was not measured; orange is an assumption so the
link has some hover feedback.
- **Badge hover** was not measured, so there is none.
===== END src/components/CircleBadgeHero.md =====
===== FILE (required): public/circle-badge-hero/port-trucks.webp =====
[binary asset — download https://astro.baysixmedia.com/circle-badge-hero/port-trucks.webp and save it to public/circle-badge-hero/port-trucks.webp]
===== FILE (required): public/circle-badge-hero/client-1.webp =====
[binary asset — download https://astro.baysixmedia.com/circle-badge-hero/client-1.webp and save it to public/circle-badge-hero/client-1.webp]
===== FILE (required): public/circle-badge-hero/client-2.webp =====
[binary asset — download https://astro.baysixmedia.com/circle-badge-hero/client-2.webp and save it to public/circle-badge-hero/client-2.webp]
===== FILE (required): public/circle-badge-hero/client-3.webp =====
[binary asset — download https://astro.baysixmedia.com/circle-badge-hero/client-3.webp and save it to public/circle-badge-hero/client-3.webp]
--- 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.