﻿/* ==========================================================================
   Header, drawer, footer, and homepage sections.
   ========================================================================== */

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

.od-header {
	position: sticky;
	top: 0;
	z-index: var(--od-z-header);
	background: var(--od-surface);
	border-bottom: 1px solid transparent;
	transition: border-color var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease);
	/* NOTE: deliberately no backdrop-filter. It turns the header into a
	   containing block for position:fixed children, which collapses the
	   mobile drawer to a sliver. */
}

.od-header.is-scrolled {
	border-bottom-color: var(--od-line);
	box-shadow: var(--od-shadow-1);
}

.od-header__inner {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--od-s-4);
	min-height: var(--od-header-h);
}

.od-logo {
	display: inline-flex;
	align-items: center;
	flex: none;
	text-decoration: none;
	color: var(--od-ink);
}

/* Constrain ANY logo image, not just our own markup — get_custom_logo() emits
   .custom-logo with hardcoded width/height attributes, so it needs the same cap
   and an explicit height:auto to keep the aspect ratio. */
.od-logo img {
	/* 56px inside a 76px header leaves 10px of breathing room top and bottom,
	   which is the most the bar can carry without the logo touching its edges. */
	max-height: 56px;
	width: auto;
	height: auto;
	object-fit: contain;
}

.od-logo__text {
	font-family: var(--od-font-head);
	font-size: 1.25rem;
	font-weight: 500;
	letter-spacing: -0.02em;
	color: var(--od-ink);
}

.od-logo__text strong {
	font-weight: var(--od-fw-display);
	color: var(--od-primary);
}

/* Desktop nav is hidden until there is room for it. */
.od-nav {
	display: none;
}

.od-nav__list {
	display: flex;
	align-items: center;
	gap: var(--od-s-5);
}

.od-nav__list > li {
	position: relative;
}

.od-nav__list a {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	padding: var(--od-s-2) 0;
	font-size: var(--od-fs-sm);
	/*
	 * 600, which is the ceiling: Inter is self-hosted at 400-600 only, so
	 * font-weight: 700 here would render no heavier than this while claiming
	 * to be bolder. Paired with --od-ink rather than --od-body, because at
	 * this size weight alone reads as barely different -- the darker colour is
	 * doing as much of the work as the weight.
	 */
	font-weight: 600;
	color: var(--od-ink);
	text-decoration: none;
	transition: color var(--od-dur) var(--od-ease);
}

.od-nav__list a:hover,
.od-nav__list .current-menu-item > a {
	color: var(--od-primary-600);
}

/* A chevron marks the parent as openable before the pointer arrives. */
.od-nav__list .menu-item-has-children > a::after {
	content: "";
	width: 6px;
	height: 6px;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	transition: transform var(--od-dur) var(--od-ease);
}

.od-nav__list .menu-item-has-children:hover > a::after,
.od-nav__list .menu-item-has-children:focus-within > a::after {
	transform: translateY(1px) rotate(-135deg);
}

/* Submenus */
.od-nav__list .sub-menu {
	position: absolute;
	top: 100%;
	left: calc(var(--od-s-4) * -1);
	min-width: 220px;
	padding: var(--od-s-3);
	background: var(--od-surface);
	border: 1px solid var(--od-line);
	border-radius: var(--od-r);
	box-shadow: var(--od-shadow-2);
	opacity: 0;
	visibility: hidden;
	transform: translateY(6px);
	transition: opacity var(--od-dur) var(--od-ease),
		transform var(--od-dur) var(--od-ease),
		visibility var(--od-dur);
}

.od-nav__list > li:hover > .sub-menu,
.od-nav__list > li:focus-within > .sub-menu {
	opacity: 1;
	visibility: visible;
	transform: none;
}

/*
 * The dropdown stays at 500. If the children matched the top level, the open
 * menu would be nine items all shouting at the same volume and "Services"
 * would stop reading as their parent.
 */
.od-nav__list .sub-menu a {
	display: block;
	padding: var(--od-s-2) var(--od-s-3);
	border-radius: var(--od-r-sm);
	font-weight: 500;
	color: var(--od-body);
}

.od-nav__list .sub-menu a:hover {
	background: var(--od-primary-50);
}

.od-header__actions {
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	flex: none;
}

.od-header__cta {
	display: none;
}

/* Burger */
.od-burger {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 48px;
	height: 48px;
	margin-right: -8px;
	border-radius: var(--od-r-sm);
	color: var(--od-ink);
}

.od-burger__bars,
.od-burger__bars::before,
.od-burger__bars::after {
	display: block;
	width: 22px;
	height: 2px;
	background: currentColor;
	border-radius: 2px;
	transition: transform var(--od-dur) var(--od-ease), opacity var(--od-dur) var(--od-ease);
}

.od-burger__bars {
	position: relative;
}

.od-burger__bars::before,
.od-burger__bars::after {
	content: "";
	position: absolute;
	left: 0;
}

.od-burger__bars::before { top: -7px; }
.od-burger__bars::after  { top: 7px; }

.od-burger[aria-expanded="true"] .od-burger__bars {
	background: transparent;
}

.od-burger[aria-expanded="true"] .od-burger__bars::before {
	transform: translateY(7px) rotate(45deg);
}

.od-burger[aria-expanded="true"] .od-burger__bars::after {
	transform: translateY(-7px) rotate(-45deg);
}

@media (min-width: 900px) {
	.od-nav { display: block; }
	.od-header__cta { display: inline-flex; }
	.od-burger { display: none; }
}

/* --- Drawer -------------------------------------------------------------- */

.od-drawer {
	position: fixed;
	top: 0;
	right: 0;
	bottom: 0;
	z-index: var(--od-z-drawer);
	display: flex;
	flex-direction: column;
	width: min(88vw, 360px);
	background: var(--od-surface);
	border-left: 1px solid var(--od-line);
	transform: translateX(100%);
	visibility: hidden;
	/* Closing: hold visibility until the slide-out finishes. */
	transition: transform var(--od-dur) var(--od-ease), visibility 0s linear var(--od-dur);
	overflow-y: auto;
	overscroll-behavior: contain;
}

.od-drawer.is-open {
	transform: none;
	visibility: visible;
	/* Opening: flip visibility IMMEDIATELY. If it waits for the transition,
	   the focus trap tries to focus a still-hidden element and silently
	   does nothing. */
	transition: transform var(--od-dur) var(--od-ease), visibility 0s;
}

.od-drawer__overlay {
	position: fixed;
	inset: 0;
	z-index: var(--od-z-overlay);
	background: rgba(20, 20, 43, 0.5);
}

.od-drawer-open {
	overflow: hidden;
}

.od-drawer__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	min-height: var(--od-header-h);
	padding: 0 var(--od-s-5);
	border-bottom: 1px solid var(--od-line);
}

.od-drawer__title {
	font-family: var(--od-font-body);
	font-size: var(--od-fs-xs);
	font-weight: 600;
	letter-spacing: var(--od-ls-eyebrow);
	text-transform: uppercase;
	color: var(--od-muted);
}

.od-drawer__close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 44px;
	height: 44px;
	margin-right: -10px;
	font-size: 1.75rem;
	line-height: 1;
	color: var(--od-ink);
	border-radius: var(--od-r-sm);
}

.od-drawer__nav {
	padding: var(--od-s-4) var(--od-s-5);
	flex: 1;
}

.od-drawer__list li + li {
	border-top: 1px solid var(--od-line);
}

.od-drawer__list a {
	display: block;
	padding: var(--od-s-4) 0;
	font-family: var(--od-font-head);
	font-size: 1.125rem;
	/* Rubik ships 400-800, so 700 is a real instance here, unlike in the
	   desktop nav where Inter caps at 600. */
	font-weight: 700;
	color: var(--od-ink);
	text-decoration: none;
	letter-spacing: -0.01em;
}

.od-drawer__list a:hover {
	color: var(--od-primary);
}

/*
 * Collapsed until the parent is tapped. main.js toggles .is-open on the <li>
 * via [data-submenu-toggle]; without this rule the toggle ran but every
 * submenu was already visible, so nothing appeared to happen.
 */
.od-drawer__list .sub-menu {
	display: none;
	padding-left: var(--od-s-4);
	padding-bottom: var(--od-s-3);
}

.od-drawer__list li.is-open > .sub-menu {
	display: block;
}

/* A chevron on any parent, rotating when open. */
.od-drawer__list .menu-item-has-children > a::after {
	content: "";
	display: inline-block;
	width: 7px;
	height: 7px;
	margin-left: var(--od-s-2);
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	transition: transform var(--od-dur) var(--od-ease);
}

.od-drawer__list li.is-open > a::after {
	transform: translateY(1px) rotate(-135deg);
}

.od-drawer__list .sub-menu a {
	padding: var(--od-s-2) 0;
	font-family: var(--od-font-body);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	color: var(--od-body);
	border: 0;
}

/* CTA pinned at the bottom, always in thumb reach. */
.od-drawer__foot {
	position: sticky;
	bottom: 0;
	display: flex;
	flex-direction: column;
	gap: var(--od-s-3);
	padding: var(--od-s-5);
	background: var(--od-alt);
	border-top: 1px solid var(--od-line);
	text-align: center;
}

.od-drawer__email {
	font-size: var(--od-fs-sm);
	color: var(--od-body);
	text-decoration: none;
}

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

.od-footer {
	background: var(--od-ink);
	color: var(--od-on-ink-muted);
	padding-top: var(--od-section-y);
	padding-bottom: var(--od-s-6);
}

.od-footer__top {
	display: grid;
	gap: var(--od-s-7);
	grid-template-columns: 1fr;
	/* The rule below needs air above it, or it reads as underlining the last
	   link column rather than separating the legal strip. */
	padding-bottom: var(--od-s-8);
}

@media (min-width: 600px) {
	.od-footer__top {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 900px) {
	.od-footer__top {
		grid-template-columns: 1.6fr repeat(3, minmax(0, 1fr));
		gap: var(--od-s-6);
	}
}

.od-footer__brand {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-4);
	align-items: flex-start;
}

.od-logo--footer img {
	max-height: 104px;
	width: auto;
}

/* The footer mark is the brand's last word on the page, so it can be much
   larger than the header's. Caps the width too — a wide wordmark hitting a
   104px height cap would otherwise overrun the narrow first footer column. */
.od-logo--footer,
.od-logo--footer img {
	max-width: 100%;
}

/*
 * Knocked out to white. brightness(0) flattens every colour to black first, then
 * invert(1) lifts the whole mark to white — one clean silhouette. Plain
 * invert(1) alone would turn the violet globe a sickly yellow-green, which is
 * why the mark used to be parked on a white plate instead.
 *
 * The trade is the violet: a single flat PNG cannot keep a coloured mark and a
 * white wordmark at once. Upload a light-version logo to Orion Settings →
 * Footer logo and this rule stops applying.
 */
.od-logo--knockout img {
	filter: brightness(0) invert(1);
	opacity: 0.92;
	transition: opacity var(--od-dur) var(--od-ease);
}

.od-logo--knockout:hover img {
	opacity: 1;
}

.od-logo--footer .od-logo__text {
	color: var(--od-on-ink);
}

.od-logo--footer .od-logo__text strong {
	color: #B49BFF;
}

.od-footer__about {
	font-size: var(--od-fs-sm);
	line-height: 1.6;
	color: var(--od-on-ink-muted);
	max-width: 42ch;
	margin: 0;
}

.od-footer__socials {
	display: flex;
	flex-wrap: wrap;
	gap: var(--od-s-3);
	margin: 0;
	padding: 0;
	list-style: none;
}

/*
 * Filled discs rather than hollow outlines. On the ink band a 1px border at
 * --od-on-ink-line is nearly invisible, so the icons read as loose glyphs
 * floating under the paragraph instead of a set of controls. A faint white fill
 * gives each one a body, and the inset hairline is what actually defines the
 * edge — a border alone at this contrast does not.
 */
.od-footer__socials a {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 46px;
	height: 46px;
	border: 0;
	border-radius: 50%;
	background: rgba(255, 255, 255, 0.07);
	box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
	color: var(--od-on-ink);
	text-decoration: none;
	transition: transform var(--od-dur) var(--od-ease),
		background-color var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease),
		color var(--od-dur) var(--od-ease);
}

/* The icons ship at assorted intrinsic sizes; pin them so the discs match. */
.od-footer__socials a svg {
	width: 20px;
	height: 20px;
}

.od-footer__socials a:hover,
.od-footer__socials a:focus-visible {
	background: var(--od-primary);
	color: #fff;
	transform: translateY(-3px);
	box-shadow: inset 0 0 0 1px var(--od-primary),
		0 10px 20px rgba(78, 18, 240, 0.38);
}

@media (prefers-reduced-motion: reduce) {
	.od-footer__socials a {
		transition: background-color var(--od-dur) var(--od-ease),
			color var(--od-dur) var(--od-ease);
	}

	.od-footer__socials a:hover,
	.od-footer__socials a:focus-visible {
		transform: none;
	}
}

/*
 * Set as a heading rather than an eyebrow: at --od-fs-xs uppercase these read
 * as labels on the links, which left the footer with no typographic hierarchy
 * at all. The uppercase tracking goes with the size increase — letterspacing is
 * a small-caps device and looks stretched once the type is at h3.
 */
.od-footer__col-title {
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h3);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	line-height: 1.2;
	text-transform: none;
	color: var(--od-on-ink);
	margin: 0 0 var(--od-s-5);
}

/* Aligns the link columns with the top of the brand logo beside them. */
@media (min-width: 900px) {
	.od-footer__col {
		padding-top: var(--od-s-2);
	}
}

.od-footer__contact {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--od-s-4);
}

.od-footer__contact .od-footer__links {
	gap: var(--od-s-2);
}

/* The address is the one contact line that is not a link, so it must not read
   as one — it keeps the muted colour and gets its own leading. */
.od-footer__email {
	word-break: break-word;
}

.od-footer__links {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-3);
}

.od-footer__links a {
	font-size: var(--od-fs-sm);
	color: var(--od-on-ink-muted);
	text-decoration: none;
	transition: color var(--od-dur) var(--od-ease);
}

.od-footer__links a:hover {
	color: var(--od-on-ink);
}

.od-footer__address {
	font-size: var(--od-fs-sm);
	line-height: 1.6;
	color: var(--od-on-ink-muted);
	max-width: 30ch;
}

/*
 * The contact column is a flex column with its own gap, so the title's
 * bottom margin was being ADDED to that gap -- 24px + 16px of empty band
 * above the email address. The other columns still need the margin, so it is
 * zeroed here rather than on .od-footer__col-title.
 */
.od-footer__contact .od-footer__col-title {
	margin-bottom: 0;
}

/* --- NAP lines: icon + text ------------------------------------------- */

.od-footer__nap li {
	display: flex;
	align-items: flex-start;
	gap: var(--od-s-3);
}

/*
 * flex: none, or the glyph is squashed by the long address beside it. The
 * top margin optically centres a 16px icon on the first line of text rather
 * than sitting it on the cap height.
 */
.od-footer__nap-icon {
	flex: none;
	margin-top: 3px;
	color: var(--od-primary-100);
}

.od-footer__nap-icon svg {
	width: 16px;
	height: 16px;
	display: block;
}

/* The address is the one NAP line that is not a link, so the wrapper carries
   the type styles the <a> gets in the other two. */
.od-footer__address span {
	display: block;
}

.od-footer__bottom {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-3);
	padding-top: var(--od-s-5);
	border-top: 1px solid var(--od-on-ink-line);
}

@media (min-width: 600px) {
	.od-footer__bottom {
		flex-direction: row;
		align-items: center;
		justify-content: space-between;
	}
}

.od-footer__copy {
	font-size: var(--od-fs-xs);
	color: var(--od-on-ink-muted);
	margin: 0;
	max-width: none;
}

.od-footer__legal {
	display: flex;
	flex-wrap: wrap;
	gap: var(--od-s-4);
}

.od-footer__legal a {
	font-size: var(--od-fs-xs);
	color: var(--od-on-ink-muted);
	text-decoration: none;
}

.od-footer__legal a:hover {
	color: var(--od-on-ink);
}

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

/*
 * A fixed 80px on top, deliberately off the section-rhythm scale. The hero sits
 * directly beneath a 76px sticky header, so it is the one section whose top
 * spacing is measured against the header rather than against the page rhythm —
 * --section-y-xl stacked with the header and left a dead band above the
 * eyebrow, and --section-y-tight overcorrected. Generous spacing stays at the
 * bottom, where it separates the hero from the credibility strip.
 */
/*
 * Three stacked layers, all CSS, no image request:
 *
 *   1. a warm violet bloom off the top-right, behind the form card
 *   2. a cooler, wider bloom low-left, so the gradient has a direction rather
 *      than being one blob in a corner
 *   3. a faint vertical fade to --od-alt at the very bottom, which hands off to
 *      the ink credibility strip instead of meeting it as a hard white edge
 *
 * Every stop is under 10% alpha. The point is that you should not be able to
 * name the colour — only notice the section is not flat white.
 */
.od-hero {
	position: relative;
	padding-block: 80px var(--od-s-8);
	background:
		radial-gradient(58ch 42ch at 88% -6%, rgba(78, 18, 240, 0.10), transparent 62%),
		radial-gradient(64ch 48ch at 4% 82%, rgba(78, 18, 240, 0.08), transparent 66%),
		linear-gradient(180deg, var(--od-surface) 62%, var(--od-alt) 100%);
	overflow: hidden;
}

/*
 * A single soft violet orb sitting behind the form card. Blurred far past its
 * own edges so it never reads as a shape — it just makes the card look lit from
 * behind rather than pasted onto a flat field.
 */
.od-hero::before {
	content: "";
	position: absolute;
	top: -12%;
	right: -8%;
	width: min(46vw, 620px);
	aspect-ratio: 1;
	border-radius: 50%;
	background: radial-gradient(circle, rgba(122, 74, 255, 0.16), transparent 68%);
	filter: blur(40px);
	pointer-events: none;
	z-index: 0;
}

/*
 * A fine grid over the wash, masked to the top-left so it sits under the copy
 * and fades out well before the form card. Same device as the services band,
 * but weaker — the hero already carries two gradients and the grid is here to
 * give them something to sit on, not to be noticed on its own.
 */
.od-hero::after {
	content: "";
	position: absolute;
	inset: 0;
	background-image:
		repeating-linear-gradient(to right, rgba(20, 20, 43, 0.045) 0 1px, transparent 1px 72px),
		repeating-linear-gradient(to bottom, rgba(20, 20, 43, 0.045) 0 1px, transparent 1px 72px);
	-webkit-mask-image: radial-gradient(70ch 52ch at 12% 8%, #000 4%, transparent 68%);
	mask-image: radial-gradient(70ch 52ch at 12% 8%, #000 4%, transparent 68%);
	pointer-events: none;
	z-index: 0;
}

/* Content above the wash. */
.od-hero > * {
	position: relative;
	z-index: 1;
}

.od-hero__inner {
	display: grid;
	gap: var(--od-s-7);
	grid-template-columns: 1fr;
	align-items: start;
}

/*
 * The matching orb, bottom-left, diagonally opposite the one behind the form.
 * Two blooms on a diagonal read as lighting; one on its own reads as a stray
 * blob in a corner.
 *
 * It hangs off .od-hero__inner rather than .od-hero because the hero's own
 * ::before and ::after are already spent on the top-right orb and the grid.
 * Inner is position:relative with z-index 1, so it forms a stacking context and
 * a z-index of -1 here puts the orb behind the copy while keeping it above the
 * section's background wash.
 */
.od-hero__inner::before {
	content: "";
	position: absolute;
	left: calc(var(--od-gutter) * -1);
	bottom: -22%;
	width: min(52vw, 660px);
	aspect-ratio: 1;
	border-radius: 50%;
	/* Fainter than the top-right one. That orb has the form card to justify it;
	   this one only has to keep the corner from going dead. */
	background: radial-gradient(circle, rgba(122, 74, 255, 0.13), transparent 66%);
	filter: blur(44px);
	pointer-events: none;
	z-index: -1;
}

/*
 * The eyebrow is the page h1, so it now picks up the base heading rules as
 * well as .od-pill. .od-pill already overrides family, size, weight, tracking
 * and colour at class specificity; these are the three properties it does not
 * set, which the h1 rules would otherwise change — a taller line box, a top
 * margin from the heading reset, and pretty wrapping on a one-line badge.
 *
 * Two classes so this cannot lose to anything scoping headings by element.
 */
.od-hero .od-hero__eyebrow {
	line-height: 1.2;
	margin-top: 0;
	text-wrap: nowrap;
}

/* Under 600px the eyebrow is too long for one line, so let it wrap there. */
@media (max-width: 599px) {
	.od-hero .od-hero__eyebrow {
		text-wrap: pretty;
	}
}

/* No character cap — the 50/50 column is now the constraint. Capping as well
   would narrow the headline to less than the space it has. */
.od-hero__title {
	margin-bottom: var(--od-s-5);
}

/*
 * Reversed weight contrast: the lead-in is the lighter of the pair and the
 * violet half is black, so emphasis lands on "Digital Growth" rather than on
 * "Your Partner In". Scoped to the hero — .od-accent-text keeps its default
 * behaviour elsewhere.
 *
 * The lead-in is 600, not 400: at display size 400 read as thin against an
 * 800 accent rather than as a deliberate pairing. Rubik is loaded as a
 * 400–800 variable face, so 600 is a real instance, not a synthesised one.
 */
.od-hero__title {
	font-weight: 600;
}

/* The accent's 700 now comes from .od-accent-text in typography.css, so it
   applies wherever the two-tone device is used rather than only here. */

.od-hero__lead {
	margin-bottom: var(--od-s-5);
}

/*
 * Full-width proof rail under both hero columns. A 2x2 on phones — all four
 * bullets now fit, so nothing has to be hidden at small sizes.
 */
.od-hero__proof {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--od-s-5) var(--od-s-4);
	margin-top: var(--od-s-8);
	padding-top: var(--od-s-6);
	border-top: 1px solid var(--od-line);
}

.od-hero__bullet {
	display: flex;
	align-items: flex-start;
	gap: var(--od-s-3);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	line-height: 1.45;
	color: var(--od-ink);
}

/* These bullets are claims about the business ("since 2014", "owner-led"), so
   they get the evidence colour — set as a filled disc, which reads at a glance
   where a thin stroke on white does not. */
/* The class lands on the <svg> itself, not on a wrapper, so the disc is drawn
   with padding + background on the element rather than around it. */
.od-hero__bullet-icon {
	flex: none;
	box-sizing: border-box;
	width: 26px;
	height: 26px;
	padding: 5px;
	border-radius: 50%;
	background: var(--od-green);
	color: #fff;
	stroke-width: 2.5;
	margin-top: 1px;
}

/*
 * The commitments that close the copy column. One block per sentence: as a
 * single run-on line they were scanned past, and each of these is a separate
 * promise worth reading on its own. Violet, so they read as commitments rather
 * than as more of the green proof bullets below.
 */
/*
 * An even grid, not a wrapping flex row: unequal chip widths broke 2-then-1 and
 * looked accidental. Equal columns make it read as a deliberate set.
 *
 * One emphasis device, not three. The earlier version stacked a tint, a rule
 * and violet text on a small element and shouted. Now the rule carries the
 * violet and the words sit in ink, so they are legible first and branded second.
 */
.od-hero__promise {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-s-4);
	margin-top: var(--od-s-6);
	padding-top: var(--od-s-6);
	border-top: 1px solid var(--od-line);
}

.od-hero__commit {
	padding-left: var(--od-s-4);
	border-left: var(--od-rule) solid var(--od-primary);
	font-size: var(--od-fs-sm);
	font-weight: 600;
	line-height: 1.4;
	color: var(--od-ink);
}

@media (min-width: 600px) {
	.od-hero__promise {
		grid-template-columns: repeat(3, minmax(0, 1fr));
		gap: var(--od-s-5);
	}
}

/*
 * Hero action row. Shown at every breakpoint: not everyone will fill in the
 * form on arrival, and these give them a way to start without one.
 */
.od-hero__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--od-s-3);
	margin-top: var(--od-s-6);
}

/* Below 600px the two buttons stack full width rather than shrinking into two
   cramped half-buttons. */
@media (max-width: 599px) {
	.od-hero__actions .od-btn {
		width: 100%;
	}
}

/* WhatsApp keeps its own brand colour on hover — it is a recognised mark, and
   this is the one place a third colour earns its place. */
.od-hero__whatsapp:hover {
	border-color: #25D366;
	color: #128C4A;
}

.od-hero__actions .od-placeholder-note {
	flex-basis: 100%;
}

.od-hero__form {
	margin-top: var(--od-s-6);
}

@media (min-width: 900px) {
	.od-hero__inner {
		/* Even split. The copy column previously took 1.15fr and left the form
		   card looking like a sidebar; the form is the conversion point and
		   should carry the same weight as the headline. */
		grid-template-columns: 1fr 1fr;
		gap: var(--od-s-8);
		/* Copy stays top-aligned with the form card, not vertically centred
		   against it — centring left a large dead gap above the headline. */
		align-items: start;
	}

	.od-hero__mobile-cta {
		display: none;
	}

	/* One row of four, separated by hairlines rather than gaps so the rail
	   reads as a single continuous band. */
	.od-hero__proof {
		grid-template-columns: repeat(4, minmax(0, 1fr));
		gap: 0;
	}

	.od-hero__bullet {
		padding-inline: var(--od-s-5);
	}

	.od-hero__bullet + .od-hero__bullet {
		border-left: 1px solid var(--od-line);
	}

	.od-hero__bullet:first-child {
		padding-left: 0;
	}

	.od-hero__form {
		margin-top: 0;
	}
}

@media (min-width: 1200px) {
	.od-hero__inner {
		grid-template-columns: minmax(0, 1fr) minmax(480px, 1fr);
	}

	/* Form card sits level with the copy column. It previously carried a
	   negative top margin to break the optical baseline, which is what stopped
	   the two columns aligning at the top. */
	.od-hero__form {
		margin-top: 0;
	}
}

/* --- Results proof frame ------------------------------------------------- */

/* Header runs the full container width here rather than the usual measure cap,
   so the heading and its one-line intro sit centred over the frame. */
.od-results .od-section-header,
.od-results .od-section-header__subtitle {
	max-width: none;
}

/* The violet frame is doing the work here: it says "this is a real screenshot,
   look at it" without cropping or decorating the evidence itself. */
/*
 * Frame proportions follow the reference: a wide, shallow window that crops the
 * dashboard mid-chart the way a screenshot of a real tool is cropped. The
 * aspect ratio does the cropping, so the drawing inside can be any height.
 */
.od-results__frame {
	max-width: var(--od-container-wide);
	margin: 0 auto;
	padding: 8px;
	border: 1px solid var(--od-primary);
	border-radius: var(--od-r-lg);
	background: var(--od-surface);
	box-shadow: var(--od-shadow-3);
	overflow: hidden;
	aspect-ratio: 1200 / 620;
}

/* Metrics folded in from the retired proof section. Empty unless there is
   something genuinely measured to show. */
.od-results__metrics {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--od-s-6) var(--od-gap-grid);
	margin-bottom: var(--od-s-8);
}

@media (min-width: 900px) {
	.od-results__metrics {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.od-results__img {
	display: block;
	width: 100%;
	height: auto;
	border-radius: var(--od-r-sm);
}

/* The drawn dashboard sizes like the screenshot it stands in for. */
.od-dashboard {
	display: block;
	width: 100%;
	height: auto;
	border-radius: var(--od-r-sm);
}

.od-results__note {
	margin-top: var(--od-s-3);
	text-align: center;
}

/* Case grid: one card per client, two per row from 600px and three from 900px,
   matching the reference's results layout. */
.od-results__grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-s-6);
}

@media (min-width: 600px) {
	.od-results__grid {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (min-width: 900px) {
	.od-results__grid {
		grid-template-columns: repeat(3, 1fr);
	}
}

/* Inside a card the frame is squarer and full-bleed within the card width. */
/*
 * The case frame takes its height from the report inside it.
 *
 * The base .od-results__frame pins a 1200/620 ratio and clips with overflow,
 * which suits the drawn dashboard it was written for — that graphic is built to
 * the ratio. A real export is whatever shape the screen it was captured from
 * happened to be, and forcing it into a fixed box cut the summary row off the
 * top of the taller ones. `auto` lets each slide be as tall as its own image.
 */
.od-results__frame--case {
	max-width: none;
	aspect-ratio: auto;
	margin-bottom: var(--od-s-4);
}

.od-results__case-name {
	font-size: var(--od-fs-h4);
	margin-bottom: var(--od-s-3);
}

.od-results__captions--case {
	justify-content: flex-start;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--od-s-2);
	margin-top: 0;
}

.od-results__captions--case .od-results__caption {
	padding-left: 0;
	border-left: 0;
	font-size: var(--od-fs-sm);
}

.od-results__captions {
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-start;
	gap: var(--od-s-3) var(--od-s-6);
	margin-top: var(--od-s-6);
}

.od-results__caption {
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	font-size: var(--od-fs-body);
	font-weight: 500;
	color: var(--od-ink);
}

/* A rule between captions, matching the stat bar's separator treatment. */
.od-results__caption + .od-results__caption {
	padding-left: var(--od-s-6);
	border-left: 1px solid var(--od-line);
}

/* Evidence, so green — the figures beside it are outcomes, not actions. */
.od-results__check {
	flex: none;
	width: 22px;
	height: 22px;
	color: var(--od-green);
	stroke-width: 3;
}

/* --- Credibility bar ----------------------------------------------------- */

/*
 * A ribbon, not four tiles.
 *
 * Boxed stats on a dark band are the single most generic block on an agency
 * homepage, and four of them stacked their own padding on top of the section's
 * so a strip meant to be glanced at ran to nearly 200px of height.
 *
 * This reads as one continuous line instead: figure and caption side by side on
 * a single baseline, items separated by a hairline that fades out at both ends,
 * and the whole thing sharing the section's own background. No card, no border,
 * no inner padding to pay for — roughly half the height, and it looks like a
 * masthead rather than a dashboard.
 */
.od-cred {
	overflow: hidden;
}

/*
 * Something for the glass to refract.
 *
 * backdrop-filter over a flat field is invisible — a blur of nothing looks like
 * nothing. Two soft blooms behind the tile row give the panels colour to pick
 * up and bend, which is what actually sells the material. They sit under the
 * content because .od-section--grain already lifts every child to z-index 1.
 */
.od-cred::before {
	content: "";
	position: absolute;
	inset: 0;
	background:
		radial-gradient(40ch 22ch at 18% 118%, rgba(122, 74, 255, 0.62), transparent 72%),
		radial-gradient(34ch 20ch at 82% -18%, rgba(74, 222, 158, 0.22), transparent 74%);
	pointer-events: none;
}

/*
 * Glass tiles.
 *
 * Two earlier passes tried to do this without surfaces — a single ribbon line,
 * then stacked cells under a hairline — and both read as thin. On a large field
 * of ink there is nothing for the eye to hold onto unless something has an edge.
 * These are panels: a few percent of white, a hairline border, a light-catch
 * along the top edge, and enough padding that the figure has room to be large.
 *
 * No violet rules anywhere in here any more. Four small brand marks repeated
 * across four tiles was decoration standing in for structure; the tiles do the
 * structural job themselves.
 */
/*
 * TWO classes, deliberately.
 *
 * base/typography.css carries `.od-section--ink h2 { color: var(--od-on-ink) }`.
 * That is a class plus an element — specificity (0,1,1) — so a single-class
 * rule here loses to it no matter which file loads last, and the heading kept
 * rendering the off-white token instead of the white set below. Scoping to the
 * section as well takes this to (0,2,0) and it wins.
 */
.od-cred .od-cred__label {
	font-family: var(--od-font-head);
	/* Matches every other section heading on the page. At --od-fs-h3 it was the
	   only h2 set smaller than its peers, which read as a caption over the
	   figures rather than the band's title. */
	font-size: var(--od-fs-h2);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	text-transform: none;
	line-height: 1.15;
	/* Pure white, not the off-white on-ink token. The token is tuned for body
	   copy on a dark band; a heading at this size wants the full contrast. */
	color: #fff;
	text-align: center;
	/* Widened with the type. At 22ch a heading this size broke into four short
	   lines. */
	max-width: 26ch;
	margin: 0 auto var(--od-s-7);
	text-wrap: balance;
}

.od-cred__list {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--od-s-4);
}

@media (min-width: 900px) {
	.od-cred__list {
		grid-template-columns: repeat(4, minmax(0, 1fr));
		gap: var(--od-s-5);
	}
}

.od-cred__item {
	position: relative;
	display: flex;
	flex-direction: column;
	/* Centred: the figure is the object and the caption is its label, so they
	   share one axis instead of hanging off a left edge the tile never shows. */
	align-items: center;
	text-align: center;
	gap: var(--od-s-3);
	padding: var(--od-s-6) var(--od-s-5) var(--od-s-6);
	border-radius: var(--od-r-lg);
	/*
	 * Graded, not flat. Real glass is brighter where the light hits the top
	 * bevel and darker in the body, so a single uniform tint always reads as a
	 * grey box no matter how faint it is.
	 */
	background: linear-gradient(
		158deg,
		rgba(255, 255, 255, 0.14) 0%,
		rgba(255, 255, 255, 0.045) 48%,
		rgba(255, 255, 255, 0.09) 100%
	);
	border: 1px solid rgba(255, 255, 255, 0.16);
	/* The refraction. saturate() alongside the blur is what stops blurred colour
	   going muddy grey as it spreads. */
	-webkit-backdrop-filter: blur(22px) saturate(1.7);
	backdrop-filter: blur(22px) saturate(1.7);
	/* Bevels top AND bottom, plus the outer drop that lifts the panel off the
	   band instead of embedding it in it. */
	box-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.2),
		inset 0 -1px 0 rgba(255, 255, 255, 0.08),
		0 22px 48px rgba(0, 0, 0, 0.32);
	overflow: hidden;
}

/*
 * The light catch. One hairline of brighter white across the top edge, fading
 * out at both ends — this is the whole difference between a translucent panel
 * and a flat rectangle with a border.
 */
.od-cred__item::before {
	content: "";
	position: absolute;
	left: 10%;
	right: 10%;
	top: 0;
	height: 1px;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.7), transparent);
}

/* And the same along the bottom edge, dimmer. A pane catches light on both
   bevels; lighting only the top one makes it read as a card with a highlight
   drawn on rather than as a piece of glass. */
.od-cred__item::after {
	content: "";
	position: absolute;
	left: 18%;
	right: 18%;
	bottom: 0;
	height: 1px;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.34), transparent);
}

.od-cred__value {
	display: flex;
	align-items: baseline;
	justify-content: center;
	gap: 1px;
	margin: 0;
	font-family: var(--od-font-head);
	font-weight: var(--od-fw-display);
	/* The figures are the section. Nearly 50% larger than the ribbon version. */
	font-size: clamp(2.75rem, 1.5rem + 4vw, 4.25rem);
	line-height: 1;
	letter-spacing: var(--od-ls-numeral);
	color: var(--od-on-ink);
	font-variant-numeric: tabular-nums;
}

/*
 * Full size, on the same baseline — part of the figure, not a footnote to it.
 *
 * As a raised 0.36em mark the "+" and the "h" read as superscript annotations,
 * which made "24h" look like a citation rather than a duration. At 1em they are
 * simply the last character of the number.
 *
 * Still green: the credibility strip is entirely evidence, and green-light
 * rather than green because this sits on ink.
 */
.od-cred__suffix {
	font-size: 1em;
	font-weight: var(--od-fw-display);
	color: var(--od-green-light);
	letter-spacing: var(--od-ls-numeral);
}

/*
 * Sentence case, not tiny uppercase. Wide-tracked uppercase under a large
 * numeral is the least readable thing on the page. The numeral is already the
 * emphasis; the caption only has to be legible.
 */
/*
 * Near-white at body size, not muted small print.
 *
 * At --od-fs-sm in --od-on-ink-muted the caption was the quietest thing in a
 * tile built to be looked at, so each panel read as a number with an apology
 * under it. It says what the number MEANS — without it the figure is noise —
 * so it gets full weight and full brightness.
 */
.od-cred__caption {
	margin: 0;
	font-size: var(--od-fs-body);
	font-weight: 600;
	letter-spacing: 0;
	text-transform: none;
	line-height: 1.45;
	color: var(--od-on-ink);
	max-width: 16ch;
	margin-inline: auto;
	text-wrap: balance;
}

/* Quick-nav styles removed — the section is not rendered on the homepage. The
   partial remains on disk for a future inner-page builder; if it returns, its
   card styling comes back with it. */

/* --- Problem ------------------------------------------------------------- */

.od-problem__inner {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-gap-header);
}

@media (min-width: 1000px) {
	.od-problem__inner {
		/* Even split. The 0.62/1.8 ratio squeezed the heading to five or six
		   short lines while leaving the card with a wide empty right half, so
		   neither column was using its width. */
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		gap: var(--od-gap-grid-lg);
		align-items: start;
	}

	.od-problem__inner > .od-section-header {
		margin-bottom: 0;
	}
}

/* Controls sit under the header copy, not over the reel. */
.od-problem__nav {
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	margin-top: var(--od-s-6);
}

.od-problem__dots {
	display: flex;
	gap: var(--od-s-2);
	margin-left: var(--od-s-3);
}

/*
 * The track scrolls, so it clips — and it was clipping the cards' own borders
 * and hover shadow flat against its edges. Padding on all four sides gives them
 * room; the negative inline margin pulls that padding back out so the cards
 * still line up with the column, and scroll-padding keeps the snap position
 * honest (without it every slide settles one padding-width off).
 */
.od-problem__list {
	counter-reset: od-problem;
	list-style: none;
	padding: var(--od-s-3) var(--od-s-4) var(--od-s-6);
	margin-inline: calc(var(--od-s-4) * -1);
	scroll-padding-inline: var(--od-s-4);
}

/*
 * Scoped through the track deliberately. blocks.css is enqueued AFTER
 * sections.css, so the generic `.od-slider__slide` width (31.5% at 1200) was
 * beating a bare `.od-problem__slide` and cramming three cards into the column —
 * every quote wrapped every three words. Two classes wins the cascade back.
 */
/*
 * One card at a time, full width of the column. Two-up meant every quote and
 * every answer wrapped in a half-width measure; at full width the same copy
 * reads in two or three long lines instead of six short ones.
 *
 * Scoped through the track deliberately: blocks.css is enqueued AFTER
 * sections.css, so a bare .od-problem__slide loses to the generic
 * .od-slider__slide width. Two classes wins the cascade back.
 */
.od-problem__list .od-problem__slide {
	flex: 0 0 100%;
	scroll-snap-align: center;
	counter-increment: od-problem;
}

/*
 * A tinted violet card rather than another white box on a white band. At
 * --od-primary-50 the tint is barely a colour, but it is enough to lift the
 * card off the section and mark it as the thing being read.
 *
 * The min-height is load-bearing now that the reel rotates on its own: without
 * it the card resizes to each quote and the section jumps every few seconds,
 * shoving whatever is below it up and down the page.
 */
.od-problem__item {
	/* Containing block for the absolutely-placed index numeral. */
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--od-s-4);
	height: 100%;
	min-height: 260px;
	padding: var(--od-s-7) var(--od-s-6);
	background: var(--od-primary-50);
	border: 1px solid var(--od-primary-100);
	border-radius: var(--od-r-lg);
	box-shadow: var(--od-shadow-0);
	transition: border-color var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease);
}

.od-problem__item:hover {
	border-color: var(--od-primary);
	box-shadow: var(--od-shadow-2);
}


/* The index plate. A counter costs no markup, and it gives every card a fixed
   anchor so the quotes all start on the same line regardless of length. */
.od-problem__index {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 62px;
	height: 62px;
	flex: none;
	border-radius: var(--od-r);
	background: var(--od-primary-100);
	color: var(--od-primary);
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h4);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	line-height: 1;
	font-variant-numeric: tabular-nums;
}

.od-problem__index::before {
	content: counter(od-problem, decimal-leading-zero);
}

.od-problem__pain {
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h4);
	font-weight: var(--od-fw-head);
	line-height: 1.35;
	letter-spacing: var(--od-ls-h3);
	color: var(--od-ink);
	margin: 0;
	max-width: none;
	/* pretty, not balance. Balance evens the line lengths, which on a short
	   quote in a narrow card produced a tidy three-word column. */
	text-wrap: pretty;
}

.od-problem__answer {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-2);
	/* Pins the answer to the bottom, so the "what we do" rules line up across
	   cards whose quotes run to different lengths. */
	margin-top: auto;
	padding-top: var(--od-s-4);
	border-top: 1px solid var(--od-line-hair);
	width: 100%;
}

/*
 * A named label instead of a bare arrow. The arrow alone did not say what the
 * second paragraph was, so the card read as a quote followed by an unattributed
 * sentence rather than as problem -> answer.
 */
.od-problem__answer-label {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
	font-size: var(--od-fs-xs);
	font-weight: 600;
	letter-spacing: var(--od-ls-eyebrow);
	text-transform: uppercase;
	color: var(--od-primary-600);
}

.od-problem__arrow {
	flex: none;
}

.od-problem__arrow svg {
	width: 16px;
	height: 16px;
}

.od-problem__solution {
	/* Body size, now that the card is full width and the line has room. */
	font-size: var(--od-fs-body);
	line-height: 1.6;
	color: var(--od-body);
	margin: 0;
	max-width: 68ch;
}

/*
 * Both of these default to --od-line, which vanishes against the violet tint.
 * Declared after their base rules so they actually win.
 */
.od-problem__item .od-problem__index {
	background: var(--od-surface);
}

.od-problem__item .od-problem__answer {
	border-top-color: var(--od-primary-100);
}

/* --- Process ------------------------------------------------------------- */

.od-stepper {
	counter-reset: od-step;
}

.od-process {
	overflow: hidden;
}

.od-process__inner {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-gap-header);
}

/*
 * Rail layout: the four steps stack as full-width rows separated by hairlines
 * instead of sitting in a 4-up grid of equal cards. A row can then carry an
 * oversized ghost numeral, which a narrow column cannot.
 */
.od-stepper--rail {
	display: flex;
	flex-direction: column;
}

.od-stepper--rail .od-stepper__step {
	position: relative;
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-s-3);
	padding-block: var(--od-s-7);
	border-top: 1px solid var(--od-on-ink-line);
	overflow: hidden;
}

.od-stepper--rail .od-stepper__step:first-child {
	border-top: 0;
	padding-top: 0;
}

/* The ghost numeral. Huge, barely-there, and behind the copy — the single
   strongest anti-flat device available without imagery. */
.od-stepper--rail .od-stepper__num {
	position: absolute;
	/*
	 * Sitting on the divider, not floating in the middle of the row. Centred, it
	 * read as a watermark dropped behind the text; anchored to the bottom edge it
	 * reads as part of the rule the step sits on. The small negative offset is
	 * optical — line-height leaves a sliver of descender space below the digits,
	 * so bottom: 0 alone still leaves a visible gap under them.
	 */
	bottom: -0.1em;
	right: 0;
	z-index: 0;
	font-family: var(--od-font-head);
	/* About half the shared numeral size. At 0.75 the digits were taller than
	   the copy beside them and set the height of the whole row, so every step
	   carried empty space underneath it that had nothing to do with the text.
	   At this scale the numeral marks the step without driving the layout.
	   Scoped here rather than changed on the token, which other sections use. */
	font-size: calc(var(--od-fs-numeral) * 0.52);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-numeral);
	line-height: 1;
	color: rgba(255, 255, 255, 0.07);
	pointer-events: none;
}

.od-stepper--rail .od-stepper__title,
.od-stepper--rail .od-stepper__text,
.od-stepper--rail .od-stepper__duration {
	position: relative;
	z-index: 1;
}

.od-stepper--rail .od-stepper__title {
	font-size: var(--od-fs-h3);
	color: var(--od-on-ink);
	margin: 0;
}

.od-stepper--rail .od-stepper__text {
	color: var(--od-on-ink-muted);
	font-size: var(--od-fs-body);
}

/*
 * The timeframe as a marker, not a footnote.
 *
 * "Week 1" set as 12px muted grey, floating a long way under the paragraph, was
 * the least visible thing in a section whose entire promise is that the client
 * knows when things happen. As an outlined chip with a violet node it reads as
 * a point on a schedule — and it is the one element in the step that is allowed
 * to look like a control, because it is the answer to "when".
 */
.od-stepper--rail .od-stepper__duration {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
	margin-top: 0;
	padding: 6px var(--od-s-4) 6px var(--od-s-3);
	border: 1px solid var(--od-on-ink-line);
	border-radius: var(--od-r-pill);
	background: rgba(255, 255, 255, 0.04);
	font-size: var(--od-fs-xs);
	font-weight: 600;
	letter-spacing: var(--od-ls-eyebrow);
	text-transform: uppercase;
	color: var(--od-on-ink);
	width: fit-content;
}

.od-stepper--rail .od-stepper__duration::before {
	content: "";
	width: 7px;
	height: 7px;
	flex: none;
	border-radius: 50%;
	background: #B49BFF;
}

@media (min-width: 900px) {
	.od-stepper--rail .od-stepper__step {
		grid-template-columns: minmax(0, 0.34fr) minmax(0, 1fr);
		/* Row gap and column gap set separately: at a single --od-s-7 the chip
		   sat 48px below its own paragraph, which is what made it read as
		   detached from the step rather than part of it. */
		gap: var(--od-s-4) var(--od-s-7);
		align-items: start;
		/* The ghost numeral no longer sets the row height, so this is the only
		   thing separating the four steps. Each step is a distinct stage of the
		   engagement and wants to be read as one, so the rules are spaced well
		   clear of the copy rather than sitting just off it. */
		padding-block: var(--od-s-8);
	}

	/* Title in the left column, body and timeframe in the right. */
	.od-stepper--rail .od-stepper__title {
		grid-row: 1;
	}

	.od-stepper--rail .od-stepper__text {
		grid-column: 2;
		grid-row: 1;
	}

	.od-stepper--rail .od-stepper__duration {
		grid-column: 2;
		grid-row: 2;
		margin-top: 0;
	}
}

@media (min-width: 1200px) {
	.od-process__inner {
		grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr);
		gap: var(--od-gap-grid-lg);
		align-items: start;
	}
}

/* --- Services: bento grid ------------------------------------------------ */
/*
 * Replaced the stack of six identical wide cards. Layout, spans and the nested
 * benefit plate now come from blocks.css (.od-bento / .od-panel / .od-plate);
 * only the service-specific details live here.
 */

.od-services__bento {
	--od-services-gap: var(--od-gap-grid);
}

.od-service__title {
	font-size: var(--od-fs-h3);
}

.od-service__title a {
	color: inherit;
	text-decoration: none;
}

.od-service__title a:hover {
	color: var(--od-primary-600);
}

/* The full-width feature cell reads as a banner, so it lays its copy and its
   benefit plate side by side rather than stacking them. */
@media (min-width: 900px) {
	.od-bento__cell--6.od-service {
		display: grid;
		grid-template-columns: 1.1fr 0.9fr;
		/* min-content rows + start alignment: without these the tall benefit
		   plate stretches the three left-hand rows and opens a dead gap between
		   the title and its description. */
		grid-auto-rows: min-content;
		align-content: start;
		column-gap: var(--od-s-8);
		row-gap: var(--od-s-4);
	}

	.od-bento__cell--6.od-service .od-squircle,
	.od-bento__cell--6.od-service .od-service__title,
	.od-bento__cell--6.od-service .od-panel__text {
		grid-column: 1;
	}

	.od-bento__cell--6.od-service .od-service__benefits {
		grid-column: 2;
		grid-row: 1 / span 4;
		align-self: center;
		margin: 0;
	}
}

/*
 * Benefit rows — hairline separated, no fill. The tinted plate behind these
 * boxed a list inside an already-bordered card, which read as clutter; the
 * hairlines alone carry the structure.
 */
.od-service__benefits {
	display: flex;
	flex-direction: column;
	background: none;
	padding: 0;
	border-radius: 0;
}

/* No rules between rows. With the plate fill gone as well, spacing alone
   separates them — three short lines do not need ruling off from each other. */
.od-service__benefit {
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	color: var(--od-ink);
}

.od-service__benefits {
	gap: var(--od-s-3);
}

/*
 * A filled bolt, not an outlined tick. The tick is already the page's
 * evidence mark (hero proof rail, credibility, CTA list); reusing it a fourth
 * time inside every service card made the whole page read as one long
 * checklist. Green is kept — each row still asserts something the client gets.
 */
.od-service__check {
	flex: none;
	color: var(--od-green);
	align-self: center;
}

.od-service__check svg {
	width: 15px;
	height: 15px;
	/* .od-icon sets fill:none globally — override so the bolt reads as a solid
	   mark at this size rather than a thin outline. */
	fill: currentColor;
	stroke: currentColor;
	stroke-width: 1.5;
	stroke-linejoin: round;
}

.od-service__link {
	margin-top: var(--od-s-3);
}

/*
 * The blob is sized as a percentage of card WIDTH with aspect-ratio 1, so on a
 * short two-column cell it ends up taller than the card and reads as a pale
 * wedge rather than a soft corner glow. Scoped down here rather than in
 * blocks.css, because the tall panels that also use --blob are sized correctly
 * by the global rule.
 */
.od-service.od-panel--blob::after {
	right: -14%;
	bottom: -34%;
	width: 38%;
}

/*
 * Whole-card click target. The link keeps its own text and arrow — the
 * pseudo-element simply stretches its hit area over the card, which is why the
 * card stays one <article> with one real <a> rather than being wrapped in an
 * anchor around block content.
 */
.od-service {
	position: relative;
}

/* Only cards that actually have a link get the pointer. */
.od-service:has(.od-service__link) {
	cursor: pointer;
}

/*
 * Hovering anywhere on the card drives the title's own hover state, so the
 * whole block reads as one control. The underline is .od-underline's ::after
 * sweeping in from the left — it only ever fired on :hover of the anchor
 * itself, which the stretched hit layer now sits on top of.
 */
.od-service:hover .od-service__link .od-arrow {
	transform: translateX(4px);
}

.od-service:hover .od-service__title a {
	color: var(--od-primary-600);
}

.od-service:hover .od-service__title a::after {
	transform: scaleX(1);
}

/*
 * MUST be static. `.od-panel > *` sets position: relative on every direct
 * child, which made the link its own containing block — so inset: 0 stretched
 * the hit area over the link's own text and nothing else. Static hands the
 * containing block back to .od-service.
 */
.od-service .od-service__link {
	position: static;
}

.od-service .od-service__link::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 2;
}

/* The title's own anchor points at the same URL, so it is raised above the
   stretched layer rather than left focusable-but-dead beneath it. */
.od-service .od-service__title a {
	position: relative;
	z-index: 3;
}

/* --- Mid-page CTA band --------------------------------------------------- */

/*
 * A tinted panel, not a white box on a white page.
 *
 * On --od-surface with a grey hairline the band was the same colour as the page
 * behind it, so the only thing marking it out as an offer was the button. A
 * violet wash and a violet hairline make it read as an inset panel; the rule
 * down the left edge is what stops it reading as merely "a paler rectangle".
 */
/*
 * A solid violet block, not a tinted white one.
 *
 * The pale version had the same problem in a milder form: it sat on a white
 * page as a slightly-less-white rectangle. Full brand gradient makes the band
 * the one saturated object between the process rail and the footer, which is
 * what a mid-page CTA is for. Everything inside it therefore inverts.
 */
.od-band__inner {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--od-s-5);
	padding: var(--od-s-7) var(--od-s-6);
	border: 0;
	border-radius: var(--od-r-xl);
	background:
		radial-gradient(46ch 34ch at 92% -20%, rgba(255, 255, 255, 0.22), transparent 68%),
		linear-gradient(125deg, var(--od-primary) 0%, var(--od-primary-700) 78%);
	box-shadow: 0 20px 48px rgba(78, 18, 240, 0.28);
	overflow: hidden;
}

/* Light catch along the top edge — the same device as the credibility tiles, so
   the two saturated blocks on the page are lit the same way. */
.od-band__inner::before {
	content: "";
	position: absolute;
	left: 8%;
	right: 8%;
	top: 0;
	height: 1px;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.55), transparent);
}

.od-band__title {
	font-size: var(--od-fs-h3);
	letter-spacing: var(--od-ls-tight);
	color: #fff;
	margin-bottom: var(--od-s-3);
}

/*
 * Body size, not small print — it carries the actual offer, which is the reason
 * to press the button. Held just off pure white so the heading still leads.
 */
.od-band__text {
	font-size: var(--od-fs-body);
	line-height: 1.6;
	color: rgba(255, 255, 255, 0.82);
	margin: 0;
	max-width: 62ch;
}

/*
 * White button on violet. A violet button on a violet block would disappear,
 * and the inversion is the strongest possible affordance: the only white solid
 * in the band is the thing to press.
 */
.od-band__action .od-btn--primary {
	background: #fff;
	color: var(--od-primary-700);
	box-shadow: 0 8px 20px rgba(12, 12, 28, 0.22);
	/*
	 * Containing block and clip for the gloss below. isolation keeps the sweep
	 * inside this button's own stacking context, so it cannot paint over the
	 * band's ::before wash.
	 */
	position: relative;
	overflow: hidden;
	isolation: isolate;
}

/*
 * A gloss travelling across the face of the button.
 *
 * This is the page's one hard ask, sitting alone on a violet band, so it gets
 * the only moving highlight on a control anywhere on the site. It is a tinted
 * sweep rather than a white one: the button is already white, so a white streak
 * would be invisible — the violet tint reads as a reflection passing over a
 * glossy surface.
 *
 * Skewed and narrow on purpose. A wide, straight band looks like a loading
 * shimmer; a narrow diagonal one looks like light moving across a solid object.
 * The long pause between passes is what keeps it from nagging.
 */
.od-band__action .od-btn--primary::after {
	content: "";
	position: absolute;
	top: -60%;
	bottom: -60%;
	left: -85%;
	/*
	 * Wide and blurred rather than narrow and hard.
	 *
	 * The first version was a 28% hard-edged band at 16% alpha, which is close
	 * to invisible on white — raising the alpha alone would have turned it into
	 * a purple bar sliding across a button. Spreading the same light over twice
	 * the width and softening the edges makes it read as a sheen: the peak is
	 * stronger, so it registers, but there is no edge anywhere for the eye to
	 * catch on.
	 *
	 * Widened again from 55% to 80%, which pairs with the faster pass below: a
	 * quick sweep needs a broad highlight or it reads as a flicker rather than
	 * as light crossing a surface.
	 */
	width: 80%;
	/*
	 * No negative z-index here. A negative one would put the sweep behind the
	 * button's own white background, which paints over it — the gloss would
	 * never be visible. At auto it sits above the background and passes over
	 * the label.
	 */
	background: linear-gradient(
		90deg,
		rgba(78, 18, 240, 0) 0%,
		rgba(78, 18, 240, 0.10) 28%,
		rgba(93, 40, 245, 0.30) 50%,
		rgba(78, 18, 240, 0.10) 72%,
		rgba(78, 18, 240, 0) 100%
	);
	filter: blur(7px);
	transform: skewX(-18deg);
	/*
	 * Quick, and mostly at rest. The pass is 18% of a 5s cycle -- about 0.9s,
	 * down from 2.3s, which is what made it look like it was labouring across
	 * the button. The other 82% is a pause; that gap is what stops a repeating
	 * highlight from nagging, so shortening the sweep must not shorten the
	 * cycle by the same amount.
	 *
	 * Eased out rather than in-and-out: the sweep is already at speed when it
	 * enters the frame, so it reads as light passing over rather than starting
	 * on the button.
	 */
	animation: od-btn-gloss 5s cubic-bezier(0.25, 0.6, 0.3, 1) infinite;
	pointer-events: none;
}

/*
 * Travels well past the right edge, then holds off-frame for the rest of the
 * cycle. The clip on the button ends the pass; the hold is the gap before the
 * next one.
 */
@keyframes od-btn-gloss {
	0% {
		left: -85%;
	}
	18%,
	100% {
		left: 150%;
	}
}

/* The gloss is decoration, not information — it simply does not run. */
@media (prefers-reduced-motion: reduce) {
	.od-band__action .od-btn--primary::after {
		animation: none;
		opacity: 0;
	}
}

.od-band__action .od-btn--primary:hover {
	background: var(--od-primary-50);
	color: var(--od-primary-700);
}

.od-band__action {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--od-s-3);
	flex: none;
}

.od-band__note {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-3);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	line-height: 1.3;
	color: rgba(255, 255, 255, 0.86);
	white-space: nowrap;
}

/* green-light and a translucent ring: --od-green with its solid --od-green-100
   halo is tuned for a light surface and turns into a pale blob on violet. */
.od-band__note::before {
	content: "";
	width: 7px;
	height: 7px;
	flex: none;
	border-radius: 50%;
	background: var(--od-green-light);
	box-shadow: 0 0 0 3px rgba(74, 222, 158, 0.22);
}

@media (min-width: 900px) {
	.od-band__inner {
		flex-direction: row;
		align-items: center;
		justify-content: space-between;
		gap: var(--od-s-7);
		padding: var(--od-s-7) var(--od-s-8);
	}

	.od-band__action {
		align-items: center;
	}
}

/* --- Sticky mobile CTA ---------------------------------------------------- */

.od-stickybar {
	position: fixed;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 90;
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	padding: var(--od-s-3) var(--od-gutter);
	/* Clear the iOS home indicator. */
	padding-bottom: calc(var(--od-s-3) + env(safe-area-inset-bottom, 0px));
	background: rgba(255, 255, 255, 0.96);
	border-top: 1px solid var(--od-line);
	box-shadow: 0 -4px 20px rgba(20, 20, 43, 0.08);
}

.od-stickybar[hidden] {
	display: none;
}

.od-stickybar__alt {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 48px;
	height: 48px;
	flex: none;
	border: 1.5px solid var(--od-line-strong);
	border-radius: 50%;
	color: var(--od-green-600);
	text-decoration: none;
}

/* Desktop keeps the header CTA — a bottom bar there is just clutter. */
@media (min-width: 900px) {
	.od-stickybar {
		display: none !important;
	}
}

/* --- Founder ------------------------------------------------------------- */

.od-founder__inner {
	display: grid;
	gap: var(--od-s-7);
	grid-template-columns: 1fr;
}

@media (min-width: 1000px) {
	/*
	 * Copy left, portrait right, in equal halves. The copy column used to be the
	 * wider of the two on the grounds that the story does the persuading — but
	 * the portrait is now a slideshow of the founder with people he has worked
	 * alongside, which is evidence in its own right and earns the same width.
	 */
	.od-founder__inner {
		grid-template-columns: minmax(0, 1fr) minmax(320px, 1fr);
		gap: var(--od-gap-grid-lg);
		/* Stretch, not start: the portrait takes its height from the copy column
		   beside it, so the two halves square off top and bottom instead of the
		   picture ending halfway down and leaving a hole. */
		align-items: stretch;
	}

	/*
	 * The column is a flex box and the plate takes the leftover space, rather
	 * than both being height:100%. That failed: the plate's own margins were
	 * added on top of a full-height box, so it overran the column instead of
	 * filling it, and the photo stayed its natural size. Margins become padding
	 * on the parent, and `flex: 1` measures what is actually left.
	 */
	.od-founder__media {
		height: 100%;
		display: flex;
		flex-direction: column;
		padding: var(--od-s-5) var(--od-s-5) var(--od-s-7) 0;
	}

	.od-founder__plate {
		flex: 1;
		/* Without this a flex item refuses to shrink below its content height. */
		min-height: 0;
		margin: 0;
	}

	/* Fills whatever height the copy sets. The fixed portrait ratio is dropped
	   here — with a cover fit the crop stays correct at any height. */
	.od-founder__photo {
		height: 100%;
		aspect-ratio: auto;
	}
}

.od-founder__title {
	font-size: var(--od-fs-h2);
	margin-bottom: var(--od-s-5);
	max-width: 20ch;
}

.od-founder__bio p {
	font-size: var(--od-fs-body);
	line-height: 1.65;
	color: var(--od-body);
	margin-bottom: var(--od-s-4);
	max-width: var(--od-measure);
}

/*
 * The opening paragraph leads, but at --od-fs-lead rather than --od-fs-quote.
 * At quote size it ran to eight lines of near-headline type and swallowed the
 * whole column, which left the second paragraph looking like a footnote.
 */
.od-founder__bio > p:first-child {
	font-size: var(--od-fs-lead);
	font-weight: 500;
	line-height: 1.55;
	color: var(--od-ink);
	margin-bottom: var(--od-s-4);
}

/* --- The journey rail --- */

/*
 * The story told as a dated sequence rather than as prose.
 *
 * Two paragraphs covering 2014 to today asked the reader to work out the shape
 * of a decade from a sentence. Milestones on a rail show it: the years are the
 * left-hand column, the rail joins them, and the distance travelled is legible
 * before a single word is read.
 *
 * The rail is drawn per item rather than once on the list, so it stops at the
 * final node instead of hanging past it — a line running off the bottom of a
 * timeline reads as "and then nothing", which is the opposite of the point.
 */
.od-journey {
	--od-journey-col: 3.5rem;
	list-style: none;
	margin: var(--od-s-6) 0 0;
	padding: 0;
}

.od-journey__item {
	position: relative;
	display: grid;
	grid-template-columns: var(--od-journey-col) minmax(0, 1fr);
	gap: 0 var(--od-s-5);
	padding-bottom: var(--od-s-6);
}

.od-journey__item:last-child {
	padding-bottom: 0;
}

.od-journey__item:not(:last-child)::before {
	content: "";
	position: absolute;
	/* Centred on the year chip above it. */
	left: calc(var(--od-journey-col) / 2 - 1px);
	/* Starts where the circle ends, not where the row does. */
	top: calc(var(--od-journey-col) + 0.35rem);
	bottom: 0.35rem;
	width: 2px;
	background: var(--od-primary-100);
}

/*
 * The year is the anchor of the row, so it is set in Rubik at heading weight
 * and given a plate. Tabular figures keep 2014 and 2022 the same width, which
 * is what stops the column looking hand-set.
 */
.od-journey__year {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* A true circle: width and height both driven by the column token, so the
	   node can never drift into an oval as the type scale changes. */
	width: var(--od-journey-col);
	height: var(--od-journey-col);
	flex: none;
	border-radius: 50%;
	background: var(--od-primary-100);
	color: var(--od-primary-600);
	font-family: var(--od-font-head);
	font-weight: 700;
	/* Tight enough that "Today" fits the same circle as "2014" without a second
	   size rule — five characters is the widest label the field allows. */
	font-size: 0.8125rem;
	font-variant-numeric: tabular-nums;
	letter-spacing: -0.02em;
	white-space: nowrap;
}

/* The present is where the reader is being invited to stand — it gets the
   solid brand fill, so the eye lands on "Today" rather than on 2014. */
.od-journey__item:last-child .od-journey__year {
	background: var(--od-primary);
	color: #fff;
}

.od-journey__title {
	font-size: var(--od-fs-h4);
	line-height: 1.25;
	margin: 0.3rem 0 var(--od-s-2);
	color: var(--od-ink);
}

.od-journey__text {
	font-size: var(--od-fs-body);
	line-height: 1.65;
	color: var(--od-body);
	margin: 0;
	max-width: var(--od-measure);
}

@media (min-width: 600px) {
	.od-journey {
		--od-journey-col: 4rem;
	}

	.od-journey__year {
		font-size: 0.875rem;
	}
}

/* --- Signature plate --- */

/*
 * A violet signature chip, not a pale full-width bar.
 *
 * It was --od-alt on white with a grey icon — the lowest-contrast object in a
 * section whose whole argument is "a named person is accountable for this".
 * Solid brand violet makes it the second thing the eye lands on after the
 * headline, and `width: fit-content` stops it stretching the column, so it reads
 * as a signature rather than as another row of the layout.
 */
/*
 * A full-width bar across the foot of the portrait.
 *
 * Not a pill floating over the picture — spanning the frame edge to edge, it
 * reads as a nameplate belonging to the photograph, which is exactly the claim
 * the section makes. Its bottom corners follow the frame's radius so the two
 * shapes share an outline; the top two stay square, because a fully rounded bar
 * inside a rounded frame leaves two awkward slivers of image in the corners.
 *
 * z-index clears the plate's scrim, which is an ::after and therefore paints
 * above ordinary children.
 */
.od-founder__sign {
	position: absolute;
	/* Floats off the bottom-left corner rather than spanning the frame edge to
	   edge. A full-width bar reads as a caption printed into the photograph; an
	   object that breaks two edges at once reads as laid on top of it — the same
	   move the white years plate makes at the opposite corner, so the two read
	   as a pair. */
	left: calc(var(--od-s-4) * -1);
	/* Overhangs both side edges by the same amount, so it reads as a plate laid
	   across the foot of the photograph rather than a chip parked in the corner.
	   Both insets set, so the box stretches — content width is not wanted. */
	right: calc(var(--od-s-4) * -1);
	bottom: calc(var(--od-s-6) * -1);
	z-index: 3;
	display: flex;
	align-items: stretch;
	gap: var(--od-s-6);
	width: auto;
	max-width: none;
	margin-top: 0;
	/* Pill, matching the years plate. Both are now free-floating objects on the
	   same photograph and should carry the same corner. */
	border-radius: var(--od-r-pill);
	/* Even padding on both ends now the avatar has gone. It used to be lopsided
	   because the 56px disc supplied the optical inset on the left. */
	padding: var(--od-s-5) var(--od-s-6);
	background: linear-gradient(135deg, var(--od-primary), var(--od-primary-700));
	border: 0;
	box-shadow: 0 16px 34px rgba(12, 12, 28, 0.3);
}


.od-founder__name {
	margin: 0;
	margin-right: auto;
	color: #fff;
	/* Centres the name/role pair against the tenure block beside it, now that
	   the plate stretches its children rather than centring them — stretch is
	   what lets the divider run the full height of the content. */
	display: flex;
	flex-direction: column;
	justify-content: center;
}

.od-founder__name strong {
	display: block;
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h3);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	line-height: 1.15;
	color: #fff;
}

/*
 * A shimmer travels across the name, reusing the hero form's keyframes.
 *
 * Same shape as the hero form title, inverted for white text: the hero holds a
 * dark blue base and sweeps a light band through it, so here the base stays
 * white and the travelling band is the tinted one. Guarded on background-clip
 * so browsers without it keep solid white instead of transparent text, and the
 * same 240% / 100%→0% sizing: a narrower background runs off the box mid-cycle
 * and the glyphs disappear.
 */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
	.od-founder__name strong {
		background-image: linear-gradient(100deg, #FFFFFF 0%, #FFFFFF 34%, #B9A2FF 50%, #FFFFFF 66%, #FFFFFF 100%);
		background-size: 240% 100%;
		-webkit-background-clip: text;
		background-clip: text;
		-webkit-text-fill-color: transparent;
		color: transparent;
		animation: od-shimmer 3.4s linear infinite;
	}
}

@media (prefers-reduced-motion: reduce) {
	.od-founder__name strong {
		animation: none;
	}
}

.od-founder__role {
	display: block;
	font-size: var(--od-fs-sm);
	line-height: 1.3;
	letter-spacing: 0.005em;
	color: rgba(255, 255, 255, 0.76);
	/* A little air under the name, which sits tight at 1.15 line-height. */
	margin-top: 4px;
}

/* Social icons, when there are any, have to invert inside the chip. */
.od-founder__sign .od-founder__links a {
	background: rgba(255, 255, 255, 0.14);
	border-color: rgba(255, 255, 255, 0.28);
	color: #fff;
}

.od-founder__sign .od-founder__links a:hover {
	background: #fff;
	border-color: #fff;
	color: var(--od-primary);
}

.od-founder__links {
	display: flex;
	gap: var(--od-s-2);
}

.od-founder__links a {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	border: 1px solid var(--od-line-strong);
	border-radius: 50%;
	background: var(--od-surface);
	color: var(--od-ink);
	transition: background-color var(--od-dur) var(--od-ease),
		border-color var(--od-dur) var(--od-ease),
		color var(--od-dur) var(--od-ease);
}

.od-founder__links a:hover {
	background: var(--od-primary);
	border-color: var(--od-primary);
	color: #fff;
}

/* --- Stat tiles --- */

.od-founder__stats {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: var(--od-gap-grid);
	margin-top: var(--od-s-5);
}

.od-founder__stat {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-1);
	padding: var(--od-s-4) var(--od-s-5);
	background: var(--od-surface);
	border: 1px solid var(--od-line);
	border-radius: var(--od-r);
	box-shadow: var(--od-shadow-0);
}

/* green-600, not green: these are text-sized figures on a white surface, so
   they must clear 4.5:1 rather than the 3:1 graphics threshold. */
.od-founder__stat-value {
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h3);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	color: var(--od-green-600);
	line-height: 1.1;
	font-variant-numeric: tabular-nums;
}

.od-founder__stat-label {
	font-size: var(--od-fs-xs);
	line-height: 1.4;
	color: var(--od-muted);
}

/* --- Portrait --- */

/*
 * The section is white, so the portrait carries the dark register on its own:
 * an ink plate that the image sits offset from, breaking its edge. Keeps the
 * change of tone without spending a fourth full-bleed ink band on it.
 */
/*
 * The photo sits square in its frame; the ink block is a separate offset shape
 * behind it. Previously the image itself was translated inside a padded ink box,
 * which left an L of dark padding down two sides and read as a printing error
 * rather than as a deliberate offset.
 */
.od-founder__plate {
	position: relative;
	/* Room for the badge, which hangs off the bottom-left corner. */
	margin: var(--od-s-5) var(--od-s-5) var(--od-s-7) 0;
}

/*
 * Violet, not ink. A black block behind the photograph read as a drop shadow
 * that had slipped, and it put a fourth dark mass on a page that already has
 * three. In the brand violet it reads as a deliberate backing plate and ties
 * the section to the CTA band it leads towards.
 */
.od-founder__plate::before {
	content: "";
	position: absolute;
	inset: calc(var(--od-s-6) * -1) calc(var(--od-s-6) * -1) auto auto;
	width: 58%;
	height: 58%;
	/* Rounded on all four corners, not just the two that clear the frame. Two
	   of them sit behind the photograph, but the shape carries its own radius
	   so nothing depends on where the frame happens to crop it. */
	border-radius: var(--od-r-xl);
	background: linear-gradient(150deg, var(--od-primary), var(--od-primary-700));
}

.od-founder__photo {
	position: relative;
	display: block;
	width: 100%;
	/* A portrait crop, held to one ratio at every width so the framing never
	   changes as the column narrows. */
	aspect-ratio: 4 / 5;
	border-radius: var(--od-r-xl);
	object-fit: cover;
	/*
	 * Anchored low. The source photograph is a wide balcony shot with the
	 * subject in the bottom half and a tower block skyline above; centring it
	 * would frame the buildings and cut the face. This crops to head and
	 * shoulders and drops most of the background out of the frame entirely.
	 */
	object-position: 50% 68%;
	/* Pulls the busy background back a step without touching skin tone. */
	filter: saturate(0.88) contrast(1.04);
	box-shadow: var(--od-shadow-3);
}

/*
 * The portrait as a slideshow.
 *
 * Every selector here carries two classes. blocks.css defines the base slider
 * and loads AFTER sections.css, so a single-class override at equal specificity
 * would silently lose — the same trap the panel/stretched-link rule documents.
 *
 * The frame's radius and shadow move from the image to the track, because the
 * images now scroll inside a fixed window rather than being the window.
 */
.od-founder__slider .od-slider__track {
	/* One image at a time, edge to edge: the defaults show two or three across
	   with a gutter between them, which is right for cards and wrong for a
	   single portrait frame. */
	gap: 0;
	padding-bottom: 0;
	height: 100%;
	border-radius: var(--od-r-xl);
	box-shadow: var(--od-shadow-3);
	/* Clips the images to the frame; without it the corners square off as soon
	   as the track scrolls. */
	overflow: hidden;
	overflow-x: auto;
}

.od-founder__slider .od-slider__slide {
	flex: 0 0 100%;
	scroll-snap-align: center;
}

.od-founder__slider .od-founder__photo {
	/* The shadow lives on the track now — three stacked shadows inside one frame
	   would read as a seam at every slide boundary. */
	box-shadow: none;
	border-radius: 0;
	height: 100%;
	/*
	 * Framed for the upper body. The single-portrait rule anchors low at 68%
	 * because that source is a wide balcony shot with the subject beneath a
	 * skyline; the added photographs are ordinary standing shots whose faces sit
	 * in the top third, and that same anchor cropped them at the chin.
	 */
	object-position: 50% 28%;
}

.od-founder__slider,
.od-founder__slide {
	height: 100%;
}

/*
 * A scrim over the top of the frame. What is left of the skyline after the crop
 * is still the noisiest part of the picture, and it sits directly against the
 * headline. Darkening it settles the image down and leaves the face untouched —
 * the gradient is gone by the time it reaches the subject.
 */
.od-founder__plate::after {
	content: "";
	position: absolute;
	inset: 0;
	border-radius: var(--od-r-xl);
	background: linear-gradient(180deg, rgba(12, 12, 28, 0.34) 0%, rgba(12, 12, 28, 0.10) 26%, transparent 48%);
	pointer-events: none;
}

.od-founder__monogram {
	display: flex;
	align-items: center;
	justify-content: center;
	aspect-ratio: 4 / 5;
	border-radius: var(--od-r-lg);
	background: linear-gradient(145deg, rgba(206, 155, 255, 0.22), rgba(206, 155, 255, 0.06));
	border: 1px solid var(--od-on-ink-line);
	transform: translate(var(--od-s-5), calc(var(--od-s-5) * -1));
}

.od-founder__monogram span {
	font-family: var(--od-font-head);
	font-size: clamp(3rem, 8vw, 4.5rem);
	font-weight: var(--od-fw-display);
	letter-spacing: -0.04em;
	color: #B49BFF;
}

/*
 * The tenure chip inside the nameplate.
 *
 * This replaced a separate white badge floating at the opposite corner of the
 * portrait. Two plates over one photograph left the picture obscured at both
 * corners — worse now the portrait cycles through three images — and the years
 * read as a free-floating claim rather than as this person's. Inside the plate
 * it is a credential belonging to the name beside it.
 *
 * The plate already stretched edge to edge with the name pushed left by its own
 * margin-right: auto, so the chip needs no positioning; it simply occupies the
 * space that was already empty.
 */
.od-founder__tenure {
	display: flex;
	/* Centred against each other, not baseline-locked. On a baseline the figure
	   sat level with the label's FIRST line, which hung the second line below
	   the digits and made the pair look dropped. */
	align-items: center;
	gap: var(--od-s-4);
	flex: none;
	/* A hairline, not a solid rule: the plate is a violet gradient and a hard
	   divider would cut it in two. Generous inset on the left so the divider is
	   clearly separating two blocks rather than fencing the figure in. */
	padding-left: var(--od-s-6);
	border-left: 1px solid rgba(255, 255, 255, 0.26);
	color: #fff;
}

.od-founder__tenure-value {
	font-family: var(--od-font-head);
	/*
	 * Deliberately the same size and weight as the name. The two are the plate's
	 * two claims — who did the work, and for how long — and setting the figure
	 * smaller made it read as a footnote to the name rather than its equal.
	 */
	font-size: var(--od-fs-h3);
	font-weight: var(--od-fw-display);
	letter-spacing: var(--od-ls-tight);
	/* Flush at 1: the value is a single line, so anything more adds half-leading
	   above and below and pushes it out of centre against the label. */
	line-height: 1;
	color: #fff;
	/* Keeps the figure from shifting the label sideways as the year count
	   changes — od_years_practice() moves in steps of five. */
	font-variant-numeric: tabular-nums;
}

.od-founder__tenure-label {
	font-size: var(--od-fs-xs);
	font-weight: 500;
	line-height: 1.3;
	/* Not full white. At equal weight to the name it competed with it; stepped
	   back it reads as the caption to the figure it sits beside. */
	color: rgba(255, 255, 255, 0.76);
	/*
	 * Two lines, not three. At 11ch it broke as "Years Helping / Businesses /
	 * Grow" — a one-word orphan on the third line, which is what made the chip
	 * look ragged beside a clean two-line name block. This width takes it to
	 * "Years Helping / Businesses Grow", matching the name's two lines exactly.
	 */
	max-width: 17ch;
	text-wrap: balance;
	/* Both blocks read left-to-right off their own divider, so the label ranges
	   left like the role does rather than centring under itself. */
	text-align: left;
}

/*
 * Under 1100px the plate runs out of room for a name, a role and a labelled
 * figure on one line. The label goes and the figure stays: "10+" beside a name
 * still reads as years of practice, where a wrapped caption breaks the plate.
 */
@media (max-width: 1099px) {
	.od-founder__tenure-label {
		display: none;
	}
}

/* On phones the nameplate would hang off the viewport edge. */
@media (max-width: 599px) {
	.od-founder__sign {
		left: 0;
		right: 0;
	}
}

/* --- Proof --------------------------------------------------------------- */

/* A 2x2 grid on mobile, never a horizontal scroller — these are scannable
   proof points, not a gallery. */
.od-proof__metrics {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--od-s-5) var(--od-gap-grid);
	margin-bottom: var(--od-s-8);
}

@media (min-width: 900px) {
	.od-proof__metrics {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.od-proof__note {
	margin-inline: auto;
	font-size: var(--od-fs-sm);
}

/* --- Industries ---------------------------------------------------------- */

.od-industries__inner {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-gap-header);
}

/* A single non-wrapping row; the marquee scrolls it. */
.od-industries__list {
	display: flex;
	flex-wrap: nowrap;
	gap: var(--od-gap-grid);
	align-items: stretch;
	height: 100%;
}

.od-industries__item {
	flex: none;
	display: flex;
	min-height: 168px;
}

/* Breaks the container so chips scroll off the real viewport edge instead of
   stopping short at the gutter. */
.od-industries__marquee {
	margin-inline: calc(var(--od-gutter) * -1);
	/* Room for the hover lift and its shadow — overflow: hidden on .od-marquee
	   would otherwise clip both. */
	padding-block: var(--od-s-2);
}

/* The header's own bottom margin would push the blocks out of alignment with it
   once the two sit side by side. */
@media (min-width: 1200px) {
	.od-industries__inner > .od-section-header {
		margin-bottom: 0;
	}
}

@media (min-width: 1200px) {
	.od-industries__inner {
		grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.6fr);
		gap: var(--od-gap-grid-lg);
		/* Stretch, not centre: the blocks take their height from the header
		   column beside them, so the two halves of the section share one
		   baseline top and bottom instead of the reel floating in the middle. */
		align-items: stretch;
	}

	/* Inside the two-column layout it only breaks out on the right. */
	.od-industries__marquee {
		margin-inline: 0 calc(var(--od-gutter) * -1);
	}
}

/*
 * Blocks, not pills. A row of capsules gave every industry the same shape as a
 * tag and made the reel read as decoration; a card with its own icon reads as a
 * thing the business actually does. The label sits at the bottom and the icon at
 * the top, so eight cards of differing name lengths still line up on two axes.
 */
.od-industries__card {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	align-items: flex-start;
	gap: var(--od-s-5);
	width: 252px;
	padding: var(--od-s-5);
	background: var(--od-surface);
	border: 1px solid var(--od-line);
	border-radius: var(--od-r-lg);
	box-shadow: var(--od-shadow-0);
	color: var(--od-ink);
	text-decoration: none;
	transition:
		border-color var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease),
		transform var(--od-dur) var(--od-ease);
}

.od-industries__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 84px;
	height: 84px;
	border-radius: var(--od-r);
	background: var(--od-primary-100);
	color: var(--od-primary);
	flex: none;
	transition: background-color var(--od-dur) var(--od-ease),
		color var(--od-dur) var(--od-ease),
		transform var(--od-dur) var(--od-ease);
}

/* Large enough to be read as the card's subject rather than as a bullet. The
   stroke weight is scaled down to compensate — a 24px-grid icon drawn at 44px
   with its default 1.6 stroke looks like a heavy sketch. */
.od-industries__icon .od-icon svg {
	width: 44px;
	height: 44px;
	/* Down from the global 1.75. A 24px-grid icon drawn at 44px keeps its stroke
	   in absolute pixels, so at this size the default weight reads as a heavy
	   marker sketch rather than a line icon. */
	stroke-width: 1.4;
}

.od-industries__body {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-2);
}

.od-industries__name {
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h4);
	font-weight: var(--od-fw-mid);
	letter-spacing: var(--od-ls-h3);
	line-height: 1.25;
	color: var(--od-ink);
	text-wrap: balance;
}

.od-industries__note {
	font-size: var(--od-fs-sm);
	line-height: 1.45;
	color: var(--od-muted);
}

/*
 * Hover applies to every card, not just the linked ones. The previous rule was
 * scoped to a.od-industries__card and no industry has a link yet, so the whole
 * reel was inert — the cursor crossed eight cards and nothing acknowledged it.
 * The plate inverting is the payload; the lift and the shadow are support.
 */
.od-industries__card:hover,
.od-industries__card:focus-visible {
	border-color: var(--od-primary);
	box-shadow: var(--od-shadow-2);
	transform: translateY(-4px);
}

.od-industries__card:hover .od-industries__icon,
.od-industries__card:focus-visible .od-industries__icon {
	background: var(--od-primary);
	color: #fff;
	transform: scale(1.04);
}

@media (prefers-reduced-motion: reduce) {
	.od-industries__card,
	.od-industries__icon {
		transition: none;
	}

	.od-industries__card:hover,
	.od-industries__card:focus-visible,
	.od-industries__card:hover .od-industries__icon {
		transform: none;
	}
}

/* --- Why us -------------------------------------------------------------- */

.od-whyus__header {
	margin-bottom: var(--od-gap-header);
}

/*
 * Separate cards with real gutters, not one hairline-divided field. Joined
 * cells packed four abreast gave the text no breathing room at either edge and
 * the whole block read as a table.
 */
.od-whyus__grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-gap-grid);
	counter-reset: od-why;
	list-style: none;
}

@media (min-width: 600px) {
	.od-whyus__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 1200px) {
	.od-whyus__grid {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.od-whyus__item {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-4);
	padding: var(--od-s-6);
	background: var(--od-surface);
	border: 1px solid var(--od-line);
	border-radius: var(--od-r-lg);
	box-shadow: var(--od-shadow-0);
	counter-increment: od-why;
	transition: background-color var(--od-dur) var(--od-ease),
		border-color var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease);
}

/* No lift: there is nothing to click here, so the feedback stays a surface
   change rather than a false affordance. */
.od-whyus__item:hover {
	border-color: var(--od-primary);
	box-shadow: var(--od-shadow-2);
}

/*
 * The numeral row sits apart from the copy, under its own rule. Without the
 * separation the numeral, the tag and the title stacked at even spacing and the
 * cell had no internal structure — just three lines of decreasing size.
 */
.od-whyus__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--od-s-3);
	padding-bottom: var(--od-s-4);
	border-bottom: 1px solid var(--od-line-hair);
}

/*
 * Ghost numeral. Large, hollow and in --od-line so it sits behind the content
 * in the visual hierarchy while still giving each cell a fixed anchor — set at
 * full ink weight it would compete with the titles for first read.
 */
.od-whyus__num {
	font-family: var(--od-font-head);
	font-size: 3.25rem;
	font-weight: var(--od-fw-display);
	line-height: 1;
	letter-spacing: var(--od-ls-tight);
	color: var(--od-line-strong);
	font-variant-numeric: tabular-nums;
	transition: color var(--od-dur) var(--od-ease);
}

.od-whyus__num::before {
	content: counter(od-why, decimal-leading-zero);
}

.od-whyus__item:hover .od-whyus__num {
	color: var(--od-primary);
}

.od-whyus__tag {
	flex: none;
	padding: 5px var(--od-s-3);
	border-radius: var(--od-r-pill);
	background: var(--od-alt-2);
	color: var(--od-muted);
	font-size: 0.6875rem;
	font-weight: 600;
	letter-spacing: var(--od-ls-eyebrow);
	text-transform: uppercase;
	transition:
		background var(--od-dur) var(--od-ease),
		color var(--od-dur) var(--od-ease);
}

/* The tag fills solid violet with white text on hover, so the two marks at the
   top of the card move together instead of one lighting up beside a dead one. */
.od-whyus__item:hover .od-whyus__tag {
	background: var(--od-primary);
	color: #fff;
}

.od-whyus__title {
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h4);
	font-weight: var(--od-fw-head);
	letter-spacing: var(--od-ls-h3);
	line-height: 1.3;
	color: var(--od-ink);
	margin: 0;
	text-wrap: balance;
}

.od-whyus__text {
	/* Up from --od-fs-sm. Four cards of caption-sized copy under a full-size
	   heading read as fine print, and there is room for body size here. */
	font-size: var(--od-fs-body);
	line-height: 1.65;
	color: var(--od-body);
	margin: 0;
	/* Holds the copy off the bottom edge when a neighbouring card is taller. */
	margin-bottom: auto;
}

/* --- Testimonials -------------------------------------------------------- */

/* Mobile: scroll-snap track, no JS. The peeking next card is the swipe cue. */
.od-testimonials__track {
	display: flex;
	gap: var(--od-gap-grid);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
	scroll-padding-inline: var(--od-gutter);
	padding-bottom: var(--od-s-4);
	margin-inline: calc(var(--od-gutter) * -1);
	padding-inline: var(--od-gutter);
	scrollbar-width: thin;
}

.od-testimonials__slide {
	flex: 0 0 88%;
	scroll-snap-align: start;
}

@media (min-width: 900px) {
	/* Desktop is a static grid — a carousel here would hide content from
	   users and crawlers alike. */
	.od-testimonials__track {
		display: grid;
		grid-template-columns: repeat(3, minmax(0, 1fr));
		overflow: visible;
		margin-inline: 0;
		padding-inline: 0;
	}

	.od-testimonials__slide {
		flex: initial;
	}
}

/* --- Insights ------------------------------------------------------------ */

.od-card--post .od-card__title {
	font-size: 1.125rem;
}

/* --- FAQ ----------------------------------------------------------------- */

.od-faq__inner {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--od-gap-header);
}

/* Header holds position in a left column while the questions scroll past it,
   instead of sitting centred above a narrow capped stack. */
@media (min-width: 1200px) {
	.od-faq__inner {
		grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr);
		gap: var(--od-gap-grid-lg);
		align-items: start;
	}
}

/* --- Final CTA ----------------------------------------------------------- */

.od-cta__inner {
	display: grid;
	gap: var(--od-s-7);
	grid-template-columns: 1fr;
	align-items: start;
}

/* The page should close as loudly as it opens — display scale, not h2. */
.od-cta__title {
	margin-bottom: var(--od-s-5);
}

/* A deeper ink than the other bands, so the white form card separates cleanly. */
.od-cta {
	background: var(--od-ink-3);
	overflow: hidden;
}

.od-cta::before {
	content: "";
	position: absolute;
	inset: 0;
	background: var(--od-wash-ink);
	pointer-events: none;
}

.od-cta__form .od-formwrap {
	border-radius: var(--od-r-xl);
	box-shadow: var(--od-shadow-ink);
}

.od-cta__points {
	margin-top: var(--od-s-6);
}

.od-cta__points li {
	color: var(--od-on-ink);
	font-size: var(--od-fs-sm);
}

.od-cta__alt {
	display: flex;
	flex-wrap: wrap;
	gap: var(--od-s-5);
	margin-top: var(--od-s-7);
	padding-top: var(--od-s-5);
	border-top: 1px solid var(--od-on-ink-line);
}

.od-cta__alt-link {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	color: var(--od-on-ink);
	text-decoration: none;
}

.od-cta__alt-link:hover {
	color: #B49BFF;
}

.od-cta__alt-link .od-icon {
	color: #B49BFF;
}

@media (min-width: 900px) {
	.od-cta__inner {
		/* 40/60 in the form's favour — it is the one section on the page whose
		   entire job is getting filled in, so it takes the larger share. */
		grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr);
		gap: var(--od-gap-grid-lg);
		/* Both columns start at the same line. Centred, the copy floated down the
		   middle of the taller form and the heading no longer lined up with
		   anything, which read as a mistake rather than a balance. */
		align-items: start;
	}
}

/*
 * The feature cell with a portrait in it. The photo fills the cell and the copy
 * sits over the bottom of it under a scrim — rather than photo-above-text, which
 * would have needed a fixed image height and cropped differently at every
 * breakpoint.
 */
.od-whyus__item--photo {
	position: relative;
	padding: 0;
	overflow: hidden;
	min-height: 420px;
}

.od-whyus__media {
	position: absolute;
	inset: 0;
}

.od-whyus__img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	/* Anchored to the top: a centre crop cuts faces off at this aspect ratio. */
	object-position: 50% 22%;
}

/*
 * The scrim. Opaque enough at the bottom to hold body copy at AA, and gone by
 * the top so the photo is not just a dark rectangle. Sits between the image and
 * the text, so neither needs its own background.
 */
.od-whyus__item--photo .od-whyus__media::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(180deg, rgba(12, 12, 28, 0.15) 0%, rgba(12, 12, 28, 0.62) 46%, rgba(12, 12, 28, 0.94) 100%);
}

.od-whyus__item--photo .od-whyus__content {
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--od-s-3);
	/* Pushes the copy to the bottom of the cell, where the scrim is darkest. */
	margin-top: auto;
	padding: var(--od-s-6);
}

/* The blob decoration would sit on top of the photograph and read as a smudge. */
.od-whyus__item--photo.od-panel--blob::before {
	display: none;
}

/*
 * The hero form card, lifted. It is the conversion target of the whole page, so
 * it gets the one treatment nothing else on the site has: a hairline violet
 * border and a top edge that catches light, as though the orb behind it were a
 * real light source. Cheap to draw, and it makes the card read as an object
 * sitting above the wash rather than a white rectangle punched into it.
 */
.od-hero__form .od-formwrap {
	position: relative;
	border-color: rgba(78, 18, 240, 0.14);
	background:
		linear-gradient(180deg, rgba(255, 255, 255, 0.9), var(--od-surface) 42%),
		var(--od-surface);
}

.od-hero__form .od-formwrap::before {
	content: "";
	position: absolute;
	top: 0;
	left: 12%;
	right: 12%;
	height: 1px;
	background: linear-gradient(90deg, transparent, rgba(78, 18, 240, 0.45), transparent);
	pointer-events: none;
}
