/* ==========================================================================
   Sections — hero, services, work, about, stack, CTA, page headers, blog.
   ========================================================================== */

/* --- Generic section ------------------------------------------------------ */

.zd-section {
	padding-block: var(--section-gap);
	position: relative;
}

.zd-section + .zd-section {
	padding-top: 0;
}

.zd-section--bordered {
	border-top: 1px solid var(--color-border);
	padding-top: var(--section-gap);
}

/* Opt out of the top section gap, for a section that follows a page hero and so
   already has that hero's 64px of bottom padding above it. Stacking both put
   184px between a page title and the thing it titles, which reads as a break
   between two sections rather than a header and its content.

   Applied per template rather than to `.zd-page-hero + .zd-section`, so adopting
   it elsewhere stays a deliberate choice — today that's the blog index and the
   single post. */
.zd-section--tight {
	padding-top: 0;
}

.zd-section-header {
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
	margin-bottom: var(--space-8);
	max-width: var(--measure);
}

.zd-section-header--center {
	align-items: center;
	margin-inline: auto;
	text-align: center;
}

.zd-section-title {
	font-size: var(--fs-h2);
}

/* A section header standing in as the page heading — see the Services template.
   Keyed off the tag rather than a modifier class: if it is the document's h1 it
   should carry h1 weight, and there is nothing to remember to pass. */
h1.zd-section-title {
	font-size: var(--fs-h1);
	letter-spacing: var(--tracking-display);
	line-height: var(--lh-tight);
}

.zd-section-lead {
	color: var(--color-text-muted);
	font-size: var(--fs-lg);
}

/* 64px under a section header is a desktop interval. On a phone, where the
   header is already three or four stacked lines, it reads as a gap rather than
   as rhythm. */
@media (max-width: 768px) {
	.zd-section-header {
		margin-bottom: var(--space-6);
	}
}

/* ==========================================================================
   Hero
   ========================================================================== */

.zd-hero {
	overflow: hidden;
	padding-block: var(--space-20) var(--section-gap);
	position: relative;
}

/* One column of copy. The cube scene isn't a second column — it's absolutely
   positioned over this one (see .zd-cubes), so as the viewport narrows the
   cluster drifts across the copy instead of being squeezed. 640px matches the
   width the old two-column split gave the text at container size. */
.zd-hero__inner {
	display: grid;
	grid-template-columns: minmax(0, 640px);
	position: relative;
	z-index: 1;
}

/* Above the cluster, so the overlap always resolves in favour of the copy. */
.zd-hero__content {
	position: relative;
	z-index: 1;
}

.zd-hero__title {
	font-size: var(--fs-display);
	font-weight: 800;
	letter-spacing: var(--tracking-display);
	line-height: var(--lh-tight);
	margin-block: var(--space-3);
}

/* The second beat lands in brand yellow, lit. White then yellow reads as
   emphasis; white then gray would read as the line trailing off. */
.zd-hero__title span {
	color: var(--color-brand);
	display: block;
	text-shadow: var(--glow-text);
}

.zd-hero__lead {
	color: var(--color-text-muted);
	font-size: var(--fs-lg);
	max-width: 34rem;
}

.zd-hero__actions {
	margin-top: var(--space-5);
}

.zd-hero__meta {
	border-top: 1px solid var(--color-border);
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-5);
	margin-top: var(--space-8);
	padding-top: var(--space-3);
}

.zd-hero__stat {
	display: flex;
	flex-direction: column;
	gap: var(--space-half);
}

.zd-hero__stat b {
	color: var(--color-text);
	font-family: var(--font-heading);
	font-size: var(--fs-h3);
	font-weight: 800;
	letter-spacing: var(--tracking-heading);
}

.zd-hero__stat span {
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	letter-spacing: var(--tracking-label);
	text-transform: uppercase;
}

/* --- Isometric cubes ------------------------------------------------------
   Built in CSS rather than shipped as an image: it stays sharp at any size,
   costs no request, and recolors from tokens. Dark blocks, one lit yellow. */

/* Pinned to the right of the container and centred against the copy, rather
   than sharing the grid with it. The width is clamped instead of fluid, so a
   narrowing viewport slides the cluster leftward over the text — it keeps its
   presence instead of collapsing into a thumbnail. */
.zd-cubes {
	aspect-ratio: 1;
	display: grid;
	perspective: 1200px;
	place-items: center;
	position: absolute;
	right: 0;
	top: 50%;
	transform: translateY(-50%);
	width: clamp(440px, 46vw, 600px);
	z-index: 0;
}

.zd-cubes__scene {
	height: 260px;
	position: relative;
	transform: rotateX(55deg) rotateZ(45deg);
	transform-style: preserve-3d;
	width: 260px;
}

/* Anchored to the centre of the scene, so the --x/--y offsets below read as
   displacement from the middle rather than from the top-left corner. Position
   lives in custom properties so one shared float keyframe can lift every cube
   from wherever it sits — no per-cube keyframes to keep in sync. */
.zd-cube {
	--s: 88px;
	--x: 0px;
	--y: 0px;
	--z: 0px;
	--lift: 6px;
	height: var(--s);
	left: 50%;
	margin-left: calc(var(--s) / -2);
	margin-top: calc(var(--s) / -2);
	position: absolute;
	top: 50%;
	transform: translate3d(var(--x), var(--y), var(--z));
	transform-style: preserve-3d;
	width: var(--s);
}

/* Wireframe, not illustration. Flat three-tone shading is what makes CSS cubes
   read as cartoonish, so the faces stay nearly transparent and the glowing
   edges carry the form — closer to a schematic than a drawing. */
.zd-cube__face {
	position: absolute;
	inset: 0;
	border: 1px solid rgba(255, 255, 255, 0.14);
}

/* Just enough alpha separation between faces to read as volume. */
.zd-cube__face--top {
	background: rgba(255, 255, 255, 0.05);
	transform: translateZ(calc(var(--s) / 2));
}

.zd-cube__face--left {
	background: rgba(255, 255, 255, 0.025);
	transform: rotateY(90deg) translateZ(calc(var(--s) / 2));
}

.zd-cube__face--right {
	background: rgba(0, 0, 0, 0.25);
	transform: rotateX(-90deg) translateZ(calc(var(--s) / 2));
}

/* The lit cube: translucent brand faces, hot edges, and bloom on every plane
   so it reads as emitting light rather than being painted yellow. */
.zd-cube--lit .zd-cube__face {
	border-color: var(--color-brand);
	box-shadow: var(--glow-lg), inset 0 0 32px rgba(var(--brand-rgb), 0.3);
}

.zd-cube--lit .zd-cube__face--top {
	background: rgba(var(--brand-rgb), 0.22);
}

.zd-cube--lit .zd-cube__face--left {
	background: rgba(var(--brand-rgb), 0.12);
}

.zd-cube--lit .zd-cube__face--right {
	background: rgba(var(--brand-rgb), 0.07);
}

/* --- Ground plane ---------------------------------------------------------
   A square grid living inside the rotated scene, so it inherits the isometric
   projection for free and lands exactly on the base cubes' underside. It reads
   as the graph paper the cluster sits on. One cube per cell, so the lines run
   along the block edges instead of cutting through them, and masked to a circle
   so it dissolves rather than ending on a cut edge. */
.zd-cubes__plane {
	position: absolute;
	/* One line per tile at 88px — a single cell — rather than a repeating
	   gradient across the whole box. A repeating gradient anchors its phase to
	   the edge the gradient angle starts from (bottom for the horizontals, left
	   for the verticals), so the two axes registered differently and both
	   shifted whenever the plane was resized. Tiling one cell makes the phase
	   depend only on background-position.

	   Alpha sits a shade above --dot-color: seen through the scene's 55° tilt,
	   the lines foreshorten to less than a pixel of coverage. */
	background-image:
		linear-gradient(to right, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px),
		linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px);
	background-size: 88px 88px;
	/* Registers the grid to the cubes: they're centred on the 260px scene, so
	   their edges land 86px into each cell. Without it the lines cut through
	   the blocks instead of tracing their footprints. Holds for any `inset`
	   that's a whole multiple of the cell. */
	background-position: 86px 86px;
	/* Runs past the cluster and holds full strength a way out before it fades,
	   so the lattice reads as a floor the cubes are standing on rather than a
	   patch under them. Two cells; see the note on background-position. */
	inset: -176px;
	-webkit-mask-image: radial-gradient(circle at 50% 50%, #000 34%, transparent 80%);
	mask-image: radial-gradient(circle at 50% 50%, #000 34%, transparent 80%);
	pointer-events: none;
	/* Bottom face of the base cubes: they sit at z 0 and are 88px deep. */
	transform: translateZ(-44px);
}

/* Bloom behind the cluster. Sits on the scene plane, under everything. */
.zd-cubes__glow {
	background: radial-gradient(circle, rgba(var(--brand-rgb), 0.28) 0%, transparent 62%);
	filter: blur(32px);
	height: 340px;
	left: 50%;
	pointer-events: none;
	position: absolute;
	top: 50%;
	transform: translate(-50%, -50%);
	width: 340px;
	z-index: 0;
}

/* Stepped arrangement. Z lifts the lit cube clear of the base layer. */
.zd-cube--a {
	--x: -88px;
	--y: -88px;
}

.zd-cube--b {
	--y: -88px;
}

.zd-cube--c {
	--x: -88px;
}

.zd-cube--d {
	--x: 88px;
	--y: -88px;
}

.zd-cube--e {
	--x: 88px;
}

.zd-cube--f {
	--y: 88px;
}

.zd-cube--lit {
	--z: 88px;
	--lift: 8px;
}

/* --- Float ----------------------------------------------------------------
   Only the lit cube moves. It rises and settles along the scene's own Z axis,
   so the drift reads as depth rather than as sliding across the page, and the
   still base layer gives it something to be measured against — a whole cluster
   in motion reads as fidgety and there's nothing left to hold the eye. Inside
   the 8px motion budget, on a long period. */
@keyframes zd-float {
	0%,
	100% {
		transform: translate3d(var(--x), var(--y), var(--z));
	}
	50% {
		transform: translate3d(var(--x), var(--y), calc(var(--z) + var(--lift)));
	}
}

.zd-cube--lit {
	animation: zd-float 14s var(--ease) infinite;
}

/* --- Neon breathe ----------------------------------------------------------
   A slow swell with two shallow blinks per cycle, on a 7s period — a dip every
   ~3.5s rather than every 12s, which reads as a tube struggling to hold rather
   than a light that occasionally hiccups.

   The two guards that keep it short of irritating both survive the faster
   cadence: a shallow floor (never below 0.72, so it dips rather than switches
   off) and 420ms transitions — 0.3Hz of dips, an order of magnitude under the
   WCAG 2.3.1 three-flashes-per-second threshold. If the period is shortened
   further, widen the keyframe gaps to hold those transitions above ~350ms.

   The faces and the bloom share one animation so they never drift apart; a
   cube and its glow pulsing out of phase looks broken. */
@keyframes zd-neon-breathe {
	0%,
	100% {
		opacity: 0.88;
	}
	30% {
		opacity: 1;
	}
	54% {
		opacity: 1;
	}
	60% {
		opacity: 0.72;
	}
	66% {
		opacity: 1;
	}
	84% {
		opacity: 1;
	}
	90% {
		opacity: 0.78;
	}
	96% {
		opacity: 1;
	}
}

.zd-cube--lit .zd-cube__face,
.zd-cubes__glow {
	animation: zd-neon-breathe 7s var(--ease) infinite;
}

@media (prefers-reduced-motion: reduce) {
	.zd-cube,
	.zd-cube--lit .zd-cube__face,
	.zd-cubes__glow {
		animation: none;
	}
}

/* Below this the cluster would be sitting on top of the copy rather than
   drifting behind its edge, so it goes entirely. The hero is strong enough on
   type alone, and a shrunken cube scene stacked above the headline reads as a
   decoration that lost its argument. */
@media (max-width: 1024px) {
	.zd-cubes {
		display: none;
	}
}

/* With the cluster gone the hero is a column of type, and 160px of lead-in above
   it is a desktop measurement — on a phone it's most of a screen of nothing
   before the headline arrives. */
@media (max-width: 768px) {
	.zd-hero {
		padding-top: var(--space-15);
	}
}

@media (max-width: 560px) {
	.zd-hero {
		padding-top: var(--space-10);
	}

	/* Wrapping flex items left the widest stat alone on the first row with the
	   other two paired under it, which reads as a mistake rather than a set. A
	   grid gives them a shared left edge whatever the labels measure. */
	.zd-hero__meta {
		display: grid;
		gap: var(--space-3) var(--space-4);
		grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
	}
}

/* ==========================================================================
   Services
   ========================================================================== */

/* Six columns, cards spanning two, so the five services resolve to 3 + 2 with
   the second row balanced at 3 columns each. Five cards in a plain 3-column
   grid would leave a hole, which reads as a mistake rather than a decision. */
.zd-services__grid {
	display: grid;
	gap: var(--space-2);
	grid-template-columns: repeat(6, 1fr);
}

.zd-services__grid > * {
	grid-column: span 2;
}

.zd-services__grid > :nth-child(4),
.zd-services__grid > :nth-child(5) {
	grid-column: span 3;
}

.zd-service__index {
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	letter-spacing: var(--tracking-label);
	position: absolute;
	right: var(--space-3);
	top: var(--space-3);
}

@media (max-width: 900px) {
	.zd-services__grid {
		grid-template-columns: repeat(2, 1fr);
	}

	.zd-services__grid > *,
	.zd-services__grid > :nth-child(4),
	.zd-services__grid > :nth-child(5) {
		grid-column: span 1;
	}

	/* Odd card out spans the full row rather than leaving a gap. */
	.zd-services__grid > :nth-child(5) {
		grid-column: span 2;
	}
}

@media (max-width: 560px) {
	.zd-services__grid {
		grid-template-columns: 1fr;
	}

	.zd-services__grid > *,
	.zd-services__grid > :nth-child(4),
	.zd-services__grid > :nth-child(5) {
		grid-column: span 1;
	}
}

/* ==========================================================================
   Work
   ========================================================================== */

.zd-work__list {
	display: flex;
	flex-direction: column;
	gap: var(--space-2);
}

.zd-work-card {
	align-items: center;
	display: grid;
	gap: var(--space-8);
	grid-template-columns: 1fr auto;
	padding: var(--space-6);
}

.zd-work-card__eyebrow {
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	letter-spacing: var(--tracking-label);
	text-transform: uppercase;
}

.zd-work-card__title {
	font-size: var(--fs-h2);
	margin-block: var(--space-2);
}

.zd-work-card__text {
	color: var(--color-text-muted);
}

.zd-work-card__tags {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-1);
	list-style: none;
	margin: var(--space-3) 0 0;
	padding: 0;
}

.zd-work-card__tags li {
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	padding: var(--space-half) var(--space-2);
}

.zd-metrics {
	display: flex;
	flex-direction: column;
	gap: var(--space-4);
	min-width: 200px;
}

.zd-metric b {
	color: var(--color-brand);
	display: block;
	font-family: var(--font-heading);
	font-size: clamp(2.25rem, 1.8rem + 2vw, 3rem);
	font-weight: 800;
	letter-spacing: var(--tracking-display);
	line-height: 1;
}

.zd-metric span {
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	letter-spacing: var(--tracking-label);
	text-transform: uppercase;
}

@media (max-width: 900px) {
	.zd-work-card {
		gap: var(--space-5);
		grid-template-columns: 1fr;
		padding: var(--space-4);
	}

	/* Under the copy rather than beside it. A grid rather than a flex row: the
	   row kept the 200px floor and a 48px gap, so on a narrow phone the second
	   metric was pushed past the card edge and off the viewport entirely.
	   Columns that share the row can't overflow it, and auto-fit drops to one
	   column on the narrowest phones without another breakpoint to keep in sync. */
	.zd-metrics {
		border-top: 1px solid var(--color-border);
		display: grid;
		gap: var(--space-3) var(--space-4);
		grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
		min-width: 0;
		padding-top: var(--space-3);
	}

	/* The number no longer has a column of its own to grow into, so it takes a
	   lower floor than the desktop clamp. "p95 800ms" is the widest value in the
	   set and it has to sit inside half a card. */
	.zd-metric b {
		font-size: clamp(1.75rem, 1.3rem + 2.2vw, 2.5rem);
	}
}

/* ==========================================================================
   About / principles
   ========================================================================== */

.zd-about__grid {
	display: grid;
	gap: var(--space-8);
	grid-template-columns: 0.9fr 1.1fr;
}

.zd-principles {
	display: grid;
	gap: var(--space-2);
	grid-template-columns: repeat(2, 1fr);
	list-style: none;
	margin: 0;
	padding: 0;
}

.zd-principle {
	border-top: 1px solid var(--color-border);
	padding-top: var(--space-3);
}

.zd-principle h3 {
	align-items: center;
	display: flex;
	font-size: var(--fs-lg);
	gap: var(--space-1);
	margin-bottom: var(--space-1);
}

.zd-principle p {
	color: var(--color-text-muted);
	font-size: var(--fs-sm);
}

@media (max-width: 900px) {
	.zd-about__grid {
		grid-template-columns: 1fr;
		gap: var(--space-6);
	}
}

@media (max-width: 560px) {
	.zd-principles {
		grid-template-columns: 1fr;
	}
}

/* ==========================================================================
   Process
   ========================================================================== */

.zd-process__grid {
	display: grid;
	gap: var(--space-8);
	grid-template-columns: 0.9fr 1.1fr;
}

.zd-process__list {
	list-style: none;
	margin: 0;
	padding: 0;
}

/* Numbered marker in the first column, copy in the second. A phase is a sequence
   rather than a set, so this reads down a spine instead of across a card grid —
   the services cards and the method row already own the horizontal patterns. */
.zd-process__step {
	display: grid;
	gap: var(--space-3);
	grid-template-columns: var(--space-6) 1fr;
	padding-bottom: var(--space-5);
	position: relative;
}

.zd-process__step:last-child {
	padding-bottom: 0;
}

/* The spine, drawn between one marker and the next. Skipped on the last step so
   the line terminates on a phase rather than trailing into the section padding. */
.zd-process__step:not(:last-child)::before {
	border-left: 1px solid var(--color-border);
	bottom: 0;
	content: "";
	left: calc(var(--space-6) / 2);
	position: absolute;
	top: var(--space-6);
}

.zd-process__marker {
	align-items: center;
	background: var(--color-surface-2);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-btn);
	color: var(--color-brand);
	display: flex;
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	height: var(--space-6);
	justify-content: center;
	letter-spacing: var(--tracking-label);
	width: var(--space-6);
}

/* Optical alignment: the marker is 48px tall and the title is not, so centre the
   heading against the marker rather than letting both sit on the same baseline. */
.zd-process__body {
	padding-top: var(--space-1);
}

.zd-process__body h3 {
	font-size: var(--fs-lg);
	margin-bottom: var(--space-1);
}

.zd-process__body p {
	color: var(--color-text-muted);
	font-size: var(--fs-sm);
}

@media (max-width: 900px) {
	.zd-process__grid {
		gap: var(--space-6);
		grid-template-columns: 1fr;
	}
}

/* ==========================================================================
   Method
   ========================================================================== */

/* Three beats of one argument, so they read as a sequence rather than as three
   more cards — the services grid sits directly above this on the Services page
   and a second card grid would flatten both. The hairline is the through-line;
   each dot is a node sitting on it. */
.zd-method__steps {
	display: grid;
	gap: var(--space-4);
	grid-template-columns: repeat(3, 1fr);
	list-style: none;
	margin: 0;
	padding: 0;
}

.zd-method__step {
	border-top: 1px solid var(--color-border);
	padding-top: var(--space-3);
	position: relative;
}

/* Centred on the border, not below it: the dot reads as a point on the wire. */
.zd-method__step .zd-dot {
	left: 0;
	position: absolute;
	top: 0;
	transform: translateY(-50%);
}

.zd-method__step h3 {
	font-size: var(--fs-lg);
	margin-bottom: var(--space-1);
}

.zd-method__step p {
	color: var(--color-text-muted);
	font-size: var(--fs-sm);
}

@media (max-width: 900px) {
	.zd-method__steps {
		grid-template-columns: 1fr;
		gap: var(--space-5);
	}
}

/* ==========================================================================
   Tech stack
   ========================================================================== */

.zd-stack__grid {
	border: 1px solid var(--color-border);
	border-radius: var(--radius-card);
	display: grid;
	gap: 1px;
	grid-template-columns: repeat(6, 1fr);
	list-style: none;
	margin: 0;
	overflow: hidden;
	padding: 0;
	background: var(--color-border);
}

/* 1px grid gap over a border-colored background gives hairline dividers
   without per-cell border math. */
.zd-stack__item {
	align-items: center;
	background: var(--color-bg);
	display: flex;
	gap: var(--space-2);
	justify-content: center;
	padding: var(--space-4) var(--space-2);
	transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.zd-stack__item span {
	color: var(--color-text-muted);
	font-family: var(--font-mono);
	font-size: var(--fs-sm);
	transition: color var(--dur-fast) var(--ease);
}

.zd-stack__item:hover {
	background: var(--color-surface);
}

.zd-stack__item:hover span {
	color: var(--color-text);
}

/* Sizing lives on .zd-tech in components.css; this is only the section's own
   treatment of it. */
.zd-stack__item .zd-tech {
	color: var(--color-text-muted);
	opacity: 0.55;
	transition:
		color var(--dur-fast) var(--ease),
		opacity var(--dur-fast) var(--ease),
		filter var(--dur-fast) var(--ease);
}

.zd-stack__item:hover .zd-tech {
	color: var(--color-brand);
	filter: drop-shadow(var(--glow-text));
	opacity: 1;
}

/* Pads the last row out so its hairlines still land on cells. */
.zd-stack__item--empty {
	pointer-events: none;
}

@media (max-width: 1024px) {
	.zd-stack__grid {
		grid-template-columns: repeat(4, 1fr);
	}
}

@media (max-width: 560px) {
	.zd-stack__grid {
		grid-template-columns: repeat(2, 1fr);
	}
}

/* ==========================================================================
   Toolbox
   ========================================================================== */

/* Rows rather than the stack's grid: this is an index to scan for one name, not
   a shortlist to read. Category on the left, the tools in it wrapping on the
   right, hairlines between — closer to a spec sheet than a logo wall. */
.zd-toolbox__groups {
	border-bottom: 1px solid var(--color-border);
}

.zd-toolbox__group {
	border-top: 1px solid var(--color-border);
	display: grid;
	gap: var(--space-2);
	grid-template-columns: 200px 1fr;
	padding-block: var(--space-3);
}

.zd-toolbox__label {
	color: var(--color-text-muted);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	font-weight: 400;
	letter-spacing: var(--tracking-label);
	text-transform: uppercase;
}

.zd-toolbox__items {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2) var(--space-4);
	list-style: none;
	margin: 0;
	padding: 0;
}

.zd-toolbox__item {
	align-items: center;
	display: flex;
	gap: var(--space-1);
}

/* This is an inventory someone scans for their own stack, so the names read at
   full strength and the marks carry the only dimming — enough to keep the row
   from turning into a logo wall, not so much that the list looks switched off. */
.zd-toolbox__item .zd-tech {
	color: var(--color-text-muted);
	opacity: 0.85;
	transition:
		color var(--dur-fast) var(--ease),
		opacity var(--dur-fast) var(--ease),
		filter var(--dur-fast) var(--ease);
}

.zd-toolbox__item span {
	color: var(--color-text);
	font-family: var(--font-mono);
	font-size: var(--fs-sm);
}

.zd-toolbox__item:hover .zd-tech {
	color: var(--color-brand);
	filter: drop-shadow(var(--glow-text));
	opacity: 1;
}

/* Stacks at the same width as the process and method sections, so the page has
   one breakpoint rhythm rather than three. */
@media (max-width: 900px) {
	.zd-toolbox__group {
		gap: var(--space-2);
		grid-template-columns: 1fr;
	}

	/* Columns instead of wrapping. Free-flowing items are fine on one long line,
	   but once they wrap the rows come out ragged and nothing lines up down the
	   group — and a group ending in a single orphan ("Linux", "Ruby on Rails")
	   reads as a mistake. A fixed grid keeps the left edges true. */
	.zd-toolbox__items {
		display: grid;
		gap: var(--space-2);
		grid-template-columns: repeat(4, 1fr);
	}
}

@media (max-width: 720px) {
	.zd-toolbox__items {
		grid-template-columns: repeat(3, 1fr);
	}
}

@media (max-width: 560px) {
	.zd-toolbox__items {
		grid-template-columns: repeat(2, 1fr);
	}
}

/* ==========================================================================
   CTA
   ========================================================================== */

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

.zd-cta__box {
	background: var(--color-surface);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-card);
	overflow: hidden;
	padding: var(--space-12) var(--space-6);
	position: relative;
	text-align: center;
}

.zd-cta__box > *:not(.zd-dotgrid) {
	position: relative;
	z-index: 1;
}

.zd-cta__title {
	font-size: var(--fs-h1);
	margin-block: var(--space-2) var(--space-3);
}

.zd-cta__lead {
	color: var(--color-text-muted);
	font-size: var(--fs-lg);
	margin-inline: auto;
	max-width: 46ch;
}

.zd-cta .zd-btn-group {
	justify-content: center;
	margin-top: var(--space-5);
}

.zd-cta .zd-label {
	justify-content: center;
}

/* The box's own inset is what squeezes the headline on a phone: 48px a side
   left a 40px display type just 231px to wrap in, which broke "Tell us what
   you're building." into four lines of two words. The frame matters less than
   the line it's framing. */
@media (max-width: 768px) {
	.zd-cta__box {
		padding: var(--space-10) var(--space-4);
	}
}

@media (max-width: 560px) {
	.zd-cta__box {
		padding: var(--space-8) var(--space-3);
	}
}

/* ==========================================================================
   Page header (interior pages)
   ========================================================================== */

.zd-page-hero {
	overflow: hidden;
	padding-block: var(--space-15) var(--space-8);
	position: relative;
}

.zd-page-hero__inner {
	position: relative;
	z-index: 1;
}

.zd-page-hero__title {
	font-size: var(--fs-h1);
	margin-block: var(--space-2);
}

.zd-page-hero__lead {
	color: var(--color-text-muted);
	font-size: var(--fs-lg);
	max-width: var(--measure);
}

/* The search form sits under the lead on the blog index and the results page.
   Spaced as a separate beat rather than as another line of the hero text. */
.zd-page-hero__inner .zd-search {
	margin-top: var(--space-4);
}

/* Same argument as the home hero: the 120px lead-in is measured against a
   desktop viewport, and on a phone it pushes the page title under the fold. */
@media (max-width: 768px) {
	.zd-page-hero {
		padding-block: var(--space-10) var(--space-6);
	}
}

/* ==========================================================================
   Blog
   ========================================================================== */

.zd-posts {
	display: grid;
	gap: var(--space-2);
	grid-template-columns: repeat(3, 1fr);
}

.zd-post-card {
	gap: var(--space-2);
	justify-content: flex-start;
	padding: var(--space-3);
}

.zd-post-card__thumb {
	aspect-ratio: 16 / 10;
	background: var(--color-surface-2);
	border-radius: var(--radius-btn);
	margin-bottom: var(--space-2);
	overflow: hidden;
}

.zd-post-card__thumb img {
	height: 100%;
	object-fit: cover;
	width: 100%;
}

.zd-post-card__title {
	font-size: var(--fs-h3);
	font-weight: 700;
}

.zd-post-meta {
	align-items: center;
	color: var(--color-text-faint);
	display: flex;
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	gap: var(--space-1);
	letter-spacing: var(--tracking-label);
	text-transform: uppercase;
}

.zd-post-card__excerpt {
	color: var(--color-text-muted);
	font-size: var(--fs-sm);
}

@media (max-width: 900px) {
	.zd-posts {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (max-width: 560px) {
	.zd-posts {
		grid-template-columns: 1fr;
	}
}

/* --- Single post ----------------------------------------------------------- */

/* No bottom margin: this is the only thing inside the hero, so a margin here
   just stacked on the hero's own padding and pushed the post 48px further down
   than every other page's content. */
.zd-single__header {
	max-width: var(--measure);
}

.zd-single__title {
	margin-block: var(--space-2);
}

.zd-single__thumb {
	border-radius: var(--radius-card);
	margin-bottom: var(--space-6);
	overflow: hidden;
}

.zd-single__footer {
	border-top: 1px solid var(--color-border);
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	justify-content: space-between;
	margin-top: var(--space-8);
	max-width: var(--measure);
	padding-top: var(--space-3);
}

/* ==========================================================================
   Contact
   ========================================================================== */

.zd-contact__grid {
	display: grid;
	gap: var(--space-8);
	grid-template-columns: 1.1fr 0.9fr;
}

/* The booking panel follows the page header directly, so it takes the hero's own
   bottom padding as its top gap rather than stacking another --section-gap on
   top of it and floating away from the title it belongs to.

   The pair below is stated explicitly because `.zd-section + .zd-section` is the
   more specific selector and would otherwise zero the following section's top
   padding, dropping its border rule straight onto the copy. */
.zd-section--scheduler {
	padding-block: 0 var(--space-12);
}

.zd-section--scheduler + .zd-section--bordered {
	padding-top: var(--space-12);
}

.zd-contact__list {
	display: flex;
	flex-direction: column;
	gap: var(--space-3);
	list-style: none;
	margin: 0;
	padding: 0;
}

.zd-contact__item {
	border-top: 1px solid var(--color-border);
	padding-top: var(--space-3);
}

.zd-contact__item dt {
	color: var(--color-text-faint);
	font-family: var(--font-mono);
	font-size: var(--fs-label);
	letter-spacing: var(--tracking-label);
	margin-bottom: var(--space-half);
	text-transform: uppercase;
}

.zd-contact__item dd {
	color: var(--color-text);
	font-size: var(--fs-lg);
	margin: 0;
}

@media (max-width: 900px) {
	.zd-contact__grid {
		grid-template-columns: 1fr;
		gap: var(--space-6);
	}
}

/* The email address is the one thing on this page a phone visitor actually
   taps, and as a bare line of text it offered 21px of target. The dd is a
   single line, so a min-height grows the box without moving the label. */
@media (max-width: 768px) {
	.zd-contact__item dd a {
		align-items: center;
		display: inline-flex;
		min-height: var(--space-6);
	}
}

/* ==========================================================================
   Empty states / 404
   ========================================================================== */

.zd-empty {
	padding-block: var(--space-15);
	text-align: center;
}

/* h1 on the 404, h2 on the search empty state, which sits under a page hero that
   already owns the h1. Both need the same gap. */
.zd-empty :is(h1, h2) {
	margin-block: var(--space-2) var(--space-3);
}

.zd-empty p {
	color: var(--color-text-muted);
	margin-inline: auto;
	max-width: 48ch;
}

/* The one graphic element on an otherwise empty page, so it carries the page. It
   used to render at --fs-label, the eyebrow size, which put a page hero at caption
   scale. Display scale, and the glow, because a yellow mono numeral on black is the
   whole idea. Negative tracking here reads as tightening rather than cramping —
   mono digits are wide to begin with.

   It is a <p>, so `.zd-empty p` above outranked a bare `.zd-empty__code` and painted
   it muted grey. The brand colour had never once shown. Hence the two-class selector
   and the position after that rule: same file, higher specificity, later in the
   cascade. Don't move it back up. */
.zd-empty .zd-empty__code {
	color: var(--color-brand);
	font-family: var(--font-mono);
	font-size: var(--fs-display);
	font-weight: 500;
	letter-spacing: var(--tracking-display);
	line-height: var(--lh-tight);
	max-width: none;
	text-shadow: var(--glow-text);
}

.zd-empty .zd-btn-group,
.zd-empty .zd-search {
	justify-content: center;
	margin-inline: auto;
	margin-top: var(--space-5);
}
