/* ==========================================================================
   Composite blocks — bento grids, nested panels, marquees, sliders and the
   micro-interaction layer.

   These exist because a page built from "section header + uniform card grid",
   repeated fourteen times, reads as generic no matter how good the tokens are.
   The fix is format variety: cards of different SIZES in one grid, panels that
   contain other panels, and a couple of things that move.

   Loaded after sections.css, so anything here wins on equal specificity.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Bento grid
   Mixed-span cells on one grid. Below 900px everything is a single column —
   a bento that tries to keep its spans on a phone just makes unreadable
   slivers.
   -------------------------------------------------------------------------- */

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

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

@media (min-width: 900px) {
	.od-bento {
		grid-template-columns: repeat(6, minmax(0, 1fr));
		grid-auto-flow: dense;
	}

	/* Span helpers. Named by width in a 6-column field. */
	.od-bento__cell--2 { grid-column: span 2; }
	.od-bento__cell--3 { grid-column: span 3; }
	.od-bento__cell--4 { grid-column: span 4; }
	.od-bento__cell--6 { grid-column: span 6; }

	.od-bento__cell--tall { grid-row: span 2; }
}

/* --------------------------------------------------------------------------
   2. Panel — the base surface for every bento cell.
   A panel can contain other panels; that nesting is the point.
   -------------------------------------------------------------------------- */

.od-panel {
	position: relative;
	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);
	overflow: hidden;
	height: 100%;
	transition: transform var(--od-dur) var(--od-ease),
		box-shadow var(--od-dur) var(--od-ease),
		border-color var(--od-dur) var(--od-ease);
}

.od-panel--lg {
	padding: var(--od-s-7);
	border-radius: var(--od-r-xl);
}

.od-panel--tint {
	background: var(--od-primary-50);
	border-color: transparent;
}

.od-panel--ink {
	background: var(--od-ink);
	border-color: transparent;
}

.od-panel--ink .od-panel__title,
.od-panel--ink .od-panel__kicker {
	color: var(--od-on-ink);
}

.od-panel--ink .od-panel__text {
	color: var(--od-on-ink-muted);
}

/* An inner plate — a panel inside a panel. The device that makes a block read
   as composed rather than as one more rectangle. */
.od-plate {
	padding: var(--od-s-5);
	background: var(--od-alt);
	border-radius: var(--od-r);
}

.od-panel--ink .od-plate {
	background: rgba(255, 255, 255, 0.06);
}

.od-panel__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);
	color: var(--od-ink);
	margin: 0;
}

.od-panel__text {
	font-size: var(--od-fs-sm);
	color: var(--od-body);
	margin: 0;
}

/* Bold kicker pinned to the bottom of a panel — names the idea the card is an
   instance of, and gives every card a consistent bottom edge. */
.od-panel__kicker {
	margin-top: auto;
	padding-top: var(--od-s-4);
	font-size: var(--od-fs-xs);
	font-weight: 700;
	letter-spacing: 0.04em;
	color: var(--od-primary-600);
}

/* Soft corner blob. Purely decorative depth, drawn with a radial gradient so
   there is no extra element and no image request. */
.od-panel--blob::after {
	content: "";
	position: absolute;
	right: -18%;
	bottom: -28%;
	width: 62%;
	aspect-ratio: 1;
	border-radius: 50%;
	background: radial-gradient(circle at 30% 30%, var(--od-alt-2), transparent 70%);
	pointer-events: none;
	z-index: 0;
}

.od-panel--ink.od-panel--blob::after {
	background: radial-gradient(circle at 30% 30%, rgba(206, 155, 255, 0.16), transparent 70%);
}

.od-panel > * {
	position: relative;
	z-index: 1;
}

/* --------------------------------------------------------------------------
   3. Squircle icon plate
   Not a circle — circles are already spoken for by the service icons. A
   superellipse-ish rounded square reads as a different family of object.
   -------------------------------------------------------------------------- */

.od-squircle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 72px;
	height: 72px;
	border-radius: 30%;
	background: var(--od-ink);
	color: #fff;
	transition: transform var(--od-dur) var(--od-ease),
		background-color var(--od-dur) var(--od-ease);
}

.od-squircle--violet { background: var(--od-primary); }
.od-squircle--tint {
	background: var(--od-primary-100);
	color: var(--od-primary);
}

/*
 * The glyph fills roughly 55% of the plate. At the previous 26px in a 56px
 * plate it sat marooned in the middle and read as an icon someone forgot to
 * size. Stroke thins as the glyph grows so the weight stays constant.
 */
.od-squircle .od-icon svg {
	width: 38px;
	height: 38px;
	stroke-width: 1.5;
}

/* --------------------------------------------------------------------------
   4. Pill badge — the eyebrow, upgraded.
   -------------------------------------------------------------------------- */

.od-pill {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
	padding: 7px var(--od-s-4);
	border-radius: var(--od-r-pill);
	background: var(--od-primary-100);
	color: var(--od-primary-600);
	font-family: var(--od-font-body);
	font-size: var(--od-fs-eyebrow);
	font-weight: 700;
	letter-spacing: var(--od-ls-eyebrow);
	text-transform: uppercase;
	margin-bottom: var(--od-s-4);
	/*
	 * inline-flex is not enough on its own. As a child of a flex column or a
	 * grid the pill becomes an item, and the default `stretch` alignment blows
	 * it out to the full width of the container — which is how the testimonials
	 * panel ended up with a badge running edge to edge. These pin it to its
	 * content in both layout models.
	 */
	align-self: flex-start;
	justify-self: start;
	width: fit-content;
}

.od-pill::before {
	content: "";
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: currentColor;
	flex: none;
}

/* An ink PANEL inside a light section needs the same inversion as an ink
   section — the testimonials summary is exactly that case, and without it the
   pill kept its light-surface fill and glowed on the dark plate. */
.od-section--ink .od-pill,
.od-panel--ink .od-pill {
	background: rgba(206, 155, 255, 0.16);
	color: #C9B6FF;
}

/* --------------------------------------------------------------------------
   5. Marquee
   An infinitely scrolling track. The row is duplicated in the markup and the
   whole thing translates by exactly -50%, so the seam is invisible. Pauses on
   hover and on focus-within, and stops entirely under reduced-motion.
   -------------------------------------------------------------------------- */

.od-marquee {
	position: relative;
	overflow: hidden;
	/* Fade the two ends so items enter and leave instead of being cut off. */
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

.od-marquee__track {
	display: flex;
	width: max-content;
	gap: var(--od-s-4);
	animation: od-marquee var(--od-marquee-dur, 42s) linear infinite;
}

.od-marquee--reverse .od-marquee__track {
	animation-direction: reverse;
}

.od-marquee:hover .od-marquee__track,
.od-marquee:focus-within .od-marquee__track {
	animation-play-state: paused;
}

@keyframes od-marquee {
	from { transform: translate3d(0, 0, 0); }
	to   { transform: translate3d(-50%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
	.od-marquee__track {
		animation: none;
		flex-wrap: wrap;
		width: 100%;
		justify-content: center;
	}

	.od-marquee {
		-webkit-mask-image: none;
		mask-image: none;
	}
}

/* --------------------------------------------------------------------------
   6. Slider — scroll-snap track with real buttons.
   No library. Scroll-snap does the movement; the buttons just scrollBy().
   Keyboard and touch work for free because it is a scroll container.
   -------------------------------------------------------------------------- */

.od-slider {
	position: relative;
}

.od-slider__track {
	display: flex;
	gap: var(--od-gap-grid);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	-webkit-overflow-scrolling: touch;
	padding-bottom: var(--od-s-4);
	scrollbar-width: none;
}

.od-slider__track::-webkit-scrollbar {
	display: none;
}

.od-slider__slide {
	flex: 0 0 86%;
	scroll-snap-align: start;
}

@media (min-width: 600px) {
	.od-slider__slide { flex-basis: 46%; }
}

@media (min-width: 1200px) {
	.od-slider__slide { flex-basis: 31.5%; }
}

.od-slider__nav {
	display: flex;
	gap: var(--od-s-3);
	margin-top: var(--od-s-5);
}

.od-slider__btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 46px;
	height: 46px;
	border-radius: 50%;
	border: 1.5px solid var(--od-line-strong);
	background: var(--od-surface);
	color: var(--od-ink);
	cursor: pointer;
	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-slider__btn:hover:not(:disabled) {
	background: var(--od-primary);
	border-color: var(--od-primary);
	color: #fff;
}

.od-slider__btn:disabled {
	opacity: 0.35;
	cursor: default;
}

.od-section--ink .od-slider__btn {
	background: transparent;
	border-color: var(--od-on-ink-line);
	color: var(--od-on-ink);
}

/* --------------------------------------------------------------------------
   7. Review card
   -------------------------------------------------------------------------- */

/*
 * height: 100% alone did not equalise these. The slide is a flex item and does
 * stretch, but a percentage height on its child only resolves against a parent
 * with a definite height — so short quotes kept collapsing to their content and
 * the row came out ragged. Making the slide itself a flex container removes the
 * percentage from the equation entirely.
 */
.od-slider__slide {
	display: flex;
}

.od-slider__slide > * {
	width: 100%;
}

.od-review {
	display: flex;
	flex-direction: column;
	gap: var(--od-s-4);
	height: 100%;
	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);
	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-review:hover {
	border-color: var(--od-primary);
	box-shadow: var(--od-shadow-2);
	transform: translateY(-4px);
}

/* The quote is the point of the card, so it is what gains on hover. */
.od-review:hover .od-review__avatar {
	background: var(--od-primary);
	color: #fff;
}

.od-review__avatar {
	transition: background-color var(--od-dur) var(--od-ease),
		color var(--od-dur) var(--od-ease);
}

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

	.od-review:hover {
		transform: none;
	}
}

/*
 * Green, not the amber --od-star. A rating is an evidence mark, which is exactly
 * what the second accent is reserved for, and amber was a third colour on a page
 * that runs on two. green-600 rather than green: these are small glyphs on a
 * white card, so they need the 4.5:1 text threshold, not the 3:1 graphics one.
 */
.od-review__stars {
	display: inline-flex;
	gap: 2px;
	color: var(--od-green-600);
	font-size: var(--od-fs-body);
	letter-spacing: 2px;
}

.od-review__text {
	font-size: var(--od-fs-body);
	line-height: 1.6;
	color: var(--od-ink);
	margin: 0;
}

.od-review__foot {
	display: flex;
	align-items: center;
	gap: var(--od-s-3);
	margin-top: auto;
	padding-top: var(--od-s-4);
	border-top: 1px solid var(--od-line);
}

/* Initials avatar — no stock photography, ever. */
.od-review__avatar {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	width: 42px;
	height: 42px;
	border-radius: 50%;
	background: var(--od-primary-100);
	color: var(--od-primary-600);
	font-family: var(--od-font-head);
	font-weight: 700;
	font-size: var(--od-fs-sm);
}

.od-review__name {
	display: block;
	font-weight: 600;
	font-size: var(--od-fs-sm);
	color: var(--od-ink);
}

.od-review__meta {
	display: block;
	font-size: var(--od-fs-xs);
	color: var(--od-muted);
}

/* Loud, unmissable, editors-only. These quotes are placeholders and must never
   be mistaken for real client feedback. */
.od-review--dummy {
	border-style: dashed;
	border-color: var(--od-star);
}

.od-dummy-warning {
	display: flex;
	align-items: center;
	gap: var(--od-s-2);
	margin-bottom: var(--od-s-5);
	padding: var(--od-s-4) var(--od-s-5);
	border-radius: var(--od-r);
	background: #FFF6E5;
	border: 1px solid var(--od-star);
	color: #7A4E00;
	font-size: var(--od-fs-sm);
	font-weight: 600;
}

/* --------------------------------------------------------------------------
   7b. Testimonials — summary panel beside the reel
   -------------------------------------------------------------------------- */

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

.od-testimonials__title {
	font-size: var(--od-fs-h2);
	color: var(--od-on-ink);
	margin: 0;
	/* Balanced, or a four-word heading breaks as three words then one orphan. */
	text-wrap: balance;
}

/*
 * One line under the heading, where three ticked statements used to sit.
 *
 * The panel is a narrow column beside the quote reel, so anything stacked in it
 * is read before the quotes are reached. Three claims about our own honesty,
 * each with a green tick, were both more furniture than the point needed and
 * slightly self-defeating. The plain sentence says it once.
 */
.od-testimonials__lead {
	margin: 0;
	font-size: var(--od-fs-base);
	line-height: 1.6;
	color: var(--od-on-ink-muted);
	max-width: 34ch;
}

@media (min-width: 1200px) {
	.od-testimonials__inner {
		grid-template-columns: minmax(320px, 0.82fr) minmax(0, 1.4fr);
		gap: var(--od-gap-grid-lg);
		align-items: stretch;
	}

	/* The reel is allowed to run off the right edge — a slider that stops
	   neatly inside the container does not read as scrollable. */
	.od-testimonials__slider {
		margin-right: calc(var(--od-gutter) * -1);
	}

	.od-testimonials__slider .od-slider__slide {
		flex-basis: 46%;
	}
}

/* --------------------------------------------------------------------------
   8. Micro-interactions
   Everything here is transform/opacity only, and everything is disabled under
   prefers-reduced-motion by the global rule in reset.css.
   -------------------------------------------------------------------------- */

/* Hover lift, applied by adding the class — never by element type. */
.od-lift:hover {
	transform: translateY(-4px);
	box-shadow: var(--od-shadow-3);
	border-color: var(--od-primary);
}

.od-lift:hover .od-squircle {
	transform: scale(1.06) rotate(-4deg);
}

/* Link with an underline that draws in from the left. */
.od-underline {
	position: relative;
	text-decoration: none;
}

.od-underline::after {
	content: "";
	position: absolute;
	left: 0;
	bottom: -3px;
	width: 100%;
	height: 1.5px;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: left;
	transition: transform var(--od-dur) var(--od-ease);
}

.od-underline:hover::after {
	transform: scaleX(1);
}

/*
 * Reveal stagger. The observer sets --od-i on each child, so siblings arrive
 * in sequence instead of snapping in as one block. Capped so a long list never
 * leaves the last item waiting seconds to appear.
 */
.js [data-reveal] {
	transition-delay: calc(var(--od-i, 0) * 70ms);
}

@media (prefers-reduced-motion: reduce) {
	.js [data-reveal] {
		transition-delay: 0ms;
	}
}

/* --- Screenshot slider ---------------------------------------------------- */
/*
 * A one-up slider for framed screenshots: arrows outside the frame, dots below,
 * and the figures the slide proves listed under it. Built on .od-slider, which
 * already gives the scroll-snap track and the arrow buttons — this only changes
 * the slide to full width and moves the controls.
 */

.od-shots {
	position: relative;
}

/*
 * The track scrolls, so it clips. --od-shadow-3 is a 60px-blur drop shadow and
 * the previous 8px of padding cut it off flat against the track edge, which read
 * as a hard grey band down the sides of the frame rather than as a shadow.
 *
 * The padding is now pulled back out with an equal negative margin so the slides
 * still line up with the container. That margin is --od-gutter exactly, not a
 * spacing step: the gutter is the container's own inline padding, so cancelling
 * it lets the track run to the viewport edge and can never overflow the page —
 * a fixed 48px would have done at every width below 1200. The scroll-padding
 * keeps snapping honest; without it each slide settles one padding off centre.
 */
.od-shots__track {
	padding: var(--od-s-5) var(--od-gutter) var(--od-s-7);
	margin-inline: calc(var(--od-gutter) * -1);
	scroll-padding-inline: var(--od-gutter);
	gap: var(--od-s-6);
}

.od-shots__slide {
	/* One at a time. The default .od-slider__slide shows two or three across,
	   which would make a dashboard screenshot unreadable. */
	flex: 0 0 100%;
	scroll-snap-align: center;
	/*
	 * REQUIRED. .od-slider__slide is display:flex (so review cards fill their
	 * slide height), and without this the frame and its caption row inherit
	 * `row` — the screenshot collapsed to half width with the points stacked
	 * beside it instead of sitting underneath.
	 */
	flex-direction: column;
}

.od-shots__frame {
	margin: 0;
	/*
	 * The frame takes its height from the report inside it.
	 *
	 * .od-results__frame pins `aspect-ratio: 1200 / 620` and clips with
	 * `overflow: hidden`. That is right for the drawn dashboard it was written
	 * for — that graphic is built to the ratio — but a real export is whatever
	 * shape the screen it came from happened to be, and the fixed box cut the
	 * bottom off every one of them.
	 *
	 * This has to live here rather than on .od-results__frame--case: the slide
	 * markup is `class="od-results__frame od-shots__frame"` and never carries
	 * the --case modifier, so a rule written against it does nothing. Equal
	 * specificity to the base rule, and blocks.css loads after sections.css,
	 * so this wins on order.
	 */
	aspect-ratio: auto;
	max-width: none;
}

/* Arrows sit vertically centred against the frame, outside it where there is
   room and overlapping its edge where there is not. */
.od-shots__arrow {
	position: absolute;
	top: calc(50% - 60px);
	z-index: 2;
	box-shadow: var(--od-shadow-2);
	font-size: 1.5rem;
	line-height: 1;
	padding-bottom: 3px;
}

.od-shots__arrow--prev { left: calc(var(--od-s-2) * -1); }
.od-shots__arrow--next { right: calc(var(--od-s-2) * -1); }

@media (min-width: 1200px) {
	.od-shots__arrow--prev { left: calc(var(--od-s-8) * -1); }
	.od-shots__arrow--next { right: calc(var(--od-s-8) * -1); }
}

.od-shots__arrow:disabled {
	opacity: 0.35;
	cursor: default;
}

.od-shots__meta {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--od-s-3);
	margin-top: var(--od-s-5);
	text-align: center;
}

.od-shots__name {
	font-size: var(--od-fs-h4);
	color: var(--od-ink);
	margin: 0;
}

.od-shots__points {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: stretch;
	gap: var(--od-s-3);
}

/*
 * Points as plates, not a bare tick and a line of text. The reference sets them
 * as plain text separated by a pipe, which disappears under a large screenshot;
 * a tinted plate holds its own against the frame above it and gives the tick
 * somewhere to live.
 */
.od-shots__point {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-3);
	padding: var(--od-s-3) var(--od-s-5) var(--od-s-3) var(--od-s-3);
	background: var(--od-surface);
	border: 1px solid var(--od-line);
	border-radius: var(--od-r-pill);
	box-shadow: var(--od-shadow-0);
	font-size: var(--od-fs-sm);
	font-weight: 500;
	color: var(--od-ink);
	text-align: left;
}

/* Green: these are the section's evidence claims, which is exactly what the
   second accent is reserved for. */
.od-shots__tick {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 26px;
	height: 26px;
	flex: none;
	border-radius: 50%;
	background: var(--od-green-100);
	color: var(--od-green-600);
}

.od-shots__tick .od-icon {
	width: 15px;
	height: 15px;
}

.od-shots__dots {
	display: flex;
	justify-content: center;
	gap: var(--od-s-2);
	margin-top: var(--od-s-5);
}

.od-slider__dot {
	width: 8px;
	height: 8px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: var(--od-line-strong);
	cursor: pointer;
	transition: background-color var(--od-dur) var(--od-ease),
		width var(--od-dur) var(--od-ease);
}

/* The active dot stretches rather than only changing colour — colour alone is
   not a sufficient state indicator. */
.od-slider__dot.is-active {
	width: 24px;
	border-radius: var(--od-r-pill);
	background: var(--od-primary);
}

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

/* --- Testimonials reel ---------------------------------------------------- */
/*
 * The quotes drift on their own, like the industries strip. Height is taken from
 * the summary panel beside them so the two halves of the section square off,
 * which is why every link in the chain — marquee, track, item, card — carries a
 * full height: a marquee track is inline-flex sized to its content by default
 * and would otherwise collapse to the tallest quote.
 */
.od-testimonials__marquee {
	height: 100%;
	/*
	 * Breathing room INSIDE the clip box.
	 *
	 * .od-marquee is overflow: hidden — it has to be, or the duplicated track
	 * would show past both ends — and that clip cuts the review cards' hover
	 * lift and shadow off flat at the top and bottom edges. overflow-x: hidden
	 * with overflow-y: visible is not an option: the spec forces the visible
	 * axis to auto as soon as the other is hidden, which would add a scrollbar.
	 * Padding gives the lift somewhere to go without changing the clip.
	 */
	/* Asymmetric on purpose: the top only has to clear a 4px lift, while the
	   bottom has to clear --od-shadow-2's 10px offset plus its 24px blur. */
	padding-block: var(--od-s-4) var(--od-s-6);
}

.od-testimonials__track {
	height: 100%;
	align-items: stretch;
	gap: var(--od-gap-grid);
}

.od-testimonials__item {
	display: flex;
	flex: 0 0 auto;
	width: min(84vw, 380px);
}

.od-testimonials__item > .od-review {
	width: 100%;
}

/* ==========================================================================
   Contact page (page-contact.php)
   ========================================================================== */

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

/*
 * TWO classes. .od-pill sets the eyebrow's own type, but the element is an
 * <h1> here, so the bare h1 rule in base/typography.css still supplies a
 * display weight and the reset supplies a top margin. Same guard the homepage
 * hero eyebrow carries for the same reason.
 */
.od-contact .od-contact__eyebrow {
	margin-top: 0;
	line-height: 1.2;
}

/*
 * No character cap. It was 18ch, which at display size broke a six-word
 * heading over three lines inside a column already narrower than the measure
 * -- the column is the constraint, so capping again just made it worse.
 */
.od-contact__title {
	margin-bottom: var(--od-s-5);
}

.od-contact__lead {
	margin-bottom: var(--od-s-6);
	max-width: 54ch;
}

.od-contact__prose {
	margin-bottom: var(--od-s-6);
}

.od-contact__points {
	margin-bottom: 0;
}

@media (min-width: 900px) {
	.od-contact__inner {
		/*
		 * Even halves, not 45/55. With the details and the map moved into
		 * their own band below, the copy column is heading + lead + three
		 * ticks, which is close to the form's height; the wider column also
		 * gives the display-size heading enough room for two lines.
		 */
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		gap: var(--od-gap-grid-lg);
	}
}

/* --- The details / map band -------------------------------------------- */

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

.od-contact-info__title {
	font-size: var(--od-fs-h3);
	margin: 0 0 var(--od-s-6);
}

.od-contact__details {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: var(--od-s-5);
}

.od-contact__detail {
	display: flex;
	align-items: flex-start;
	gap: var(--od-s-4);
	color: var(--od-body);
}

/*
 * A tinted plate rather than a bare glyph: the icons ship at assorted
 * intrinsic sizes, so the SVG is pinned rather than left to size itself.
 */
.od-contact__icon {
	flex: none;
	width: 44px;
	height: 44px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: var(--od-r-sm);
	background: var(--od-primary-100);
	color: var(--od-primary-600);
}

.od-contact__icon svg {
	width: 20px;
	height: 20px;
}

.od-contact__label {
	display: block;
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin-bottom: 2px;
}

.od-contact__detail a {
	color: var(--od-ink);
	font-weight: 600;
	text-decoration: none;
}

.od-contact__detail a:hover,
.od-contact__detail a:focus-visible {
	color: var(--od-primary-600);
	text-decoration: underline;
}

/* The map is the second grid cell here, so it must not carry the standalone
   top margin it uses when it follows body copy. */
.od-contact-info .od-map {
	margin-top: 0;
}

@media (min-width: 900px) {
	.od-contact-info__inner {
		/* Details a little narrower than the map: the address is the longest
		   line and reads better on two lines than on four, and the map needs
		   the width to be worth showing at all. */
		grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
		gap: var(--od-gap-grid-lg);
	}
}


/* ==========================================================================
   Office map (template-parts/map.php)
   ========================================================================== */

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

.od-map__label {
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin: 0 0 var(--od-s-3);
}

/*
 * aspect-ratio on the frame rather than a fixed height on the iframe: the
 * embed has no intrinsic size, so without this it collapses to the browser's
 * default 150px and the pin sits off-centre.
 */
.od-map__frame {
	position: relative;
	aspect-ratio: 16 / 10;
	border-radius: var(--od-r);
	overflow: hidden;
	border: var(--od-rule) solid var(--od-line);
	background: var(--od-alt);
}

.od-map__frame iframe {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	border: 0;
	display: block;
}

.od-map__link {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
	margin-top: var(--od-s-4);
	font-size: var(--od-fs-sm);
	font-weight: 600;
	color: var(--od-primary-600);
	text-decoration: none;
}

.od-map__link:hover,
.od-map__link:focus-visible {
	text-decoration: underline;
}

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

/* --- In the ink footer ------------------------------------------------- */

/*
 * Shorter and squarer than the contact-page map: the footer column is narrow,
 * and a 16:10 frame in it would push the copyright line most of a screen down.
 */
/*
 * Full column width. The 320px cap left the map floating in a column wider
 * than it was, reading as an image someone forgot to size rather than as a
 * map of where we are.
 */
.od-footer .od-map--footer {
	margin-top: var(--od-s-5);
	width: 100%;
}

.od-footer .od-map--footer .od-map__frame {
	aspect-ratio: 3 / 2;
	/* The hairline is invisible against the ink band; this is the same inset
	   treatment the footer social discs use. */
	border-color: var(--od-on-ink-line);
	background: var(--od-ink-3);
}

.od-footer .od-map--footer .od-map__link {
	color: var(--od-on-ink);
}

.od-footer .od-map--footer .od-map__link:hover,
.od-footer .od-map--footer .od-map__link:focus-visible {
	color: var(--od-primary-100);
}

/*
 * The two-tone heading device on the ink footer.
 *
 * .od-accent-text is --od-primary (#4E12F0), which is a dark violet: on the
 * ink band it reads as a hole in the text rather than a highlight. The tint is
 * what carries the same idea on a dark surface, exactly as the founder
 * nameplate does. Scoped to .od-footer so the accent keeps its normal colour
 * everywhere else on the site.
 */
.od-footer .od-accent-text {
	color: var(--od-primary-100);
}

/* ==========================================================================
   Archives (blog index, case studies) and single post / case study
   ========================================================================== */

/*
 * TWO classes, same reason as the hero and contact-page eyebrows: this is an
 * <h1> carrying .od-pill, so the bare h1 rule in base/typography.css still
 * supplies a display weight and the reset a top margin.
 */
.od-archive .od-archive__eyebrow,
.od-case .od-case__eyebrow {
	margin-top: 0;
	line-height: 1.2;
}

/*
 * Full width, and scoped to the two archive templates only -- .od-archive is
 * on /blog/ and /case-studies/ and nowhere else, so the measure cap every
 * other section header carries is untouched.
 *
 * Two selectors are needed: .od-section-header sets max-width: var(--od-measure)
 * and .od-section-header--lead overrides it with --od-container-text, so
 * releasing one still leaves the other holding the heading in.
 *
 * The subtitle under these headings was removed with the cap. An archive is a
 * heading over a grid of cards; a paragraph of intro above the first row only
 * pushed the cards further down the page.
 */
.od-archive .od-archive__header,
.od-archive .od-section-header--lead {
	max-width: none;
	margin-bottom: var(--od-s-8);
}

.od-archive .od-section-header__title {
	max-width: none;
}

/* --- Empty states ------------------------------------------------------ */

.od-archive__empty {
	text-align: center;
	max-width: 44rem;
	margin-inline: auto;
}

.od-archive__empty-title {
	font-size: var(--od-fs-h3);
	margin: 0 0 var(--od-s-4);
}

.od-archive__empty p {
	color: var(--od-body);
	margin: 0 0 var(--od-s-6);
}

/* Editor-only, so it is deliberately quiet -- it is scaffolding, not copy. */
.od-archive__note {
	margin: var(--od-s-6) 0 0;
	font-size: var(--od-fs-sm);
	color: var(--od-muted);
}

/* --- Single case study / single post ----------------------------------- */

.od-case__title {
	margin-bottom: var(--od-s-4);
}

.od-case__meta,
.od-post__meta {
	font-size: var(--od-fs-sm);
	color: var(--od-muted);
	margin: 0;
}

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

.od-post__eyebrow a:hover,
.od-post__eyebrow a:focus-visible {
	text-decoration: underline;
}

/*
 * Editor images inside an article, and the block editor's wide/full
 * alignments with them.
 *
 * The block editor emits .alignwide / .alignfull expecting a theme that opts
 * into full-bleed layout. This one does not -- everything lives inside
 * .od-container -- so an aligned image escaped the column and ran to the
 * viewport edge while the paragraph above it stopped at the margin. These
 * pull it back to the column it sits in.
 *
 * The standalone .od-case__media / .od-post__media rules that used to be here
 * carried `margin: var(--od-s-7) 0`, and the `0` overwrote .od-container's
 * own `margin-inline: auto` -- which is why that figure sat left-aligned
 * rather than centred. Both classes are gone now; the shorthand is the part
 * worth remembering.
 */
.od-prose img,
.od-prose figure {
	max-width: 100%;
	height: auto;
}

.od-prose .alignwide,
.od-prose .alignfull {
	width: 100%;
	max-width: 100%;
	margin-inline: 0;
}

.od-case__img,
.od-post__img {
	display: block;
	width: 100%;
	height: auto;
}

.od-case__metric {
	margin: var(--od-s-7) 0;
}

.od-case__foot,
.od-post__foot {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: var(--od-s-4);
	margin-top: var(--od-s-8);
	padding-top: var(--od-s-6);
	border-top: var(--od-rule) solid var(--od-line);
}

/* --- Next / previous --------------------------------------------------- */

.od-post-nav {
	margin-top: var(--od-s-7);
}

.od-post-nav .nav-links {
	display: grid;
	gap: var(--od-s-4);
}

.od-post-nav a {
	display: block;
	padding: var(--od-s-5);
	border: var(--od-rule) solid var(--od-line);
	border-radius: var(--od-r);
	text-decoration: none;
	color: var(--od-ink);
}

.od-post-nav a:hover,
.od-post-nav a:focus-visible {
	border-color: var(--od-primary);
}

.od-post-nav__label {
	display: block;
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin-bottom: 2px;
}

.od-post-nav__title {
	font-family: var(--od-font-head);
	font-weight: var(--od-fw-head);
}

/* Paged content inside one post. Rare, but unstyled it is a row of bare
   numbers with no spacing at all. */
.od-page-links {
	display: flex;
	gap: var(--od-s-3);
	margin-top: var(--od-s-6);
	font-weight: 600;
}

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

/* --- Single post: split hero, TOC, sticky rail -------------------------- */

.od-post__hero-inner {
	display: grid;
	/* s-6, not s-7: with the hero band no longer --lg, an s-7 gap between
	   the copy and the image was the largest space left on the screen. */
	gap: var(--od-s-6);
	grid-template-columns: 1fr;
	/* Top-aligned, not centred: the copy column is the taller of the two, and
	   centring floated the image into the middle of a ragged gap. */
	align-items: start;
}

/*
 * The hero and the article are one continuous read, so the bottom of the band
 * only has to separate the byline from what follows. A full section padding
 * there, on top of .od-post__main's own top padding, stacked into an empty
 * strip between the two.
 */
.od-post__hero {
	padding-bottom: clamp(28px, 2vw + 16px, 48px);
}

/*
 * Deliberately smaller than a landing-page H1. Those headings carry a whole
 * page; this one is a title above the thing the reader already chose to open,
 * and at the full display size it was the loudest element on a page of prose.
 */
.od-post__title {
	font-size: clamp(1.75rem, 1.25rem + 2vw, 2.75rem);
	line-height: 1.15;
	margin: 0 0 var(--od-s-4);
	max-width: 22ch;
}

.od-post__standfirst {
	margin-bottom: var(--od-s-5);
	max-width: 52ch;
}

/*
 * The byline is a definition-style row: small grey label, value under it. As a
 * single run-on line ("Orion Digital - 23 August 2026 - 2 minutes") it read as
 * one string of unrelated facts.
 */
.od-post__byline {
	list-style: none;
	margin: 0;
	padding: var(--od-s-4) 0 0;
	border-top: var(--od-rule) solid var(--od-line);
	display: flex;
	flex-wrap: wrap;
	gap: var(--od-s-6);
}

.od-post__byline-label {
	display: block;
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin-bottom: 2px;
}

.od-post__byline-value {
	font-weight: 600;
	color: var(--od-ink);
}

.od-post__hero-media {
	margin: 0;
	border-radius: var(--od-r-lg);
	overflow: hidden;
	box-shadow: var(--od-shadow-2, 0 18px 48px rgba(20, 20, 43, 0.12));
}

.od-post__img {
	display: block;
	width: 100%;
	height: auto;
}

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

.od-post__tag {
	display: inline-block;
	font-size: var(--od-fs-sm);
	font-weight: 600;
	color: var(--od-primary-600);
	text-decoration: none;
}

.od-post__tag:hover,
.od-post__tag:focus-visible {
	text-decoration: underline;
}

/* --- The two-column body ------------------------------------------------ */

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

/*
 * The hero band already closed the gap above, so this only needs enough to
 * separate the two -- a full section top padding on top of the hero bottom
 * padding read as a missing section.
 */
.od-post__main {
	padding-top: var(--od-s-6);
}

/*
 * The reading column is capped inside a full-width container rather than the
 * container being narrowed: that keeps the rail beside it and the page edges
 * aligned with the rest of the site, while body copy stays at a measure
 * anyone can track back from.
 */
/*
 * No measure cap here: the grid column IS the measure. With a 1140px
 * container and a 280-340px rail beside it, the reading column lands around
 * 736px, and a 68ch cap (~620px) stopped the text short of its own column and
 * left a ragged strip of white before the rail.
 *
 * The inner caps have to be lifted too, or the paragraphs stay at 68ch inside
 * an uncapped parent and nothing appears to change.
 */
.od-post__body,
.od-prose.od-post__body p,
.od-prose.od-post__body ul,
.od-prose.od-post__body ol {
	max-width: none;
}

/*
 * Tighter than the shared .od-prose rhythm, which is tuned for a few short
 * paragraphs on an inner page. Over a long article that spacing broke every
 * paragraph into its own island. Both values are em-based, so they track the
 * type size instead of jumping at a breakpoint.
 *
 * Two classes, and it has to stay that way: .od-prose > * + * lives in
 * base/layout.css and would otherwise win on source order alone.
 */
.od-prose.od-post__body > * + * {
	margin-top: 1.15em;
}

/*
 * Fixed units, NOT em. An em on a heading resolves against the heading's own
 * font-size, so the 1.9em here was 1.9 x 40px = 76px above every h2 and a
 * different figure again above every h3 -- the gap grew with the very element
 * it was meant to sit under. rem keeps the rhythm the same everywhere.
 */
.od-prose.od-post__body h2 {
	margin-top: 2.5rem;
}

.od-prose.od-post__body h3 {
	margin-top: 1.75rem;
}

/* --- Table of contents -------------------------------------------------- */

.od-toc {
	/* s-6, not s-7. The card carries its own internal padding, so an s-7
	   margin under it plus that padding read as a gap the article had
	   fallen out of. */
	margin-bottom: var(--od-s-6);
	border: var(--od-rule) solid var(--od-line);
	border-radius: var(--od-r);
	background: var(--od-alt);
	overflow: hidden;
}

.od-toc__heading {
	margin: 0;
	font-size: inherit;
	letter-spacing: normal;
}

/*
 * A button, not a heading with a click handler: the toggle is the control, so
 * it needs to be the focusable, Enter/Space-operable element. The <h2> around
 * it keeps the document outline intact.
 */
.od-toc__toggle {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--od-s-4);
	width: 100%;
	padding: var(--od-s-5) var(--od-s-6);
	background: none;
	border: 0;
	cursor: pointer;
	text-align: left;
	color: var(--od-ink);
	font-family: var(--od-font-head);
	font-size: var(--od-fs-h5, 1.125rem);
	font-weight: 600;
	letter-spacing: var(--od-ls-head, -0.02em);
}

.od-toc__toggle:hover .od-toc__title {
	color: var(--od-primary-600);
}

/* Border-drawn chevron, matching the accordions elsewhere on the site. */
.od-toc__chev {
	flex: none;
	width: 10px;
	height: 10px;
	border-right: 2px solid var(--od-primary);
	border-bottom: 2px solid var(--od-primary);
	transform: rotate(45deg) translate(-2px, -2px);
	transition: transform var(--od-dur, 220ms) ease;
}

.od-toc__toggle[aria-expanded="false"] .od-toc__chev {
	transform: rotate(-45deg) translate(-2px, -2px);
}

.od-toc__panel {
	padding: 0 var(--od-s-6) var(--od-s-6);
}

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

.od-toc__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: var(--od-s-3);
	counter-reset: none;
}

/* Sub-headings step in, so the shape of the article is readable at a glance. */
.od-toc__item--h3 {
	padding-left: var(--od-s-5);
}

.od-toc__link {
	display: inline-flex;
	align-items: baseline;
	gap: var(--od-s-2);
	color: var(--od-ink);
	text-decoration: none;
	font-weight: 500;
	line-height: 1.4;
}

.od-toc__link:hover,
.od-toc__link:focus-visible {
	color: var(--od-primary-600);
	text-decoration: underline;
}

/* --- The sidebar consultation card -------------------------------------- */

.od-postcta {
	border: var(--od-rule) solid var(--od-line);
	border-radius: var(--od-r-lg);
	overflow: hidden;
	background: var(--od-surface);
	box-shadow: var(--od-shadow-1, 0 8px 24px rgba(20, 20, 43, 0.06));
}

.od-postcta__body {
	padding: var(--od-s-6);
}

.od-postcta__eyebrow {
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-primary-600);
	font-weight: 600;
	margin: 0 0 var(--od-s-3);
}

.od-postcta__title {
	font-size: var(--od-fs-h4);
	margin: 0 0 var(--od-s-4);
}

.od-postcta__text {
	font-size: var(--od-fs-sm);
	color: var(--od-body);
	margin: 0 0 var(--od-s-5);
}

.od-postcta__note {
	font-size: var(--od-fs-xs);
	color: var(--od-muted);
	margin: var(--od-s-4) 0 0;
	text-align: center;
}

@media (min-width: 900px) {
	.od-post__hero-inner {
		/* Even halves. */
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		gap: var(--od-gap-grid-lg);
	}

	.od-post__layout {
		grid-template-columns: minmax(0, 1fr) minmax(280px, 340px);
		gap: var(--od-gap-grid-lg);
	}

	/*
	 * Sticky, so the offer stays on screen for the whole read. The offset
	 * clears the sticky site header; without it the card slides underneath.
	 */
	.od-post__rail {
		position: sticky;
		top: calc(var(--od-header-h) + 32px);
	}
}


/* ==========================================================================
   Page hero -- the shared top-of-page treatment
   ========================================================================== */

/*
 * The violet wash, the blurred orb and the faint grid that .od-svc-hero
 * carries, lifted into a class every template can use.
 *
 * It could not simply be reused: .od-svc-hero lives in service.css, which is
 * enqueued only on is_singular('od_service'), so the contact page and the two
 * archives had no access to it and opened flat white while every other page
 * on the site opened on the wash. .od-svc-hero keeps its own copy rather than
 * being rewritten to use this one -- the service hero is a two-column layout
 * with its own measurements, and merging them would have meant reworking a
 * page type that is already signed off.
 */
.od-pagehero {
	position: relative;
	/* Matches .od-svc-hero: nothing sits above a page hero but the header, so
	   a full band of section padding is dead space before the H1. */
	padding-top: clamp(32px, 2vw + 20px, 56px);
	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;
}

.od-pagehero::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;
}

.od-pagehero::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. A pseudo-element paints above ordinary children, so
 * without this the grid overlay sits on top of the heading and the form.
 */
.od-pagehero > * {
	position: relative;
	z-index: 1;
}

/*
 * A second .od-archive .od-section-header--lead used to sit here, capping the
 * header at --od-container-text. It was written before the archive subtitles
 * were removed, and being LATER in the file it beat the max-width: none set
 * near .od-archive__header at equal specificity -- which is why the headings
 * kept wrapping to two lines after the cap had supposedly been lifted.
 * Deleted; the rule higher up is now the only one.
 */

/* ==========================================================================
   Author box (template-parts/author-box.php)
   ========================================================================== */

.od-authorbox {
	display: grid;
	gap: var(--od-s-5);
	grid-template-columns: 1fr;
	align-items: start;
	margin-top: var(--od-s-8);
	padding: var(--od-s-6);
	border: var(--od-rule) solid var(--od-line);
	border-radius: var(--od-r-lg);
	background: var(--od-alt);
}

.od-authorbox__media {
	width: 96px;
	aspect-ratio: 1;
	border-radius: 50%;
	overflow: hidden;
	flex: none;
}

.od-authorbox__img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: 50% 25%;
}

.od-authorbox__eyebrow {
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin: 0 0 var(--od-s-2);
}

.od-authorbox__name {
	font-size: var(--od-fs-h4);
	margin: 0;
}

.od-authorbox__role {
	color: var(--od-primary-600);
	font-weight: 600;
	font-size: var(--od-fs-sm);
	margin: 2px 0 var(--od-s-4);
}

.od-authorbox__bio {
	color: var(--od-body);
	margin: 0;
}

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

/*
 * Filled discs rather than bordered circles. A 1px border at this size reads
 * as a smudge next to the solid marks inside it, which is the same reason the
 * footer socials were rebuilt as discs.
 */
.od-authorbox__social {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	background: var(--od-surface);
	border: var(--od-rule) solid var(--od-line);
	color: var(--od-ink);
	text-decoration: none;
	transition: background var(--od-dur) var(--od-ease), color var(--od-dur) var(--od-ease), transform var(--od-dur) var(--od-ease);
}

.od-authorbox__social:hover,
.od-authorbox__social:focus-visible {
	background: var(--od-primary);
	border-color: var(--od-primary);
	color: #fff;
	transform: translateY(-2px);
}

/* The icons ship at assorted intrinsic sizes, so the SVG is pinned. */
.od-authorbox__social .od-icon svg {
	width: 18px;
	height: 18px;
}

@media (min-width: 600px) {
	.od-authorbox {
		grid-template-columns: 96px minmax(0, 1fr);
		gap: var(--od-s-6);
		padding: var(--od-s-7);
	}
}


/* ==========================================================================
   Breadcrumbs
   ========================================================================== */

/*
 * Visible navigation only -- the BreadcrumbList schema is Rank Math's job.
 * Sized down to eyebrow scale so it reads as orientation above the heading
 * rather than as a second line of copy competing with it.
 */
.od-crumbs {
	margin-bottom: var(--od-s-4);
}

.od-crumbs__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--od-s-2);
	font-size: var(--od-fs-sm);
	line-height: 1.4;
}

.od-crumbs__item {
	display: inline-flex;
	align-items: center;
	gap: var(--od-s-2);
}

.od-crumbs__link {
	color: var(--od-muted);
	text-decoration: none;
}

.od-crumbs__link:hover,
.od-crumbs__link:focus-visible {
	color: var(--od-primary-600);
	text-decoration: underline;
}

.od-crumbs__current {
	color: var(--od-body);
	font-weight: 500;
	/* The leaf is the page title, which can be long. Truncating keeps the
	   trail on one line instead of wrapping to three. */
	max-width: 32ch;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.od-crumbs__sep {
	color: var(--od-line);
}


/* ==========================================================================
   Single case study
   ========================================================================== */

.od-case__hero-inner {
	display: grid;
	gap: var(--od-s-6);
	grid-template-columns: 1fr;
	align-items: start;
	margin-bottom: var(--od-s-7);
}

.od-case__hero-media {
	margin: 0;
	border-radius: var(--od-r-lg);
	overflow: hidden;
	box-shadow: var(--od-shadow-2, 0 18px 48px rgba(20, 20, 43, 0.12));
}

.od-case__hero .od-case__eyebrow {
	margin-bottom: var(--od-s-4);
}

/* Matched to .od-post__title: smaller than a landing-page H1, because the
   heading sits above the thing the reader already chose to open. */
.od-case__hero .od-case__title {
	font-size: clamp(1.75rem, 1.25rem + 2vw, 2.75rem);
	line-height: 1.15;
	max-width: 24ch;
}

.od-case__lead {
	margin-bottom: var(--od-s-6);
}

/* --- The fact strip ----------------------------------------------------- */

/*
 * A <dl>, not a row of divs: label/value pairs are what a description list is
 * for, and it gives a screen reader the pairing for free.
 */
.od-case__facts {
	display: grid;
	gap: var(--od-s-5);
	grid-template-columns: 1fr;
	margin: 0;
	padding-top: var(--od-s-5);
	border-top: var(--od-rule) solid var(--od-line);
}

.od-case__fact-label {
	font-size: var(--od-fs-xs);
	text-transform: uppercase;
	letter-spacing: var(--od-ls-eyebrow);
	color: var(--od-muted);
	margin-bottom: 4px;
}

.od-case__fact-value {
	margin: 0;
	font-weight: 600;
	color: var(--od-ink);
}

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

.od-case__chip {
	padding: 4px var(--od-s-3);
	border-radius: var(--od-r-pill);
	background: var(--od-primary-100);
	color: var(--od-primary-600);
	font-size: var(--od-fs-sm);
	font-weight: 600;
}

/* --- The results band --------------------------------------------------- */

.od-case__results-title {
	font-size: var(--od-fs-h3);
	color: var(--od-on-ink);
	margin: 0 0 var(--od-s-7);
}

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

/* The old single-metric rule set a vertical margin; inside a grid that pushed
   every tile out of alignment with its neighbours. */
.od-case__metrics .od-case__metric {
	margin: 0;
}

.od-case__results .od-metric__value {
	color: var(--od-green-light);
}

.od-case__results .od-metric__label {
	color: var(--od-on-ink);
}

/*
 * The comparison line. Quiet on purpose -- it is the qualifier that makes the
 * number above it mean something, not a second claim.
 */
.od-case__metric-note {
	display: block;
	margin-top: var(--od-s-2);
	font-size: var(--od-fs-xs);
	color: var(--od-on-ink-muted);
	max-width: 26ch;
}

/* --- Story and the delivered rail --------------------------------------- */

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

/*
 * No measure cap: the grid column is the measure, same as the blog post body.
 * The inner caps have to be lifted too -- .od-prose p and .od-prose ul/ol
 * each re-apply 68ch from base/, and would otherwise hold the text at the old
 * width inside an uncapped parent.
 */
.od-case__body,
.od-prose.od-case__body p,
.od-prose.od-case__body ul,
.od-prose.od-case__body ol {
	max-width: none;
}

.od-case__movement + .od-case__movement {
	margin-top: var(--od-s-7);
}

.od-case__movement-title {
	font-size: var(--od-fs-h3);
	margin: 0 0 var(--od-s-5);
}

/* In the main column now, so it gets room to breathe above it and the list
   runs in two columns rather than one long stack. */
.od-case__delivered {
	margin-top: var(--od-s-7);
}

.od-case__back {
	margin: var(--od-s-7) 0 0;
	padding-top: var(--od-s-6);
	border-top: var(--od-rule) solid var(--od-line);
}

.od-case__delivered-title {
	font-size: var(--od-fs-h4);
	margin: 0 0 var(--od-s-5);
}

@media (min-width: 600px) {
	.od-case__delivered-list {
		columns: 2;
		column-gap: var(--od-s-6);
	}

	/* Without this a tick and its text can be split across the column break. */
	.od-case__delivered-list li {
		break-inside: avoid;
	}
}

/* --- The quote band ----------------------------------------------------- */

/*
 * The component default is a card in a grid of cards -- white surface,
 * hairline border, body-size text. Alone on a band it read as a small pale box
 * marooned in a wide empty section, so the card treatment comes off and the
 * words carry it.
 *
 * On --od-alt, the violet-tinted light band. Green stays on the mark, the tick
 * of evidence and the name -- a client saying the work paid off is exactly
 * what the two-accent rule reserves green for -- but the quote itself is ink,
 * because --od-green does not clear 4.5:1 and a whole quote in colour is a
 * wall, not an accent.
 */
.od-case__quote-band .od-case__quote {
	position: relative;
	max-width: 42rem;
	margin-inline: auto;
	padding: 0;
	border: 0;
	background: none;
	text-align: center;
	align-items: center;
	/* The <figure> is a flex column; this is the ONLY gap between the mark,
	   the quote and the attribution, so spacing is set here rather than with
	   margins that would compound with it. */
	gap: var(--od-s-5);
}

/*
 * In flow, not absolutely positioned. Floating it behind the text meant its
 * height did not count, so the section's own padding was all that separated it
 * from the heading above -- and it overlapped the first line at narrow widths.
 */
.od-case__quote-mark {
	display: block;
	font-family: var(--od-font-head);
	font-size: 3.5rem;
	font-weight: 700;
	line-height: 0.6;
	height: 1.6rem;
	color: var(--od-green);
	pointer-events: none;
}

.od-case__quote .od-quote__text {
	font-family: var(--od-font-head);
	font-size: clamp(1.25rem, 1rem + 0.9vw, 1.625rem);
	font-weight: 500;
	line-height: 1.5;
	letter-spacing: var(--od-ls-h3);
	color: var(--od-ink);
	text-wrap: pretty;
}

/* A short green rule between the quote and who said it. Without it the
   attribution reads as another line of the quote. */
.od-case__quote-band .od-quote__cite {
	position: relative;
	padding-top: var(--od-s-5);
}

.od-case__quote-band .od-quote__cite::before {
	content: "";
	position: absolute;
	top: 0;
	left: 50%;
	transform: translateX(-50%);
	width: 40px;
	height: 2px;
	border-radius: 2px;
	background: var(--od-green);
}

/* --od-green-600, not --od-green: this is small text, where the lighter green
   misses 4.5:1. The same reason the check-list ticks split the two. */
.od-case__quote-band .od-quote__name {
	color: var(--od-green-600);
	font-size: var(--od-fs-body);
}

.od-case__quote-band .od-quote__role {
	color: var(--od-muted);
	font-size: var(--od-fs-sm);
}

.od-case__quote .od-quote__cite {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--od-s-3);
}

.od-quote__avatar {
	width: 48px;
	height: 48px;
	border-radius: 50%;
	object-fit: cover;
	flex: none;
}

.od-quote__who {
	display: flex;
	flex-direction: column;
	text-align: left;
}

/*
 * The closing section is the shared .od-cta component (ink band, copy left,
 * form right) rather than a case-study-specific block, so it needs no rules
 * of its own -- the bespoke .od-case__cta-* set that used to live here was
 * deleted with the button-only version it styled.
 */

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

	.od-case__metrics {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 900px) {
	.od-case__hero-inner {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		gap: var(--od-gap-grid-lg);
		align-items: center;
	}

	/*
	 * All four on one row. The services cell holds chips rather than a single
	 * word, so it takes a wider share of the row instead of an equal quarter
	 * -- at 1fr the chips wrapped to three lines and dragged the row's height
	 * with them.
	 */
	.od-case__facts {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 0.7fr) minmax(0, 1.6fr);
		gap: var(--od-s-6);
	}

	.od-case__metrics {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}

	.od-case__layout {
		grid-template-columns: minmax(0, 1fr) minmax(280px, 320px);
		gap: var(--od-gap-grid-lg);
	}

	.od-case__rail {
		position: sticky;
		top: calc(var(--od-header-h) + 32px);
	}

}
