/*
Theme Name: Zero Digital Base
Theme URI: https://zero-digital.co.uk/
Author: Zero Digital
Description: Styling/presentation layer only. All block logic, CPTs, options pages and MCP abilities live in the Zero Digital Components plugin — never here (Section 2).
Requires at least: 7.0
Requires PHP: 8.0
Version: 0.67.2
License: GPLv2 or later
Text Domain: zerodigital-theme
Update URI: https://updates.zero-digital.co.uk/zero-digital-base
*/

/* --------------------------------------------------------------------------
 * Montserrat, self-hosted.
 *
 * This is the one typeface the theme owns outright rather than offering: the
 * footer credit mark is always Montserrat whatever a site sets its own fonts
 * to. Every other family here is declared in theme.json, where a child theme
 * can replace it.
 *
 * WHY IT IS HERE AND NOT IN theme.json. A child theme that declares its own
 * `settings.typography.fontFamilies` REPLACES the parent's array rather than
 * merging into it — measured on a live site, where the parent's Schibsted
 * Grotesk @font-face was not emitted at all. A mark that must always render in
 * Montserrat therefore cannot depend on a theme.json entry a child can delete
 * by accident. style.css always loads, so this always applies.
 *
 * It also used to be a request to fonts.googleapis.com, which meant two extra
 * origins in the critical path (DNS + TLS + round trip each, before the font
 * file itself was even discovered) and every visitor's IP address sent to
 * Google before any consent was possible. Both are now gone.
 *
 * ONE VARIABLE FILE, NOT FOUR STATIC ONES. Google already serves Montserrat as
 * a variable font — its stylesheet returns the same file for wght 400, 500, 600
 * and 700 — so this is the same bytes, self-hosted, and 100 900 covers weights
 * the old URL never requested. The site was rendering 500 with a 400 file.
 *
 * Subsets are latin and latin-ext only. The unicode-range means latin-ext is
 * fetched solely by a page that actually contains an accented character, so it
 * costs nothing until a name needs it. Cyrillic and Vietnamese are deliberately
 * not carried; a page needing them falls back to a system font.
 *
 * Refreshing these files: assets/fonts/README.md.
 * ------------------------------------------------------------------------ */
@font-face {
	font-family: "Montserrat";
	font-style: normal;
	font-weight: 100 900;
	font-display: swap;
	src: url("assets/fonts/montserrat-variable.woff2") format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Montserrat";
	font-style: normal;
	font-weight: 100 900;
	font-display: swap;
	src: url("assets/fonts/montserrat-variable-latin-ext.woff2") format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}


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

body {
	margin: 0;
}

img,
video {
	max-width: 100%;
	height: auto;
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
	outline: 2px solid var(--wp--preset--color--accent, #c9922a);
	outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

/* Dark-surface focus rings. The a/button:focus-visible baseline above uses the
 * accent, which is tuned dark for AA on light backgrounds and reads too
 * low-contrast on the dark footer, credit bar and full-bleed hero — so switch
 * those to a light ring. (The floating header handles its own bar further
 * down.) WCAG 1.4.11 / 2.4.11.
 *
 * The list was short of every primary-coloured ground on the site. Accent
 * against primary measured 1.00:1 in Health (the two are the same colour
 * there), 1.66:1 in Trades, 1.72:1 in Default, 2.70:1 in Professional and
 * 3.02:1 in Broker — four of five under the 3:1 a focus indicator needs. So:
 *
 *   - The CTA Banner, which is primary by default. Not when an editor has
 *     given it a light colour: the block marks that .zd-bg-light, and a light
 *     ring on a light band is the same failure the other way round.
 *   - The compliance strip inside the footer, which takes the footer's
 *     primary. Again not on a light footer (.zd-bg-light on the group).
 *   - Any .zd-bg-dark section — a block whose editor-picked colour or media
 *     background the plugin measured as dark. Controls inside a light card
 *     sitting on that band are left on the accent ring: their ring is drawn
 *     on the card's light surface, not on the band. The card itself, when it
 *     is the link, rings on the band and gets the light ring.
 *
 * Footer Columns takes the same light-footer exclusion; on a footer switched to
 * a light colour its white ring had nothing to show against. The credit bar
 * does not: it paints its own foreground-coloured ground whatever the footer
 * is. Every exclusion sits in :where() so the specificity stays where the
 * block rules above and below expect it. */
.zerodigital-footer-columns:not(:where(.zd-bg-light *)) :where(a, button):focus-visible,
.zerodigital-site-credit :where(a):focus-visible,
.zerodigital-hero-home--full-bg-overlay :where(a, button):focus-visible,
.zerodigital-cta-banner:not(:where(.zd-bg-light)) :where(a, button):focus-visible,
footer .wp-block-group:not(:where(.zd-bg-light)) > .zerodigital-compliance-bar :where(a, button):focus-visible,
.zd-bg-dark :where(a, button, summary, input, select, textarea, [tabindex]):not(:where(.zd-light-card *)):focus-visible {
	outline-color: var(--wp--preset--color--background, #fff);
}

/* --------------------------------------------------------------------------
 * An address or a number never widens the page.
 *
 * An email address is one unbreakable word to a browser, and Business Details
 * supplies them to blocks AND to the `[zd_business field=email link=true]`
 * shortcode — which an editor can put in any block's copy, at any width. Fixing
 * the two blocks that render one directly (Footer Columns, Contact) left that
 * shortcode uncovered, and it is the case with the widest reach.
 *
 * So the rule lives here, on the link itself, wherever it lands. A long address
 * in a narrow column did not merely overflow: the Contact block's copy of one
 * pushed a whole site to 396px in a 375px viewport, and every page scrolled
 * sideways on a phone.
 *
 * `anywhere` and not `break-word` because both wrap the visible line, but only
 * `anywhere` also shrinks the element's min-content width, which is what stops a
 * grid or flex track being sized to the unbreakable word to begin with. It
 * breaks a word only when it would otherwise overflow, so an address that fits
 * is untouched.
 *
 * :where() keeps this at zero specificity: any block that wants to style its own
 * contact links still wins without needing to know this rule exists.
 * ------------------------------------------------------------------------ */
:where( a[href^="mailto:"], a[href^="tel:"] ) {
	overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
 * Borderless link affordance (utility).
 *
 * A link styled as an action but with no border or fill can read as plain
 * text. The convention here: mark any such link with a trailing chevron so it
 * clearly signals "this is a link". Add .zerodigital-link-arrow to the anchor;
 * the chevron nudges right and the label underlines on hover. Lives in the
 * theme, not a block, so it's available to every borderless link everywhere.
 * ------------------------------------------------------------------------ */
.zerodigital-link-arrow {
	text-decoration: none;
}

.zerodigital-link-arrow::after {
	/* NBSP keeps the chevron glued to the last word; › (U+203A) is a cleaner
	 * mark than a literal ">". inline-block so it can be nudged on hover —
	 * transforms don't apply to plain inline boxes. */
	content: "\00A0\203A";
	display: inline-block;
	transition: transform var(--zd-ease, 180ms ease);
}

.zerodigital-link-arrow:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
	text-decoration-thickness: 2px;
}

.zerodigital-link-arrow:hover::after {
	transform: translateX(0.2em);
}

/* Visually hidden until focused — used by the skip link in parts/header.html. */
.zerodigital-skip-link {
	position: absolute;
	top: -999px;
	left: 0;
	z-index: 100;
	padding: 0.75rem 1.25rem;
	background: var(--wp--preset--color--background, #fff);
	color: var(--wp--preset--color--foreground, #1a1a1a);
}

.zerodigital-skip-link:focus {
	top: 0;
}

/* --------------------------------------------------------------------------
 * Default skin — shared design primitives. The token SCALES now live in
 * theme.json (settings.shadow.presets, settings.custom.transition /
 * keyline / eyebrowTracking), so there's a single opinionated source of
 * truth. The aliases below map those onto the short --zd-* names the blocks
 * use, each keeping the raw value as a fallback so the plugin still renders
 * sensibly if the theme (and its generated global styles) isn't present.
 * ------------------------------------------------------------------------ */
:root {
	--zd-shadow-sm: var(--wp--preset--shadow--sm, 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.1));
	--zd-shadow-md: var(--wp--preset--shadow--md, 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0 2px 4px -2px rgba(16, 24, 40, 0.06));
	--zd-shadow-lg: var(--wp--preset--shadow--lg, 0 14px 28px -8px rgba(16, 24, 40, 0.16));
	--zd-ease: var(--wp--custom--transition, 180ms cubic-bezier(0.4, 0, 0.2, 1));
	/* ---- Motion layer ------------------------------------------------------
	 * `--zd-ease` above is a TRANSITION shorthand — duration and curve in one
	 * value — which is all a hover state needs. These are the separate knobs the
	 * section entrance needs: how far a section travels as it arrives, what it
	 * starts from, how long it takes, and how far each item in a row lags the
	 * one before it.
	 *
	 * The curve is the Home Hero's own, so a section arriving mid-page moves
	 * the way the hero moved on load and the page reads as one piece.
	 *
	 * Two controls, deliberately split. These tokens are the PACE, and they are
	 * variation-settable — the sector a site runs decides how fast and how far it
	 * moves. Which SHAPE the movement takes (settle, lift, wipe, or none) is
	 * Site Settings -> Animation, a body class. Pace belongs to the design,
	 * shape belongs to the site owner, and between them no two sites on this
	 * platform arrive the same way. A child theme can override the pace for one
	 * client; it must not re-declare the animations.
	 */
	--zd-motion-rise: var(--wp--custom--motion--rise, 26px);
	/* The heroes travel half as far by default. 26px reads as poise on a card
	 * and as a lurch on a headline block, which is a difference of kind rather
	 * than degree, so it gets its own token instead of sharing one. Set
	 * settings.custom.motion.heroRise in a variation, or --zd-motion-hero-rise
	 * in a child theme, to give one client something else; leave it alone and
	 * it tracks whatever --zd-motion-rise is set to. */
	--zd-motion-hero-rise: var(--wp--custom--motion--hero-rise, calc(var(--zd-motion-rise) * 0.5));
	/* What the keyframes actually read. The indirection exists so the hero can
	 * point it at --zd-motion-hero-rise on its own elements: redefining
	 * --zd-motion-rise there instead would be a custom-property cycle, since
	 * the hero token is defined in terms of it, and the whole declaration would
	 * be dropped at computed-value time with no error anywhere. */
	--zd-motion-travel: var(--zd-motion-rise);
	--zd-motion-from: var(--wp--custom--motion--from, 0);
	--zd-motion-scale-from: var(--wp--custom--motion--scale-from, 0.96);
	--zd-motion-duration: var(--wp--custom--motion--duration, 600ms);
	--zd-motion-ease: var(--wp--custom--motion--ease, cubic-bezier(0.2, 0.7, 0.2, 1));
	--zd-motion-stagger: var(--wp--custom--motion--stagger, 80ms);
	/* Multiplied into the travel rather than baked into it, so a filter or a
	 * child theme can damp the whole system without touching each token. */
	--zd-motion-scale: 1;
	--zd-border: 1px solid var(--wp--preset--color--border, #e3e8ee);
	/* Raised cards resolve their fill through this one alias, never the raw
	 * `background` token — which IS the page canvas, so a card painted with it
	 * is the same colour as the page and vanishes (measured: a Health FAQ card
	 * came out pixel-identical to the page). `surface` is distinct from
	 * `background` in every variation, so this reads as a card in all five; and
	 * centralising it here lets a variation retint every card at once later. */
	--zd-card-bg: var(--wp--preset--color--surface, #fff);
	/* Shared tap-target floor for every primary button, so they read as one
	 * primitive and clear the 44px accessible-size guidance (WCAG 2.5.5). Now
	 * variation-settable, so a sector can run chunkier or more compact controls
	 * without any block needing to know. */
	--zd-btn-min-h: var(--wp--custom--control--min-height, 2.875rem);

	/* ---- Component shape layer -------------------------------------------
	 * The radius scale is a SIZE scale (sm/md/lg/full), so components used to
	 * pick a step by how big the corner should look — which meant buttons,
	 * inputs, photos and cards all shared radius--md. A variation could then
	 * only make everything rounder or everything sharper, never give itself
	 * pill buttons while keeping square photos.
	 *
	 * These name the ROLE instead. Each defaults to the step its components
	 * already used, so introducing them changed nothing; a variation overrides
	 * one to reshape just that component family. */
	--zd-btn-radius: var(--wp--custom--shape--button, var(--wp--custom--radius--md, 0.5rem));
	--zd-card-radius: var(--wp--custom--shape--card, var(--wp--custom--radius--lg, 0.75rem));
	--zd-media-radius: var(--wp--custom--shape--media, var(--wp--custom--radius--lg, 0.75rem));
	--zd-chip-radius: var(--wp--custom--shape--chip, var(--wp--custom--radius--full, 999px));
	/* A tag is not a chip. Both are small enclosed labels, but a chip is a
	 * control (a filter, a toggle) and reads as UI, where a tag is a piece of
	 * measured content. The hero feature tags took --zd-btn-radius at first,
	 * reasoning that it was 'the button radius, not the chip radius' — which
	 * is not a distinction on a variation with pill buttons, where the two
	 * collapse to the same 999px and the tags came out as the exact shape they
	 * were meant to avoid. Its own role, defaulting square-ish. */
	--zd-tag-radius: var(--wp--custom--shape--tag, var(--wp--custom--radius--sm, 0.5rem));
	--zd-input-radius: var(--wp--custom--shape--input, var(--wp--custom--radius--md, 0.5rem));
	/* Approximate header bar height — used to sit the mobile drawer below the
	 * header so the logo/phone/toggle stay visible. Adjust if the logo is tall. */
	--zd-header-height: 4.75rem;

	/* How far above an anchor target the page stops, so a linked heading clears
	 * the sticky header instead of landing underneath it. Derived from the
	 * header rather than fixed, so a taller logo moves both together — the
	 * failure this prevents is silent, since the page does scroll, just to a
	 * spot where the thing you asked for is hidden behind the bar. */
	--zd-anchor-offset: calc(var(--zd-header-height, 4.75rem) + var(--wp--preset--spacing--sm, 1rem));

	/* The height a hero can actually fill — what the visitor can see, not what
	 * the viewport measures. The header sits in normal flow above the hero, so a
	 * plain 100svh hero puts its own bottom edge exactly one header below the
	 * fold: the section says "full screen" and the page still starts scrolled.
	 * Subtracting the bar is what makes the tallest hero option end where the
	 * screen does. Overridden below for the floating header, which is absolutely
	 * positioned over the hero and so takes no flow height at all.
	 *
	 * vh here, svh in the @supports after this block: on mobile vh is the
	 * LARGEST viewport (browser chrome retracted), so a vh hero is cropped by
	 * the address bar until you scroll. The two-step exists because a custom
	 * property's value is not validated at parse time — a browser that has never
	 * heard of svh stores the string happily and min-height then computes to
	 * auto, so the usual "declare the fallback first" trick fails here. */
	--zd-hero-viewport: calc(100vh - var(--zd-header-height, 4.75rem));

	/* How far the menu's CONTENT sits inside the header bar — the bar's own
	 * inline padding, and therefore the x where the logo starts and the phone
	 * button ends. The bar shares the hero's wide-size container, so a hero
	 * aligned left or right reads this to land on the same vertical line as the
	 * logo instead of 20px outside it. It IS the bar's padding rather than a copy
	 * of it (see .zerodigital-header__bar), so the two cannot drift. */
	--zd-menu-inset: var(--wp--preset--spacing--md, 1.5rem);

	/* The content column every section sits on: the wide size less the header
	 * bar's own inset, so a section's content edge is the logo's edge. Sections
	 * are bands whose width follows their alignment; their content is capped here
	 * whatever that alignment is, so Wide and Full share one edge. Each block
	 * repeats the calc as its fallback, since plugin CSS must work without the
	 * theme. */
	--zd-content-max: calc(var(--wp--style--global--wide-size, 1600px) - 2 * var(--zd-menu-inset));

	/* A section's inline padding: wherever the header's content starts, so a
	 * section's content starts there too. That is the bar's own inset plus as
	 * much of the gutter outside the bar as the viewport has not already used up.
	 * Below the wide size the bar is held off the edge by the whole gutter; above
	 * it the bar is capped and centred, the space either side absorbs the gutter,
	 * and only the inset is left. On a standard header the gutter is 0, so this
	 * is simply the inset — 1.5rem, what every section always had.
	 *
	 * 100vw rather than 100%: padding percentages resolve against the parent, not
	 * the section. It counts a classic scrollbar, which moves the result by at
	 * most half the scrollbar's width, and only while the viewport is within a
	 * gutter's width of the wide size. */
	--zd-section-gutter: calc(var(--zd-menu-inset) + max(0px, var(--zd-header-gutter) - max(0px, (100vw - var(--wp--style--global--wide-size, 1600px)) / 2)));

	/* The gutter OUTSIDE the header bar. Zero by default: the standard bar runs
	 * to its container's edge and insets its contents with its own padding. The
	 * floating bar is a pill that has to be held off the viewport edge, so it
	 * overrides this. A hero adds the two together to land its content on the
	 * same vertical line as the menu's. */
	--zd-header-gutter: 0px;

	/* The curve where a full-bleed section meets the page (see the section
	 * corner treatment below). Derived from the variation's own radius scale
	 * rather than fixed, so it follows how rounded that theme already is:
	 * Health (lg 1.25rem) curves a generous 2.5rem, Trades (lg 0.25rem,
	 * deliberately industrial) barely at all. Set to 0 to switch it off. */
	--zd-section-corner: calc(var(--wp--custom--radius--lg, 0.75rem) * 2);

	/* Accent system (the signature — see Design Brief §4).
	 * The accent marks intent: the keyline that opens a spec label, the
	 * hover/focus state on interactive elements, and emphasis/state. Each
	 * sector variation sets the keyline's own geometry; which SHAPE that
	 * geometry is drawn as is the site owner's, and lives in Site Settings. */
	--zd-accent: var(--wp--preset--color--accent, #c4263c);
	--zd-accent-tint: color-mix(in srgb, var(--wp--preset--color--accent, #c4263c) 8%, transparent);
	--zd-eyebrow-tracking: var(--wp--custom--eyebrow-tracking, 0.12em);

	/* The keyline's WEIGHT: its thickness as a line, and the unit every
	 * symmetric shape measures its diameter from. The indirection is not
	 * decoration. A shape class has to say "a dot one and a half times the
	 * weight", and --zd-accent-bar-h: calc(var(--zd-accent-bar-h) * 1.5) is a
	 * custom-property cycle — dropped silently at computed-value time with no
	 * error anywhere. --zd-motion-travel above exists for the same reason.
	 *
	 * It reads the theme.json token rather than --zd-accent-bar-h so the cycle
	 * stays impossible however the shape class is placed, including on the same
	 * element — which is exactly what WordPress's editor-style transform does
	 * when it rewrites both :root and a leading body to .editor-styles-wrapper.
	 *
	 * This is the knob for resizing the mark. Setting --zd-accent-bar-h in a
	 * child theme reaches Line only; every other shape overwrites it.
	 *
	 * WHY custom--keyline AND NOT custom--accent-keyline. 0.35.0 shipped the new
	 * geometry under the old key and it reached nobody. Applying a style
	 * variation copies settings.custom into the USER global-styles layer in the
	 * database, and that layer sits above theme.json AND above the variation file
	 * it came from — so every site was still being served the accent-keyline it
	 * froze the day someone picked its sector look, and editing styles/*.json did
	 * nothing. The proof was the hairline shipped beside it: rule.width was a
	 * brand-new key, no stale copy existed, and it came through on the first
	 * load. A key nobody has a copy of is the only one that reaches an existing
	 * site, which is why this group is renamed rather than re-tuned again.
	 *
	 * The corollary is worth knowing: a site that has applied a variation reads
	 * the BASE theme.json for any new key, not that variation's file, until
	 * someone re-applies it in Appearance → Styles. Per-sector keyline tuning
	 * therefore lands on new builds and on re-applies, and everyone else gets
	 * the base — which is the right way round, since the base is the design. */
	--zd-keyline-weight: var(--wp--custom--keyline--weight, 0.625rem);

	/* The keyline as resolved for this page — what the marks actually read, and
	 * what a shape class overwrites from <body>. Line is the default and has no
	 * class of its own; see the shape rules below. Length runs about 1.8x the
	 * weight: at 2.5x the mark read as a dash that happened to have round ends
	 * rather than as a pill. */
	--zd-accent-bar-w: var(--wp--custom--keyline--length, 1.125rem);
	--zd-accent-bar-h: var(--zd-keyline-weight);
	--zd-accent-bar-radius: var(--wp--custom--keyline--radius, 999px);

	/* A plain hairline: the nav underline, the mobile submenu's left edge, the
	 * open FAQ item's. All three borrowed --zd-accent-bar-h back when the
	 * keyline was always a rule, which would have made them vanish the moment it
	 * became a dot. Each variation sets this to the weight its keyline had
	 * before they parted company, so no pixel moved on any skin. */
	--zd-rule-w: var(--wp--custom--rule--width, 0.1875rem);

	/* Whether spec labels shout in caps or speak in sentence case —
	 * sector-settable. Caps read technical and precise; sentence case reads
	 * human, which is what a wellbeing skin wants. */
	--zd-spec-transform: var(--wp--custom--spec-transform, uppercase);

	/* The utility face (the redesign signature). Every "spec" — section labels,
	 * ratings, prices, credentials, hours, phone, indices — renders here, so the
	 * site reads as one catalogued system rather than a stack of sections. The
	 * ui-monospace fallback means this degrades gracefully to the platform mono
	 * if the IBM Plex Mono files aren't present. */
	--zd-mono: var(--wp--preset--font-family--mono, ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace);
	--zd-spec-tracking: var(--wp--custom--spec-tracking, 0.14em);
	--zd-spec-size: 0.72rem;

	/* Display scale. Section headings used to top out around 2.25rem, which is
	 * what made the pages read as flat and templated — nothing ever earned
	 * emphasis. These are the shared steps blocks size against, so the jump from
	 * display to body is deliberate and consistent everywhere. */
	--zd-step-display: clamp(2.5rem, 1.25rem + 4.4vw, 4.5rem);
	--zd-step-h2: clamp(1.9rem, 1.35rem + 2.2vw, 2.9rem);
	--zd-step-h3: clamp(1.2rem, 1.05rem + 0.7vw, 1.5rem);
	--zd-step-lede: clamp(1.0625rem, 1rem + 0.45vw, 1.25rem);
}

/* See --zd-hero-viewport above for why this is a second declaration rather
 * than a fallback pair inside the same block. */
@supports (height: 100svh) {
	:root {
		--zd-hero-viewport: calc(100svh - var(--zd-header-height, 4.75rem));
	}
}

/* --------------------------------------------------------------------------
 * The signature: the monospace "spec" label.
 *
 * A section label set in the utility face, uppercase and tracked, led by a
 * short accent tick. It replaces the centred accent-bar eyebrow as the device
 * that opens a section — left-aligned, quieter, and part of a system rather
 * than an ornament. Paired with .zd-data below (which puts every number in the
 * same face), it is what makes the pages read as bespoke instead of templated.
 *
 * Markup: <p class="zd-spec">Services / What we do</p>
 * ------------------------------------------------------------------------ */
.zd-spec {
	display: inline-flex;
	align-items: center;
	/* The small symmetric shapes tighten this; see the shape rules. */
	gap: var(--zd-keyline-gap, 0.65rem);
	margin: 0;
	font-family: var(--zd-mono);
	font-size: var(--zd-spec-size, 0.72rem);
	font-weight: 500;
	line-height: 1;
	letter-spacing: var(--zd-spec-tracking, 0.14em);
	text-transform: var(--zd-spec-transform, uppercase);
	color: var(--wp--preset--color--muted, #565b62);
}

/* A section label above a heading (zerodigital_section_eyebrow()). The heading
 * is a block box, so the inline-flex label already sits on its own line and
 * follows the section's text alignment; this only spaces it. */
.zd-spec[class*="__eyebrow"] {
	margin: 0 0 var(--wp--preset--spacing--sm, 1rem);
}

/* The keyline is the only accent in a section header — the accent stays scarce
 * so it still means something where it does appear.
 *
 * Every declaration here is a var with an inert default, which is what lets a
 * shape be nothing but VALUES: border is 0 and rotate is none for Line, at no
 * cost, so no shape rule ever has to name this selector or the CTA band's. A
 * third consumer gets every shape for free by reading the same properties,
 * exactly as any block gets the accent by reading --zd-accent. */
.zd-spec::before {
	content: "";
	flex: none;
	display: var(--zd-keyline-display, block);
	inline-size: var(--zd-accent-bar-w, 1.125rem);
	block-size: var(--zd-accent-bar-h, 0.625rem);
	border-radius: var(--zd-accent-bar-radius, 999px);
	background: var(--zd-keyline-fill, var(--zd-accent, var(--wp--preset--color--accent, #c4263c)));
	border: var(--zd-keyline-border, 0) solid var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	rotate: var(--zd-keyline-rotate, none);
}

/* The per-instance opt-out, and it must stay BELOW the rule above: this and
 * --zd-keyline-display are two declarations of the same property at the same
 * specificity, so source order is the only thing making "this one label has no
 * mark" win over whatever the site has chosen. Do not reorder. */
.zd-spec--plain::before {
	display: none;
}

/* --------------------------------------------------------------------------
 * Keyline shapes (Site Settings, via a body class).
 *
 * A shape is a set of custom properties and nothing more — see .zd-spec::before
 * above for why. The class lands on <body>, and what makes that beat the
 * variation's :root is NOT specificity: those two declarations sit on different
 * elements and never compete. It is inheritance proximity. A mark resolves each
 * var from the nearest ancestor that has one, and <body> is nearer than <html>,
 * which holds against a LATER :root as well — the one WordPress generates for
 * the active style variation, and a child theme's own. Do not "simplify" these
 * to :root.zd-keyline-dot; that is the version that breaks.
 *
 * Line gets no rule at all. It is what :root already resolves to, so the
 * default costs nothing and cannot drift from the tokens.
 *
 * The multipliers are optical corrections, not decoration. A square of side d
 * puts d² of ink on the page against a circle's 0.79d², so it runs smaller; a
 * diamond is read along its 1.41d diagonal, so smaller still; a hollow ring
 * reads lightest of anything, so it runs largest. Left unmultiplied, a dot at
 * the line's own weight loses two thirds of its ink beside the capsule and
 * arrives as a stray full stop.
 *
 * They were raised in 0.36.0. The first set was tuned against a 0.5rem weight
 * and put every symmetric shape between 9 and 14px, where a dot, a square and
 * a diamond stop being three shapes and become three smudges — the difference
 * between them is carried by their corners, and corners need size to read.
 * Against the 0.625rem weight these land between 12 and 19px, which is where
 * they separate.
 *
 * The radius is stated outright rather than inherited because Health sets
 * keyline.radius to 999px — inheriting it would draw the Dot and call it
 * Square. A shape named literally is drawn literally.
 * ------------------------------------------------------------------------ */
.zd-keyline-dot {
	--zd-accent-bar-w: calc(var(--zd-keyline-weight) * 1.6);
	--zd-accent-bar-h: calc(var(--zd-keyline-weight) * 1.6);
	--zd-accent-bar-radius: 999px;
	--zd-keyline-gap: 0.5rem;
}

.zd-keyline-square {
	--zd-accent-bar-w: calc(var(--zd-keyline-weight) * 1.35);
	--zd-accent-bar-h: calc(var(--zd-keyline-weight) * 1.35);
	--zd-accent-bar-radius: 0;
	--zd-keyline-gap: 0.5rem;
}

/* rotate, not transform: the independent property composes rather than
 * overwrites, and the motion system transforms .zd-rise elements — the hero
 * eyebrow is one. The rotation changes no layout (transforms never do), so the
 * flex line, the label's baseline and the CTA band's centring all still see the
 * square box; the corners simply paint a little outside it, which is why this
 * shape alone keeps a wider gap. */
.zd-keyline-diamond {
	--zd-accent-bar-w: calc(var(--zd-keyline-weight) * 1.25);
	--zd-accent-bar-h: calc(var(--zd-keyline-weight) * 1.25);
	--zd-accent-bar-radius: 0;
	--zd-keyline-rotate: 45deg;
	--zd-keyline-gap: 0.625rem;
}

/* The one shape drawn as a stroke rather than a fill, which is why the mark
 * carries a border at all. The stroke is a fraction of the keyline's own weight
 * rather than the site's hairline: --zd-rule-w is tuned for a nav underline and
 * is a fixed 1.5-3px, so borrowing it made the ring's stroke and the ring's
 * size move independently — a chunky sector got a big ring drawn in a thin
 * line. At 0.3 the hole stays open at every weight, which is the whole job. */
.zd-keyline-ring {
	--zd-accent-bar-w: calc(var(--zd-keyline-weight) * 1.9);
	--zd-accent-bar-h: calc(var(--zd-keyline-weight) * 1.9);
	--zd-accent-bar-radius: 999px;
	--zd-keyline-fill: none;
	--zd-keyline-border: calc(var(--zd-keyline-weight) * 0.3);
	--zd-keyline-gap: 0.5rem;
}

/* display rather than visibility: a hidden box would still hold its place on
 * the flex line and leave the label with a gap in front of nothing, and would
 * keep the CTA band's bottom margin. Removing the box removes both, so "none"
 * needs no gap handling of its own. */
.zd-keyline-none {
	--zd-keyline-display: none;
}

/* Over a dark ground the label and its tick both need lifting. .zd-bg-dark is
 * already handled by the dark-background rules at the foot of this file; the
 * full-bleed hero paints its own scrim instead, so it needs saying here. */
.zerodigital-hero-home--full-bg-overlay .zd-spec {
	color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 85%, transparent);
}

/* Numbers as spec: prices, ratings, counts, hours, phone. Tabular figures so
 * digits stay in column wherever they stack (pricing tiers, opening hours). */
.zd-data {
	font-family: var(--zd-mono);
	font-variant-numeric: tabular-nums;
	letter-spacing: 0.04em;
}

/* --------------------------------------------------------------------------
 * Section header: spec label, heading, optional lede — on one left axis.
 * The old pattern centred all three, which is the single biggest reason every
 * section looked like the last one. Blocks opt in by wrapping their existing
 * heading markup; no field changes are involved.
 * ------------------------------------------------------------------------ */
.zd-section-head {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--wp--preset--spacing--2-xs, 0.5rem);
	max-width: 44rem;
	margin-block-end: clamp(var(--wp--preset--spacing--lg, 2rem), 4vw, var(--wp--preset--spacing--xl, 3rem));
}

.zd-section-head__title {
	margin: 0;
	font-family: var(--wp--preset--font-family--heading, inherit);
	font-size: var(--zd-step-h2);
	line-height: 1.1;
	letter-spacing: -0.015em;
	text-wrap: balance;
}

.zd-section-head__lede {
	margin: 0;
	max-width: 52ch;
	font-size: var(--zd-step-lede);
	line-height: 1.6;
	color: var(--wp--preset--color--muted, #565b62);
}

/* Centred remains available where a layout genuinely calls for it (a solo
 * featured item, the CTA band) — it is now the exception, not the default. */
.zd-section-head--centre {
	align-items: center;
	text-align: center;
	margin-inline: auto;
}

/* Eyebrow: small, tracked, accent-coloured label used above headings. */
.zd-eyebrow {
	display: block;
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: 0.8125rem;
	font-weight: 600;
	line-height: 1.2;
	text-transform: uppercase;
	letter-spacing: var(--zd-eyebrow-tracking);
	color: var(--zd-accent);
	margin: 0 0 var(--wp--preset--spacing--xs, 0.75rem);
}

::selection {
	background: color-mix(in srgb, var(--wp--preset--color--accent, #c9922a) 28%, transparent);
}

/* --------------------------------------------------------------------------
 * Site header. The <header> landmark comes from the header template part;
 * the bar inside it is constrained to the content width so the logo and
 * nav don't run hard against the screen edges.
 * ------------------------------------------------------------------------ */
.wp-site-blocks > header {
	position: relative;
	z-index: 100;
	background: var(--wp--preset--color--surface, #fff);
	border-bottom: var(--zd-border, 1px solid #e4e6e9);
}

.zerodigital-header__bar {
	position: relative;
	z-index: 110;
	max-width: var(--wp--style--global--wide-size, 1600px);
	margin-inline: auto;
	align-items: center;
	gap: var(--wp--preset--spacing--md, 1.5rem);
	padding: 0.85rem var(--zd-menu-inset, 1.5rem);
}

.zerodigital-header__brand {
	align-items: center;
	gap: var(--wp--preset--spacing--xs, 0.75rem);
}

/* The brand mark itself — logo (from Business Details) or business-name
 * wordmark — is the zerodigital/site-brand block; it owns its own sizing and
 * type in the block stylesheet. The header only positions it. */
.zerodigital-header__brand .zerodigital-site-brand {
	display: inline-flex;
	align-items: center;
}

.zerodigital-header__actions {
	align-items: center;
	gap: clamp(var(--wp--preset--spacing--sm, 1rem), 2.5vw, var(--wp--preset--spacing--lg, 2rem));
}

.zerodigital-header__actions .zerodigital-business-phone {
	color: var(--wp--preset--color--primary, #0e4b52);
	font-weight: 600;
}

/* Header nav — accent-underline on hover / current (accent system). */
.zerodigital-header__actions .wp-block-navigation {
	font-weight: 500;
}

/* Desktop bar nav reads neater a step below body size. Scoped to the bar's own
 * width (>=1200px) so the mobile drawer keeps its larger, tap-friendly size.
 * The nav and its items carry WordPress's has-medium-font-size preset, which
 * sets the size with !important — so this override must be !important too, and
 * has to reach the items, not just the container. */
@media (min-width: 1200px) {
	.zerodigital-header__actions .wp-block-navigation,
	.zerodigital-header__actions .wp-block-navigation .wp-block-navigation-item {
		font-size: 0.9375rem !important;
		letter-spacing: 0.01em;
	}
}

.zerodigital-header__actions .wp-block-navigation a {
	position: relative;
	color: var(--wp--preset--color--foreground, #16181b);
	text-decoration: none;
	padding-block: 0.35rem;
}

.zerodigital-header__actions .wp-block-navigation a::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: var(--zd-rule-w, 0.1875rem);
	background: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	transform: scaleX(0);
	transform-origin: left;
	transition: transform var(--zd-ease, 180ms ease);
}

.zerodigital-header__actions .wp-block-navigation a:hover::after,
.zerodigital-header__actions .wp-block-navigation .current-menu-item > a::after {
	transform: scaleX(1);
}

/* Phone rendered as an outlined pill at the end of the header. */
.zerodigital-header__actions .zerodigital-business-phone {
	padding: 0.5rem 1rem;
	border: 1.5px solid color-mix(in srgb, currentColor 28%, transparent);
	border-radius: var(--wp--custom--radius--full, 999px);
	transition: background-color var(--zd-ease, 180ms ease), border-color var(--zd-ease, 180ms ease), color var(--zd-ease, 180ms ease);
}

.zerodigital-header__actions .zerodigital-business-phone:hover {
	background: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	border-color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	color: var(--wp--preset--color--background, #fff);
}

/* On small screens the phone collapses to an icon-only pill (only when an
 * icon is present, so a text-only phone still shows its number). */
@media (max-width: 600px) {
	.zerodigital-header__actions .zerodigital-business-phone:has(.zerodigital-business-phone__icon) .zerodigital-business-phone__label {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip: rect(0 0 0 0);
		white-space: nowrap;
	}

	.zerodigital-header__actions .zerodigital-business-phone:has(.zerodigital-business-phone__icon) {
		padding: 0.55rem;
	}
}

/* --------------------------------------------------------------------------
 * Nav collapse breakpoint. The native Navigation block collapses at 600px,
 * which cramps a header carrying a logo, several menu items and a phone CTA.
 * These rules move the collapse to 1200px by overriding the block's own
 * responsive media queries — still the native block and its native open/close
 * JS, just a wider, more usable collapse point.
 * ------------------------------------------------------------------------ */
@media (max-width: 1199.98px) {
	.zerodigital-header__actions .wp-block-navigation__responsive-container-open:not(.always-shown) {
		display: flex;
	}

	/* The closed container is hidden by the modal rules below (fixed +
	 * visibility:hidden), not display:none — that keeps the card in the DOM
	 * so it can animate out to the right when the menu closes. */
}

@media (min-width: 1200px) {
	.zerodigital-header__actions .wp-block-navigation__responsive-container-open {
		display: none;
	}

	/* Never leave the overlay stuck open if resized up from mobile. */
	.zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open {
		display: flex;
		position: static;
		width: auto;
		background: transparent;
	}
}

/* --------------------------------------------------------------------------
 * Mobile menu = floating modal card. It opens BELOW the header (never
 * overlapping it), floats clear of every screen edge, and slides in from the
 * right. The page dims behind it; the logo, phone and toggle stay visible.
 * The card stays in the DOM when closed (translated off-screen), so it can
 * animate both IN and OUT with a transform transition rather than a one-way
 * keyframe. The hamburger's spot shows a cross while open.
 * ------------------------------------------------------------------------ */

/* When collapsed, the phone sits before the toggle so the ☰ (and the ✕ that
 * replaces it) land at the far right, with the phone icon still visible. */
@media (max-width: 1199.98px) {
	.zerodigital-header__actions .zerodigital-business-phone {
		order: -1;
	}

	/* Collapsed, only the phone and the ☰ toggle remain in this row. The wide
	 * clamp gap that spaces out the full desktop nav leaves them drifting apart,
	 * so pull them together. */
	.zerodigital-header__actions {
		gap: var(--wp--preset--spacing--2-xs, 0.5rem);
	}
}

/* Replace WordPress's two default nav buttons (open ☰ + close ✕) with our
 * single animated toggle below — but ONLY while .zd-nav-js is on <html>. The
 * class is printed inline in the head (functions.php) and withdrawn at
 * DOMContentLoaded if the deferred nav.js never ran. Hiding them unconditionally
 * meant a blocked or failed script left the native controls invisible AND our
 * toggle inert, so there was no way whatsoever to open the menu on mobile.
 * Gated, the failure degrades to WordPress's own working buttons instead. */
.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container-open,
.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container-close {
	display: none !important;
}

/* Custom toggle: a pure-CSS hamburger that morphs to a cross. Shown only at
 * the collapse width; sits above the drawer so it's always clickable. */
.zerodigital-nav-toggle {
	display: none;
	position: relative;
	z-index: 130;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
	border: 0;
	background: transparent;
	color: inherit;
	cursor: pointer;
}

/* Shown only alongside the gate above, so the two controls can never both be
 * visible and can never both be missing. */
@media (max-width: 1199.98px) {
	.zd-nav-js .zerodigital-nav-toggle {
		display: inline-flex;
	}
}

.zerodigital-nav-toggle__box {
	position: relative;
	display: block;
	width: 1.5rem;
	height: 1rem;
}

.zerodigital-nav-toggle__line {
	position: absolute;
	left: 0;
	width: 100%;
	height: 2px;
	border-radius: 2px;
	background: currentColor;
	transition: top 0.3s ease, transform 0.3s ease, opacity 0.2s ease, background-color var(--zd-ease, 180ms ease);
}

.zerodigital-nav-toggle__line:nth-child(1) {
	top: 0;
}

.zerodigital-nav-toggle__line:nth-child(2) {
	top: 50%;
	transform: translateY(-50%);
}

.zerodigital-nav-toggle__line:nth-child(3) {
	top: 100%;
	transform: translateY(-100%);
}

/* Active → rotate the outer lines into an X, fade the middle out. Colour
 * stays currentColor throughout, so no jump to black. */
.zerodigital-nav-toggle.is-active .zerodigital-nav-toggle__line:nth-child(1) {
	top: 50%;
	transform: translateY(-50%) rotate(45deg);
}

.zerodigital-nav-toggle.is-active .zerodigital-nav-toggle__line:nth-child(2) {
	opacity: 0;
}

.zerodigital-nav-toggle.is-active .zerodigital-nav-toggle__line:nth-child(3) {
	top: 50%;
	transform: translateY(-50%) rotate(-45deg);
}

.zerodigital-nav-toggle:focus-visible {
	outline: 2px solid var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	outline-offset: 2px;
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
}

/* Lock page scroll while the drawer is open. */
body.zd-menu-open {
	overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
	.zerodigital-nav-toggle__line {
		transition: none;
	}
}

@media (max-width: 1199.98px) {
	/* Invisible click-catching layer — deliberately NOT a dim. The site stays
	 * fully visible around the card: a strip down each side on phones, and the
	 * whole left of the page once the card caps at its max-width. The layer
	 * exists only so a tap outside the card closes the menu; !important
	 * defeats the menu background WordPress paints onto this container.
	 * Kept mounted but inert when closed so the card can transition out — the
	 * delayed visibility flip lets the slide-out finish before it hides. */
	.zerodigital-header__actions .wp-block-navigation__responsive-container {
		position: fixed;
		inset: 0;
		z-index: 90;
		display: block;
		width: auto;
		background: transparent !important;
		visibility: hidden;
		pointer-events: none;
		transition: visibility 0s linear 400ms;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open {
		visibility: visible;
		pointer-events: auto;
		transition: visibility 0s;
	}

	/* Neutralise WordPress's inner close wrapper so our card owns the layout.
	 * With the dialog fixed-positioned, this wrapper collapses to nothing,
	 * leaving the container itself to catch backdrop clicks. */
	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation__responsive-close {
		background: transparent !important;
		max-width: none;
		width: auto;
		height: auto;
		padding: 0;
	}

	/* The floating card. Equal left/right insets give the edge gap on small
	 * screens (effectively width: calc(100% - 2rem)); max-width caps it and
	 * margin-left:auto anchors it to the right on wider screens. Pinning both
	 * top and bottom makes it a fixed, full-height panel — its height comes
	 * from the viewport, never from the number of menu items, so two links and
	 * twenty produce the same card. It slides in from off-screen right via a
	 * transform transition — reversible on close. */
	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation__responsive-dialog {
		position: fixed;
		/* --zd-menu-top is measured from the real header by nav.js on open, so
		 * the card clears whichever header variant is active (the traditional
		 * bar and the floating bar differ in height). The token is only a
		 * fallback for the first paint / no-JS. */
		top: calc(var(--zd-menu-top, var(--zd-header-height, 4.75rem)) + 1rem);
		right: 1rem;
		left: 1rem;
		bottom: 1rem;
		max-width: 380px;
		margin: 0 0 0 auto;
		padding: clamp(1.25rem, 5vw, 1.75rem);
		background: var(--wp--preset--color--surface, #fff);
		/* The card owns its text colour. It's a DOM child of the header bar
		 * even though it's fixed-positioned, so it would otherwise inherit
		 * the bar's colour — which the floating variant flips to near-white
		 * for the coloured bar, leaving white text on this white card. Pinning
		 * foreground here keeps the card readable whatever the bar is doing,
		 * and pairs correctly with the surface background in every variation. */
		color: var(--wp--preset--color--foreground, #16181b);
		border: 1px solid var(--wp--preset--color--border, #e4e6e9);
		border-radius: var(--wp--custom--radius--lg, 1rem);
		box-shadow: 0 24px 60px rgba(15, 18, 22, 0.28);
		overflow-y: auto;
		transform: translateX(calc(100% + 1.5rem));
		opacity: 0;
		transition: transform 360ms cubic-bezier(0.16, 1, 0.3, 1), opacity 220ms ease;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-dialog {
		transform: translateX(0);
		opacity: 1;
	}

	/* Menu list: vertical, full-width rows stacked inside the card. */
	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation__container {
		flex-direction: column;
		align-items: stretch;
		gap: 0;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation-item {
		width: 100%;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation-item a {
		display: block;
		padding: 0.85rem 0.25rem;
		font-size: 1.15rem;
		font-weight: 500;
		color: var(--wp--preset--color--foreground, #16181b);
		border-bottom: 1px solid var(--wp--preset--color--border, #e4e6e9);
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation-item:last-child a {
		border-bottom: 0;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation-item a::after {
		display: none;
	}

	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation-item a:hover {
		color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	}
}

@media (prefers-reduced-motion: reduce) {
	.zerodigital-header__actions .wp-block-navigation__responsive-container,
	.zerodigital-header__actions .wp-block-navigation__responsive-container .wp-block-navigation__responsive-dialog {
		transition: none;
	}
}

/* The drawer's in-dialog close button, added by nav.js. The visible close
 * control is the toggle, which sits outside the aria-modal dialog; a screen
 * reader that confines itself to the dialog (iOS VoiceOver) could not reach it.
 * This one is for that reader: clipped to nothing until it takes focus, then an
 * ordinary row at the foot of the card — static, so it lands after the menu
 * rather than over it, and in the drill-down card's flex column it takes its
 * own row below the panels. Keyboard Tab skips it; the toggle is in that ring. */
.zerodigital-nav-close {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
	background: transparent;
	color: inherit;
	font: inherit;
	cursor: pointer;
}

.zerodigital-nav-close:focus {
	position: static;
	flex: 0 0 auto;
	width: auto;
	height: auto;
	min-height: var(--zd-btn-min-h, 2.875rem);
	margin: var(--wp--preset--spacing--sm, 1rem) auto 0 0;
	padding: 0 var(--wp--preset--spacing--sm, 1rem);
	overflow: visible;
	clip-path: none;
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
	font-weight: 600;
}

/* The drill-down card gives its padding to the panels, so the button has to
 * bring the panels' inset with it or it sits on the card's edge. */
.zd-menu-drilldown .zerodigital-nav-close:focus {
	margin: 0 auto clamp(1.25rem, 5vw, 1.75rem) clamp(1.25rem, 5vw, 1.75rem);
}

/* Above the collapse width the navigation is the desktop bar, not a dialog,
 * and a "Close menu" button in it would be announced for a menu that is not
 * open and cannot be. */
@media (min-width: 1200px) {
	.zerodigital-nav-close {
		display: none;
	}
}

/* --------------------------------------------------------------------------
 * Mobile drawer — submenus.
 *
 * Inside its own overlay, core force-expands every submenu and hides the
 * control that would collapse one again:
 *
 *   .wp-block-navigation__responsive-container.is-menu-open …
 *     .wp-block-navigation__submenu-container { position: static; height: auto;
 *       visibility: visible; padding-left: 2rem; }
 *   … .wp-block-navigation__submenu-icon { display: none; }
 *
 * Which is survivable for a three-item menu and unusable for a real one:
 * parents and children arrive as one undifferentiated stack, indented 2rem,
 * with nothing to say which rows belong to which — or that the list could be
 * shortened at all. A menu with three parents and fifteen children is eighteen
 * identical rows.
 *
 * Both halves are undone here and replaced with a real disclosure. Two modes,
 * chosen in Site Settings → Mobile Menu Submenus and arriving as a body class:
 *
 *   .zd-menu-accordion  children drop down in place, under their parent.
 *   .zd-menu-drilldown  children slide in as their own panel, with a back
 *                       button. nav.js builds those panels; see the note on
 *                       .zerodigital-nav-drill below for why they are a flat
 *                       stack rather than the nested lists core renders.
 *
 * EVERY rule below is gated on .zd-nav-js, for exactly the reason the toggle
 * gate near the top of this file gives. Collapsing a submenu is only safe while
 * something can open it again; if nav.js never runs, this whole section
 * switches off and core's always-expanded submenus — long, but complete — come
 * back. A script failure must cost the visitor tidiness, never a page.
 *
 * The selectors carry .is-menu-open purely for weight: core's rules above are
 * four classes, and nothing guarantees this stylesheet is printed after the
 * block's own. Five beats four whatever the order settles on.
 * ------------------------------------------------------------------------ */
@media (max-width: 1199.98px) {

	/* ---- The list itself ------------------------------------------------- */

	/* Core lays the dialog's content out as a flex ROW, which leaves the menu
	 * as wide as its longest label rather than as wide as the card — so the
	 * hairlines stop mid-row and a right-aligned control has nothing to sit
	 * against. The theme has always asked for a full-width column here; these
	 * two rules are what make the asking stick. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
		display: block;
		width: 100%;
	}

	/* Core hands the drawer the horizontal bar's own layout, and both halves of
	 * it are wrong once the menu is a stacked list:
	 *
	 *   … .wp-block-navigation__container { gap: inherit }
	 *   … .wp-block-navigation-item { align-items:
	 *       var(--navigation-layout-justification-setting, initial) }
	 *
	 * `gap: inherit` picks up the block gap — 2em by default — and opens a hole
	 * between every row, so four menu items fill a phone screen. The alignment
	 * shrinks each row to the width of its own label, which is why a hairline
	 * under a short item stopped halfway across the card. Both rules are four
	 * classes; the theme has always asked for a tight full-width column here and
	 * was asking with three. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container,
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container {
		gap: 0;
		align-items: stretch;
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
		width: 100%;
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item {
		width: 100%;
		align-items: stretch;
	}

	/* Likewise the row height. Core zeroes the padding on every link inside its
	 * overlay — `… .is-menu-open … .wp-block-navigation-item__content
	 * { padding: 0 }`, four classes, against the three the theme's own row rule
	 * carries — so the rows have been collapsing to the height of their text.
	 * That was survivable while a row was only a link. It is not survivable now
	 * a row is a link AND a control: the two have to be the same height, and
	 * both have to clear the 44px target size (WCAG 2.5.5). */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
		display: flex;
		align-items: center;
		min-height: var(--zd-btn-min-h, 2.875rem);
		padding: 0.85rem 0.25rem;
	}

	/* ---- Collapsed, and the row that opens it ---------------------------- */

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container {
		position: static;
		width: 100%;
		min-width: 0;
		height: 0;
		margin: 0;
		padding: 0;
		border: 0;
		border-radius: 0;
		background: transparent;
		box-shadow: none;
		opacity: 0;
		visibility: hidden;
		/* Clipped so the rows inside can be revealed by height alone. The child
		 * links carry outline-offset: -2px below because of it — a focus ring
		 * drawn outside the row would be cut off against these edges. */
		overflow: hidden;
		transition: height 280ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease, visibility 0s linear 280ms;
	}

	/* Keyed on aria-expanded rather than a class, which is the one part of this
	 * contract core will not quietly rename: the attribute is what the button
	 * means, and nav.js and assistive technology already read the same value. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container {
		height: auto;
		opacity: 1;
		visibility: visible;
		transition: height 280ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms ease, visibility 0s;
	}

	/* A parent becomes a split row: the label still goes to the parent's own
	 * page, and a separate control opens the children. Making the whole row open
	 * the submenu would be the bigger tap target, but it would also be the only
	 * row on the site whose text is not a link to the thing it names. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item.has-child {
		display: flex;
		/* Stated, not left to inherit: core sets every item in its overlay to
		 * flex-direction: column, so a row that only asks for `display: flex`
		 * gets a column and the arrow lands underneath the label. */
		flex-direction: row;
		flex-wrap: wrap;
		align-items: stretch;
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item.has-child > .wp-block-navigation-item__content {
		flex: 1 1 auto;
		min-width: 0;
	}

	/* flex: 0 0 auto, and the width comes from the rule above — NOT flex-basis:
	 * 100%. Given a definite flex-basis the engine sizes this list from its
	 * content and the collapsed height is ignored, inline `height: 0` included,
	 * so the submenu stands permanently open. Measured, not reasoned about. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item.has-child > .wp-block-navigation__submenu-container {
		flex: 0 0 auto;
	}

	/* ---- The disclosure -------------------------------------------------- */

	/* Core already renders this button, server-side, next to the link — with a
	 * real aria-expanded on it — and then hides it in the overlay. Showing it
	 * again costs nothing and means no markup is injected, so the arrow is in
	 * the HTML at first paint rather than appearing when a script gets round to
	 * it.
	 *
	 * :not(...__content) matters. With "open on click" set on the Navigation
	 * block, core merges the toggle INTO the label button, and sizing that to
	 * 2.75rem would crush the row to a square. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle:not(.wp-block-navigation-item__content) {
		display: flex;
		flex: 0 0 auto;
		align-items: center;
		justify-content: center;
		width: 2.75rem;
		/* height:auto, because core sizes this control as an icon —
		 * `.wp-block-navigation__submenu-icon { width: .6em; height: .6em }` —
		 * and a definite cross size opts a flex item out of stretching. Left
		 * alone the button came out 7px shorter than the link beside it, which
		 * put its half of the row's hairline 7px higher than the other half. */
		height: auto;
		align-self: stretch;
		min-height: var(--zd-btn-min-h, 2.875rem);
		margin: 0;
		padding: 0;
		border: 0;
		border-bottom: 1px solid var(--wp--preset--color--border, #e4e6e9);
		/* The hairline is the whole affordance: without it the arrow reads as
		 * decoration on the end of the link rather than as its own control. */
		border-left: 1px solid var(--wp--preset--color--border, #e4e6e9);
		background: transparent;
		color: inherit;
		cursor: pointer;
	}

	/* Core's chevron is a 12×12 box stroked at 1.5, which next to the site's
	 * icons (Tabler, 24×24 stroked at 2) reads noticeably heavier. 1 in a 12
	 * box is the same stroke-to-box ratio as 2 in 24, so the two match exactly
	 * — and it is a restyle of core's own SVG, not a second icon shipped. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle svg {
		width: 1.125rem;
		height: 1.125rem;
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle svg path {
		stroke: currentColor;
		stroke-width: 1;
		stroke-linecap: round;
		stroke-linejoin: round;
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle:hover,
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle[aria-expanded="true"] {
		color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	}

	/* Inset, because the button sits flush against the card's own edge — an
	 * outline drawn outside it would be clipped by the card. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle:focus-visible {
		outline: 2px solid var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
		outline-offset: -2px;
		border-radius: var(--wp--custom--radius--sm, 0.375rem);
	}

	/* The link's own hairline is dropped on the last row of a list; the button
	 * shares that row, so it has to be dropped there too or the line runs on
	 * under nothing. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:last-child > .wp-block-navigation-submenu__toggle {
		border-bottom: 0;
	}

	/* …unless that last row is open, where the same line stops being the end of
	 * the list and becomes the seam between a parent and its own children. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:last-child:has(.wp-block-navigation-submenu__toggle[aria-expanded="true"]) > .wp-block-navigation-item__content,
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:last-child:has(.wp-block-navigation-submenu__toggle[aria-expanded="true"]) > .wp-block-navigation-submenu__toggle {
		border-bottom: 1px solid var(--wp--preset--color--border, #e4e6e9);
	}

	/* ---- Child rows ------------------------------------------------------ */

	/* Secondary by size and weight only. Fading the colour would be the usual
	 * move and would have to clear 4.5:1 in five different palettes to stay
	 * legal; the type scale says the same thing and cannot fail a contrast
	 * check. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container .wp-block-navigation-item a {
		padding-top: 0.7rem;
		padding-bottom: 0.7rem;
		font-size: 1rem;
		font-weight: 400;
		outline-offset: -2px;
	}

	/* A submenu's last child still needs its hairline — the row after it belongs
	 * to the level above, and without a line the two levels run together. Only
	 * the very last row in the card goes without. */
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container .wp-block-navigation-item:last-child a {
		border-bottom: 1px solid var(--wp--preset--color--border, #e4e6e9);
	}

	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:last-child > .wp-block-navigation__submenu-container > .wp-block-navigation-item:last-child a {
		border-bottom: 0;
	}
}

/* --------------------------------------------------------------------------
 * Mode 1 — dropdown. Children open in place, under the parent that names them.
 * ------------------------------------------------------------------------ */
@media (max-width: 1199.98px) {
	/* The turn is animated only here. In slide-across the arrow points one way
	 * and stays there, and a transition on it meant the whole row of arrows
	 * swung into place every time the drawer opened. */
	.zd-nav-js .zd-menu-accordion .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle svg {
		transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
	}

	.zd-nav-js .zd-menu-accordion .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle[aria-expanded="true"] svg {
		transform: rotate(180deg);
	}

	/* Indented and ticked down the side with the accent, which is the mark this
	 * site already uses for "this belongs to that". It is drawn at the hairline
	 * weight rather than the keyline's: the keyline is a shape now, and a site
	 * set to Dot would have had this edge disappear with it. Core's flat 2rem
	 * indent said nothing; this says whose children these are. */
	.zd-nav-js .zd-menu-accordion .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container {
		margin-left: 0.25rem;
		padding-left: 1rem;
		border-left: var(--zd-rule-w, 0.1875rem) solid var(--zd-accent-tint, rgba(196, 38, 60, 0.16));
	}
}

/* --------------------------------------------------------------------------
 * Mode 2 — slide across.
 *
 * nav.js builds a FLAT stack of panels, one per level, all siblings inside
 * .zerodigital-nav-drill — not the nested lists core renders. Nesting was tried
 * first and cannot work: a panel that scrolls is a scroll container, a scroll
 * container clips both axes, and the child panel waiting off to the right lives
 * inside it. A long submenu could then scroll or slide, never both. As siblings
 * each panel scrolls itself, and one custom property on the viewport moves the
 * whole stack.
 *
 * The panels are cloned from core's markup rather than moved out of it, so the
 * lists the desktop dropdown depends on are still exactly where core left them
 * and nothing has to be put back on resize. The clone is runtime-only: the HTML
 * WordPress serves is unchanged.
 * ------------------------------------------------------------------------ */
@media (max-width: 1199.98px) {
	/* Padding moves from the card to the panels so a slide runs edge to edge
	 * instead of stopping short inside a frame. */
	.zd-nav-js .zd-menu-drilldown .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-dialog {
		display: flex;
		flex-direction: column;
		padding: 0;
		overflow: hidden;
	}

	.zd-nav-js .zd-menu-drilldown .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
		display: flex;
		flex-direction: column;
		flex: 1 1 auto;
		min-height: 0;
	}

	/* The list core rendered is the source the panels were cloned from. It stays
	 * in the DOM and steps out of the way. */
	.zd-nav-js .zd-menu-drilldown .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content > .wp-block-navigation__container {
		display: none;
	}

	.zd-nav-js .zd-menu-drilldown .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle svg {
		transform: rotate(-90deg);
	}
}

.zerodigital-nav-drill {
	position: relative;
	flex: 1 1 auto;
	/* Stated rather than left to the flex container to stretch: every child is
	 * absolutely positioned, so an unstretched viewport measures zero wide and
	 * takes the whole stack of panels down to nothing with it. */
	width: 100%;
	min-height: 0;
	overflow: hidden;
}

/* Every panel is placed by the same expression: its own depth minus the depth
 * currently on screen. One custom property therefore animates all of them
 * together, in one direction, with no per-panel bookkeeping. */
.zerodigital-nav-drill__panel {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	padding: clamp(1.25rem, 5vw, 1.75rem);
	overflow-y: auto;
	overscroll-behavior: contain;
	visibility: hidden;
	transform: translateX(calc(100% * (var(--zd-panel-depth, 0) - var(--zd-drill-depth, 0))));
	transition: transform 300ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s linear 300ms;
}

/* Visible along the whole open path, not just at the end of it — the panel
 * being left has to stay on screen for as long as it takes to leave. nav.js
 * marks everything but the current panel inert so the ones parked off-screen
 * are not reachable while they wait. */
.zerodigital-nav-drill__panel.is-open {
	visibility: visible;
	transition: transform 300ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s;
}

/* Applied for one frame while nav.js puts the stack back to the top level as
 * the drawer reopens. Without it the visitor watches the menu rewind itself
 * every time they tap the hamburger. */
.zerodigital-nav-drill.is-static .zerodigital-nav-drill__panel {
	transition: none;
}

.zerodigital-nav-drill__head {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	gap: 0.6rem;
	margin-bottom: 0.35rem;
	padding-bottom: 0.85rem;
	border-bottom: 1px solid var(--wp--preset--color--border, #e4e6e9);
}

.zerodigital-nav-drill__back {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	margin-left: -0.6rem;
	padding: 0;
	border: 0;
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
	background: transparent;
	color: inherit;
	cursor: pointer;
}

/* The same chevron as the rows, turned. One glyph doing three jobs — down to
 * open, right to go in, left to come back — reads as a system; three different
 * arrows would read as three decisions. */
.zerodigital-nav-drill__back svg {
	width: 1.125rem;
	height: 1.125rem;
	transform: rotate(90deg);
}

.zerodigital-nav-drill__back svg path {
	stroke: currentColor;
	stroke-width: 1;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.zerodigital-nav-drill__back:hover {
	color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
}

.zerodigital-nav-drill__back:focus-visible {
	outline: 2px solid var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	outline-offset: 2px;
}

/* .zd-spec is the site's own section label — mono, tracked, led by the accent
 * tick. Reusing it is the point: arriving at a submenu should feel like
 * arriving at a section of the site, in the mark the site already uses to open
 * one, rather than in a header invented for the menu. */
.zerodigital-nav-drill__title {
	min-width: 0;
}

/* Above the collapse point the whole drawer is dismantled into a horizontal
 * bar, and a stack of absolutely-positioned panels inside it would sit on top
 * of the page. */
@media (min-width: 1200px) {
	.zerodigital-nav-drill {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__submenu-container,
	.zd-nav-js .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle[aria-expanded="true"] ~ .wp-block-navigation__submenu-container,
	.zd-nav-js .zd-menu-accordion .zerodigital-header__actions .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-submenu__toggle svg,
	.zerodigital-nav-drill__panel,
	.zerodigital-nav-drill__panel.is-open {
		transition: none;
	}
}

/* --------------------------------------------------------------------------
 * Floating header variant (Business Details → Header Style → Floating).
 * A rounded bar that sits over the top of the hero. It scrolls away with the
 * page unless Site Settings → Header on scroll says otherwise (see "Header on
 * scroll" below). The hero gains top padding to clear it.
 * ------------------------------------------------------------------------ */
.zd-header-floating .wp-site-blocks > header {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 100;
	background: transparent;
	border-bottom: none;
	padding-inline: var(--zd-header-gutter);
	pointer-events: none;
}

.zd-header-floating .zerodigital-header__bar {
	pointer-events: auto;
	margin-block: clamp(0.75rem, 2vw, 1.25rem);
	padding: 0.65rem var(--zd-menu-inset, 1.25rem);
	background: var(--wp--preset--color--primary, #0e4b52);
	color: var(--wp--preset--color--background, #fff);
	border-radius: var(--wp--custom--radius--lg, 0.75rem);
	box-shadow: var(--zd-shadow-lg, 0 14px 28px -8px rgba(16, 24, 40, 0.16));
}

/* On the coloured floating bar, brand / phone read light. These stay in the
 * bar at every width, so they invert at every width. (The logo image is the
 * client's own asset; only the name-wordmark fallback needs inverting.) */
.zd-header-floating .zerodigital-header__brand .zerodigital-site-brand__name,
.zd-header-floating .zerodigital-header__actions .zerodigital-business-phone {
	color: var(--wp--preset--color--background, #fff);
}

/* The nav is the exception: it only sits on the coloured bar from the collapse
 * point up. Below that it moves into the white modal card, where light text
 * would be invisible — so it must keep the standard foreground colour there.
 * Scoping this to the bar's own breakpoint keeps the two cases from fighting
 * (they otherwise carry equal specificity, and this rule wins on source order). */
@media (min-width: 1200px) {
	.zd-header-floating .zerodigital-header__actions .wp-block-navigation a {
		color: var(--wp--preset--color--background, #fff);
	}
}

.zd-header-floating .zerodigital-header__actions .zerodigital-business-phone {
	border-color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 45%, transparent);
}

/* --------------------------------------------------------------------------
 * Floating header, contrast on the dark bar (WCAG 1.4.11 / 2.4.7).
 *
 * The accent is tuned dark for AA text on light backgrounds, which leaves it
 * too low-contrast against this near-black bar — for both the focus ring and
 * the nav underline. On the bar, elements that always sit there (brand, phone,
 * toggle) take a light (background-coloured) focus ring, which clears 3:1 on
 * every variation's primary. The nav is the awkward one: on the bar above the
 * collapse point, but inside the WHITE modal card below it — so only the
 * on-bar case is overridden, and the modal keeps the standard accent ring.
 * ------------------------------------------------------------------------ */
.zd-header-floating .zerodigital-header__brand a:focus-visible,
.zd-header-floating .zerodigital-header__actions .zerodigital-business-phone:focus-visible,
.zd-header-floating .zerodigital-nav-toggle:focus-visible {
	outline-color: var(--wp--preset--color--background, #fff);
}

@media (min-width: 1200px) {
	.zd-header-floating .zerodigital-header__actions .wp-block-navigation a:focus-visible {
		outline-color: var(--wp--preset--color--background, #fff);
	}

	/* Current/hover underline: match the (white) link text rather than the
	 * dark accent, so the current-page state is visible on the bar. The
	 * traditional light header keeps its accent underline. */
	.zd-header-floating .zerodigital-header__actions .wp-block-navigation a::after {
		background: currentColor;
	}
}

/* --------------------------------------------------------------------------
 * Header nav submenu (the desktop dropdown).
 *
 * Nothing here had ever been styled for a nested nav item, because until the
 * therapies menu gained children there weren't any. The result was invisible
 * rather than ugly: the floating-bar rule above paints EVERY nav link white,
 * core renders the submenu as its own white card, and the items vanished into
 * it — white on white.
 *
 * Deliberately placed after the floating-header block. Several of these
 * selectors carry the same specificity as the rules they need to beat, so
 * source order is what decides, exactly as the nav colour rule at the top of
 * that block already notes.
 *
 * Scoped to the bar's own breakpoint. Below it there is no dropdown: the nav
 * moves into the white modal drawer and core stacks the children inline, where
 * a floating card with its own shadow would be wrong.
 * ------------------------------------------------------------------------ */
@media (min-width: 1200px) {
	.zerodigital-header__actions .wp-block-navigation__submenu-container {
		min-width: 14rem;
		padding-block: 0.375rem;
		background-color: var(--wp--preset--color--surface, #fff);
		border: 1px solid var(--wp--preset--color--border, #e4e6e9);
		border-radius: var(--wp--custom--radius--md, 0.5rem);
		box-shadow: var(--zd-shadow-lg, 0 14px 28px -8px rgba(16, 24, 40, 0.16));
	}

	.zerodigital-header__actions .wp-block-navigation__submenu-container li > a {
		display: block;
		width: 100%;
		padding: 0.5rem 1rem;
		color: var(--wp--preset--color--foreground, #16181b);
	}

	/* The underline belongs to the horizontal bar, where it reads as an accent
	 * beneath a row of links. Stacked in a dropdown the same bar reads as a rule
	 * between rows, so the hover state becomes a background tint instead. */
	.zerodigital-header__actions .wp-block-navigation__submenu-container a::after {
		content: none;
	}

	.zerodigital-header__actions .wp-block-navigation__submenu-container li > a:hover,
	.zerodigital-header__actions .wp-block-navigation__submenu-container li > a:focus-visible {
		background-color: color-mix(in srgb, var(--wp--preset--color--foreground, #16181b) 7%, transparent);
	}

	/* On the floating bar the dropdown continues the bar rather than
	 * interrupting it: the same primary fill, which also means the white link
	 * colour the bar already sets becomes correct instead of something to fight.
	 * Tints are struck from the background token so they follow each style
	 * variation's own palette rather than a hardcoded white. */
	.zd-header-floating .zerodigital-header__actions .wp-block-navigation__submenu-container {
		background-color: var(--wp--preset--color--primary, #0e4b52);
		border-color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 22%, transparent);
	}

	.zd-header-floating .zerodigital-header__actions .wp-block-navigation__submenu-container li > a {
		color: var(--wp--preset--color--background, #fff);
	}

	.zd-header-floating .zerodigital-header__actions .wp-block-navigation__submenu-container li > a:hover,
	.zd-header-floating .zerodigital-header__actions .wp-block-navigation__submenu-container li > a:focus-visible {
		background-color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 15%, transparent);
	}
}

/* Hero clears the floating bar. */
.zd-header-floating .zerodigital-hero-home,
.zd-header-floating .zerodigital-hero-subpage {
	padding-top: clamp(7rem, 6rem + 6vh, 9.5rem);
}

/* With the bar floating over the hero rather than sitting above it, there is
 * no header height to subtract — the screen is the hero's to fill. The extra
 * padding-top above stays inside that height, since box-sizing is border-box
 * globally, so the hero ends at the fold either way. */
.zd-header-floating {
	--zd-hero-viewport: 100vh;
	/* The floating bar is padded tighter than the standard one. */
	--zd-menu-inset: 1.25rem;
	--zd-header-gutter: clamp(1rem, 3vw, 2rem);

	/* Restated, not inherited: a custom property's var() references resolve
	 * where it is declared, so the :root copies still hold the standard header's
	 * inset and gutter. Declaring them again here, on the element that carries
	 * the floating values, is what makes sections line up with this bar. */
	--zd-content-max: calc(var(--wp--style--global--wide-size, 1600px) - 2 * var(--zd-menu-inset));
	--zd-section-gutter: calc(var(--zd-menu-inset) + max(0px, var(--zd-header-gutter) - max(0px, (100vw - var(--wp--style--global--wide-size, 1600px)) / 2)));
}

@supports (height: 100svh) {
	.zd-header-floating {
		--zd-hero-viewport: 100svh;
	}
}

/* Pages that don't open with a hero still reserve space for the floating bar. */
.zd-header-floating .wp-site-blocks > main:not(:has(.zerodigital-hero-home)):not(:has(.zerodigital-hero-subpage)) {
	padding-top: clamp(6rem, 5rem + 5vh, 8rem);
}

/* --------------------------------------------------------------------------
 * Header on scroll (Site Settings → Header on scroll).
 *
 * zd-header-scroll-static   scrolls away with the page. No rule here: the
 *                           header keeps the positioning above.
 * zd-header-scroll-sticky   stays at the top of the screen all the way down.
 * zd-header-scroll-dynamic  sticky, and slides away while the visitor scrolls
 *                           down. assets/js/header.js decides when, by putting
 *                           zd-header-is-hidden on <html>; without the script
 *                           the header is simply sticky, never lost.
 *
 * The traditional bar uses `position: sticky`, so it keeps its place in the
 * flow and nothing below it moves when it sticks — the heroes' "full screen"
 * height, which subtracts the bar, stays right. It sticks against
 * .wp-site-blocks, which runs the full length of the page, footer included.
 * The floating bar takes no flow height to begin with, so it goes from
 * absolute to fixed instead.
 *
 * The hidden state is a transform, and the visible state is `transform: none`
 * rather than translateY(0). That is load-bearing: any transform makes the
 * header the containing block for its fixed-position children, and the mobile
 * drawer is one of them — under translateY(0) it would open inside the
 * header's own box instead of over the page.
 * ------------------------------------------------------------------------ */
:root {
	/* How far down the screen a stuck header reaches, for anything else that
	 * sticks and has to sit below it (the FAQ block's split heading). Nothing
	 * sticks on a static header, so nothing needs clearing. */
	--zd-header-offset: 0px;
}

.zd-header-scroll-sticky,
.zd-header-scroll-dynamic {
	/* Under the admin bar when one is showing. WordPress only declares the
	 * variable when it prints the bar, so a visitor gets 0. */
	--zd-sticky-top: var(--wp-admin--admin-bar--height, 0px);
	/* --zd-header-live is the header's measured height, set on <html> by
	 * header.js. --zd-header-height is the estimate, for the first paint and
	 * if the script never runs. */
	--zd-header-offset: calc(var(--zd-header-live, var(--zd-header-height, 4.75rem)) + var(--zd-sticky-top));
	/* Deliberately the whole header even when Dynamic has hidden it. Following
	 * a link back UP the page brings the header back mid-scroll, and a margin
	 * worked out while it was hidden would land the heading underneath it.
	 *
	 * The admin bar is NOT added here, unlike --zd-header-offset above.
	 * WordPress's admin-bar.css already sets `scroll-padding-top` on <html> to
	 * the bar's height, and the two add together: 0.57.0 counted the bar twice,
	 * so a logged-in editor saw every linked heading land 32px low. */
	--zd-anchor-offset: calc(var(--zd-header-live, var(--zd-header-height, 4.75rem)) + var(--wp--preset--spacing--sm, 1rem));
}

/* Below 600px WordPress lets the admin bar scroll away with the page, so a
 * header held under it would stop 46px short of the top once it has gone. */
@media (max-width: 600px) {
	.zd-header-scroll-sticky,
	.zd-header-scroll-dynamic {
		--zd-sticky-top: 0px;
	}
}

.zd-header-scroll-sticky .wp-site-blocks > header,
.zd-header-scroll-dynamic .wp-site-blocks > header {
	position: sticky;
	top: var(--zd-sticky-top);
	transition: transform 260ms var(--zd-motion-ease, ease), box-shadow var(--zd-ease, 180ms ease);
}

.zd-header-floating.zd-header-scroll-sticky .wp-site-blocks > header,
.zd-header-floating.zd-header-scroll-dynamic .wp-site-blocks > header {
	position: fixed;
	top: var(--zd-sticky-top);
}

/* Once the page has moved, a soft shadow sets the stuck bar above the content
 * sliding under it; at the very top it sits flush, as a static header does.
 * The traditional bar only — the floating bar carries its own shadow at all
 * times, and its header element is a transparent frame around it. */
.zd-header-is-scrolled .zd-header-traditional.zd-header-scroll-sticky .wp-site-blocks > header,
.zd-header-is-scrolled .zd-header-traditional.zd-header-scroll-dynamic .wp-site-blocks > header {
	box-shadow: var(--zd-shadow-md, 0 4px 8px -2px rgba(16, 24, 40, 0.1));
}

/* Hidden: lifted clear of the screen, the admin bar's height included, and
 * without the shadow, which would otherwise leave a grey line along the top.
 * A keyboard user tabbing into the header brings it straight back — the
 * :focus-visible test, not :focus-within, because a mouse click on a menu link
 * also leaves focus there, and the header would then refuse to hide for the
 * rest of the page. A browser without :has() drops the whole rule, and the
 * header stays sticky. */
.zd-header-is-hidden .zd-header-scroll-dynamic .wp-site-blocks > header:not(:has(:focus-visible)) {
	transform: translateY(calc(-100% - var(--zd-sticky-top)));
	box-shadow: none;
}

.zd-header-is-hidden .zd-header-scroll-dynamic {
	--zd-header-offset: var(--zd-sticky-top);
}

@media (prefers-reduced-motion: reduce) {
	.zd-header-scroll-sticky .wp-site-blocks > header,
	.zd-header-scroll-dynamic .wp-site-blocks > header {
		transition: none;
	}
}

/* A printed page gets the header once, at the top, where it started. */
@media print {
	.zd-header-scroll-sticky .wp-site-blocks > header,
	.zd-header-scroll-dynamic .wp-site-blocks > header {
		position: relative;
		top: auto;
		transform: none;
		box-shadow: none;
	}

	.zd-header-floating.zd-header-scroll-sticky .wp-site-blocks > header,
	.zd-header-floating.zd-header-scroll-dynamic .wp-site-blocks > header {
		position: absolute;
		top: 0;
	}
}

/* --------------------------------------------------------------------------
 * Heroes: always full-bleed, and flush to the header (no seam).
 * When a page's content opens with a hero, drop the default gap that
 * WordPress puts between the header and <main> so the hero's background
 * meets the nav. Scoped via :has() so non-hero pages keep their spacing.
 * ------------------------------------------------------------------------ */
.wp-site-blocks > main:has(> .zerodigital-hero-home:first-child),
.wp-site-blocks > main:has(> .zerodigital-hero-subpage:first-child),
.wp-site-blocks > main:has(> * > .zerodigital-hero-home:first-child),
.wp-site-blocks > main:has(> * > .zerodigital-hero-subpage:first-child) {
	margin-block-start: 0;
}

/* Belt-and-braces: heroes never carry a top margin of their own. */
.zerodigital-hero-home,
.zerodigital-hero-subpage {
	margin-block-start: 0;
}

/* --------------------------------------------------------------------------
 * Section corner treatment.
 *
 * Heroes curve into the page along their bottom edge, and the footer curves
 * away from it along its top, so both read as panels layered over the page
 * rather than bands butted flat against it. Scaled by --zd-section-corner,
 * which tracks the active variation's own radius scale — so a rounded theme
 * gets a rounded treatment and a sharp one stays sharp, without either being
 * imposed. A child theme can retune or disable it by setting that one token.
 *
 * The curve only reads where the section and the page behind it differ in
 * colour, which is the normal case: heroes are surface (or an image) over the
 * page background, and the footer carries the primary colour.
 *
 * Deliberately no `overflow: hidden` on the footer — it isn't needed (nothing
 * inside paints a background into the top corners) and it would clip the
 * offset focus rings on the footer's links (WCAG 2.4.11). The Home Hero
 * already clips, but that's pre-existing and required by its background media.
 * ------------------------------------------------------------------------ */
.zerodigital-hero-home,
.zerodigital-hero-subpage {
	border-bottom-left-radius: var(--zd-section-corner);
	border-bottom-right-radius: var(--zd-section-corner);
}

footer.wp-block-template-part > .wp-block-group {
	border-top-left-radius: var(--zd-section-corner);
	border-top-right-radius: var(--zd-section-corner);
}

/* Mobile caps the curve. The scale is tuned against a full-width desktop
 * section; on a phone the identical radius takes a far bigger bite out of a
 * much narrower box, and Health's 2.5rem (40px) in particular reads as heavy.
 * 32px keeps the effect without it dominating the section. min() means only
 * the themes that exceed the cap are touched — the gentler ones (Default 24px
 * down to Trades 8px) are left exactly as they are. */
@media (max-width: 782px) {
	:root {
		--zd-section-corner: min(calc(var(--wp--custom--radius--lg, 0.75rem) * 2), 32px);
	}
}

/* --------------------------------------------------------------------------
 * Footer gutter + width. Two core defaults work against the footer group, and
 * both have to go for it to line up with every other section (24px gutter,
 * wide-size cap):
 *   1. Core gives ANY group carrying a background colour a default
 *      `padding: 1.25em 2.375em`. The footer group has one, so ~44px of side
 *      padding stacked on top of the 24px gutter its inner blocks already
 *      carry — double-padding the content.
 *   2. A group with no explicit layout is "constrained", which capped the
 *      footer's contents at contentSize (800px) and beat the block's own
 *      `max-width: wide-size` on source order.
 * parts/footer.html now sets padding:0 and a flow layout, which is the real
 * fix — but a footer part saved in the Site Editor overrides the theme file
 * entirely, so these pin the same result regardless of where the markup came
 * from. The inner blocks own the gutter; the group just carries the colour.
 * ------------------------------------------------------------------------ */
footer.wp-block-template-part > .wp-block-group {
	padding-inline: 0;
}

footer.wp-block-template-part > .wp-block-group.wp-block-group-is-layout-constrained > * {
	max-width: var(--wp--style--global--wide-size, 1600px);
}

/* --------------------------------------------------------------------------
 * Footer site-credit bar (Design §16) — the dark bottom bar carrying the
 * "Another ZeroDigital Website" credit and the auto-updating copyright.
 * The plugin injects this markup into the footer template part on render
 * (see includes/footer-credit.php); the styling lives here in the theme.
 * ------------------------------------------------------------------------ */
.zerodigital-site-credit {
	background: var(--wp--preset--color--foreground, #16181b);
	color: var(--wp--preset--color--background, #fff);
	text-align: center;
	padding: clamp(var(--wp--preset--spacing--md, 1.5rem), 3vw, var(--wp--preset--spacing--lg, 2rem)) var(--wp--preset--spacing--md, 1.5rem);
}

.zerodigital-site-credit__badge {
	margin: 0 0 var(--wp--preset--spacing--2-xs, 0.5rem);
	font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
	font-size: 0.9rem;
	letter-spacing: 0.01em;
}

.zerodigital-site-credit__badge a {
	color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 80%, transparent);
	text-decoration: none;
	transition: color var(--zd-ease, 180ms ease);
}

.zerodigital-site-credit__badge a:hover {
	color: var(--wp--preset--color--background, #fff);
}

.zerodigital-site-credit__brand {
	font-weight: 700;
	color: var(--wp--preset--color--background, #fff);
}

/* "Digital" is the fixed ZeroDigital brand orange — this is the agency's own
 * credit mark, so it stays #FF7B00 on every client site regardless of the
 * variation's accent. On the dark foreground bar it clears AA (≈7:1 on the
 * near-black variations, still comfortably above 4.5:1 on health's lighter
 * charcoal). */
.zerodigital-site-credit__brand-accent {
	color: #ff7b00;
}

.zerodigital-site-credit__copyright {
	margin: 0;
	font-size: 0.8rem;
	color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 58%, transparent);
}

/* --------------------------------------------------------------------------
 * Cookie consent (SEOPress). Styled to read as part of the site, not a
 * bolt-on: a floating card in the theme's surface colour, border, radius and
 * large shadow, with the two actions rendered as the site's own primary and
 * ghost buttons. Everything is tokenised, so the card inherits each style
 * variation's palette automatically. The rules only match SEOPress's own
 * markup, so they cost nothing on sites without the plugin.
 *
 * SEOPress prints its own default inline <style>, so the layout- and
 * colour-critical declarations are !important to win over it in place (rather
 * than requiring the admin to disable SEOPress's default styles).
 *
 * Real SEOPress DOM (confirmed from the plugin's default stylesheet — the
 * buttons are DIRECT children of the container, NOT wrapped in a <p>):
 *   .seopress-user-consent                 container
 *     .seopress-user-message ( > p, > a )  message + cookie-policy link
 *     #seopress-user-consent-accept        accept
 *     #seopress-user-consent-close         decline / close
 *     .seopress-edit-choice                optional "manage cookies" link
 *     .seopress-user-consent-hide          state class added once a choice
 *                                          is made
 * Layout: the message is given a full flex row (basis 100%) so the two
 * buttons wrap onto the next row together and share it — no button wrapper
 * element is assumed.
 * ------------------------------------------------------------------------ */
.seopress-user-consent {
	position: fixed !important;
	left: var(--wp--preset--spacing--sm, 1rem) !important;
	bottom: var(--wp--preset--spacing--sm, 1rem) !important;
	z-index: 9999 !important;
	width: 440px !important;
	max-width: calc(100vw - 2rem) !important;
	/* SEOPress can centre the bar with a transform; neutralise it. */
	transform: none !important;
	display: flex !important;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--sm, 1rem) !important;
	padding: var(--wp--preset--spacing--md, 1.5rem) !important;
	background: var(--wp--preset--color--surface, #fff) !important;
	color: var(--wp--preset--color--foreground, #16181b) !important;
	border: var(--zd-border, 1px solid #e4e6e9) !important;
	border-radius: var(--wp--custom--radius--lg, 0.75rem) !important;
	box-shadow: var(--zd-shadow-lg, 0 14px 28px -8px rgba(16, 24, 40, 0.16)) !important;
	font-family: var(--wp--preset--font-family--body, inherit);
	font-size: 0.9375rem;
	line-height: 1.6;
}

/* Respect SEOPress hiding the bar after a choice — this must still win even
 * though the container above is display:flex !important. */
.seopress-user-consent.seopress-user-consent-hide {
	display: none !important;
}

/* The message takes a full row so the buttons wrap onto the next line. */
.seopress-user-consent .seopress-user-message {
	flex: 1 1 100%;
}

.seopress-user-consent .seopress-user-message p {
	margin: 0;
}

.seopress-user-consent .seopress-user-message p + p {
	margin-top: var(--wp--preset--spacing--2-xs, 0.5rem);
}

/* SEOPress's message can lead with an empty paragraph — don't let it add a gap. */
.seopress-user-consent .seopress-user-message p:empty {
	display: none;
}

/* An optional title, if one is added to the message HTML. */
.seopress-user-consent .seopress-user-message :is(h2, h3, h4) {
	margin: 0 0 var(--wp--preset--spacing--2-xs, 0.5rem);
	font-family: var(--wp--preset--font-family--heading, inherit);
	font-size: 1.0625rem;
	font-weight: 700;
	line-height: 1.25;
}

/* The cookie-policy link inside the message. */
.seopress-user-consent .seopress-user-message a {
	color: var(--wp--preset--color--primary, #0e4b52);
	text-decoration: underline;
	text-underline-offset: 0.15em;
	transition: color var(--zd-ease, 180ms ease);
}

.seopress-user-consent .seopress-user-message a:hover {
	color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
}

/* SEOPress wraps the two buttons in an element (a <p>), so make that wrapper
 * the full-width button row — otherwise the buttons sit at their natural width
 * and hug the left, leaving dead space. :has() targets whichever element holds
 * the accept button; if a build instead renders the buttons as direct children
 * of the card, this matches nothing and they fill the card row directly. Either
 * way the buttons below end up sharing one full-width row 50/50. */
.seopress-user-consent :has(> #seopress-user-consent-accept) {
	flex: 1 1 100% !important;
	display: flex !important;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--wp--preset--spacing--sm, 1rem);
	margin: 0 !important;
}

/* Buttons: the two actions share the row, each filling half of it. They ARE the
 * site's buttons — body font, 600 weight and the same radius as the primary
 * CTAs — so the card reads as native chrome. The transparent 1.5px border keeps
 * the filled "accept" the same height as the bordered "decline". */
.seopress-user-consent button {
	flex: 1 1 0 !important;
	margin: 0 !important;
	padding: 0.7rem 1.35rem !important;
	font-family: var(--wp--preset--font-family--body, inherit) !important;
	font-size: 0.9375rem !important;
	font-weight: 600 !important;
	line-height: 1.2;
	border: 1.5px solid transparent !important;
	border-radius: var(--wp--custom--radius--md, 0.5rem) !important;
	cursor: pointer;
	transition: background-color var(--zd-ease, 180ms ease), border-color var(--zd-ease, 180ms ease), color var(--zd-ease, 180ms ease);
}

/* Accept = the site's primary (filled) button. */
#seopress-user-consent-accept {
	background: var(--wp--preset--color--primary, #0e4b52) !important;
	border-color: var(--wp--preset--color--primary, #0e4b52) !important;
	color: var(--wp--preset--color--background, #fff) !important;
}

#seopress-user-consent-accept:hover {
	background: var(--wp--preset--color--secondary, #0a3339) !important;
	border-color: var(--wp--preset--color--secondary, #0a3339) !important;
}

/* Decline / close = outlined (ghost) button, same family as the header phone. */
#seopress-user-consent-close {
	background: transparent !important;
	border-color: color-mix(in srgb, var(--wp--preset--color--foreground, #16181b) 24%, transparent) !important;
	color: var(--wp--preset--color--foreground, #16181b) !important;
}

#seopress-user-consent-close:hover {
	background: var(--wp--preset--color--background, #f6f7f8) !important;
	border-color: color-mix(in srgb, var(--wp--preset--color--foreground, #16181b) 40%, transparent) !important;
}

/* Optional "manage cookies" link — small, unobtrusive, on its own row. */
.seopress-user-consent .seopress-edit-choice {
	flex: 1 1 100%;
	margin: 0;
	font-size: 0.8125rem;
	text-align: center;
	color: var(--wp--preset--color--muted, #565b62);
	text-decoration: underline;
	text-underline-offset: 0.15em;
	transition: color var(--zd-ease, 180ms ease);
}

.seopress-user-consent .seopress-edit-choice:hover {
	color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
}

/* Mobile: a full-width card pinned to the bottom, with stacked buttons. */
@media (max-width: 600px) {
	.seopress-user-consent {
		left: var(--wp--preset--spacing--sm, 1rem) !important;
		right: var(--wp--preset--spacing--sm, 1rem) !important;
		width: auto !important;
		max-width: none !important;
	}

	.seopress-user-consent button {
		flex: 1 1 100%;
	}
}


/* ---------------------------------------------------------------------------
 * Dark custom background -> light text
 *
 * A block whose Layout Settings background is a dark solid colour (or a media
 * background, which the block scrims) gets `zd-bg-dark` from PHP, decided by the
 * colour's perceived brightness (zerodigital_dark_bg_class). The copy is authored
 * for light surfaces, so flip the text elements to the light end here. The
 * descendant selectors carry enough specificity to beat the blocks' own colour
 * rules; the CTAs are brand-coloured controls and keep their own colour.
 * ------------------------------------------------------------------------- */
/* Two things are left out of the flip, both inside :where() so the exclusions
 * add no specificity — the flip has to stay at (0,1,1), or it starts beating the
 * softer [class*="label"] rule below and every block's own dark-ground rules.
 *   - Star ratings. They are spans, and the gold fill went white.
 *   - Light cards sitting on the dark band (services, pricing, FAQ, testimonial
 *     and post cards). Their text is on the card's surface, not on the band, and
 *     flipping it wrote white text onto a white card. */
.zd-bg-dark,
.zd-bg-dark :is(h1, h2, h3, h4, h5, h6):not(:where(.zd-light-card, .zd-light-card *)),
.zd-bg-dark p:not(:where(.zd-light-card *)),
.zd-bg-dark li:not(:where(.zd-light-card, .zd-light-card *)),
.zd-bg-dark span:not(:where(.zerodigital-stars, .zerodigital-stars *, .zd-light-card *)) {
	color: var(--wp--preset--color--background, #fff);
}

/* The wrapper still takes the light colour, so the empty-star track — mixed from
 * currentColor — reads as a faint light outline on the dark ground. */
.zd-bg-dark .zerodigital-stars:not(:where(.zd-light-card *)) {
	color: var(--wp--preset--color--background, #fff);
}

/* The cards reset what they inherit from the band. Zero specificity, so each
 * card's own colour rules still apply on top. */
:where(.zd-bg-dark) :where(.zd-light-card) {
	color: var(--wp--preset--color--foreground, #16181b);
}

/* Secondary copy stays a step down from the headings, still comfortably legible. */
.zd-bg-dark [class*="subtext"]:not(:where(.zd-light-card *)),
.zd-bg-dark [class*="label"]:not(:where(.zd-light-card *)) {
	color: color-mix(in srgb, var(--wp--preset--color--background, #fff) 82%, transparent);
}

/* The other direction, for the blocks whose DEFAULT ground is dark — the CTA
 * Banner and the footer — when an editor picks a light colour for them
 * (zerodigital_bg_tone_class). Each block restyles its own button. */
.zd-bg-light {
	color: var(--wp--preset--color--foreground, #16181b);
}

/* --------------------------------------------------------------------------
 * Sections space themselves with padding, never with margin.
 *
 * WordPress's layout support puts the block gap between every child of a flow
 * or constrained container — `:root :where(.is-layout-constrained) > *`, 24px
 * on this theme — and that is right for prose, where a paragraph has no padding
 * of its own. It is wrong for a band: every section block already carries its
 * own top and bottom padding, so the margin added nothing but a 24px stripe of
 * page showing between two coloured sections that were meant to meet.
 *
 * `zd-section` is on every band block's wrapper, from
 * zerodigital_block_wrapper_attributes(). Two class-level selectors' worth of
 * specificity, so it beats that rule wherever the stylesheets land, and core
 * blocks an editor drops between two sections keep their gap.
 * ------------------------------------------------------------------------ */
:root .zd-section {
	margin-block: 0;
}

/* ...unless the section paints an INSET CARD rather than a full-bleed band.
 *
 * The rule above rests on one thing being true of every section: that its air
 * is its own padding, so removing the margin removes nothing you could see.
 * Three variants break that — the CTA Banner's `boxed`, and the Compliance
 * Bar's `dedicated-section` and `accent-line`. Each paints its surface on the
 * section root and then insets that root from the page, so its padding sits
 * INSIDE the paint and buys no air outside it at all. Flush against a coloured
 * band above, the card's edge met the band's edge with no seam, and a narrower
 * card tucked under a wider band reads as a mistake — two full-bleed bands
 * meeting is the thing 0.51.5 wanted, this is not it.
 *
 * So a card is never flush. It takes back the gap it used to get from the
 * layout margin, which is the same 24px, and two cards in a row collapse to one
 * gap between them the way sibling margins do.
 *
 * `zd-section--card` comes from PHP for the same reason `zd-has-ground` does:
 * it says what the section IS, so a variant added later is covered by claiming
 * the class rather than by editing a list of block names in here. */
:root .zd-section--card {
	margin-block: var(--wp--preset--spacing--md, 1.5rem);
}

/* A section with something painted behind it (`zd-has-ground`, added by
 * zerodigital_with_section_background() for a colour and by About for its media)
 * reads as a card once it is clear of the screen edge, so it takes the card
 * radius there. The band is already wider than its content column by its own
 * inline padding — that overhang is what makes it a card — so NOTHING here
 * changes a width. Insetting it further would push the copy one gutter inside
 * the edge every other section sits on, and that alignment is worth more.
 *
 * Never at Full: that band meets both screen edges by definition, and a curve
 * there cuts the corner off the screen.
 *
 * WHY min() AND NOT A MEDIA QUERY. A band is capped at the wide size (or the
 * content size, with no alignment) and centred, so it only clears the edge once
 * the viewport passes that cap — 1600px here, but Site Settings → Site width
 * narrows it to 1440 or 1280 per site, a child theme may set its own, and a
 * media query cannot read a custom property. The second term reads the
 * same token the layout does: it is 0 until the band is a gutter clear of the
 * edge and enormous afterwards, so min() returns 0, then the card radius. Change
 * the wide size and the rounding follows, with nothing here to update.
 *
 * The gutter in that term is load-bearing twice over: 100vw includes the
 * scrollbar, so without it the curve would appear a few pixels before the band
 * had actually left the edge, and a band only 3px clear would get a curve it has
 * no room for. */
.zd-has-ground.alignwide {
	border-radius: min(
		var(--zd-card-radius, var(--wp--custom--radius--lg, 0.75rem)),
		max(0px, (100vw - var(--wp--style--global--wide-size, 1600px)) / 2 - var(--zd-section-gutter, var(--wp--preset--spacing--md, 1.5rem))) * 1000
	);
}

.zd-has-ground:not(.alignwide):not(.alignfull) {
	border-radius: min(
		var(--zd-card-radius, var(--wp--custom--radius--lg, 0.75rem)),
		max(0px, (100vw - var(--wp--style--global--content-size, 800px)) / 2 - var(--zd-section-gutter, var(--wp--preset--spacing--md, 1.5rem))) * 1000
	);
}

/* ...and once it is a card, it needs page around it. A grounded band is a card
 * for the same reason the three `zd-section--card` variants are: its padding
 * sits inside its paint, so the zeroed layout margin left it butting against
 * the band above with no air at all. At the screen edge it is a band again,
 * the colour runs to both sides, and a gap there would be a stripe of page
 * between two sections that should meet.
 *
 * So the margin reads the same test as the radius above and switches on at the
 * same width: flush and square at the edge, a card-gap clear and rounded once
 * it has room. Two grounded cards in a row collapse to one gap between them.
 *
 * This replaces the squared seam 0.51.5 gave two grounded bands that met. With
 * the gap and the curve arriving together a grounded card never touches
 * another section, and at the edge the radius is already 0.
 *
 * The card variants are left out: they keep their gap at every width, and this
 * rule would take it off them on a phone. */
:root .zd-has-ground.alignwide:not(.zd-section--card) {
	margin-block: min(
		var(--wp--preset--spacing--md, 1.5rem),
		max(0px, (100vw - var(--wp--style--global--wide-size, 1600px)) / 2 - var(--zd-section-gutter, var(--wp--preset--spacing--md, 1.5rem))) * 1000
	);
}

:root .zd-has-ground:not(.alignwide):not(.alignfull):not(.zd-section--card) {
	margin-block: min(
		var(--wp--preset--spacing--md, 1.5rem),
		max(0px, (100vw - var(--wp--style--global--content-size, 800px)) / 2 - var(--zd-section-gutter, var(--wp--preset--spacing--md, 1.5rem))) * 1000
	);
}

/* --------------------------------------------------------------------------
 * Section entrance.
 *
 * A section's heading, intro and repeating items animate in once, when they
 * arrive on screen. Three shapes to choose from, picked per site in Site
 * Settings -> Animation and applied as a body class:
 *
 *   settle   fade and a short rise. The Home Hero's own animation, reused
 *            verbatim. The restrained choice, and the right one for a
 *            compliance-led site that wants movement but not personality.
 *   lift     rise and a slight scale-up. The default: cards arrive rather
 *            than merely appear.
 *   wipe     a masked reveal upward, with no transparency at any point.
 *            The crispest, and the only one that cannot produce a haze
 *            over a busy page, because nothing is ever semi-opaque.
 *
 * The shape is the site owner's choice; the PACE — duration, travel, stagger — is
 * the style variation's, from the tokens above. The two compose, which is what
 * stops five sites on one platform arriving identically.
 *
 * This used to be a scroll-driven animation (`animation-timeline: view()`) with
 * no JavaScript at all. It was replaced because a scroll timeline is SCRUBBED:
 * progress follows scroll position, so an element never finishes — it holds
 * whatever opacity matches where it sits on screen. A dozen of those across a
 * page put everything in the lower third at partial opacity simultaneously, and
 * it read as a translucent sheet over the bottom of the viewport rather than as
 * anything animating. CSS `animation-trigger` would give a timed animation from
 * a scroll position with no script; no browser ships it yet. See assets/js/motion.js.
 *
 * Two guards, both load-bearing:
 *
 *   .zd-motion-js  printed inline in the head by functions.php, and taken back
 *                  off at DOMContentLoaded if motion.js (deferred) never ran.
 *                  Every hidden state below is
 *                  behind it, so a blocked or failed script leaves sections
 *                  plainly visible instead of blank. Content that JavaScript
 *                  hides and JavaScript reveals must never depend on the
 *                  JavaScript arriving.
 *   no-preference  rather than leaning on the global reduced-motion reset near
 *                  the top of this file. That reset only SHORTENS animations;
 *                  gating here means the hidden state is never applied at all,
 *                  so nothing can be left part-way.
 *
 * Both media queries also say `screen`, and there is a print reset after the
 * keyframes: printing a page before scrolling it used to print every section
 * that had not been revealed yet as empty space.
 *
 * "Off" needs no rule of its own: it emits `zd-motion-off`, which matches
 * nothing here, so no element is ever hidden in the first place.
 *
 * What is deliberately never revealed: the Home Hero's heading and media, and
 * the Subpage Hero's heading. Those are the Largest Contentful Paint
 * candidates, and an element at opacity 0 does not count as painted — hiding
 * one would push the metric these sites are ranked on.
 * ------------------------------------------------------------------------ */
@media screen and (prefers-reduced-motion: no-preference) {

	/* ---- The hidden state, one per shape ----
	 *
	 * Only while NOT yet revealed. These used to apply to every target, with
	 * the animation's `fill-mode: both` holding the final keyframe over them
	 * forever after. An animation's held value outranks every normal rule, so
	 * `transform: none` and `clip-path: inset(0)` stayed on each card for the
	 * life of the page: the Pricing highlighted tier's lift and the Post Feed
	 * card's hover lift never moved, and under Reveal the permanent clip cut
	 * off hover shadows and focus rings. Now the hidden state ends the moment
	 * `is-revealed` arrives, `fill-mode: backwards` covers the stagger delay,
	 * and when the animation finishes nothing is held at all.
	 *
	 * The :not() sits inside :where() so the hidden state keeps the (0,2,0) it
	 * always had, and the carousel override at the end of this block still
	 * clears it.
	 */

	.zd-motion-js .zd-motion-settle :where(.zd-rise, .zd-rise-group > *):where(:not(.is-revealed)) {
		opacity: var(--zd-motion-from, 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)));
	}

	.zd-motion-js .zd-motion-lift :where(.zd-rise, .zd-rise-group > *):where(:not(.is-revealed)) {
		opacity: var(--zd-motion-from, 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)))
			scale(var(--zd-motion-scale-from, 0.96));
	}

	/*
	 * No opacity at all on the wipe — that is the entire point of it. The element
	 * is either clipped or it is not, so a page of them cannot accumulate into a
	 * haze the way a page of fades does.
	 */
	.zd-motion-js .zd-motion-wipe :where(.zd-rise, .zd-rise-group > *):where(:not(.is-revealed)) {
		clip-path: inset(100% 0 0 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)));
	}

	/* ---- The trigger, shared ---- */

	.zd-motion-js :is(.zd-motion-settle, .zd-motion-lift, .zd-motion-wipe)
		:where(.zd-rise, .zd-rise-group > *).is-revealed {
		animation-duration: var(--zd-motion-duration);
		animation-timing-function: var(--zd-motion-ease);
		/* Backwards, not both: the start keyframe covers the delay, and nothing
		 * outlives the animation. See the hidden state above. */
		animation-fill-mode: backwards;
		/*
		 * The sweep across a row. motion.js sets --zd-motion-i on each child of a
		 * group, cycling 0/1/2; every child is observed separately, so vertical
		 * order is already handled by when each one arrives and this only has to
		 * offset the ones that arrive together.
		 */
		animation-delay: calc(var(--zd-motion-i, 0) * var(--zd-motion-stagger));
	}

	.zd-motion-js .zd-motion-settle :where(.zd-rise, .zd-rise-group > *).is-revealed {
		animation-name: zd-settle;
	}

	.zd-motion-js .zd-motion-lift :where(.zd-rise, .zd-rise-group > *).is-revealed {
		animation-name: zd-lift;
	}

	.zd-motion-js .zd-motion-wipe :where(.zd-rise, .zd-rise-group > *).is-revealed {
		animation-name: zd-wipe;
	}

	/*
	 * Swiper owns its slides: it transforms the track and clips it, so a second
	 * transform on the same box fights it and a clip-path on a slide fights the
	 * track's own clipping. A carousel layout reveals as one section instead.
	 *
	 * Not written with :where() here, unlike everything else in this block.
	 * :where() zeroes specificity, and the rules above carry weight from their
	 * body-class prefixes — so a zero-specificity override loses to them and the
	 * slides animate anyway. Verified in the browser: this is the one place in
	 * the entrance that has to carry weight rather than give it away.
	 */
	.zd-motion-js :is(.zd-motion-settle, .zd-motion-lift, .zd-motion-wipe)
		.zd-rise-group.swiper-wrapper > * {
		opacity: 1;
		clip-path: none;
		transform: none;
		animation: none;
	}
}

/* --------------------------------------------------------------------------
 * Hero entrance.
 *
 * The same three shapes, the same tokens, the same Site Settings choice — so a
 * site set to Reveal reveals its hero too, and a site set to Off has a hero that
 * genuinely does not move. Before 0.30.0 the hero ran its own hardcoded
 * animation and ignored all three, which meant "Off" was not off.
 *
 * Two things differ from the section entrance below, both deliberate:
 *
 *   It is not gated on .zd-motion-js. This is a LOAD animation, not a scroll
 *   one — there is no observer, nothing is hidden waiting for a script, and it
 *   therefore works with JavaScript disabled. The section entrance cannot make
 *   that claim, which is why it needs the gate and this does not.
 *
 *   It travels half as far, via --zd-motion-hero-rise. See the token.
 *
 * The heading and the media are still excluded, in both heroes. They are the
 * Largest Contentful Paint candidates and an element at opacity 0 does not count
 * as painted; the headline is simply there on arrival and the supporting detail
 * settles in around it, which reads as more assured anyway.
 *
 * Each block assigns --zd-motion-i to its own elements (see the hero style.css
 * files), so the order is the block's business and the timing is this file's.
 * ------------------------------------------------------------------------ */
@media screen and (prefers-reduced-motion: no-preference) {
	:is(.zd-motion-settle, .zd-motion-lift, .zd-motion-wipe) :where(.zd-hero-rise) {
		--zd-motion-travel: var(--zd-motion-hero-rise);
		animation-duration: var(--zd-motion-duration);
		animation-timing-function: var(--zd-motion-ease);
		/* Backwards for the reason the section entrance gives: `both` held
		 * `transform: none` on the hero's CTAs for good, over their own hover
		 * rules, and under Reveal clipped their focus ring. */
		animation-fill-mode: backwards;
		animation-delay: calc(var(--zd-motion-i, 0) * var(--zd-motion-stagger));
	}

	.zd-motion-settle :where(.zd-hero-rise) {
		animation-name: zd-settle;
	}

	.zd-motion-lift :where(.zd-hero-rise) {
		animation-name: zd-lift;
	}

	.zd-motion-wipe :where(.zd-hero-rise) {
		animation-name: zd-wipe;
	}
}

/*
 * No `to` for opacity or transform, on purpose. A keyframe that leaves a
 * property out animates towards the element's OWN value, so a card that rests
 * at translateY(-0.75rem) — the Pricing highlighted tier — rises into that
 * position and stays there, instead of animating to `none` and jumping when
 * the animation lets go. Nothing jumps at the end because the end IS the
 * element's natural state.
 *
 * The wipe keeps an explicit `to` for clip-path: `none` is not interpolable
 * against inset(), so leaving it out would snap the clip open halfway through
 * rather than sweep it. inset(0) and none look the same for everything inside
 * the box; the only visible change as the animation ends is that paint outside
 * the box — a resting shadow, a focus ring — is no longer cut off.
 */
@keyframes zd-settle {
	from {
		opacity: var(--zd-motion-from, 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)));
	}
}

@keyframes zd-lift {
	from {
		opacity: var(--zd-motion-from, 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)))
			scale(var(--zd-motion-scale-from, 0.96));
	}
}

@keyframes zd-wipe {
	from {
		clip-path: inset(100% 0 0 0);
		transform: translateY(calc(var(--zd-motion-travel) * var(--zd-motion-scale, 1)));
	}

	to {
		clip-path: inset(0 0 0 0);
	}
}

/*
 * Printing. Both entrances above are scoped to `screen`, so a printout never
 * receives the hidden state — a page printed before scrolling used to print
 * every unrevealed section as blank space. This is the belt to that brace: an
 * explicit reset that holds whatever the media scoping above is later changed
 * to, and that settles any animation still running when the print was taken.
 * The transform reset is limited to unrevealed items so a revealed card's own
 * resting transform still prints.
 */
@media print {
	.zd-motion-js :where(.zd-rise, .zd-rise-group > *),
	:where(.zd-hero-rise) {
		animation: none !important;
		opacity: 1 !important;
		clip-path: none !important;
	}

	.zd-motion-js :where(.zd-rise, .zd-rise-group > *):where(:not(.is-revealed)) {
		transform: none !important;
	}
}


/* --------------------------------------------------------------------------
 * Carousel controls.
 *
 * Four blocks run a Swiper track — Team, Testimonials, Services Grid and
 * Pricing — and each carried its own copy of these ~55 lines. They had already
 * drifted: two used a stale #1b3a5c primary fallback against the others'
 * #0e4b52, and Testimonials was missing both the lock-collapse and the
 * disabled:hover reset, so its control row stayed visible when the track locked
 * and a dead arrow still lit up under the pointer.
 *
 * One definition now, in the theme with the other shared primitives. Blocks
 * keep their BEM classes on the same elements — child themes target those, and
 * the JS binds to data- attributes either way — and add these alongside.
 * ------------------------------------------------------------------------ */
.zd-carousel-controls {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--wp--preset--spacing--2-xs, 0.5rem);
	margin-top: var(--wp--preset--spacing--md, 1.5rem);
}

/* When everything already fits, Swiper locks the slider and flags the controls.
 * Collapse the row rather than leaving its margin behind an empty flex box. */
.zd-carousel-controls:has(> .swiper-button-lock) {
	display: none;
}

/* Two tokens, so the ground the controls sit on is said once and the rules below
 * never name a colour: the ink they draw with, and the tone they invert to when
 * filled. .zd-bg-dark flips both, at the foot of this section. */
.zd-carousel-nav {
	--zd-carousel-ink: var(--wp--preset--color--primary, #0e4b52);
	--zd-carousel-ink-inverse: var(--wp--preset--color--background, #fff);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
	border-radius: var(--zd-chip-radius, var(--wp--custom--radius--full, 999px));
	border: 1px solid var(--zd-carousel-ink);
	background: transparent;
	color: var(--zd-carousel-ink);
	cursor: pointer;
	transition: background-color var(--zd-ease, 180ms ease), color var(--zd-ease, 180ms ease);
}

/* Chevrons drawn in CSS so the buttons carry no markup Swiper might mistake for
 * content. Scoped to prev/next: a pause control has its own glyph, and lives
 * with the block that needs it. */
.zd-carousel-nav--prev::before,
.zd-carousel-nav--next::before {
	content: "";
	width: 0.55rem;
	height: 0.55rem;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
}

.zd-carousel-nav--prev::before {
	transform: rotate(135deg);
	margin-left: 0.2rem;
}

.zd-carousel-nav--next::before {
	transform: rotate(-45deg);
	margin-right: 0.2rem;
}

.zd-carousel-nav:hover {
	background: var(--zd-carousel-ink);
	color: var(--zd-carousel-ink-inverse);
}

/* Swiper flags the ends of a non-looping track; show that rather than leaving a
 * button that looks live but does nothing. */
.zd-carousel-nav.swiper-button-disabled {
	opacity: 0.35;
	cursor: default;
}

.zd-carousel-nav.swiper-button-disabled:hover {
	background: transparent;
	color: var(--zd-carousel-ink);
}

/* Swiper's own theme variables, set here rather than on each block's section:
 * the pagination is a sibling of the slider, and every block that has one puts
 * `.zd-carousel-pagination` on it, so this is the one element both the bullets
 * and the dark-ground rule below can be reached through. Five block stylesheets
 * each carried their own copy of these lines and none of them flipped. */
.zd-carousel-pagination {
	--swiper-theme-color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	--swiper-pagination-color: var(--zd-accent, var(--wp--preset--color--accent, #c4263c));
	--swiper-pagination-bullet-inactive-color: var(--wp--preset--color--muted, #565b62);
	/* 0.35 left the muted grey at about 1.7:1 against a light band — under the
	 * 3:1 a control has to meet, and on Broker Studio's off-white the dots read
	 * as smudges. 0.75 of the same colour measures 3.1:1 there, and more on a
	 * palette whose muted is darker. The current bullet stays apart from them by
	 * being the accent at full strength. */
	--swiper-pagination-bullet-inactive-opacity: 0.75;
	display: flex;
	justify-content: center;
	gap: var(--wp--preset--spacing--2-xs, 0.5rem);
	margin-top: var(--wp--preset--spacing--sm, 1rem);
}

/* Swiper positions bullets absolutely by default; this pagination sits in
 * normal flow under the controls instead. */
.zd-carousel-pagination.swiper-pagination {
	position: static;
}

/* The bullet you are on is a pill, twice the width of the rest, so the place in
 * the run is readable as a shape and not only as a colour — the dots sit close
 * in tone now that the inactive ones have been lifted, and hue alone is no way
 * to say which slide is showing. Swiper rounds bullets with 50%, which on a
 * wider box draws an ellipse, so the radius is restated. */
.zd-carousel-pagination .swiper-pagination-bullet-active {
	width: calc(2 * var(--swiper-pagination-bullet-width, var(--swiper-pagination-bullet-size, 8px)));
	border-radius: var(--wp--custom--radius--full, 999px);
}

/* Over a dark ground. CSS cannot read the colour behind it, so the section
 * carries `zd-bg-dark` from PHP (zerodigital_dark_bg_class) and the controls
 * follow it — until 0.51.2 they did not, and a carousel on a dark band showed a
 * teal outline and grey bullets that were all but invisible.
 *
 * No .zd-light-card exception, unlike the text flip above: the controls sit
 * under the track, on the band itself, never on a card.
 *
 * The bullets take the background colour rather than the accent, which is
 * chosen to read on the site's light ground and may be a deep one; the current
 * bullet is that tone at full strength and the rest the same tone held back, so
 * which slide you are on stays legible whatever the palette. */
.zd-bg-dark .zd-carousel-nav {
	--zd-carousel-ink: var(--wp--preset--color--background, #fff);
	--zd-carousel-ink-inverse: var(--wp--preset--color--foreground, #16181b);
}

.zd-bg-dark .zd-carousel-pagination {
	--swiper-theme-color: var(--wp--preset--color--background, #fff);
	--swiper-pagination-color: var(--wp--preset--color--background, #fff);
	--swiper-pagination-bullet-inactive-color: var(--wp--preset--color--background, #fff);
	--swiper-pagination-bullet-inactive-opacity: 0.4;
}

/* --------------------------------------------------------------------------
 * Icon base. Every icon rendered by zerodigital_icon() carries `.zd-icon`.
 * Wrapped in :where() so it contributes ZERO specificity — a block's own icon
 * class (which loads earlier) still wins on size and spacing. This just gives a
 * bare inline SVG sensible defaults so one is never left at the SVG spec's
 * 300x150 fallback, and keeps colour inheriting through currentColor.
 * ------------------------------------------------------------------------ */
:where(.zd-icon) {
	display: inline-block;
	width: 1em;
	height: 1em;
	vertical-align: middle;
	flex: none;
	color: inherit;
}
