/* AFB. Tokens come from theme.json; this file only aliases and composes them. */

:root {
	--ink: var(--wp--preset--color--ink);
	--ink-soft: var(--wp--preset--color--ink-soft);
	--muted: var(--wp--preset--color--muted);
	--paper: var(--wp--preset--color--paper);
	--blush: var(--wp--preset--color--blush);
	--hairline: var(--wp--preset--color--hairline);
	--deep: var(--wp--preset--color--deep);
	--hairline-dark: var(--wp--preset--color--hairline-dark);
	--primary: var(--wp--preset--color--primary);
	--primary-light: var(--wp--preset--color--primary-light);
	--accent-warm: var(--wp--preset--color--accent-warm);
	--on-dark: var(--wp--preset--color--on-dark);
	--on-primary: var(--wp--preset--color--on-primary);

	--font-display: var(--wp--preset--font-family--display);
	--font-body: var(--wp--preset--font-family--body);

	--radius: 13px;
	--radius-pill: 999px;
	--stroke: 1.5px;
	--focus: 2px;

	/* Lets height animate to and from auto, which the FAQ needs. */
	interpolate-size: allow-keywords;

	--gutter: 96px;
	--content: 1248px;
	--prose: 680px;
	--section: 96px;
}

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

/* scroll-padding keeps anchors clear of the sticky header. */
@media (prefers-reduced-motion: no-preference) {
	html { scroll-behavior: smooth; }
}
html { scroll-padding-top: calc(var(--header-h, 73px) + 24px); }

body {
	margin: 0;
	font-family: var(--font-body);
	font-size: 16px;
	line-height: 1.5;
	color: var(--ink);
	background: var(--paper);
	-webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }

:focus-visible {
	outline: var(--focus) solid var(--deep);
	outline-offset: 2px;
	border-radius: 4px;
}

/* One ring, two colours. The base ring is --deep, which is invisible on the
   header, the footer, the deep bands, the red CTA and both photographic heroes,
   because those surfaces are that colour. Keyboard focus disappeared exactly
   where the main navigation lives. Same list as the dark-surface block below. */
.site-header :focus-visible,
.site-footer :focus-visible,
.section--cta :focus-visible,
.section--deep :focus-visible,
.case-hero :focus-visible,
.next-case:focus-visible {
	outline-color: var(--paper);
}

/* .screen-reader-response is CF7's, and it only had hiding styles in CF7's own
   stylesheet, which this theme dequeues. Left alone it renders an empty 24px
   line box above every form. It has to stay in the DOM to announce errors. */
.screen-reader-text,
.wpcf7 .screen-reader-response {
	position: absolute !important;
	width: 1px; height: 1px;
	overflow: hidden; clip-path: inset(50%);
	white-space: nowrap;
}
.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { left: var(--gutter); top: 8px; z-index: 100; background: var(--paper); padding: 12px 24px; border-radius: var(--radius); }

/* Layout ------------------------------------------------------------------ */

/* The cap includes the padding, or the column is 2 x gutter too narrow. */
.wrap {
	width: 100%;
	max-width: calc(var(--content) + (var(--gutter) * 2));
	margin-inline: auto;
	padding-inline: var(--gutter);
}
.wrap--prose > * { max-width: var(--prose); }

.section { padding-block: var(--section); }

/* From the design system. 96 is the default and needs no class. */
.section--pad-sm { --section: 40px; }
.section--pad-md { --section: 64px; }
.section--pad-lg { --section: 80px; }
@media (max-width: 1199px) {
	.section--pad-sm { --section: 32px; }
	.section--pad-md { --section: 56px; }
	.section--pad-lg { --section: 64px; }
}
@media (max-width: 767px) {
	.section--pad-sm { --section: 24px; }
	.section--pad-md { --section: 40px; }
	.section--pad-lg { --section: 48px; }
}
.section--blush, .section--header { background: var(--blush); }
.section--cta { background: var(--primary); color: var(--paper); text-align: center; }
/* margin-block, not margin: the shorthand would cancel the margin-inline that
   centres it. Without the reset the paragraph kept the UA bottom margin, which
   the `* + *` rule above never touches because that only sets margin-top, and
   every closing CTA on the site stood 17px taller than its band in Figma. */
/* Paper, not on-primary: the tinted cream measured 4.28 on the red, under
   the 4.5 that 18px body copy needs. White on the same red is 4.92. */
.section--cta__text { color: var(--paper); font-size: 18px; line-height: 1.7; max-width: 720px; margin-block: 0; margin-inline: auto; }
/* Head stacks at 16 and sits 48 above its content, both from the Figma band. */
.section__head {
	display: flex;
	flex-direction: column;
	gap: 16px;
	margin-bottom: 48px;
}
.section__intro { font-size: 20px; line-height: 1.6; color: var(--ink-soft); max-width: 720px; margin: 0; }
/* Body M in a 680 measure. Only the hero deck is Body L at 20. */
.section__head .section__intro { font-size: 18px; line-height: 1.7; max-width: 680px; }
.section__meta { font-size: 15px; font-weight: 600; color: var(--ink-soft); }
/* Every page header heading in the Figma file that runs long is drawn in a 900
   box so it breaks; only the short ones are given the full 1248, where the box
   makes no difference. Without the cap a long one ran the whole measure on one
   line and the band came up 67px short of its design. Portfolio and Contact are
   458 and 878 wide, so both stay on one line either side of this. */
.page-header h1 { max-width: 900px; }
.section > .wrap > * + * { margin-top: 16px; }
/* The closing CTA used to override this to 24. Five of the eight frames that
   carry a CTA stack it at 16, which is simply the site rhythm above, so the
   override was the anomaly rather than the rule and it is gone. */
/* These rows are the wrap rather than sitting inside one, so the stacking
   rhythm above would land on their cells and push the grid out of line. */
.section > .stats__row > * + *,
.section > .glance__row > * + *,
.section > .rail__inner > * + *,
.section > .hero__inner > * + *,
.section > .text-media__inner > * + * { margin-top: 0; }

/* Type -------------------------------------------------------------------- */

.display-xl, .display-l, .display-m, .display-s, h1, h2, h3, h4 {
	font-family: var(--font-display);
	font-weight: 800;
	margin: 0;
}
.display-xl, h1 { font-size: 64px; line-height: 1.05; }
.display-l, h2 { font-size: 40px; line-height: 1.12; }
.display-m, h3 { font-size: 30px; line-height: 1.35; }
.display-s, h4 { font-size: 22px; line-height: 1.28; }

.label {
	font-size: 12px;
	font-weight: 800;
	line-height: 1.3;
	letter-spacing: 1.9px;
	text-transform: uppercase;
	color: var(--primary);
	margin: 0;
}
.label--on-dark { color: var(--accent-warm); }

.prose { font-size: 20px; line-height: 1.6; color: var(--ink-soft); }
/* WordPress's own global stylesheet emits `:root :where(p) { font-size: body-m }`
   from the theme.json preset. Zero specificity, but it is a rule ON the
   paragraph, and a direct rule always beats a value inherited from an ancestor
   however specific that ancestor is. So .prose has never once sized its own
   paragraphs: the legal page ran at 18 on both breakpoints against a design
   that says 20 and 15. Anything that styles prose has to reach the child. */
.prose p, .prose li { font-size: inherit; }
.prose > * + * { margin-top: 24px; }
/* h2 was 24 and h3 inherited the global 30, so a subheading came out larger
   than the heading above it. Both on the scale now, and in the right order.
   There is no 24 in this system; the legal frame draws one, and per the type
   rule a 24 in a Figma node is Display/M at 30. */
.prose h2 { font-size: 30px; line-height: 1.35; color: var(--ink); margin-top: 48px; }
.prose h3 { font-size: 22px; line-height: 1.28; color: var(--ink); margin-top: 32px; }
.prose a { color: var(--primary); text-decoration: underline; }

/* The arrow is CSS so it can move, and is never read out as link text. */
.link-arrow {
	display: inline;
	font-size: 15px;
	font-weight: 600;
	color: var(--primary);
	text-decoration: none;
}
/* inline-block: an underline does not cross an atomic inline, and transform
   does not apply to a plain one. */
.link-arrow::after {
	content: "\2192";
	display: inline-block;
	margin-left: 6px;
	transition: transform .2s cubic-bezier(.2, .6, .2, 1);
}
.link-arrow:hover { text-decoration: underline; }
.link-arrow:hover::after { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
	.link-arrow::after { transition: none; }
}

/* Buttons ----------------------------------------------------------------- */

.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	padding: 16px 32px;
	border: 0;
	border-radius: var(--radius);
	font-family: var(--font-body);
	font-size: 16px;
	font-weight: 700;
	line-height: 1.5;
	text-decoration: none;
	white-space: nowrap;
	transition: background-color .15s ease;
}
/* Three styles, matching the Button component set exactly. */
.btn--primary { background: var(--primary); color: var(--paper); }
.btn--primary:hover { background: var(--primary-light); }

.btn--secondary { background: transparent; color: var(--ink); border: var(--stroke) solid var(--ink); }
.btn--secondary:hover { background: var(--ink); color: var(--paper); }

.btn--on-dark { background: var(--paper); color: var(--primary); }
.btn--on-dark:hover { background: var(--on-primary); }

.btn[disabled], .btn[aria-disabled="true"] { opacity: .4; pointer-events: none; }

/* Hero -------------------------------------------------------------------- */

.hero__inner { display: grid; gap: 48px; }
/* The copy column gives ground before the picture does. */
.hero__inner--split { grid-template-columns: minmax(0, 1fr) clamp(320px, 32%, 380px); align-items: start; }
.hero__copy { display: flex; flex-direction: column; gap: 24px; }
.hero__copy .display-xl { max-width: 15ch; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 16px; }

/* Breadcrumb. Caption size, so it sits under everything else in the hero and
   reads as orientation rather than as content. */
.breadcrumb { margin: 0; }
.breadcrumb__list { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; font-size: 13px; line-height: 1.55; color: var(--muted); }
.breadcrumb__item + .breadcrumb__item::before { content: "/"; margin-right: 8px; color: var(--muted); }
.breadcrumb__item a { color: inherit; text-decoration: none; }
.breadcrumb__item a:hover { text-decoration: underline; }
/* Muted disappears against the case hero photograph. */
/* On the case hero this is the smallest type on the band and it sits over a
   photograph, so on-dark left it unreadable against anything bright. The title
   above it is white and the deck is on-dark at 20px; 13px over an image needs
   more contrast than either, not less. The separators stay dimmer so the trail
   still reads as a trail. */
.case-hero .breadcrumb__list { color: var(--paper); }
.case-hero .breadcrumb__item::before { color: var(--on-dark); }
.case-hero .breadcrumb__item a { opacity: .85; }
.case-hero .breadcrumb__item a:hover { opacity: 1; }

/* Quick facts, the service page's alternative to the proof card. Rows are a
   description list because each is a label and the figure against it, and the
   rule sits on the row so the card reads as a table rather than a list. */
.facts-card { background: var(--paper); border-radius: var(--radius); padding: 32px; align-self: start; }
/* 12 under the title. Figma butts the first rule straight against the title
   box, which reads as cramped once it is set in a real browser. Aaron's call,
   2026-08-15, so it is a deliberate departure from the frame, not drift. */
.facts-card__title { font-family: var(--font-display); font-size: 22px; font-weight: 800; line-height: 1.28; margin: 0 0 12px; }
.facts-card__list { margin: 0; }
.facts-card__row {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding-block: 16px;
	border-top: 1px solid var(--hairline);
}
/* The bottom padding is there to hold the text off the next hairline. The last
   row has no next hairline, so it was only ever adding to the card's own 32 and
   made the bottom read 48 against 32 on the other three sides. */
.facts-card__row:last-child { padding-bottom: 0; }
.facts-card__row dt { flex: 1 0 0; min-width: 0; font-size: 15px; line-height: 1.7; color: var(--muted); }
.facts-card__row dd { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.5; color: var(--ink); text-align: right; }

.proof-card {
	background: var(--paper);
	border-radius: var(--radius);
	padding: 32px;
	display: flex; flex-direction: column; gap: 12px;
}
.proof-card__figure { font-family: var(--font-display); font-size: 64px; line-height: 1.05; font-weight: 800; color: var(--primary); margin: 0; font-variant-numeric: tabular-nums; }
.proof-card__text { font-size: 18px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

/* Logo strip --------------------------------------------------------------- */

.logo-strip { overflow: hidden; }
.logo-strip__label {
	font-size: 12px; font-weight: 800; line-height: 1.3;
	letter-spacing: 1.9px; text-transform: uppercase;
	color: var(--muted); margin: 0 0 20px;
}
.logo-strip__track { display: flex; width: max-content; }
/* Trailing padding equals the gap, or the loop seam spacing goes odd. */
.logo-strip__list {
	display: flex;
	align-items: center;
	gap: var(--logo-gap, 64px);
	list-style: none;
	margin: 0;
	padding: 0 var(--logo-gap, 64px) 0 0;
}
.logo-strip__item { flex: none; }
/* Each logo carries its own inline width: a shared cap would flatten them. */
.logo-strip__img {
	filter: grayscale(1);
	opacity: .55;
	transition: opacity .2s ease, filter .2s ease;
}
.logo-strip__item:hover .logo-strip__img { opacity: 1; filter: grayscale(0); }

/* Wraps when not scrolling, and starts at the gutter rather than centring, so
   it lines up with its own label and with the scrolling strip above it. */
.logo-strip:not(.logo-strip--scroll) .logo-strip__track { width: 100%; }
.logo-strip:not(.logo-strip--scroll) .logo-strip__list {
	flex-wrap: wrap;
	justify-content: flex-start;
	align-items: center;
	gap: 32px 64px;
	padding: 0 var(--gutter);
	width: 100%;
}

/* Fade the edges so logos enter and leave. */
.logo-strip--scroll {
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

/* Speed is a distance over a time, and the distance here is the width of the
   logo set. A fixed duration therefore means the strip runs faster every time a
   logo is added: six logos took 60s to cover 1113px, or 19px/s, and ten took
   the same 60s to cover 1903px, or 32px/s. app.js sets the duration from the
   measured width so the pixels per second stay put whatever is in it. The
   durations below are the no-script fallback, tuned for the six it started
   with. */
@media (prefers-reduced-motion: no-preference) {
	.logo-strip--scroll .logo-strip__list {
		animation: afb-marquee linear infinite;
		animation-duration: var(--afb-marquee-duration, 60s);
	}
	.logo-strip--slow   { --afb-marquee-duration: 60s; }
	.logo-strip--medium { --afb-marquee-duration: 40s; }
	.logo-strip--fast   { --afb-marquee-duration: 25s; }
	.logo-strip--scroll:hover .logo-strip__list { animation-play-state: paused; }
}

/* Reduced motion: fall back to the still layout rather than a frozen marquee. */
@media (prefers-reduced-motion: reduce) {
	.logo-strip--scroll { -webkit-mask-image: none; mask-image: none; }
	.logo-strip--scroll .logo-strip__track { width: 100%; }
	.logo-strip--scroll .logo-strip__list {
		flex-wrap: wrap;
		justify-content: center;
		gap: 40px 64px;
		padding: 0 var(--gutter);
		width: 100%;
	}
	.logo-strip--scroll .logo-strip__list[aria-hidden="true"] { display: none; }
}

@keyframes afb-marquee {
	from { transform: translateX(0); }
	to   { transform: translateX(-100%); }
}

/* Cards ------------------------------------------------------------------- */

/* Columns 24, rows 32. Every card row in the file resolves on 24: four cards
   land on 294 and three on 400, both of which make 1248 exactly. Steps are the
   other system and take 48, below. Row gap is left at 32 because no frame in
   the file has a second row of cards to measure. */
.card-grid { display: grid; column-gap: 24px; row-gap: 32px; }
.card-grid--2 { grid-template-columns: repeat(2, 1fr); }
.card-grid--3 { grid-template-columns: repeat(3, 1fr); }
.card-grid--4 { grid-template-columns: repeat(4, 1fr); }

.card {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	gap: 12px;
	padding: 32px;
	border-radius: var(--radius);
	overflow: hidden;
}
.card--blush { background: var(--blush); }
.card--paper { background: var(--paper); }
.card__body { display: flex; flex-direction: column; gap: 12px; }
.card__media img { border-radius: var(--radius); }
.card__title { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; margin: 0; }
.card__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

/* A separate child, so space-between keeps the links level across a row. */
.card__foot {
	display: flex;
	flex-direction: column;
	gap: 8px;
	padding-top: 16px;
	border-top: 1px solid var(--hairline);
}
.card--paper .card__foot { border-top-color: var(--hairline); }
.card__meta { font-size: 15px; line-height: 1.7; color: var(--muted); margin: 0; }

/* Slider dots. Hidden on desktop, and left aligned to the gutter like Figma. */
.card-dots {
	display: none;
	gap: 8px;
	align-items: center;
	justify-content: flex-start;
	margin-top: 32px;
	padding-inline: var(--gutter);
}
/* Spelled out against .wrap to beat the section's 16px rhythm rule. */
.section > .wrap > .card-dots { margin-top: 32px; }
.card-dots__dot {
	width: 10px;
	height: 10px;
	padding: 0;
	border: 0;
	border-radius: 999px;
	background: var(--hairline);
	cursor: pointer;
	transition: background-color .2s ease, width .2s ease;
}
.card-dots__dot.is-active { background: var(--primary); width: 24px; }

/* A linked card is a real anchor, so every part of it behaves the same.

   A stretched overlay was tried first and was worse: above the copy it killed
   text selection, below it left dead patches wherever there was text, so the
   hover flickered on and off as the pointer crossed the card.

   Hover is only the link and the arrow. A background tint muddies the colour,
   and darkening the foot rule drags the eye to a divider rather than to the
   thing you are about to click. */
.card { position: relative; }
.card--link { text-decoration: none; color: inherit; display: flex; }
.card--link:hover .link-arrow { text-decoration: underline; }
.card--link:hover .link-arrow::after { transform: translateX(4px); }

.wpcf7 .afb-pills input[type="radio"]:focus-visible + .wpcf7-list-item-label {
	outline: var(--focus) solid var(--deep);
	outline-offset: 2px;
	border-radius: 4px;
}

/* FAQ ----------------------------------------------------------------------
   A rule under each question, a round toggle on the right, the answer under.
   The toggle is drawn rather than shipped as an asset, so it takes its colour
   from the state and costs no request. */

.faq__item { border-bottom: 1px solid var(--hairline); }
.faq__q {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 24px;
	padding-block: 24px;
	cursor: pointer;
	list-style: none;
}
/* Safari still paints its own triangle without this. */
.faq__q::-webkit-details-marker { display: none; }
.faq__q-text { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; }

.faq__toggle {
	position: relative;
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	border: 1px solid var(--hairline);
	border-radius: 999px;
	transition: background-color .2s ease, border-color .2s ease;
}
.faq__toggle::before {
	content: "";
	position: absolute;
	top: 50%; left: 50%;
	width: 8px; height: 8px;
	border: 2px solid var(--ink);
	border-width: 0 0 2px 2px;
	transform: translate(-50%, -75%) rotate(-45deg);
	transition: transform .25s cubic-bezier(.2, .6, .2, 1), border-color .2s ease;
}
.faq__item[open] .faq__toggle { background: var(--primary); border-color: var(--primary); }
.faq__item[open] .faq__toggle::before {
	border-color: var(--on-primary);
	transform: translate(-50%, -25%) rotate(135deg);
}
.faq__q:hover .faq__toggle { border-color: var(--ink); }
.faq__item[open] .faq__q:hover .faq__toggle { background: var(--primary-light); border-color: var(--primary-light); }

.faq__a { padding-bottom: 24px; }
.faq__a p { margin: 0; font-size: 18px; line-height: 1.7; color: var(--ink-soft); max-width: 860px; }
/* Answers can run to more than one paragraph now that wpautop is applied. */
.faq__a p + p { margin-top: 16px; }

/* Eases where the browser supports it, snaps everywhere else. */
@supports selector(details::details-content) {
	.faq__item::details-content {
		block-size: 0;
		overflow: hidden;
		transition: block-size .28s ease, content-visibility .28s allow-discrete;
	}
	.faq__item[open]::details-content { block-size: auto; }
}

/* Points list --------------------------------------------------------------
   Heading held on the left, points stacked down the right with a rule between
   each. Figma's split is a fixed 440 head against a flexible column at 96. */

.points__inner { display: grid; grid-template-columns: 440px minmax(0, 1fr); gap: 96px; align-items: start; }
.points__head { display: flex; flex-direction: column; gap: 16px; }
.points__list { margin: 0; display: flex; flex-direction: column; }

/* Head on top, points two across. Four points beside a heading make a long
   thin column on a full width dark band; the About template puts the heading
   over the top and reads them as a grid instead. */
.points--stacked .points__inner { display: block; }
.points--stacked .points__head { margin-bottom: 48px; }
.points--stacked .points__list {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	column-gap: 32px;
	/* Side by side, the rule under a point sits right on top of the next one's
	   rule with only their paddings between, which is not enough to read as two
	   rows. Grid row-gap rather than more padding, so the single column layout
	   below 768 keeps the tight stack it already has. */
	row-gap: 24px;
}
/* The rule belongs to the row, not the cell: in two columns the first item of
   each row is the one that starts level with the heading above it. */
.points--stacked .point:nth-child(-n + 2) { padding-top: 0; border-top: 0; }
/* First point has no rule, so the stack starts level with the heading. */
.point { padding-block: 24px; border-top: 1px solid var(--hairline); }
.point:first-child { padding-top: 0; border-top: 0; }
.point__title { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; margin: 0; }
.point__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 8px 0 0; }

/* Dark band. Its own background, so a row can be dark without the CTA's red. */

.section--deep { background: var(--deep); color: var(--paper); }
.section--deep .point { border-top-color: var(--hairline-dark); }
.section--deep .point__text { color: var(--on-dark); }
.section--deep .section__intro, .section--deep .card__text { color: var(--on-dark); }
/* The arrow link is red by default, which vanishes on the deep band. */
.section--deep .link-arrow { color: var(--accent-warm); }
/* List items carry their own ink-soft, which does not inherit the band's paper
   and would sit almost invisible on the deep. Same treatment as .point__text. */
.section--deep .list-columns__list li,
.section--deep .list-columns__list li::before,
.section--deep .list-columns__prose > * { color: var(--on-dark); }

/* List columns ------------------------------------------------------------
   A heading and two or three lists that answer each other. Generic on purpose:
   good fit against bad, included against not, before against after. */
.list-columns__row { display: grid; gap: 64px; margin-top: 48px; }
.list-columns__row--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.list-columns__row--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.section > .wrap > .list-columns__row { margin-top: 48px; }
/* The column is the same object as a process step: a 3px primary rule, 24 of
   padding under it, then a Display/S title. Styled off the step rather than
   from scratch so the two never drift apart. */
.list-columns__col { border-top: 3px solid var(--primary); padding-top: 24px; display: flex; flex-direction: column; gap: 16px; }
.list-columns__title { margin: 0; }
.list-columns__list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 16px; }
.list-columns__list li { position: relative; padding-left: 24px; font-size: 18px; line-height: 1.7; color: var(--ink-soft); }
.list-columns__list li::before { content: "\00B7"; position: absolute; left: 0; color: var(--ink-soft); }

/* Proof callout ----------------------------------------------------------- */
.proof-callout__inner { display: grid; grid-template-columns: 340px minmax(0, 1fr); gap: 64px; align-items: start; }
.proof-callout__figure-col { display: flex; flex-direction: column; gap: 8px; }
.proof-callout__figure {
	font-family: var(--font-display);
	font-size: 64px;
	line-height: 1.05;
	font-weight: 800;
	color: var(--paper);
	margin: 0;
}
.proof-callout__detail { display: flex; flex-direction: column; gap: 16px; }
/* Type comes from the scale, not from this file: the heading is Display/M via
   the shared class, the text is the same 18px body every other band uses. */
.proof-callout__heading { margin: 0; }
.proof-callout__text { font-size: 18px; line-height: 1.7; color: var(--on-dark); margin: 0; }
.proof-callout__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 24px; margin-top: 8px; }
.section > .wrap.proof-callout__inner > * + * { margin-top: 0; }

@media (max-width: 1023px) {
	.proof-callout__inner { grid-template-columns: 1fr; gap: 32px; }
	.list-columns__row { gap: 40px; }
}
@media (max-width: 767px) {
	.list-columns__row--2, .list-columns__row--3 { grid-template-columns: 1fr; }
	/* Display/XL and, unusually, Display/S: the mobile frame drops this heading
	   two steps from the 30 it sets on desktop, not one. 48 and 24 were my own
	   numbers and 48 was not on the scale at all. */
	.proof-callout__figure { font-size: 38px; }
	.proof-callout__heading { font-size: 20px; }
}

/* Legal ------------------------------------------------------------------
   A 320 contents rail against a 720 article, which is wider than the 680 prose
   measure everywhere else on purpose: a policy is scanned for the clause that
   applies to you rather than read start to finish, and the rail is what makes
   that possible. Sticky, because the article runs to two thousand pixels and a
   contents list you have to scroll back up to reach is decoration. */
.legal__inner { display: grid; grid-template-columns: 320px minmax(0, 720px); gap: 208px; align-items: start; }
.section > .wrap.legal__inner > * + * { margin-top: 0; }
/* The contents rail, shared by the legal pages and blog posts via
   parts/contents-rail.php. Styled on .contents, not on either page's own class,
   because the first version lived on .legal__contents and the blog rail built
   later silently got none of it. */
.contents { position: sticky; top: calc(var(--header-h, 73px) + 24px); }
/* A rule down the left the marker rides, so "where am I" is one glance rather
   than reading eighteen labels. It is the same 3px primary rule the rest of the
   site uses to mark a thing, turned vertical. */
.contents ul {
	list-style: none; margin: 16px 0 0; padding: 0 0 0 16px;
	display: flex; flex-direction: column; gap: 16px;
	border-left: 1px solid var(--hairline);
}
.contents li { position: relative; }
.contents li::before {
	content: "";
	position: absolute; left: -17px; top: 2px; bottom: 2px; width: 3px;
	background: var(--primary);
	opacity: 0;
	transition: opacity .2s ease;
}
.contents a { font-size: 15px; line-height: 1.7; color: var(--ink-soft); text-decoration: none; transition: color .2s ease; }
.contents a:hover { color: var(--primary); text-decoration: underline; }
.contents .is-current::before { opacity: 1; }
.contents .is-current > a { color: var(--ink); font-weight: 600; }

/* The summary is the label on a wide screen and a real control below it, so it
   is styled as the label and only becomes obviously tappable where it does
   something. */
.contents__toggle { list-style: none; cursor: default; }
.contents__toggle::-webkit-details-marker { display: none; }

@media (prefers-reduced-motion: reduce) {
	.contents li::before, .contents a { transition: none; }
}
/* No scroll-margin-top here. `html` already carries scroll-padding-top for the
   sticky header, and the two ADD: a clicked heading landed at 256 instead of
   128, which put it below the scrollspy's reading line, so every click marked
   the section above the one you asked for. */

/* A part divider, not a section. Kept out of the contents list by the class,
   because a contents entry that is not a place is noise. */
.legal__article .legal__part {
	margin-top: 64px;
	padding-top: 32px;
	border-top: 3px solid var(--primary);
	font-size: 22px;
	line-height: 1.28;
	color: var(--ink);
}
.legal__article > *:first-child { margin-top: 0; }
/* 320 + 208 + 720 is exactly 1248, so it only fits at full width. The gap is
   the part to give up first: the rail and the measure both have to hold. */
@media (max-width: 1279px) {
	.legal__inner { gap: 64px; }
}

/* Blog -------------------------------------------------------------------
   The featured card is the Work card turned on its side: picture left at 560,
   copy right at 688, one radius around both. */
.feature-card--post { background: var(--paper); border: 1px solid var(--hairline); }
.feature-card--post .feature-card__title { color: var(--ink); }
.feature-card__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); }

/* A row, not a card: no tile, a rule between each, and the picture is a fixed
   240 so the titles all start on the same line however tall the images are. */
.post-list { display: flex; flex-direction: column; }
.post-row {
	display: grid;
	grid-template-columns: 240px minmax(0, 1fr);
	gap: 32px;
	align-items: center;
	padding-block: 32px;
	border-top: 1px solid var(--hairline);
	text-decoration: none;
	color: inherit;
}
.post-row:first-child { border-top: 0; }
.post-row__media { display: block; border-radius: var(--radius); overflow: hidden; }
.post-row__media img { width: 100%; height: 160px; object-fit: cover; display: block; }
/* No picture, and the row would collapse its first column and shunt the copy
   left, out of line with every other row. */
.post-row:not(:has(.post-row__media)) { grid-template-columns: 1fr; }
.post-row__body { display: flex; flex-direction: column; gap: 8px; }
.post-row__title { display: block; color: var(--ink); }
.post-row__excerpt { font-size: 18px; line-height: 1.7; color: var(--ink-soft); }
.post-row:hover .post-row__title { text-decoration: underline; }

.section > .wrap > .post-list { margin-top: 0; }

/* Article ------------------------------------------------------------------
   680 of prose against a 320 rail, which is the Figma column pair. The rail
   sticks under the header so the contents stay reachable in a long post. */
.post-article__inner { display: grid; grid-template-columns: minmax(0, var(--prose)) 320px; gap: 64px; align-items: start; }
.section > .wrap.post-article__inner > * + * { margin-top: 0; }
.prose--article h2 { font-size: 30px; line-height: 1.35; margin-top: 48px; }
.prose--article > p:first-child { font-size: 22px; line-height: 1.5; color: var(--ink); }
.prose--article blockquote {
	margin: 32px 0;
	padding-left: 24px;
	border-left: 3px solid var(--primary);
	font-family: var(--font-display);
	font-size: 22px;
	line-height: 1.28;
	font-weight: 800;
	color: var(--ink);
}
.post-rail__contents { position: static; }
.post-rail { position: sticky; top: calc(var(--header-h, 73px) + 24px); display: flex; flex-direction: column; gap: 32px; }
/* The rail itself is .contents now, styled once above for both templates. */
.post-rail__offer { background: var(--blush); border-radius: var(--radius); padding: 32px; display: flex; flex-direction: column; gap: 16px; align-items: flex-start; }
.post-rail__title { margin: 0; }
.post-rail__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

.post-header__byline { font-size: 15px; line-height: 1.5; color: var(--muted); margin: 0; }
.post-hero img { width: 100%; height: 480px; object-fit: cover; display: block; }
/* Figma 39:402 draws this as a row on the prose column: a 96 round photograph,
   32, then the name and bio left aligned against it. It was built centred with
   no photograph at all, which is the only band on a post with nothing in it but
   type. The `.section > .wrap > * + *` 16 lands on the text column here,
   because this inner IS the wrap, so it is reset like the other flex wraps. */
/* `.wrap` puts the gutter inside its own max-width, so a bare `--prose` cap
   left a 488 column, not the 680 the design draws. */
.post-author__inner { max-width: calc(var(--prose) + (var(--gutter) * 2)); margin-inline: auto; display: flex; align-items: center; gap: 32px; }
.section > .post-author__inner > * + * { margin-top: 0; }
.post-author__avatar { width: 96px; height: 96px; flex-shrink: 0; border-radius: 50%; object-fit: cover; }
.post-author__text { flex: 1 1 auto; min-width: 0; }
.post-author__name { margin: 0; }
.post-author__bio { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 8px 0 0; }

/* Two columns of prose rather than two lists. No rule, no title, no bullets:
   the location band is one piece of writing continued across two columns, not
   two lists answering each other, which is what the rule and the bullets are
   there to signal. */
.list-columns__row--prose .list-columns__col { border-top: 0; padding-top: 0; }
.list-columns__prose > * { margin: 0; font-size: 18px; line-height: 1.7; color: var(--ink-soft); }
.list-columns__prose > * + * { margin-top: 24px; }

/* The line under a card row, for the things that did not earn a card. */
.card-grid__footnote { font-size: 18px; line-height: 1.7; color: var(--ink-soft); margin: 32px 0 0; }

/* Not found ---------------------------------------------------------------
   Centred, and stacked at 24 rather than the site's 16: it is four short things
   on an otherwise empty band, and at 16 they read as one paragraph. The links
   band under it uses 32, which is its own figure again. */
.not-found__inner { text-align: center; }
.section > .wrap.not-found__inner > * + * { margin-top: 24px; }
.not-found__code { color: var(--primary); }
.not-found__text { max-width: 600px; margin-inline: auto; margin-block: 0; font-size: 20px; line-height: 1.6; color: var(--ink-soft); }
.not-found__action { margin-block: 0; }
.section > .wrap > .not-found__label + .card-grid { margin-top: 32px; }
/* Margin and size kept apart on purpose: at three classes the size would
   out-rank the single class mobile rule and stay at 18 on a phone, which is
   the bug this whole sweep was about. */
.section > .wrap > .not-found__footnote { margin-top: 32px; }
.not-found__footnote { font-size: 18px; }

/* Text and image -----------------------------------------------------------
   Copy at a fixed 560 so the measure stays readable, picture takes what is
   left. Figma's band is 560 against a flexible image with 80 between them. */

/* The copy column gives ground before the picture does. A flat 560 held its
   width all the way down and the image took the whole squeeze: at 1200 the
   portrait was crushed to 353 wide against 440 tall. This lands on Figma's
   560 wherever there is room for it and shrinks first below that. */
.text-media__inner { display: grid; grid-template-columns: clamp(380px, 46%, 560px) minmax(0, 1fr); gap: 80px; align-items: center; }
.text-media__copy { display: flex; flex-direction: column; gap: 24px; }
/* Copy with no picture: one column at the prose measure, not half a band with
   an empty half beside it. */
.text-media--solo .text-media__inner { grid-template-columns: minmax(0, var(--prose)); }

.text-media__head { display: flex; flex-direction: column; gap: 16px; }
/* The site's reading measure. */
.text-media__copy { max-width: var(--prose); }
.text-media__body > * { margin: 0; font-size: 18px; line-height: 1.7; color: var(--ink-soft); }
.text-media__body > * + * { margin-top: 24px; }
.text-media__action { margin: 0; }
.text-media__image {
	width: 100%;
	height: 440px;
	object-fit: cover;
	border-radius: var(--radius);
}
/* Ordered, not reversed, so the copy is read first either way round. */
.text-media--flip .text-media__inner { grid-template-columns: minmax(0, 1fr) clamp(380px, 46%, 560px); }
.text-media--flip .text-media__copy { order: 2; }
.text-media--flip .text-media__image { order: 1; }

/* Numbered steps -----------------------------------------------------------
   A rule across the top and copy under it. No tile, so no padding and no
   background, which is what separates a step from a card. */

.card-grid--steps { gap: 48px; list-style: none; margin: 0; padding: 0; counter-reset: afb-step; }
/* Steps keep 48 at every count. This block used to tighten four across to 32 on
   the reasoning that 276 wraps the copy an extra line, but the design does not:
   Home draws four steps at 276 with a 48 gap and takes the extra line. */
/* The rule is a positioned pseudo, not a border: a border cannot be scaled on
   its own. 28 is the 4px rule plus 24px of padding. */
.step {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding-top: 24px;
	position: relative;
	counter-increment: afb-step;
}
.step::after {
	content: "";
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	height: 4px;
	background: var(--primary);
	transform-origin: 0 50%;
}
/* Generated, so reordering renumbers. Decorative: the list carries the order. */
.step::before {
	content: counter(afb-step, decimal-leading-zero);
	font-family: var(--font-display);
	font-size: 40px;
	line-height: 1.12;
	font-weight: 800;
	color: var(--primary);
}
.step__title { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; margin: 0; }
.step__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

/* A row with nothing to scroll hides its arrows. The dots on a slider row stay,
   because Figma draws them on every template whether the row scrolls or not. */
.section.is-static .carousel__controls,
.section.is-static > .wrap > .card-dots { display: none; }

/* Testimonials -------------------------------------------------------------
   A quote card on the shared carousel track. 16 between its parts rather than
   Figma's raw 14, which is not on the spacing scale. */

.quote {
	flex: 0 0 612px;
	display: flex;
	flex-direction: column;
	gap: 16px;
	margin: 0;
	padding: 40px;
	background: var(--paper);
	border: 1px solid var(--hairline);
	border-radius: var(--radius);
}
/* Drawn by CSS so it is never selected, searched or read as part of the quote. */
.quote__text::before {
	content: "\201C";
	display: block;
	font-family: var(--font-display);
	font-size: 40px;
	line-height: 1.12;
	font-weight: 800;
	color: var(--primary);
	margin-bottom: 16px;
}
.quote__text {
	margin: 0;
	font-family: var(--font-display);
	font-size: 22px;
	line-height: 1.28;
	font-weight: 800;
	color: var(--ink);
}
/* Editors only, so it can be loud. */
.quote--unapproved { border-color: var(--primary); }
.quote__flag {
	margin: 0;
	font-size: 12px;
	font-weight: 800;
	line-height: 1.3;
	letter-spacing: 1.9px;
	text-transform: uppercase;
	color: var(--primary);
}
.quote__by { display: flex; flex-direction: column; gap: 4px; }
.quote__name { font-size: 15px; font-weight: 600; line-height: 1.5; color: var(--ink); }
.quote__role { font-size: 15px; line-height: 1.7; color: var(--muted); }

/* Pricing tiers ------------------------------------------------------------
   Everything else about a tier is already .card. Only the price is new. */

.tier { justify-content: flex-start; }
.tier__price {
	font-family: var(--font-display);
	font-size: 40px;
	line-height: 1.12;
	font-weight: 800;
	color: var(--primary);
	margin: 0;
}
.tier__from { font-family: var(--font-body); font-size: 20px; font-weight: 600; }
.tier__note { font-size: 15px; line-height: 1.7; color: var(--muted); margin: 0; }
.pricing__link { margin: 48px 0 0; }

/* Case study --------------------------------------------------------------
   A full bleed image with the title over it, then the story in sections. */

/* The three dark bands each carry their own gradient from the design, so a
   case study still reads as designed before its photography arrives. */
/* The three full bleed panels below clip their own picture, so the parallax in
   the motion block can oversize it without exposing the gradient at an edge.
   Clipping makes each one a scroll container, which is why anything inside them
   reads a NAMED timeline rather than view(). */
.case-hero { position: relative; display: grid; align-items: end; min-height: 600px; overflow: hidden; background: linear-gradient(180deg, #4a2f27 0%, #2e1b16 55%, #160d0b 100%); color: var(--paper); }
.case-hero__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* A scrim, or white type over a photograph is a coin toss.
 *
 * The bottom stop is .90, not the .85 the frame shows. Measured against the
 * real rendered pixels behind the text rather than against a flat colour, the
 * deck came out at 4.48 on the brightest image on the site, just under the 4.5
 * that 20px body copy needs. The scrim is what guarantees this for any image
 * anybody adds later, so it is the scrim that had to move, not the photograph. */
.case-hero--image::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(180deg, rgba(28, 16, 13, .30) 0%, rgba(28, 16, 13, .90) 100%);
}
.case-hero__inner { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 16px; padding-block: 96px; }
.case-hero__inner .display-xl { max-width: 900px; }
/* Separated by a rule rather than a middot so the credit reads as its own
   statement and not as a continuation of the words "Case study". */
.case-hero__credit::before { content: ""; display: inline-block; width: 1px; height: 10px; margin: 0 10px -1px; background: currentColor; opacity: .5; }
.case-hero__deck { font-size: 20px; line-height: 1.6; color: var(--on-dark); max-width: 720px; margin: 0; }
/* The one action in the hero, so it reads as a button rather than joining the
   deck as another line of prose. Arrow and nudge follow .link-arrow, since a
   second arrow behaviour on the same site reads as a mistake. */
.case-hero__visit { margin: 8px 0 0; }
.case-hero__visit a {
	display: inline-flex;
	align-items: center;
	padding: 12px 24px;
	border: 1px solid rgba(255, 255, 255, .45);
	border-radius: 999px;
	font-size: 15px;
	font-weight: 700;
	color: var(--paper);
	text-decoration: none;
	transition: background .2s ease, border-color .2s ease, color .2s ease;
}
.case-hero__visit a::after {
	content: "\2192";
	display: inline-block;
	margin-left: 8px;
	transition: transform .2s cubic-bezier(.2, .6, .2, 1);
}
.case-hero__visit a:hover { background: var(--paper); border-color: var(--paper); color: var(--deep); }
.case-hero__visit a:hover::after { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
	.case-hero__visit a, .case-hero__visit a::after { transition: none; }
}

/* Tracks follow the number of figures. Hardcoded to four, a three figure row
   left a dead column on the right and the row read as though something had
   failed to load. */
.stats__row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 40px; }
/* auto-fit gives every track an equal share, which is right for three or four
   and wrong for one or two: a single figure stretched over 1248px leaves its
   label stranded under a very large number. Cap the track and group them left
   instead, so a short row reads as deliberate rather than broken. */
.stats__row:has(> .stat:only-child),
.stats__row:has(> .stat:nth-child(2):last-child) {
	grid-template-columns: repeat(auto-fit, minmax(200px, 340px));
	justify-content: start;
	gap: 64px;
}
.stat { display: flex; flex-direction: column; gap: 8px; }
.stat__value { font-family: var(--font-display); font-size: 40px; line-height: 1.12; font-weight: 800; color: var(--primary); margin: 0; }
.stat__label { font-size: 15px; font-weight: 600; line-height: 1.5; color: var(--ink); margin: 0; }
.stat__note { font-size: 13px; line-height: 1.55; color: var(--muted); margin: 0; }

.section.glance { border-bottom: 1px solid var(--hairline); }
/* margin-block, not margin: a bare margin:0 would cancel the auto side margins
   .wrap uses to centre itself, and this row is the wrap. */
.glance__row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 40px; margin-block: 0; }
.glance__item { display: flex; flex-direction: column; gap: 8px; }
.glance__label { font-size: 12px; font-weight: 800; line-height: 1.3; letter-spacing: 1.9px; text-transform: uppercase; color: var(--primary); }
.glance__value { font-size: 15px; line-height: 1.7; color: var(--ink); margin: 0; }
.glance__link { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; text-decoration-color: var(--primary); }
.glance__link:hover { color: var(--primary); }

.rail__inner { display: grid; grid-template-columns: 260px minmax(0, 680px); gap: 64px; align-items: start; }
.rail__heading { font-family: var(--font-display); font-size: 30px; line-height: 1.35; font-weight: 800; margin: 0; }
.rail__body { font-size: 20px; line-height: 1.7; color: var(--ink); }
.rail__body > * { margin: 0; }
.rail__body > * + * { margin-top: 24px; }
.rail__footnote { font-size: 13px; line-height: 1.55; color: var(--muted); }

.compare { width: 100%; border-collapse: collapse; text-align: left; }
.compare th, .compare td { padding: 16px 0 16px 24px; border-bottom: 1px solid var(--hairline); }
.compare th:first-child, .compare td:first-child { padding-left: 0; }
.compare thead th { font-size: 12px; font-weight: 800; line-height: 1.3; letter-spacing: 1.9px; text-transform: uppercase; color: var(--primary); }
.compare tbody th { font-size: 16px; font-weight: 400; line-height: 1.5; color: var(--ink); }
.compare td { width: 174px; font-size: 15px; font-weight: 600; line-height: 1.5; color: var(--ink); }
.compare td.compare__after { color: var(--primary); }

.image-band { position: relative; margin: 0; display: grid; place-items: center; overflow: hidden; background: linear-gradient(180deg, #4d3933 0%, #3e2d28 50%, #2b1e1b 100%); }
.image-band__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.image-band__hint { font-size: 12px; font-weight: 800; letter-spacing: 1.9px; text-transform: uppercase; color: var(--on-dark); }
.image-band--sm { min-height: 280px; }
.image-band--md { min-height: 420px; }
.image-band--lg { min-height: 560px; }

.next-case { position: relative; display: grid; align-items: end; min-height: 340px; overflow: hidden; background: linear-gradient(180deg, #3e2923 0%, #261713 60%, #140c0a 100%); color: var(--paper); text-decoration: none; }
/* Held below centre because the band is a wide letterbox and the subject of a
   photograph almost always sits under the horizon, so a centred crop lands on
   empty sky. */
.next-case__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 62%; }
.next-case--image::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(180deg, rgba(28, 16, 13, .3) 0%, rgba(28, 16, 13, .85) 100%);
}
.next-case__inner { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 16px; padding-block: 96px; }
.next-case__title { display: flex; align-items: center; gap: 24px; }
/* The whole band is the link, so this is decoration, not a second control: no
   tab stop, no separate hover target. It is a ring rather than a bare arrow
   because a bare arrow on a photograph does not read as somewhere to click.
   Sized and shaped off .carousel-btn so the site has one circular control.
   Figma colours the arrow accent-warm and the build was rendering it white. */
.next-case__arrow {
	flex-shrink: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 56px;
	height: 56px;
	border: 1px solid var(--paper);
	border-radius: 999px;
	color: var(--paper);
	font-size: 24px;
	line-height: 1;
	transition: background-color .2s ease, color .2s ease, border-color .2s ease, transform .2s ease;
}
.next-case:hover .next-case__arrow,
.next-case:focus-visible .next-case__arrow {
	background: var(--paper);
	border-color: var(--paper);
	color: var(--deep);
	transform: translateX(4px);
}
.next-case__deck { font-size: 18px; line-height: 1.7; color: var(--on-dark); max-width: 680px; margin: 0; }

/* Work grid ---------------------------------------------------------------
   The portfolio listing. A featured project full width, the rest three across. */

.filters { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 48px; }
@media (max-width: 767px) {
	.filters {
		flex-wrap: nowrap;
		overflow-x: auto;
		overscroll-behavior-x: contain;
		scrollbar-width: none;
		-webkit-overflow-scrolling: touch;
		margin-inline: calc(var(--gutter) * -1);
		padding-inline: var(--gutter);
		margin-bottom: 32px;
	}
	.filters::-webkit-scrollbar { display: none; }
	.pill { flex: none; }
}
.pill {
	padding: 12px 24px;
	border: 1px solid var(--hairline);
	border-radius: 999px;
	background: transparent;
	font-family: var(--font-body);
	font-size: 15px;
	font-weight: 600;
	line-height: 1.5;
	color: var(--ink);
	cursor: pointer;
	/* theme.json underlines every link on hover, which is right for prose and
	   wrong for a control. The portfolio pills are <button> so it never showed;
	   the blog pills are <a> and it did. The Pill component has two states,
	   Default and Selected, both textDecoration NONE. */
	text-decoration: none;
	transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.pill:hover { border-color: var(--ink); text-decoration: none; }
/* Primary, not ink. The Pill component's State=Selected is the brand red with
   white on it; ink was wrong on all three places pills appear, the portfolio
   filters, the contact form and now the blog. */
.pill.is-active { background: var(--primary); border-color: var(--primary); color: var(--paper); }
/* Lighten, do not ring. The plain :hover above would have drawn an ink border
   round a red pill; every other filled red control on the site lightens. */
.pill.is-active:hover { background: var(--primary-light); border-color: var(--primary-light); }
.filters__empty { margin: 0 0 48px; font-size: 18px; line-height: 1.7; color: var(--muted); }

.feature-card {
	display: grid;
	grid-template-columns: 560px minmax(0, 1fr);
	align-items: stretch;
	margin-bottom: 48px;
	border: 1px solid var(--hairline);
	border-radius: var(--radius);
	overflow: hidden;
	background: var(--paper);
	text-decoration: none;
	color: inherit;
}
/* The picture is a cover crop, so it must not be what decides how tall the card
   is. Left in flow at height:100% it resolves against a row it is itself
   sizing, and a 960x720 photo in the 560 column made the blog's featured card
   470 tall against a design of 317. Taken out of flow, the copy decides. */
.feature-card__media { position: relative; background: linear-gradient(136deg, #4d3933 0%, #2b1e1b 73%); min-height: 237px; }
.feature-card__media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.feature-card__body { display: flex; flex-direction: column; gap: 16px; padding: 48px; }
.feature-card__title { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; margin: 0; }
.feature-card:hover .feature-card__title { text-decoration: underline; }
.feature-card:hover .link-arrow::after { transform: translateX(4px); }
.feature-card__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

.work-grid__items { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
/* Figma's grid image is 240 where the carousel's is 220. */
.work-grid__items .work-card__media { height: 240px; }
.work-grid__items .work-card[hidden], .feature-card[hidden] { display: none; }
/* Figma spaces this band's children at 40. The wrap's `* + *` rule is (0,2,0)
   and a plain class is (0,1,0), so every gap here was losing to it and sitting
   at 16: the View more button ended up tucked under the last card's bottom
   border, which then read as a divider rather than a card edge. Filters drop
   their own bottom margin so the 40 is not doubled. */
.work-grid > .wrap > .filters { margin-bottom: 0; }
.section > .wrap > .feature-card,
.section > .wrap > .work-grid__items,
.section > .wrap > .work-grid__more { margin-top: 40px; }
/* The featured card carries its own 48 bottom margin for the blog, where it is
   the only card in the band. Inside the work grid that margin collapsed against
   the grid's 40 and won, so the gap under the featured card was 48 against
   Figma's 40 (35:216 ends 420, 140:919 starts 460) while the rows below it sat
   at 24. Dropped here so the band's own 40 is what applies. */
.work-grid > .wrap > .feature-card { margin-bottom: 0; }
/* Centred under the grid, and the focus target after expanding gets no ring of
   its own because the card already has one. */
.work-grid__more { display: flex; justify-content: center; }
/* The button is toggled with the hidden attribute, and display:flex above beats
   the UA [hidden] rule, so it stayed on screen once everything was expanded. */
.work-grid__more[hidden] { display: none; }
.work-grid__items > [data-cats]:focus { outline: none; }

/* Selected work ------------------------------------------------------------

   A sideways track at every width, not a grid that becomes one. The row being
   cut off at the edge is the point: there is more work than fits.

   The track sits outside .wrap so the cards run off the screen edge, with the
   gutter put back as its own padding. */

.carousel__head { display: flex; align-items: flex-end; gap: 48px; margin-bottom: 48px; }
.carousel__head .section__head { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.carousel__controls { display: flex; gap: 12px; flex-shrink: 0; }

.carousel-btn {
	width: 48px; height: 48px; flex-shrink: 0;
	border: 1px solid var(--hairline);
	border-radius: 999px;
	background: transparent;
	cursor: pointer;
	position: relative;
	transition: background-color .2s ease, border-color .2s ease, opacity .2s ease;
}
/* Chevron drawn from two rotated borders, so it needs no request. */
.carousel-btn::before {
	content: "";
	position: absolute;
	top: 50%; left: 50%;
	width: 9px; height: 9px;
	border: 2px solid var(--ink);
	border-width: 2px 2px 0 0;
	transform: translate(-25%, -50%) rotate(-135deg);
	transition: border-color .2s ease;
}
.carousel-btn + .carousel-btn::before { transform: translate(-75%, -50%) rotate(45deg); }

/* The chevron leans the way it is about to take you, the same nudge the text
   link arrows make. Only when it can actually go there.

   The chevron is 9px across, so these percentages buy about 3.5px. The first
   attempt shifted it by 20%, which came to under two pixels and read as
   nothing at all. */
.carousel-btn--on { background: var(--primary); border-color: var(--primary); }
.carousel-btn--on::before { border-color: var(--on-primary); }

/* Hovers follow the Button component set. */
.carousel-btn:hover { background: var(--ink); border-color: var(--ink); }
.carousel-btn:hover::before { border-color: var(--paper); }
.carousel-btn--on:hover { background: var(--primary-light); border-color: var(--primary-light); }
.carousel-btn--on:hover::before { border-color: var(--on-primary); }

.carousel-btn[disabled] { opacity: .35; cursor: default; }
.carousel-btn[disabled]:hover { background: transparent; border-color: var(--hairline); }
.carousel-btn[disabled]:hover::before { border-color: var(--ink); }

/* Full bleed, but its first card lines up with the heading, which is the wrap's
   content edge once the window is wider than the wrap. The bottom padding is
   room for the card rise, which overflow-y would otherwise clip. */
.carousel__track {
	display: flex;
	gap: 24px;
	overflow-x: auto;
	overscroll-behavior-x: contain;
	padding-inline: max(var(--gutter), calc((100% - var(--content)) / 2));
	padding-bottom: 32px;
	margin-bottom: -32px;
	scrollbar-width: none;
	-webkit-overflow-scrolling: touch;
}
.carousel__track::-webkit-scrollbar { display: none; }
/* The one exception to the shared ring: this track runs off both screen edges,
   so an outset ring would be clipped. */
.carousel__track:focus-visible { outline: var(--focus) solid var(--deep); outline-offset: -4px; }
/* Cards must not be draggable: a browser treats press-and-move on a link as
   drag-this-link, which seizes the pointer. */
.carousel__track.is-dragging, .card-grid.is-dragging, .filters.is-dragging { user-select: none; }
.work-card, .work-card img, .card--link { -webkit-user-drag: none; }
.carousel__track.is-dragging .work-card,
.card-grid.is-dragging > .card { user-select: none; }

/* Top aligned, so the dots land on the same number as every other section
   rather than being pushed down by a taller link beside them. */
.carousel__foot { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-top: 48px; }
.carousel__reviews { margin: 0; text-align: right; }
.carousel__reviews .link-arrow { white-space: nowrap; }
@media (max-width: 767px) {
	.carousel__foot { flex-wrap: wrap; gap: 16px; }
	.carousel__reviews { flex-basis: 100%; text-align: left; }
	.carousel__reviews-sep { display: none; }
	.carousel__reviews .link-arrow { display: inline-block; margin-top: 4px; }
}
/* This row is a slider at every width, so its dots are always on. The gutter
   and top margin come off: it is already inside .wrap. */
.carousel__foot .card-dots {
	display: flex;
	padding-inline: 0;
	margin-top: 0;
}

.work-card {
	flex: 0 0 400px;
	display: flex;
	flex-direction: column;
	border: 1px solid var(--hairline);
	border-radius: var(--radius);
	background: var(--paper);
	overflow: hidden;
	text-decoration: none;
	color: inherit;
}
.work-card__media {
	height: 220px;
	flex-shrink: 0;
	/* Stands in for a missing image rather than collapsing the card, and sits
	   under a real one as it loads. */
	background: linear-gradient(136deg, #4d3933 0%, #2b1e1b 73%);
	overflow: hidden;
}
.work-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .5s cubic-bezier(.2, .6, .2, 1); }
.work-card:hover .work-card__media img { transform: scale(1.04); }
.work-card__body { display: flex; flex-direction: column; gap: 12px; padding: 24px; }
.work-card__title { font-family: var(--font-display); font-size: 22px; line-height: 1.28; font-weight: 800; margin: 0; }
.work-card:hover .work-card__title { text-decoration: underline; }
.work-card__text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }
/* Pinned to the bottom so credits line up across a row. */
.work-card__credit {
	margin: 0; margin-top: auto;
	padding-top: 12px;
	border-top: 1px solid var(--hairline);
	font-size: 15px; font-weight: 600; line-height: 1.5; color: var(--muted);
}

/* Blog listing */


/* One anchor, same as the cards. */

.nav-links { display: flex; gap: 16px; margin-top: 64px; font-size: 15px; }

.meta-item { display: flex; flex-direction: column; gap: 8px; }
.meta-item__value { font-size: 15px; line-height: 1.7; margin: 0; }

/* No top margin: the aside is a flex column with a 24px gap, and the two
   together were doubling the space above the quote. */
.pull-quote { margin: 0; padding-top: 24px; border-top: 1px solid var(--hairline); }
.pull-quote blockquote { margin: 0; font-family: var(--font-display); font-size: 18px; font-weight: 800; line-height: 1.28; }
.pull-quote figcaption { margin-top: 12px; display: flex; flex-direction: column; gap: 4px; }
.pull-quote__name { font-size: 15px; font-weight: 600; }
.pull-quote__role { font-size: 15px; color: var(--muted); }

/* Contact ----------------------------------------------------------------- */

.contact { display: grid; grid-template-columns: 720px 448px; gap: 80px; align-items: start; }
/* Both columns are flex in the design, gap 24. Flex also drops the whitespace
   text nodes between the PHP blocks, which were otherwise generating a 24px
   line box above the form. */
.contact__form, .contact__aside { display: flex; flex-direction: column; gap: 24px; }
.contact__note { font-size: 13px; line-height: 1.55; color: var(--muted); margin: 0; }
.contact__card { gap: 24px; }
.contact__next { padding-top: 24px; border-top: 1px solid var(--hairline); display: flex; flex-direction: column; gap: 12px; }
.contact__next-text { font-size: 15px; line-height: 1.7; color: var(--ink-soft); margin: 0; }

/* Form, styled to the Input component. CF7's own stylesheet is dequeued. */

.wpcf7 label, .afb-field__label {
	display: block;
	font-size: 12px;
	font-weight: 800;
	line-height: 1.3;
	letter-spacing: 1.9px;
	text-transform: uppercase;
	color: var(--ink);
	margin-bottom: 12px;
}
.wpcf7 input[type="text"],
.wpcf7 input[type="email"],
.wpcf7 input[type="tel"],
.wpcf7 textarea,
.wpcf7 select {
	width: 100%;
	padding: 16px 24px;
	border: var(--stroke) solid var(--hairline);
	border-radius: var(--radius);
	background: var(--paper);
	font-family: var(--font-body);
	font-size: 15px;
	line-height: 1.7;
	color: var(--ink);
}
.wpcf7 ::placeholder { color: var(--muted); }
/* Heights come from the Input component rather than from the content, because
   Figma draws its 1.5px stroke inside the frame and a textarea would otherwise
   size itself off CF7's default rows attribute. Both are border-box. */
.wpcf7 input[type="text"],
.wpcf7 input[type="email"],
.wpcf7 input[type="tel"] { height: 58px; }
/* Block, or the inline baseline leaves a descender gap under the box. */
.wpcf7 textarea { display: block; height: 140px; min-height: 140px; resize: vertical; }
.wpcf7 input:focus, .wpcf7 textarea:focus, .wpcf7 select:focus {
	border-color: var(--deep);
	border-width: var(--focus);
	outline: none;
}
.wpcf7 .wpcf7-not-valid { border-color: var(--primary); }
.wpcf7 .wpcf7-not-valid-tip { font-size: 13px; line-height: 1.55; color: var(--primary); margin-top: 8px; }
.wpcf7 .wpcf7-response-output { margin: 24px 0 0; padding: 16px 24px; border: var(--stroke) solid var(--hairline); border-radius: var(--radius); font-size: 15px; }
.wpcf7 p { margin: 0 0 24px; }
/* The last field's bottom margin would stack with the note's top margin and
   double the 24px the design puts under the button. */
.wpcf7 form > p:last-of-type { margin-bottom: 0; }
.wpcf7 input[type="submit"] {
	background: var(--primary);
	color: var(--paper);
	height: 56px;
	padding: 0 32px;
	border: 0;
	border-radius: var(--radius);
	font-family: var(--font-body);
	font-size: 16px;
	font-weight: 700;
	cursor: pointer;
}
.wpcf7 input[type="submit"]:hover { background: var(--primary-light); }

/* CF7 6.1 wraps its hidden inputs in a fieldset, which draws the browser's
   default border. Nothing inside it is visible, so nor is the box. */
.wpcf7 .hidden-fields-container { display: none; }

/* Bordered and spaced, so it cannot sit there empty waiting for a message. */
.wpcf7 .wpcf7-response-output[aria-hidden="true"] { display: none; }

.contact__note a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }

/* Confirmation ------------------------------------------------------------
   Centred column, 24 gap. The wrap is the flex container, so it needs the
   same * + * reset the other rows that own their wrap do. */
.confirmation__inner {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 24px;
	text-align: center;
}
.section > .confirmation__inner > * + * { margin-top: 0; }
.confirmation__tick { display: block; width: 72px; height: 72px; }
.confirmation__inner .display-xl { margin: 0; }
/* 640 is the design's measure, and it is narrower than the heading on purpose. */
.confirmation__text { max-width: 640px; font-size: 20px; line-height: 1.6; color: var(--ink-soft); margin: 0; }

@media (max-width: 767px) {
	/* The tick stays 72 on a phone. The mobile frame draws it at the same size as
	   desktop, because it is the whole point of the page. */
}

/* Honeypot. Off-screen rather than display:none, because some bots skip
   anything hidden and it has to look fillable to be worth filling. */
.wpcf7 .afb-hp { position: absolute !important; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Choice pills, for What do you need */
.wpcf7 .afb-pills input[type="radio"] { position: absolute; opacity: 0; }
/* Each pill is wrapped in a <label>, so without this reset it inherits the
   field-label style and the pills render as 12px uppercase blocks. */
.wpcf7 .afb-pills label {
	display: inline;
	margin: 0;
	font-size: inherit;
	font-weight: inherit;
	line-height: inherit;
	letter-spacing: normal;
	text-transform: none;
	color: inherit;
}
/* Gap rather than margins, or the row's trailing 12px lands on top of the
   24px the next field already has. */
.wpcf7 .afb-pills .wpcf7-radio { display: flex; flex-wrap: wrap; gap: 12px; }
.wpcf7 .afb-pills .wpcf7-list-item { margin: 0; display: inline-block; }
.wpcf7 .afb-pills .wpcf7-list-item-label {
	display: inline-flex;
	align-items: center;
	height: 47px;
	padding: 0 24px;
	border: var(--stroke) solid var(--hairline);
	border-radius: var(--radius-pill);
	font-size: 15px;
	font-weight: 600;
	line-height: 1.5;
	cursor: pointer;
}
.wpcf7 .afb-pills input[type="radio"]:hover + .wpcf7-list-item-label { border-color: var(--ink); }
.wpcf7 .afb-pills input[type="radio"]:checked + .wpcf7-list-item-label { background: var(--primary); border-color: var(--primary); color: var(--paper); }

/* Motion ------------------------------------------------------------------
   Everything here is opt in: it only runs when JS has added .js to <html> and
   the visitor has not asked for reduced motion. With JS off, or motion off,
   the page renders exactly as it does now with no hidden content. */

@keyframes afb-rise {
	from { opacity: 0; transform: translateY(12px); }
	to   { opacity: 1; transform: none; }
}

/* Shared by the hero h1 and the card stagger. Keep it at 12: the h1 is the
   LCP element and a bigger travel on it is more noticeable than it is worth. */
@keyframes afb-lift {
	from { transform: translateY(12px); }
	to   { transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
	/* Page load: the banner comes up in sequence, nothing else.

	   Fast, and tightly spaced. It used to run .9s a piece off delays that
	   reached .58s, so the proof card did not finish arriving until nearly a
	   second and a half in. On a wide screen that is ruinous: the logo strip
	   and the section below it are both already past their scroll ranges and
	   render complete on the first frame, so the finished page sat there with
	   a half empty banner filling itself in on top. The sequence now lands
	   inside .8s, which reads as one arrival rather than a page struggling. */
	.js .hero__copy > *,
	.js .hero .proof-card,
	.js .page-header > .wrap > *,
	.js .case-hero__inner > * {
		animation: afb-rise .55s cubic-bezier(.2, .6, .2, 1) both;
	}
	.js .hero__copy > *:nth-child(1),
	.js .page-header > .wrap > *:nth-child(1),
	.js .case-hero__inner > *:nth-child(1) { animation-delay: .04s; }
	.js .hero__copy > *:nth-child(2),
	.js .page-header > .wrap > *:nth-child(2),
	.js .case-hero__inner > *:nth-child(2) { animation-delay: .10s; }
	.js .hero__copy > *:nth-child(3),
	.js .page-header > .wrap > *:nth-child(3),
	.js .case-hero__inner > *:nth-child(3) { animation-delay: .16s; }
	.js .hero__copy > *:nth-child(4),
	.js .page-header > .wrap > *:nth-child(4),
	.js .case-hero__inner > *:nth-child(4) { animation-delay: .22s; }
	.js .hero .proof-card { animation-delay: .22s; }

	/* The h1 is the LCP element. Fading it in from zero delays the largest
	   paint by its whole delay plus duration, which is a bad trade on a site
	   selling SEO. It lifts on transform only, so it is painted immediately. */
	.js .hero__copy > h1,
	.js .hero__copy > .display-xl,
	.js .page-header > .wrap > h1,
	.js .case-hero__inner > h1 {
		animation-name: afb-lift;
		animation-delay: 0s;
		animation-duration: .5s;
		opacity: 1;
	}

	/* Below the fold. The hidden state is set in CSS, gated on the .js class
	   that the inline head script adds, so it applies before first paint. Doing
	   this from app.js meant the page painted, then hid, then faded back. */
	.js .section:not(.hero):not(.page-header) {
		opacity: 0;
		transition: opacity .8s ease;
	}
	.js .section:not(.hero):not(.page-header).is-in {
		opacity: 1;
	}
}

/* Scroll driven ------------------------------------------------------------
   Where the browser supports scroll timelines, sections track the scroll
   position instead of playing a fixed animation on a trigger, so they move
   with your finger. This REPLACES the transition above rather than stacking
   on it, and browsers without support keep the version above. */

@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		/* One universal fade for every section.

		   The range is a fixed LENGTH, not a percentage. Percentages of either
		   entry or cover scale with the element's own height, which meant a
		   168px logo strip and a 660px card section faded over completely
		   different distances: the short one was fine, the tall one finished
		   while it was still below the fold and looked like it never faded.

		   Measured in pixels from first contact with the bottom of the screen,
		   every section behaves identically no matter how tall it is.

		   Fade only: a moving section leaves a band of bare page beside the next. */
		.js .section:not(.hero):not(.page-header) {
			opacity: 1;
			transform: none;
			transition: none;
			animation: afb-fade linear both;
			animation-timeline: view();
			animation-range: cover 0px cover 420px;
		}

		/* Parallax. The card drifts slower than the page, which reads as depth
		   without the seasick full-strength version. */
		.js .proof-card,
		.js .card__media img {
			animation: afb-drift linear both;
			animation-timeline: view();
			animation-range: entry 0% exit 100%;
		}

		/* Logo strip.
		   The track takes the scroll-linked nudge and the lists keep their
		   infinite marquee. Two separate elements, so the transforms never
		   fight each other. Only ever drifts left, since drifting right would
		   expose the empty side of the track.

		   NB the timeline is NAMED on the strip rather than using view() on the
		   track. The strip is overflow:hidden, which makes it a scroll
		   container, so a plain view() inside it resolves against the strip and
		   the progress never changes. Naming it on the outer element measures
		   against the page, which is what we want. */
		.js .logo-strip { view-timeline-name: --afb-strip; }

		.js .logo-strip--scroll .logo-strip__track {
			animation: afb-nudge linear both;
			animation-timeline: --afb-strip;
			animation-range: cover 0% cover 100%;
		}

		/* Logos settle just behind the band, so the section lands first and its
		   contents resolve a moment later. Fixed lengths, like everything else. */
		.js .logo-strip__img {
			animation: afb-logo-settle linear both;
			animation-timeline: --afb-strip;
			animation-range: cover 120px cover 540px;
		}
		.js .logo-strip__item:hover .logo-strip__img { animation: none; }

		/* Same trap on cards, which are also overflow:hidden. */
		.js .card { view-timeline-name: --afb-card; }
		.js .card__media img { animation-timeline: --afb-card; }

		/* Cards rise in sequence behind the section.

		   Named off the section rather than view() on the card, because at
		   mobile the grid becomes a horizontal scroller and would capture the
		   timeline itself, freezing this exactly like the logo strip did.

		   Rise only, no fade: the section is already fading, and stacking a
		   second opacity ramp on top makes the cards muddy on the way in. */
		.js .section { view-timeline-name: --afb-section; }

		/* Same reasoning as the section fade: fixed lengths, so the stagger is
		   the same on a three card row as on a nine card one. */
		/* Off the section's timeline, not their own: a timeline on a card would
		   resolve against the horizontal track and sit frozen. */
		/* Its own keyframe at 32px, not the shared 12px afb-lift. afb-lift also
		   carries the hero h1, which is the page's LCP element, so its travel
		   is not free to change.

		   Twelve pixels spread over 360px of scroll worked out at three
		   hundredths of a pixel per pixel scrolled, and a third of it was spent
		   while the card was still under the fold. It was doing something, but
		   nothing you could see. Further travel over a shorter range, starting
		   once the card is actually on screen. */
		.js .card-grid > *,
		.js .carousel__track > * {
			animation: afb-card-rise linear both;
			animation-timeline: --afb-section;
			animation-range: cover 340px cover 560px;
		}
		.js .card-grid > *:nth-child(2),
		.js .carousel__track > *:nth-child(2) { animation-range: cover 400px cover 620px; }
		.js .card-grid > *:nth-child(3),
		.js .carousel__track > *:nth-child(3) { animation-range: cover 460px cover 680px; }
		.js .card-grid > *:nth-child(4),
		.js .carousel__track > *:nth-child(4) { animation-range: cover 520px cover 740px; }

		.js .faq__item {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 180px cover 460px;
		}
		.js .faq__item:nth-child(2) { animation-range: cover 230px cover 510px; }
		.js .faq__item:nth-child(3) { animation-range: cover 280px cover 560px; }
		.js .faq__item:nth-child(4) { animation-range: cover 330px cover 610px; }
		.js .faq__item:nth-child(5) { animation-range: cover 380px cover 660px; }

		.js .points__head > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 60px cover 380px;
		}
		.js .points__head > *:nth-child(2) { animation-range: cover 110px cover 430px; }
		/* Points come in one after another down the stack, which is how you
		   read them. */
		.js .point {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 180px cover 460px;
		}
		.js .point:nth-child(2) { animation-range: cover 240px cover 520px; }
		.js .point:nth-child(3) { animation-range: cover 300px cover 580px; }
		.js .point:nth-child(4) { animation-range: cover 360px cover 640px; }

		.js .text-media__head > *,
		.js .text-media__copy > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 60px cover 380px;
		}
		.js .text-media__copy > *:nth-child(2) { animation-range: cover 110px cover 430px; }
		.js .text-media__copy > *:nth-child(3) { animation-range: cover 160px cover 480px; }
		.js .text-media__copy > *:nth-child(4) { animation-range: cover 210px cover 530px; }
		.js .text-media__image {
			animation: afb-card-rise linear both;
			animation-timeline: --afb-section;
			animation-range: cover 160px cover 480px;
		}

		.js .feature-card,
		.js .work-grid__items > * {
			animation: afb-card-rise linear both;
			animation-timeline: view();
			animation-range: cover 60px cover 360px;
		}

		/* The step rules draw in, left to right, one after another. */
		.js .step::after {
			animation: afb-draw linear both;
			animation-timeline: --afb-section;
			animation-range: cover 300px cover 500px;
		}
		.js .step:nth-child(2)::after { animation-range: cover 360px cover 560px; }
		.js .step:nth-child(3)::after { animation-range: cover 420px cover 620px; }
		.js .step:nth-child(4)::after { animation-range: cover 480px cover 680px; }

		/* The head leads, so the copy is readable before the cards arrive. */
		.js .section__head > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 60px cover 380px;
		}
		.js .section__head > *:nth-child(2) { animation-range: cover 110px cover 430px; }
		.js .section__head > *:nth-child(3) { animation-range: cover 160px cover 480px; }

		/* Case study ------------------------------------------------------
		   The banner arrives on page load with the other page headers above.
		   Everything from here down is the middle and the tail of the story.

		   Every panel below names its own timeline because all three clip
		   their picture, so a view() inside one would resolve against the
		   panel and sit frozen. Same trap as the logo strip. */

		.js .case-hero { view-timeline-name: --afb-case-hero; }
		.js .image-band { view-timeline-name: --afb-band; }
		.js .next-case { view-timeline-name: --afb-next; }

		/* The pictures drift against the page, which is what gives a flat
		   panel some depth. Oversized by more than twice the travel so the
		   gradient behind can never appear at the top or bottom edge. */
		.js .case-hero__img,
		.js .image-band__img,
		.js .next-case__img {
			height: calc(100% + 48px);
			top: -24px;
			animation: afb-drift linear both;
			animation-range: cover 0% cover 100%;
		}
		.js .case-hero__img { animation-timeline: --afb-case-hero; }
		.js .image-band__img { animation-timeline: --afb-band; }
		.js .next-case__img { animation-timeline: --afb-next; }

		/* The image band is a figure, not a section, so the universal band
		   fade never reached it and it was the one row that snapped in. */
		.js .image-band {
			animation: afb-fade linear both;
			animation-timeline: view();
			animation-range: cover 0px cover 420px;
		}

		/* Results. The figures land in sequence, left to right, and the
		   count up starts as each one arrives rather than all four at once. */
		.js .stat {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 180px cover 460px;
		}
		.js .stat:nth-child(2) { animation-range: cover 240px cover 520px; }
		.js .stat:nth-child(3) { animation-range: cover 300px cover 580px; }
		.js .stat:nth-child(4) { animation-range: cover 360px cover 640px; }

		/* At a glance. Five columns, so the stagger is tighter than the
		   figures or the last one is still arriving as you scroll off it. */
		.js .glance__item {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 180px cover 440px;
		}
		.js .glance__item:nth-child(2) { animation-range: cover 220px cover 480px; }
		.js .glance__item:nth-child(3) { animation-range: cover 260px cover 520px; }
		.js .glance__item:nth-child(4) { animation-range: cover 300px cover 560px; }
		.js .glance__item:nth-child(5) { animation-range: cover 340px cover 600px; }

		/* The heading holds the left rail, so it leads and the copy follows,
		   which is the order you read them in. */
		.js .rail__heading {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 60px cover 380px;
		}
		.js .rail__body > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 110px cover 430px;
		}
		.js .rail__body > *:nth-child(2) { animation-range: cover 160px cover 480px; }
		.js .rail__body > *:nth-child(3) { animation-range: cover 210px cover 530px; }
		.js .rail__body > *:nth-child(4) { animation-range: cover 260px cover 580px; }

		/* Before and after rows, read down the table. */
		.js .compare tbody tr {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 220px cover 500px;
		}
		.js .compare tbody tr:nth-child(2) { animation-range: cover 270px cover 550px; }
		.js .compare tbody tr:nth-child(3) { animation-range: cover 320px cover 600px; }
		.js .compare tbody tr:nth-child(4) { animation-range: cover 370px cover 650px; }

		/* Next case study. Shorter ranges than a section: the band is 340px
		   tall, so a section length stagger would still be arriving as the
		   footer came up behind it. */
		.js .next-case__inner > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-next;
			animation-range: cover 60px cover 340px;
		}
		.js .next-case__inner > *:nth-child(2) { animation-range: cover 110px cover 390px; }
		.js .next-case__inner > *:nth-child(3) { animation-range: cover 160px cover 440px; }

		/* The closing band, which every template ends on. Structurally a
		   section head without the class, so it gets the same treatment. */
		.js .section--cta > .wrap > * {
			animation: afb-enter linear both;
			animation-timeline: --afb-section;
			animation-range: cover 60px cover 380px;
		}
		.js .section--cta > .wrap > *:nth-child(2) { animation-range: cover 110px cover 430px; }
		.js .section--cta > .wrap > *:nth-child(3) { animation-range: cover 160px cover 480px; }

		/* Progress line. Full bleed at rest, on the pill's rim when stuck, inset
		   past the corner radius so it does not run out beyond the curve. */
		.js .site-header::after {
			content: "";
			position: absolute;
			left: 0;
			bottom: 0;
			height: 2px;
			width: 100%;
			transform-origin: 0 50%;
			background: var(--primary);
			animation: afb-progress linear both;
			animation-timeline: scroll(root block);
			/* Eased with the pill, or it lands under a shape still moving. */
			transition: left var(--pill-ease), width var(--pill-ease), bottom var(--pill-ease), border-radius var(--pill-ease);
		}
		.js .site-header.is-stuck::after {
			left: calc(var(--pill-side) + 48px);
			width: calc(var(--pill-w) - 96px);
			bottom: var(--pill-inset);
			border-radius: 999px;
		}
		@media (prefers-reduced-motion: reduce) {
			.js .site-header::before, .js .site-header::after { transition: none; }
		}
	}
}

@keyframes afb-enter {
	from { opacity: 0; transform: translateY(24px); }
	to   { opacity: 1; transform: none; }
}

/* Fade only, for anything painting a background edge to edge. */
@keyframes afb-fade {
	from { opacity: 0; }
	to   { opacity: 1; }
}

/* Cards only. Travels further than afb-lift so the move is visible over the
   short range it runs in. Kept separate because afb-lift is shared with the
   hero h1 and that one is the LCP element. */
@keyframes afb-card-rise {
	from { transform: translateY(32px); }
	to   { transform: none; }
}

/* A rule drawing itself in from its left edge. */
@keyframes afb-draw {
	from { transform: scaleX(0); }
	to   { transform: scaleX(1); }
}

@keyframes afb-drift {
	from { transform: translateY(14px); }
	to   { transform: translateY(-14px); }
}

@keyframes afb-nudge {
	from { transform: translateX(0); }
	to   { transform: translateX(-56px); }
}

@keyframes afb-logo-settle {
	from { opacity: .22; filter: grayscale(1) blur(1px); }
	to   { opacity: .55; filter: grayscale(1) blur(0); }
}

@keyframes afb-progress {
	from { transform: scaleX(0); }
	to   { transform: scaleX(1); }
}

/* Failsafe. If app.js never boots, the head script adds this after 2.5s and
   everything is visible again. A broken script must never blank the page. */
.motion-off .section,
.motion-off .hero__copy > *,
.motion-off .hero .proof-card,
.motion-off .case-hero__inner > *,
.motion-off .image-band,
.motion-off .next-case {
	opacity: 1 !important;
	transform: none !important;
	animation: none !important;
}

/* Cursor -------------------------------------------------------------------
   A real custom pointer icon, plus a ring that trails it. image-set gives the
   retina file without doubling the size on standard screens, and every rule
   keeps a native keyword as the last fallback so the pointer can never go
   missing if an image fails. Only on devices with a real pointer. */

@media (hover: hover) and (pointer: fine) {
	body {
		cursor: image-set(
			url("../img/cursor.png") 1x,
			url("../img/cursor@2x.png") 2x
		) 0 0, auto;
	}

	/* The system hand for anything clickable. A colour change is not a signal:
	   the shape is what says clickable. */
	a, button, summary, label,
	.btn, .link-arrow,
	[role="button"],
	input[type="submit"],
	input[type="button"] {
		cursor: pointer;
	}

	/* White arrow on the dark bands, or it disappears into them. Links inside
	   them are deliberately left out, so they keep the hand like every other
	   link on the site.
	 *
	 * .next-case is NOT in this list, even though it is a dark band, because it
	 * is the only one that is itself an anchor. A class beats the bare `a`
	 * selector above, so putting it here handed the whole clickable band a
	 * decorative arrow and no pointer. */
	.site-header, .site-footer, .section--cta, .section--deep, .case-hero {
		cursor: image-set(
			url("../img/cursor-light.png") 1x,
			url("../img/cursor-light@2x.png") 2x
		) 0 0, auto;
	}

	input[type="text"], input[type="email"], input[type="tel"], textarea, select {
		cursor: text;
	}

	/* The grab hand wins over the custom arrow on a draggable row. The `.x a`
	   selectors beat the plain a rule above. */
	.carousel__track.is-draggable,
	.carousel__track.is-draggable a,
	.card-grid.is-draggable,
	.card-grid.is-draggable a,
	.filters.is-draggable,
	.filters.is-draggable .pill { cursor: grab; }
	.carousel__track.is-dragging,
	.carousel__track.is-dragging a,
	.card-grid.is-dragging,
	.card-grid.is-dragging a,
	.filters.is-dragging,
	.filters.is-dragging .pill { cursor: grabbing; }
}

/* Header ------------------------------------------------------------------ */

/* Full bleed at rest, a floating pill once scrolled. The shape is the ::before
   box's own inset and radius, not a clip-path. A clip-path carried both insets
   as unresolved calc() trees and Chrome intermittently refused to interpolate
   them, snapping the pill to its final shape instead of morphing. top/right/
   bottom/left, border-radius and box-shadow are all plain interpolable values,
   and nothing is clipped now so a box-shadow works where a drop-shadow was
   needed before. The header's own box never resizes: it is sticky, so that
   would reflow the page. */
.site-header {
	--pill-inset: 10px;
	--pill-w: min(100% - 32px, calc(var(--content) + var(--gutter) * 2));
	--pill-side: calc((100% - var(--pill-w)) / 2);

	--pill-ease: .3s cubic-bezier(.45, .05, .25, 1);
	color: var(--paper);
	position: sticky;
	top: 0;
	z-index: 50;
}
.site-header::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	background: var(--deep);
	border-radius: 0;
	box-shadow: 0 0 0 rgba(0, 0, 0, 0);
	transition: top var(--pill-ease), right var(--pill-ease), bottom var(--pill-ease),
		left var(--pill-ease), border-radius var(--pill-ease), box-shadow var(--pill-ease);
}
.site-header__inner { position: relative; z-index: 1; }

/* The head script's pre-paint guess, so a scrolled refresh paints the pill. */
.site-header.is-stuck::before,
.afb-stuck .site-header::before {
	inset: var(--pill-inset) var(--pill-side);
	border-radius: 999px;
	box-shadow: 0 10px 22px rgba(0, 0, 0, .26);
}
/* Nothing eases on the first paint. */
.afb-preload .site-header::before,
.afb-preload .site-header::after { transition: none; }

/* Full bleed with the menu open, or a strip of page shows under the pill. */
body.nav-open .site-header::before {
	inset: 0;
	border-radius: 0;
	box-shadow: 0 0 0 rgba(0, 0, 0, 0);
}

/* No progress bar behind an open menu. */
body.nav-open .site-header::after { display: none; }

.site-nav__cta { display: none; }

/* The admin bar is fixed at the top for logged in users, so the sticky header
   has to start below it or it slides underneath. WordPress makes the bar 32px
   above 782, 46px between 601 and 782, and stops fixing it below 600, where
   it scrolls away and the header goes back to the top. */
body.admin-bar .site-header { top: 32px; }
@media (max-width: 782px) {
	body.admin-bar .site-header { top: 46px; }
}
@media (max-width: 600px) {
	body.admin-bar .site-header { top: 0; }
}
.site-header__inner {
	max-width: calc(var(--content) + (var(--gutter) * 2)); margin-inline: auto; padding: 24px var(--gutter);
	display: flex; align-items: center; justify-content: space-between; gap: 40px;
}
.site-header__logo { display: inline-flex; }
.site-logo__img, .site-header__logo img, .site-footer__logo img { width: 96px; height: auto; display: block; }
/* inline-flex, or the anchor is a block and its hit area runs the full 356px of
   the footer column, catching clicks nowhere near the icon. */
.site-footer__social { display: inline-flex; padding: 4px; margin: -4px; }
.site-nav__list { display: flex; gap: 40px; list-style: none; margin: 0; padding: 0; }

/* Dropdown. Drawn as Nav / Dropdown, option B, on the Figma Components page.
   Each row is Card / Service compressed to one line, so the panel answers which
   of the three a visitor wants and roughly what it costs before they have
   clicked anything.

   The panel is deep on a deep bar, not paper. A white card on a white and blush
   page separates itself only by its shadow, which is the shadow doing the
   design's job; and the same white card is the one white thing on the phone
   menu's dark screen, where it stops reading as a menu and starts reading as a
   dialog. Dark is one design at both widths rather than two. */
.site-nav__list .menu-item-has-children { position: relative; }
.site-nav__list .sub-menu {
	list-style: none;
	background: var(--deep);
	border-radius: var(--radius);
}
.site-nav__list .sub-menu a {
	display: block;
	padding: 14px 16px;
	border-radius: 10px;
	color: var(--paper);
	font-size: 15px;
	line-height: 1.5;
	transition: background-color .15s ease;
}
.nav-sub__top { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.nav-sub__name { font-weight: 600; }
/* flex: none, or a long service name squeezes the price onto two lines. */
.nav-sub__price { flex: none; font-size: 13px; line-height: 1.55; color: var(--accent-warm); }
.nav-sub__summary { display: block; margin-top: 2px; font-size: 13px; line-height: 1.55; color: var(--on-dark); }

/* Two states, two signals. Hover and focus take a fill, the current page takes
   the warm name and no fill at all. Both were the same fill, and since the rows
   abut with no gap between them, the page you are on and the row under the
   pointer merged into one tall block with a seam through it rather than reading
   as two different things. Only one row is ever plated now. The fill differs by
   width and is set in each block below, because the panel is darker than its
   surroundings on one and lighter on the other.

   It carried a 3px primary rule down the left as well, the top rule of .step
   turned on its side, but red on deep is the one place the brand colour fails,
   so the mark takes the warm tint the deep sections already use for the same
   reason. */
.site-nav__list .sub-menu .current-menu-item > a { color: var(--accent-warm); }
.site-nav__list .sub-menu .link-arrow { color: var(--accent-warm); }
/* The row already answers the hover with a fill and a rule; the arrow's own
   underline on top of that is a third signal for one event. */
.site-nav__list .sub-menu .link-arrow:hover { text-decoration: none; }

/* The chevron. Without one, nothing on the bar says a panel is there at all, so
   the three service pages get found by accident or not at all. Same shape at
   both widths, but drawn on the link where hover opens the panel, and on a real
   button where only a tap can. */
.site-nav__list .site-nav__chev {
	display: block;
	width: 7px; height: 7px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	transition: transform .2s ease;
}
.site-nav__sub-toggle {
	display: none;
	background: none; border: 0; margin: 0; padding: 12px;
	color: inherit; cursor: pointer;
}

/* ---- Wide: the panel floats, and hover or focus opens it ---- */
@media (min-width: 1024px) {
	/* Anchored to the left edge of its parent, never centred under it. Centring
	   needed a translateX(-50%) that then had to be cancelled again in the
	   wrapped layout, and those two rules disagreeing is what once threw the
	   panel half its own width off the left of a phone. */
	.site-nav__list .sub-menu {
		position: absolute;
		top: 100%;
		left: 0;
		width: 400px;
		max-width: calc(100vw - 32px);
		margin: 0;
		padding: 8px;
		border: 1px solid var(--hairline-dark);
		box-shadow: 0 2px 6px rgb(28 16 13 / .12), 0 18px 40px rgb(28 16 13 / .28);
		opacity: 0;
		visibility: hidden;
		transform: translateY(-4px);
		transition: opacity .18s ease, transform .18s ease, visibility .18s;
		z-index: 20;
	}
	/* Bridges the gap under the parent, or the panel closes as the pointer
	   crosses it. */
	.site-nav__list .sub-menu::before { content: ""; position: absolute; left: 0; right: 0; bottom: 100%; height: 20px; }
	.site-nav__list .menu-item-has-children:hover > .sub-menu,
	.site-nav__list .menu-item-has-children:focus-within > .sub-menu {
		opacity: 1;
		visibility: visible;
		transform: translateY(12px);
	}
	/* The panel is the bar's own colour here, so the marked row lifts. */
	.site-nav__list .sub-menu a:hover,
	.site-nav__list .sub-menu a:focus-visible { background: var(--hairline-dark); }

	/* The hub link back to the parent page. Divider inset to the row padding via
	   a pseudo-element: a border on the li would run the full width of the panel
	   and a margin would push the row itself in with it. */
	.site-nav__list .sub-menu .nav-sub__hub { position: relative; margin-top: 9px; }
	.site-nav__list .sub-menu .nav-sub__hub::before {
		content: "";
		position: absolute; top: -5px; left: 16px; right: 16px; height: 1px;
		background: var(--hairline-dark);
	}

	/* Hover already opens the panel here, so the chevron belongs on the link
	   itself. A button would be a second tab stop that duplicates what focus on
	   the link does anyway. */
	.site-nav__list .menu-item-has-children > a { display: inline-flex; align-items: center; gap: 8px; }
	.site-nav__list .menu-item-has-children > a::after {
		content: "";
		width: 7px; height: 7px;
		border-right: 1.5px solid currentColor;
		border-bottom: 1.5px solid currentColor;
		transform: translateY(-2px) rotate(45deg);
		transition: transform .2s ease;
	}
	.site-nav__list .menu-item-has-children:hover > a::after,
	.site-nav__list .menu-item-has-children:focus-within > a::after {
		transform: translateY(1px) rotate(-135deg);
	}
}

/* ---- Wrapped: the panel is a block inside the parent's row, opened by the
   chevron button. Lifted off the bar with hairline-dark rather than indented
   behind a rule: an indent alone reads as a stray link once the dividers
   between the top level items are the strongest line on the screen, which is
   what made the first version of this look like a mistake. ---- */
@media (max-width: 1023px) {
	.site-nav__list .menu-item-has-children { display: flex; flex-wrap: wrap; align-items: center; }
	/* The link keeps its own tap straight through to the hub page. Only the
	   chevron opens the panel, so nothing is taken away to add it. */
	.site-nav__list .menu-item-has-children > a { flex: 1 0 0; min-width: 0; }
	.site-nav__list .site-nav__sub-toggle { display: block; }
	.site-nav__list .is-open .site-nav__chev { transform: translateY(1px) rotate(-135deg); }

	.site-nav__list .sub-menu {
		flex: 0 0 100%;
		background: var(--hairline-dark);
		height: 0;
		visibility: hidden;
		overflow: hidden;
		margin: 0;
		padding: 0 4px;
		transition: height .25s ease, margin .25s ease, padding .25s ease, visibility .25s;
	}
	/* interpolate-size at :root is what lets height animate to and from auto.
	   visibility, not just height, or the collapsed links stay in the tab order
	   and the keyboard walks into a panel nobody can see. */
	.site-nav__list .is-open > .sub-menu {
		height: auto;
		visibility: visible;
		margin: 4px 0 8px;
		padding: 4px;
	}
	.site-nav__list .sub-menu a { padding: 12px 16px; font-size: 16px; }
	/* The panel is lighter than the bar here, so the marked row recesses to the
	   bar's own colour instead. The warm name is the same either way. */
	.site-nav__list .sub-menu a:hover,
	.site-nav__list .sub-menu a:focus-visible { background: var(--deep); }
	/* No room for a third line on a phone, and the price is the half worth
	   keeping. The hub link goes too: the parent is a plain tappable link to the
	   same page sitting directly above it. */
	.nav-sub__summary { display: none; }
	.site-nav__list .sub-menu .nav-sub__hub { display: none; }
}

@media (prefers-reduced-motion: reduce) {
	.site-nav__list .sub-menu,
	.site-nav__list .site-nav__chev,
	.site-nav__list .menu-item-has-children > a::after { transition: none; }
}
/* Nav state is brightness only. Links rest at on-dark grey; hover and the
   current page come up to white. No colour shift, no underline, no movement.

   Scoped to __list, not to every anchor in the nav. The mobile Book a call
   lives inside .site-nav, and `.site-nav a` out-specifies `.btn--primary`,
   which was painting the button label grey on the red fill. */
.site-nav__list a {
	text-decoration: none;
	font-size: 16px;
	color: var(--on-dark);
	transition: color .15s ease;
}
/* Scoped to the top level with `>`. Unscoped this also caught the dropdown,
   where white on the paper panel is invisible: the current page's item
   disappeared, and so did any item under the pointer. */
/* li:hover, not a:hover. The panel's links are siblings of the parent link, not
   its descendants, so with a:hover the parent dropped back to grey the moment
   the pointer crossed into its own open panel. focus-within does the same for
   the keyboard, and is scoped to parents so a plain item is not lit by a click
   that :focus-visible would have ignored. */
.site-nav__list > li:hover > a,
.site-nav__list > li > a:focus-visible,
.site-nav__list > .menu-item-has-children:focus-within > a,
.site-nav__list > .current-menu-item > a,
.site-nav__list > .current_page_parent > a,
.site-nav__list > .current-menu-ancestor > a {
	color: var(--paper);
}
.site-header__toggle { display: none; background: none; border: 0; color: inherit; padding: 8px; cursor: pointer; }
.site-header__burger { position: relative; display: block; width: 22px; height: 2px; background: currentColor; transition: background-color .15s ease; }
.site-header__burger::before,
.site-header__burger::after {
	content: ""; position: absolute; left: 0;
	width: 22px; height: 2px; background: currentColor;
	transition: transform .15s ease;
}
.site-header__burger::before { top: -7px; }
.site-header__burger::after { top: 7px; }

/* Open, the three bars become the close cross the Figma component shows. */
.site-header__toggle[aria-expanded="true"] .site-header__burger { background: transparent; }
.site-header__toggle[aria-expanded="true"] .site-header__burger::before { transform: translateY(7px) rotate(45deg); }
.site-header__toggle[aria-expanded="true"] .site-header__burger::after { transform: translateY(-7px) rotate(-45deg); }

@media (prefers-reduced-motion: reduce) {
	.site-header__burger, .site-header__burger::before, .site-header__burger::after, .btn, .site-nav a,
	.next-case__arrow { transition: none; }
	.next-case:hover .next-case__arrow { transform: none; }
}

/* Footer ------------------------------------------------------------------ */

.site-footer { background: var(--deep); color: var(--on-dark); padding-block: 80px 40px; }
.site-footer a { color: inherit; text-decoration: none; }
.site-footer a:hover { text-decoration: underline; }
.site-footer__inner {
	max-width: calc(var(--content) + (var(--gutter) * 2)); margin-inline: auto; padding-inline: var(--gutter);
	/* Ratios, not fixed track widths. 1.58fr against 1fr resolves to exactly
	   the 348 and 220 the Figma footer specifies once the page is wide enough,
	   but fixed pixels could not shrink, so anything between 1200 and 1439,
	   where the gutter is still 96, pushed the last column 159px past the
	   viewport and gave the whole page a horizontal scrollbar. */
	display: grid; grid-template-columns: 1.58fr repeat(3, 1fr); gap: clamp(40px, 5vw, 80px);
}
.site-footer__brand { display: flex; flex-direction: column; gap: 16px; }

.site-footer__tagline, .site-footer__place { font-size: 15px; line-height: 1.7; margin: 0; }
.site-footer__contact { font-size: 15px; font-weight: 600; color: var(--paper); margin: 0; }
.site-footer__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; font-size: 15px; }
.site-footer__col { display: flex; flex-direction: column; gap: 20px; }
.site-footer__legal {
	max-width: calc(var(--content) + (var(--gutter) * 2)); margin: 48px auto 0; padding: 32px var(--gutter) 0;
	border-top: 1px solid var(--hairline-dark);
	display: flex; justify-content: space-between; align-items: center; gap: 24px;
	font-size: 13px; line-height: 1.55;
}
.site-footer__legal p { margin: 0; }
.site-footer__legal-list { list-style: none; margin: 0; padding: 0; display: flex; gap: 16px; }

/* Tablet, 768 to 1199 ------------------------------------------------------ */

@media (max-width: 1199px) {
	:root { --gutter: 48px; --section: 80px; }
	.display-xl, h1 { font-size: 48px; }
	/* A three card row stays three across down to 1024, where the cards come
	   out 288 wide, the same as the mobile card. Dropping it to two columns
	   left the third card stranded on a row of its own with an empty half
	   beside it, which reads as a layout that has broken rather than one that
	   has adapted. A four card row has no such problem: it halves cleanly. */
	.card-grid--4 { grid-template-columns: repeat(2, 1fr); }
	.work-grid__items { grid-template-columns: repeat(2, 1fr); }
	.feature-card { grid-template-columns: 1fr 1fr; }
	.stats__row { grid-template-columns: repeat(2, 1fr); gap: 32px; }
	.glance__row { grid-template-columns: repeat(3, 1fr); gap: 32px; }
	.feature-card__body { padding: 32px; }
	/* 560 fixed leaves the picture too narrow once the wrap comes in. Ratios
	   from here down, keeping the copy the wider of the two. */
	.text-media__inner { grid-template-columns: 1.15fr 1fr; gap: 48px; }
	.points__inner { grid-template-columns: 1fr 1.4fr; gap: 48px; }
	.text-media--flip .text-media__inner { grid-template-columns: 1fr 1.15fr; }
	.contact { grid-template-columns: 1fr 380px; gap: 48px; }
	.site-footer__inner { grid-template-columns: 1fr 1fr; gap: 48px; }
}

@media (max-width: 1023px) {
	/* Stack rather than hold the 380px aside: at 900 the form measured 376
	   against the aside's 380, and by 768 it would be 244. The form is the
	   point of the page, so it takes the full width and the card sits under. */
	.contact { grid-template-columns: 1fr; }

	/* The row has to wrap or flex-basis:100% on the nav just crushes the
	   logo and the button instead of pushing the menu onto its own line. */
	/* Figma's Nav / Mobile is 390 x 73, but the floating pill is not in that node
	   and needs room to breathe. 24 of padding gives the logo 12 all round. */
	.site-header__inner { flex-wrap: wrap; gap: 16px; padding-block: 24px; }

	/* 12 a side, so the gap from the pill's edge to the logo matches the 12
	   above and below it. */
	.site-header {
		--pill-inset: 12px;
		--pill-w: min(100% - 24px, calc(var(--content) + var(--gutter) * 2));
	}
	.site-header__logo { order: 1; flex-shrink: 0; margin-right: auto; }
	.site-header__logo .site-logo__img { width: 71px; }
	.site-header__toggle { display: block; order: 2; flex-shrink: 0; }
	/* Full height overlay under the sticky bar. The CTA lives inside the panel
	   so the whole menu is one fixed element. */
	.site-nav {
		position: fixed;
		top: var(--header-h, 73px);
		left: 0; right: 0; bottom: 0;
		display: block;
		background: var(--deep);
		padding: 8px var(--gutter) 40px;
		overflow-y: auto;
		overscroll-behavior: contain;
		opacity: 0;
		visibility: hidden;
		transition: opacity .2s ease, visibility .2s ease;
	}
	.site-nav.is-open { opacity: 1; visibility: visible; }
	.site-nav__list {
		flex-direction: column;
		gap: 0;
		border-top: 1px solid var(--hairline-dark);
	}
	/* Top level only. Unscoped, the rule also ruled off each child, so the
	   dropdown read as six siblings with a ragged left edge instead of one
	   group sitting under its parent. */
	.site-nav__list > li + li { border-top: 1px solid var(--hairline-dark); }
	.site-nav__list a { display: block; padding: 16px 0; font-size: 18px; }

	/* Figma's mobile bar is logo and close only, so the bar CTA drops out and
	   the one inside the panel takes over. */
	.site-header__cta { display: none; }
	.site-nav__cta { display: flex; width: 100%; margin-top: 24px; }

	body.nav-open { overflow: hidden; }

	/* The hero stacks here, not at 767: below 1024 the copy column is too narrow
	   to set a display heading in. */
	.hero__inner--split { grid-template-columns: 1fr; }
	.hero__copy .display-xl { max-width: 22ch; }
	.proof-card { max-width: 480px; }

	/* Stacked, always copy first. */
	.text-media__inner,
	.text-media--flip .text-media__inner { grid-template-columns: 1fr; gap: 32px; }
	.text-media--flip .text-media__copy { order: 0; }
	.text-media--flip .text-media__image { order: 0; }
	.text-media__image { height: 280px; }
	.feature-card { grid-template-columns: 1fr; }
	.feature-card__body { padding: 24px; gap: 12px; }
	.feature-card__media { min-height: 200px; }
	/* Figma 39:480 stacks the author band, still left aligned, photograph the
	   same 96 and 16 above the name. */
	.post-author__inner { flex-direction: column; align-items: flex-start; gap: 16px; }
	.case-hero { min-height: 420px; }
	.case-hero__inner { padding-block: 64px; }
	.glance__row { grid-template-columns: repeat(2, 1fr); }
	.rail__inner { grid-template-columns: 1fr; gap: 24px; }
	.rail__heading { font-size: 28px; line-height: 1.15; }
	.rail__body { font-size: 18px; color: var(--ink-soft); }
	.case-hero__deck { font-size: 18px; }
	.next-case { min-height: 280px; }
	.next-case__inner { padding-block: 64px; }
	.image-band--lg { min-height: 380px; }
	.image-band--md { min-height: 300px; }
	.points__inner { grid-template-columns: 1fr; gap: 32px; }
	/* The stacked variant makes its columns on the list, not on __inner, so the
	   rule above never reached it and four points stayed two across on a phone
	   at 170px each. Same single column, and the row-gap goes back to nothing so
	   the rules between them do the separating again. */
	.points--stacked .points__list { grid-template-columns: 1fr; row-gap: 0; }
	.points--stacked .point:nth-child(2) { padding-top: 24px; border-top: 1px solid var(--hairline); }
	.section--deep .points--stacked .point:nth-child(2) { border-top-color: var(--hairline-dark); }
	/* Stacked, the points would run the full wrap width, well past a readable
	   measure. The heading is display type and is fine full width. */
	.points__list { max-width: 620px; }
	.faq__q-text { font-size: 20px; }
	.faq__toggle { width: 32px; height: 32px; }

	/* Two across, 54px sliver. The gap here is 24, hence 39 not 35. */
	.work-card { flex: 0 0 calc(50% - 39px); }
	.work-card__media { height: 180px; }
	.quote { flex: 0 0 calc(50% - 39px); padding: 32px; }
	.carousel__head { flex-direction: column; align-items: flex-start; gap: 24px; }
	.carousel__foot { margin-top: 32px; }
}

@media (max-width: 767px) {
	/* Figma's mobile band has no arrows. There is nothing to hover with, and
	   the dots plus the swipe cover it. */
	.carousel__controls { display: none; }
}

/* Mobile, up to 767 -------------------------------------------------------- */

@media (max-width: 767px) {
	:root { --gutter: 24px; --section: 64px; }
	/* Figma's mobile bands use 32 where desktop uses 48. */
	.section__head, .carousel__head { margin-bottom: 32px; }
	/* The Figma mobile hero stacks its kicker, headline, deck, buttons and
	   proof card at 24 apart, where desktop uses 16. */
	.hero__copy { gap: 24px; }
	.hero__actions { margin-top: 0; }
	/* The stack opens to 24 but the tag keeps its 16, like every other section. */
	.hero__copy > .label { margin-bottom: -8px; }
	.display-xl, h1 { font-size: 38px; }
	.display-l, h2 { font-size: 28px; }
	.display-m, h3 { font-size: 24px; }
	.display-s, h4 { font-size: 20px; }
	/* Every mobile frame in the file sets body copy at 15 and Display/S titles at
	   20. Both rules already existed here; the blocks below never saw them, for
	   one of two reasons, and it is worth knowing which is which.

	   Either the block set its own size inside its own rule, so the scale never
	   reached it at any width, or a two class selector elsewhere out-specified
	   the single class rule here, which is what kept .section__intro at 18 on
	   every page that puts one inside a .section__head. Listed out explicitly
	   rather than fixed by raising specificity, so the set stays auditable.

	   .rail__body is deliberately absent: the case study frame really does set
	   18 on a phone, and it is the only band in the file that does. */
	.section__intro, .prose,
	.section__head .section__intro,
	.carousel__head .section__intro,
	.text-media__body > *,
	.section--cta__text,
	.proof-callout__text,
	.proof-card__text,
	.confirmation__text,
	.tier__from,
	.faq__a p,
	.list-columns__list li,
	.list-columns__prose > *,
	.card-grid__footnote,
	.not-found__text,
	.not-found__footnote,
	.post-row__excerpt { font-size: 15px; }

	/* The article stacks: 680 of prose beside a 320 rail has nowhere to go on a
	   phone, and the rail stops sticking because there is nothing beside it to
	   stick against. */
	.post-article__inner { grid-template-columns: 1fr; gap: 40px; }
	.post-rail { position: static; }
	.legal__inner { grid-template-columns: 1fr; gap: 32px; }
	/* Collapsed to a single tappable row here, so the document starts at the top
	   of the screen instead of eighteen links down it. */
	.contents {
		position: static;
		background: var(--blush);
		border-radius: var(--radius);
		padding: 16px 20px;
	}
	.contents__toggle {
		cursor: pointer;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 16px;
	}
	.contents__toggle::after {
		content: "";
		width: 8px; height: 8px;
		border-right: 2px solid var(--primary);
		border-bottom: 2px solid var(--primary);
		transform: translateY(-2px) rotate(45deg);
		transition: transform .2s ease;
		flex: none;
	}
	.contents[open] .contents__toggle::after { transform: translateY(1px) rotate(-135deg); }
	.contents ul { margin-top: 16px; }
	/* Picture over copy, and the fixed 160 goes: a full width thumbnail at 160
	   is a letterbox. */
	.post-row { grid-template-columns: 1fr; gap: 16px; }
	.post-row__media img { height: 200px; }
	.post-hero img { height: 220px; }
	.prose--article h2 { font-size: 24px; margin-top: 32px; }
	.prose--article > p:first-child { font-size: 18px; }
	.prose--article blockquote { font-size: 20px; }

	.point__title, .quote__text,
	.step__title, .work-card__title,
	.feature-card__title, .facts-card__title,
	.prose h3 { font-size: 20px; }
	.prose h2 { font-size: 24px; margin-top: 32px; }

	.card__title { font-size: 20px; }
	.contact { grid-template-columns: 1fr; gap: 48px; }
	.proof-card__figure { font-size: 40px; }
	.site-footer__inner { grid-template-columns: 1fr; gap: 32px; }
	/* Footer / Mobile steps its padding down to 48 and 32, and the legal rule to
	   24. The build carried the desktop 80, 40 and 32 straight through, which is
	   why the mobile footer measured 84 over the design on every page. The
	   header does the opposite deliberately, see the pill note above. */
	/* A 64px column gap pushes the wider wordmarks onto rows of their own. */
	.logo-strip:not(.logo-strip--scroll) .logo-strip__list { gap: 24px 32px; }

	.site-footer { padding-block: 48px 32px; }
	.site-footer__legal { margin-top: 32px; padding-top: 24px; }
	.site-footer__legal { flex-direction: column; align-items: flex-start; gap: 12px; }
	.btn, .wpcf7 input[type="submit"] { width: 100%; }
}

/* Card rows become a swipeable slider, per the Figma mobile band and the
   Breakpoints rule in the design system.

   Below 1024 every card row becomes a track, whatever its column count. That
   is the one treatment that never strands an odd card on a half empty row,
   and three across at 768 would put the cards at 202px, which is too narrow
   to set body copy in. Two show at tablet, one at phone, and the sliver of
   the next card is 54px in both cases. */
@media (max-width: 1023px) {

	.section:has(> .wrap > .card-grid) > .wrap { padding-inline: 0; }
	.section:has(> .wrap > .card-grid) .section__head { padding-inline: var(--gutter); }

	/* Free scrolling, no snapping. overscroll-behavior stops a swipe that runs
	   out of track triggering the browser's back gesture. */
	.card-grid {
		display: flex;
		grid-template-columns: none;
		gap: 16px;
		overflow-x: auto;
		overscroll-behavior-x: contain;
		padding-inline: max(var(--gutter), calc((100% - var(--content)) / 2));
		/* Room for the card rise, handed back so the dots do not move. */
		padding-bottom: 32px;
		margin-bottom: -32px;
		scrollbar-width: none;
		-webkit-overflow-scrolling: touch;
	}
	.card-grid::-webkit-scrollbar { display: none; }

	/* Two cards across the track, with 54px of the next one showing. */
	/* Sizing applies to any child; padding only to tiles. */
	.card-grid > * { flex: 0 0 calc(50% - 35px); }
	.card-grid > .card { padding: 24px; }

	/* Dots sit under the track, back inside the gutter. They read out roughly
	   where you are rather than marking fixed stops, since there are none. */
	.card-dots { display: flex; }
	.card-dots__dot { width: 8px; height: 8px; }
	.card-dots__dot.is-active { width: 20px; }

	/* A row can opt out of the track and stack full width instead. Same
	   specificity as the rules above, so it has to sit after them.

	   The Thank you steps are the case the setting exists for: its mobile frame
	   draws them full width and stacked where Home's draws the same element as a
	   track, and on a confirmation page what happens next is the one thing that
	   must not be behind a swipe. The wrap gives up its gutter for a track, so a
	   stacked row takes it back on itself. */
	.card-grid--stacked {
		display: grid;
		grid-template-columns: 1fr;
		gap: 24px;
		overflow-x: visible;
		padding-inline: var(--gutter);
		padding-bottom: 0;
		margin-bottom: 0;
	}
	.card-grid--stacked > * { flex: none; }
	.card-grid--stacked + .card-dots { display: none; }
}

@media (max-width: 600px) {
	/* One across, same 54px sliver. */
	.card-grid > * { flex: 0 0 calc(100% - 54px); }
	.work-card { flex: 0 0 calc(100% - 54px); }
	.quote { flex: 0 0 calc(100% - 54px); padding: 24px; }
	.work-grid__items { grid-template-columns: 1fr; }
	.work-grid__items .work-card__media { height: 200px; }
	/* One column, and the featured card has already stacked to image over copy,
	   so it reads as another card in the same stack. Figma's mobile portfolio
	   (35:284) drops the featured card entirely and runs every row at 24, so the
	   card keeping a 40 gap under it was the only thing breaking that rhythm.
	   The 32 above is Figma's own filters to rows gap on that frame. */
	.work-grid > .wrap > .feature-card,
	.work-grid > .wrap > .work-grid__items { margin-top: 32px; }
	.work-grid > .wrap > .feature-card + .work-grid__items { margin-top: 24px; }
	/* A filter hides the featured card but the sibling selector still matches
	   it, so the 24 would end up between the pills and the grid. */
	.work-grid > .wrap > .feature-card[hidden] + .work-grid__items { margin-top: 32px; }
	.stats__row { grid-template-columns: 1fr 1fr; gap: 16px; }
	.stat__value { font-size: 28px; line-height: 1.15; }
	.glance__row { grid-template-columns: 1fr; gap: 16px; }
	.rail__inner { gap: 16px; }
	.rail__body > * + * { margin-top: 16px; }
	.image-band--md, .image-band--lg { min-height: 240px; }
	.case-hero__deck { font-size: 16px; }
	.next-case { min-height: 200px; }
	/* Smaller ring on a phone, where the band is 200 tall and the title wraps. */
	.next-case__arrow { width: 44px; height: 44px; font-size: 20px; }
	.next-case__title { gap: 16px; }
	.next-case__deck { display: none; }

	/* Three columns will not carry a metric and two figures at this width, so
	   the before and after join up on one right hand line. */
	.compare thead { display: none; }
	.compare tbody tr { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; column-gap: 8px; border-bottom: 1px solid var(--hairline); }
	.compare tbody th, .compare tbody td { padding: 12px 0; border-bottom: 0; }
	.compare tbody th { font-size: 15px; line-height: 1.7; }
	.compare tbody td { width: auto; text-align: right; color: var(--primary); }
	.compare tbody td:first-of-type::after { content: "\2192"; display: inline-block; margin-left: 8px; color: var(--muted); }
}

/* Print --------------------------------------------------------------------
   Mostly for case studies, which are the pages anyone actually saves as a PDF
   to send on. */

@media print {

	/* This one is not optional. Sections below the fold sit at opacity 0 until
	   the observer marks them .is-in, and print never scrolls, so without this
	   everything past the first screen prints as blank paper.

	   The universal rule is deliberate. Named selectors missed .stat, which has
	   its own staggered fade, and a first attempt printed two of four figures
	   with the rest invisible. Anything that animates is a candidate, so kill
	   the lot rather than maintain a list that silently rots. */
	*,
	*::before,
	*::after {
		animation: none !important;
		transition: none !important;
	}

	.js .section,
	.js .section:not(.hero):not(.page-header),
	.js .stat,
	.js .card,
	.js .work-card,
	.js .proof-card,
	.js .card__media img,
	[class*="reveal"] {
		opacity: 1 !important;
		transform: none !important;
	}

	/* Screen furniture. Navigation cannot be followed on paper, the closing CTA
	   and the next case band are both invitations to click, and a form prints as
	   a row of empty boxes. */
	.site-header,
	.site-footer,
	.skip-link,
	.next-case,
	.section--cta,
	.carousel__controls,
	.carousel-btn,
	.card-dots,
	.work-grid__more,
	.contents__toggle,
	.filters,
	.wpcf7,
	.contact__form,
	.cf-turnstile,
	iframe {
		display: none !important;
	}

	/* Ink. The dark and tinted bands are the design doing its job on screen and
	   a solid block of toner on paper. */
	.section--deep,
	.section--cta,
	.section--blush,
	.card--blush,
	.card--paper,
	.case-hero,
	.case-hero--image {
		background: #fff !important;
		color: #000 !important;
	}

	.section--deep *,
	.case-hero *,
	.card--blush * {
		color: #000 !important;
		text-shadow: none !important;
	}

	/* The hero scrim exists to hold contrast over a photograph. With the band
	   forced white it is a grey wash over nothing. */
	.case-hero--image::after,
	.case-hero--image::before {
		display: none !important;
	}

	/* The hero photograph is absolutely positioned and cropped to fill a 600px
	   band, so on paper it printed full bleed with the black title sitting
	   unreadably on top of it. Dropping it back into flow puts the picture
	   above the title, which is what a case study wants anyway. */
	.case-hero,
	.case-hero--image {
		min-height: 0 !important;
		height: auto !important;
		display: block !important;
	}

	.case-hero__img,
	.image-band__img {
		position: static !important;
		inset: auto !important;
		width: 100% !important;
		height: auto !important;
		max-height: 70mm;
		object-fit: cover;
		margin-bottom: 8pt;
	}

	.case-hero__inner {
		position: static !important;
	}

	html, body {
		background: #fff !important;
		color: #000 !important;
		font-size: 11pt;
		line-height: 1.5;
	}

	.wrap,
	.wrap--prose {
		max-width: none !important;
		width: auto !important;
		margin: 0 !important;
		padding: 0 !important;
	}

	.section,
	.section--pad,
	.section--pad-sm,
	.section--pad-md,
	.section--pad-lg {
		padding-block: 12pt !important;
	}

	/* A carousel on paper is one visible slide and the rest clipped away, so the
	   track becomes a plain stack and every quote survives. */
	.carousel__track {
		display: block !important;
		transform: none !important;
		overflow: visible !important;
		width: auto !important;
	}

	.carousel__track > * {
		width: auto !important;
		margin-bottom: 10pt;
		break-inside: avoid;
	}

	/* Keep the units of meaning whole. A statistic split from its label, or a
	   table row from its heading, is worse than a short page. */
	.card,
	.stat,
	.facts-card,
	.facts-card__row,
	.work-card,
	.tier,
	.step,
	figure,
	blockquote,
	tr,
	img {
		break-inside: avoid;
		page-break-inside: avoid;
	}

	h1, h2, h3, h4,
	.display-xl, .display-l, .display-m, .display-s {
		break-after: avoid;
		page-break-after: avoid;
		color: #000 !important;
	}

	.display-xl { font-size: 24pt; line-height: 1.1; }
	.display-l  { font-size: 20pt; line-height: 1.15; }
	.display-m  { font-size: 16pt; line-height: 1.25; }
	.display-s  { font-size: 13pt; line-height: 1.3; }

	p, li, blockquote {
		orphans: 3;
		widows: 3;
	}

	img,
	.case-hero__img,
	.work-card__media img,
	.text-media__image img {
		max-width: 100% !important;
		height: auto !important;
	}

	table {
		width: 100% !important;
		border-collapse: collapse;
	}

	th, td {
		border-bottom: 1pt solid #999;
		padding: 4pt 6pt;
		color: #000 !important;
	}

	/* An underlined phrase with no destination is useless once it is on paper.
	   Internal links stay bare because the reader has the site, and the anchor
	   and mailto exclusions stop a footnote appearing for a jump link. */
	a[href^="http"]:not([href*="afbdigitalsolutions.co.uk"])::after {
		content: " (" attr(href) ")";
		font-size: 9pt;
		word-break: break-all;
	}

	a {
		color: #000 !important;
		text-decoration: underline;
	}

	@page {
		margin: 15mm;
	}
}
