Component library
Back to catalog

Image CTA band

A rounded accent band with an outlined eyebrow, a big two-line display heading, a white pill button with a navy arrow disc and a picture held inside the band. Stacks below 810px.

ctabandimagehalftonestatic

Open preview in a new tab

Use this component

Three ways to get Image CTA band into one of your own projects. The first two carry the component's files with them, so nothing has to fetch this page.

Start here

Placement prompt

The whole thing: what to do, where it goes, what must not change, and every one of the component's 1 file appended underneath with the path each belongs at. Paste it into Claude Code inside the destination project — it fetches nothing and needs nothing else.

Read it first — the files are added underneath when you copy
Add Image CTA band from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/image-cta-band/

Place it in:
[ENTER PAGE, FILE, OR ROUTE]

Position:
[ENTER WHERE IT SHOULD APPEAR]

Every file you need is appended below, each fenced with the path it belongs at. Write them there, relative to the project root. Do not import anything from the deployed library at runtime, and do not fetch the library page — the files below are the source of truth.

Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.

Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.

Install kit

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: "Image CTA band"
Library page: https://astro.baysixmedia.com/components/image-cta-band/
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/ImageCtaBand.astro =====
---
/**
 * ImageCtaBand
 *
 * A rounded accent band that closes a page: a small outlined eyebrow, a big
 * two-line display heading, a white pill button with a navy arrow disc, and
 * a picture held inside the band on the right. A faint halftone of dots
 * drifts across the band behind the copy.
 *
 * Side by side from 810px, stacked below it with the picture under the
 * button. Static: no script.
 *
 * Reads --lx-* design tokens from any ancestor, with fallbacks.
 */

export interface ImageCtaBandProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3" | "h4";
	cta?: { label: string; href: string } | null;
	/**
	 * The picture slot. With no `src` a labelled placeholder fills the same
	 * box. `null` drops the slot and the copy takes the whole band.
	 */
	image?: { src?: string; alt?: string; hint?: string } | null;
	/** The halftone dot pattern behind the copy. */
	pattern?: boolean;
	/** The section surface around the band. */
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends ImageCtaBandProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	cta: ctaProp = null,
	image = {},
	pattern = true,
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:wght@400;500;600&display=swap",
	id,
} = Astro.props;

/* The band is its heading: without one it renders nothing, button or not. */
const cta = ctaProp?.label && ctaProp.href ? ctaProp : null;

const Heading = headingLevel;

/* Measured slot: 420 x 400 at desktop, 287 x 273 stacked — both ~21:20. */
const IMAGE_SPEC = "~21:20 · 840 × 800 px";
---

{heading && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{heading && (
<section class="icb" id={id} style={`--icb-bg: ${background};`}>
	<div
		class:list={["icb-band", { "icb-band--pattern": pattern }]}
		data-has-image={image ? "" : undefined}
	>
		<div class="icb-copy">
			<div class="icb-head">
				{eyebrow && <p class="icb-eyebrow">{eyebrow}</p>}
				<Heading class="icb-heading">{heading}</Heading>
			</div>
			{
				cta && (
					<a class="icb-cta" href={cta.href}>
						<span>{cta.label}</span>
						<span class="icb-cta-disc" aria-hidden="true">
							<svg viewBox="0 0 24 24" focusable="false">
								<path d="M5 12h13M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
							</svg>
						</span>
					</a>
				)
			}
		</div>
		{
			image && (
				<div class="icb-media">
					{image.src ? (
						<img src={image.src} alt={image.alt ?? ""} width="420" height="400" loading="lazy" decoding="async" />
					) : (
						<div class="icb-ph" role="img" aria-label={`Image placeholder: ${image.hint ?? "photo"}`}>
							<div class="icb-ph-text" aria-hidden="true">
								<span class="icb-ph-label">Image</span>
								{image.hint && <span class="icb-ph-hint">{image.hint}</span>}
								<span class="icb-ph-spec">{IMAGE_SPEC}</span>
							</div>
						</div>
					)}
				</div>
			)
		}
	</div>
</section>
)}

<style>
	.icb {
		--icb-navy: var(--lx-navy, #0a2540);
		--icb-accent: var(--lx-accent, #ff5f00);
		--icb-display: var(--lx-display, "Bebas Neue", Impact, "Arial Narrow", sans-serif);
		--icb-body: var(--lx-body, Poppins, ui-sans-serif, system-ui, sans-serif);
		--icb-dur: 220ms;
		/*
		 * Step 39: on a site grid (--lx-grid-max / --lx-grid-gutter) the card breaks out of the grid by exactly its
		 * copy inset, so the copy starts on the page's content line. The card is the grid's content width plus
		 * the inset on each side; the fallbacks give the old card (1100 - 2 x 30 + 2 x 80 = 1200px) and margin.
		 */
		--icb-grid-max: var(--lx-grid-max, 1100px);
		--icb-gutter: var(--lx-grid-gutter, 30px);
		--icb-inset: 24px;

		box-sizing: border-box;
		padding: 0 20px 60px;
		background: var(--icb-bg);
		font-family: var(--icb-body);
	}

	.icb *,
	.icb *::before,
	.icb *::after {
		box-sizing: border-box;
	}

	.icb-band {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: stretch;
		gap: 40px;
		max-width: calc(var(--icb-grid-max) - 2 * var(--icb-gutter) + 2 * var(--icb-inset));
		margin: 0 auto;
		padding: 24px;
		border-radius: 10px;
		background: var(--icb-accent);
		color: #fff;
		overflow: hidden;
		isolation: isolate;
	}

	/* The halftone: a grid of dots, faded out towards the top and the right
	   so it reads as texture under the copy rather than as a pattern. */
	.icb-band--pattern::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: -1;
		background-image: radial-gradient(circle, rgb(10 37 64 / 0.08) 0 7px, transparent 7.5px);
		background-size: 22px 22px;
		background-position: 0 0;
		-webkit-mask-image: radial-gradient(ellipse 80% 70% at 30% 100%, #000 20%, transparent 75%);
		mask-image: radial-gradient(ellipse 80% 70% at 30% 100%, #000 20%, transparent 75%);
		pointer-events: none;
	}

	.icb-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
		min-width: 0;
	}

	.icb-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 20px;
	}

	.icb-eyebrow {
		margin: 0;
		padding: 8px 16px;
		border: 1px solid rgb(255 255 255 / 0.3);
		border-radius: 100px;
		font: 500 14px/14px var(--icb-body);
		text-transform: uppercase;
		color: #fff;
	}

	.icb-heading {
		margin: 0;
		max-width: 380px;
		font-family: var(--icb-display);
		font-weight: 400;
		font-size: 30px;
		line-height: 1;
		letter-spacing: -0.02em;
		text-transform: uppercase;
		color: #fff;
		text-wrap: balance;
	}

	.icb-cta {
		display: inline-flex;
		align-items: center;
		gap: 24px;
		height: 50px;
		padding: 5px 5px 5px 19px;
		border-radius: 100px;
		background: #fff;
		color: var(--icb-navy);
		font: 600 14px/14px var(--icb-body);
		text-decoration: none;
		white-space: nowrap;
	}

	.icb-cta-disc {
		display: grid;
		place-items: center;
		width: 40px;
		height: 40px;
		border-radius: 50%;
		background: var(--icb-navy);
		color: #fff;
	}

	.icb-cta-disc svg {
		width: 24px;
		height: 24px;
		transition: transform var(--icb-dur) ease;
	}

	.icb-cta:hover .icb-cta-disc svg {
		transform: rotate(-45deg);
	}

	.icb-cta:focus-visible {
		outline: 2px solid #fff;
		outline-offset: 3px;
	}

	.icb-media {
		position: relative;
		border-radius: 10px;
		overflow: hidden;
		aspect-ratio: 287 / 273;
		width: 100%;
	}

	.icb-media img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* Placeholder for an unset `src`: fills the same box as the picture. */
	.icb-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		border: 1px dashed rgb(10 37 64 / 0.22);
		border-radius: inherit;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(10 37 64 / 0.62);
	}

	.icb-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 8px;
	}

	.icb-ph-label {
		font: 500 12px/1 var(--icb-body);
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.icb-ph-hint {
		max-width: 28ch;
		font: 500 14px/1.4 var(--icb-body);
	}

	.icb-ph-spec {
		font: 400 12px/1.2 var(--icb-body);
	}

	/* ---- tablet and up: side by side ---- */
	@media (min-width: 810px) {
		.icb {
			--icb-inset: 48px;
			padding: 0 30px 120px;
		}

		.icb-band {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			gap: 32px;
			min-height: 480px;
			padding: 40px 40px 40px var(--icb-inset);
		}

		.icb-copy {
			flex: 1 1 auto;
			gap: 60px;
		}

		.icb-heading {
			font-size: 48px;
		}

		.icb-media {
			flex: 0 1 420px;
			aspect-ratio: 420 / 400;
		}
	}

	/* The site gutter applies on desktop only, as the site header's does; the default is the same 30px. */
	@media (min-width: 960px) {
		.icb {
			padding-inline: var(--icb-gutter);
		}
	}

	@media (min-width: 1200px) {
		.icb {
			--icb-inset: 80px;
		}

		.icb-band {
			padding-left: var(--icb-inset);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.icb {
			--icb-dur: 1ms;
		}
	}

	@media (forced-colors: active) {
		.icb-band {
			border: 1px solid CanvasText;
		}

		.icb-band--pattern::before {
			display: none;
		}
	}
</style>

===== END src/components/ImageCtaBand.astro =====

===== FILE (supporting): src/components/ImageCtaBand.md =====
# Image CTA band

A rounded accent band that closes a page: a small outlined eyebrow, a big
two-line display heading, a white pill button with a navy arrow disc, and a
picture held inside the band on the right. A faint halftone of dots sits
behind the copy.

Catalog page: `/components/image-cta-band`

## Purpose

The last push before the footer — one line of intent and one button. It is
deliberately short on words: if the section needs a paragraph, it is a
different section.

## Files required

**`ImageCtaBand.astro`** — one file, the whole component. No script, no
imports, no packages. Reads `--lx-navy`, `--lx-accent`, `--lx-display` and
`--lx-body` from any ancestor, with fallbacks.

`ImageCtaBand.md` — this file. Documentation, not required to run.

No assets. The picture slot ships as a labelled placeholder — see Images.

## Basic usage

```astro
---
import ImageCtaBand from "../components/ImageCtaBand.astro";
---
<ImageCtaBand
  eyebrow="Get in touch"
  heading="Let's get your freight moving this week"
  cta={{ label: "Contact us", href: "/contact" }}
  image={{ src: "/img/dock.jpg", alt: "" }}
/>
```

## Props

| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `""` | The outlined pill label. `""` drops it. |
| `heading` | `string?` | `""` | The display heading, capped at 380px so it breaks in two. No heading → the component renders nothing, whatever else is passed. |
| `headingLevel` | `"h2" \| "h3" \| "h4"?` | `"h2"` | Fit it to your outline. |
| `cta` | `{ label, href } \| null` | `null` | The white pill. Needs both `label` and `href`; otherwise it is dropped. |
| `image` | `{ src?, alt?, hint? } \| null` | `{}` (placeholder, no hint) | No `src` renders the placeholder; `null` gives a copy-only band. |
| `pattern` | `boolean?` | `true` | The halftone dots. |
| `background` | `string?` | `#ffffff` | The section surface around the band. |
| `fontStylesheet` | `string?` | Google Fonts URL | `""` turns it off. |
| `id` | `string?` | — | Root id. |

## Images

One slot. With no `src` it renders a striped placeholder in the same box
(`role="img"`, named "Image placeholder: {hint}", or "…: photo" with no hint)
showing IMAGE, the hint if given and the size line. Pass `src` and it is an
`<img>`, `object-fit: cover`, lazy.

| Slot | Default hint | Ratio · size |
| :--- | :--- | :--- |
| `image` | none | ~21:20 · 840 × 800 px (box is 420×400 at desktop, 287×273 stacked) |

```astro
<ImageCtaBand heading="Ready when you are" image={{ src: "/img/handoff.jpg", alt: "" }} />
```

The catalog demo's eyebrow, heading, button and placeholder hint are
registry demo props, not component defaults.

## Behaviour and layout

- **810px and up:** side by side. Band max 1200px, 480px min height, 10px
  radius, padding 40/40/40/80 at 1200+ (48 on the left below that). Copy
  column: eyebrow, 20px, heading at 48/48, 60px, button. Picture 420×400 at a
  10px radius, shrinking with the band.
- **Below 810px:** stacked, 24px padding, 40px gap, heading at 30/30, picture
  full width at 287:273.
- Section gutters: 30px inline and 120px below (20/60 on a phone), no top
  padding — as measured.
- Static. The arrow rotates -45° on hover; that is the only motion, and it
  collapses to 1ms under reduced motion.

## Accessibility

- A real link, visible white focus ring offset 3px from the button.
- The picture is decorative by default; pass `alt` if it carries meaning.
  The placeholder is `role="img"` with its hint as the name, so an unfilled
  slot is announced as one rather than silently skipped.
- The halftone is a pseudo-element: nothing for assistive technology.
- Forced colours: the band gets a border and the halftone is removed.

## Approximations

- **Halftone.** The reference uses a background image of dots; here it is a
  22px radial-gradient grid in 8% navy, faded with a radial mask towards the
  top right. Dot size and fade are read from the screenshot.
- **Eyebrow border.** Computed styles show no border colour; the 30% white
  hairline is read from the screenshot.
- **Tablet (810–1199).** Not captured. The row layout is kept with the left
  padding reduced to 48px so copy and picture both fit.
- **Hover state** was not captured; the arrow rotation is this library's
  choice.
- **Picture.** The reference's stock photograph is not reproduced; the slot
  ships as a placeholder describing the photo to supply.

===== END src/components/ImageCtaBand.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.

Component reference

A fragment for a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving branding and content open. It does not carry the code.

Read it first
“Image CTA band” from my Astro Component Library:
https://astro.baysixmedia.com/components/image-cta-band/

Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.

The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.

Component readiness

Judged on recorded evidence, not on how it looks in the frame above. A description of a component is not a result.

Interactivity
Static
Profile
core only
Installation files
1 required + 1 supporting
Verified
Never
Client navigation
Not applicable — ships no script
Source fingerprint
No fingerprint recorded
Production ready
Not yet

Not yet verified

Enrolled for verification, but never run. Silence is reported as silence rather than assumed to be fine.

Open the verification fixture

src/components/library/ImageCtaBand.astro

one fileNo imports, no companion files, no packages. Copy ImageCtaBand.astro into any Astro project's components folder and render it.

---
/**
 * ImageCtaBand
 *
 * A rounded accent band that closes a page: a small outlined eyebrow, a big
 * two-line display heading, a white pill button with a navy arrow disc, and
 * a picture held inside the band on the right. A faint halftone of dots
 * drifts across the band behind the copy.
 *
 * Side by side from 810px, stacked below it with the picture under the
 * button. Static: no script.
 *
 * Reads --lx-* design tokens from any ancestor, with fallbacks.
 */

export interface ImageCtaBandProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3" | "h4";
	cta?: { label: string; href: string } | null;
	/**
	 * The picture slot. With no `src` a labelled placeholder fills the same
	 * box. `null` drops the slot and the copy takes the whole band.
	 */
	image?: { src?: string; alt?: string; hint?: string } | null;
	/** The halftone dot pattern behind the copy. */
	pattern?: boolean;
	/** The section surface around the band. */
	background?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends ImageCtaBandProps {}

const {
	eyebrow = "",
	heading = "",
	headingLevel = "h2",
	cta: ctaProp = null,
	image = {},
	pattern = true,
	background = "#ffffff",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Poppins:wght@400;500;600&display=swap",
	id,
} = Astro.props;

/* The band is its heading: without one it renders nothing, button or not. */
const cta = ctaProp?.label && ctaProp.href ? ctaProp : null;

const Heading = headingLevel;

/* Measured slot: 420 x 400 at desktop, 287 x 273 stacked — both ~21:20. */
const IMAGE_SPEC = "~21:20 · 840 × 800 px";
---

{heading && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{heading && (
<section class="icb" id={id} style={`--icb-bg: ${background};`}>
	<div
		class:list={["icb-band", { "icb-band--pattern": pattern }]}
		data-has-image={image ? "" : undefined}
	>
		<div class="icb-copy">
			<div class="icb-head">
				{eyebrow && <p class="icb-eyebrow">{eyebrow}</p>}
				<Heading class="icb-heading">{heading}</Heading>
			</div>
			{
				cta && (
					<a class="icb-cta" href={cta.href}>
						<span>{cta.label}</span>
						<span class="icb-cta-disc" aria-hidden="true">
							<svg viewBox="0 0 24 24" focusable="false">
								<path d="M5 12h13M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
							</svg>
						</span>
					</a>
				)
			}
		</div>
		{
			image && (
				<div class="icb-media">
					{image.src ? (
						<img src={image.src} alt={image.alt ?? ""} width="420" height="400" loading="lazy" decoding="async" />
					) : (
						<div class="icb-ph" role="img" aria-label={`Image placeholder: ${image.hint ?? "photo"}`}>
							<div class="icb-ph-text" aria-hidden="true">
								<span class="icb-ph-label">Image</span>
								{image.hint && <span class="icb-ph-hint">{image.hint}</span>}
								<span class="icb-ph-spec">{IMAGE_SPEC}</span>
							</div>
						</div>
					)}
				</div>
			)
		}
	</div>
</section>
)}

<style>
	.icb {
		--icb-navy: var(--lx-navy, #0a2540);
		--icb-accent: var(--lx-accent, #ff5f00);
		--icb-display: var(--lx-display, "Bebas Neue", Impact, "Arial Narrow", sans-serif);
		--icb-body: var(--lx-body, Poppins, ui-sans-serif, system-ui, sans-serif);
		--icb-dur: 220ms;
		/*
		 * Step 39: on a site grid (--lx-grid-max / --lx-grid-gutter) the card breaks out of the grid by exactly its
		 * copy inset, so the copy starts on the page's content line. The card is the grid's content width plus
		 * the inset on each side; the fallbacks give the old card (1100 - 2 x 30 + 2 x 80 = 1200px) and margin.
		 */
		--icb-grid-max: var(--lx-grid-max, 1100px);
		--icb-gutter: var(--lx-grid-gutter, 30px);
		--icb-inset: 24px;

		box-sizing: border-box;
		padding: 0 20px 60px;
		background: var(--icb-bg);
		font-family: var(--icb-body);
	}

	.icb *,
	.icb *::before,
	.icb *::after {
		box-sizing: border-box;
	}

	.icb-band {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: stretch;
		gap: 40px;
		max-width: calc(var(--icb-grid-max) - 2 * var(--icb-gutter) + 2 * var(--icb-inset));
		margin: 0 auto;
		padding: 24px;
		border-radius: 10px;
		background: var(--icb-accent);
		color: #fff;
		overflow: hidden;
		isolation: isolate;
	}

	/* The halftone: a grid of dots, faded out towards the top and the right
	   so it reads as texture under the copy rather than as a pattern. */
	.icb-band--pattern::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: -1;
		background-image: radial-gradient(circle, rgb(10 37 64 / 0.08) 0 7px, transparent 7.5px);
		background-size: 22px 22px;
		background-position: 0 0;
		-webkit-mask-image: radial-gradient(ellipse 80% 70% at 30% 100%, #000 20%, transparent 75%);
		mask-image: radial-gradient(ellipse 80% 70% at 30% 100%, #000 20%, transparent 75%);
		pointer-events: none;
	}

	.icb-copy {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 24px;
		min-width: 0;
	}

	.icb-head {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 20px;
	}

	.icb-eyebrow {
		margin: 0;
		padding: 8px 16px;
		border: 1px solid rgb(255 255 255 / 0.3);
		border-radius: 100px;
		font: 500 14px/14px var(--icb-body);
		text-transform: uppercase;
		color: #fff;
	}

	.icb-heading {
		margin: 0;
		max-width: 380px;
		font-family: var(--icb-display);
		font-weight: 400;
		font-size: 30px;
		line-height: 1;
		letter-spacing: -0.02em;
		text-transform: uppercase;
		color: #fff;
		text-wrap: balance;
	}

	.icb-cta {
		display: inline-flex;
		align-items: center;
		gap: 24px;
		height: 50px;
		padding: 5px 5px 5px 19px;
		border-radius: 100px;
		background: #fff;
		color: var(--icb-navy);
		font: 600 14px/14px var(--icb-body);
		text-decoration: none;
		white-space: nowrap;
	}

	.icb-cta-disc {
		display: grid;
		place-items: center;
		width: 40px;
		height: 40px;
		border-radius: 50%;
		background: var(--icb-navy);
		color: #fff;
	}

	.icb-cta-disc svg {
		width: 24px;
		height: 24px;
		transition: transform var(--icb-dur) ease;
	}

	.icb-cta:hover .icb-cta-disc svg {
		transform: rotate(-45deg);
	}

	.icb-cta:focus-visible {
		outline: 2px solid #fff;
		outline-offset: 3px;
	}

	.icb-media {
		position: relative;
		border-radius: 10px;
		overflow: hidden;
		aspect-ratio: 287 / 273;
		width: 100%;
	}

	.icb-media img {
		display: block;
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/* Placeholder for an unset `src`: fills the same box as the picture. */
	.icb-ph {
		display: grid;
		place-items: center;
		width: 100%;
		height: 100%;
		padding: 16px;
		text-align: center;
		border: 1px dashed rgb(10 37 64 / 0.22);
		border-radius: inherit;
		background: repeating-linear-gradient(135deg, #e6e9ee 0 12px, #edf0f3 12px 24px);
		color: rgb(10 37 64 / 0.62);
	}

	.icb-ph-text {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 8px;
	}

	.icb-ph-label {
		font: 500 12px/1 var(--icb-body);
		letter-spacing: 0.08em;
		text-transform: uppercase;
	}

	.icb-ph-hint {
		max-width: 28ch;
		font: 500 14px/1.4 var(--icb-body);
	}

	.icb-ph-spec {
		font: 400 12px/1.2 var(--icb-body);
	}

	/* ---- tablet and up: side by side ---- */
	@media (min-width: 810px) {
		.icb {
			--icb-inset: 48px;
			padding: 0 30px 120px;
		}

		.icb-band {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			gap: 32px;
			min-height: 480px;
			padding: 40px 40px 40px var(--icb-inset);
		}

		.icb-copy {
			flex: 1 1 auto;
			gap: 60px;
		}

		.icb-heading {
			font-size: 48px;
		}

		.icb-media {
			flex: 0 1 420px;
			aspect-ratio: 420 / 400;
		}
	}

	/* The site gutter applies on desktop only, as the site header's does; the default is the same 30px. */
	@media (min-width: 960px) {
		.icb {
			padding-inline: var(--icb-gutter);
		}
	}

	@media (min-width: 1200px) {
		.icb {
			--icb-inset: 80px;
		}

		.icb-band {
			padding-left: var(--icb-inset);
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.icb {
			--icb-dur: 1ms;
		}
	}

	@media (forced-colors: active) {
		.icb-band {
			border: 1px solid CanvasText;
		}

		.icb-band--pattern::before {
			display: none;
		}
	}
</style>