/*
Theme Name: The DEV Point
Theme URI: https://thedevpoint.com/
Author: The DEV Point
Author URI: https://thedevpoint.com/
Description: Native WordPress block theme foundation for The DEV Point homepage wedge (Separate native block-theme staging prototype). Serves the homepage only during Milestone 1 staging validation. See docs/theme-migration-decision.md.
Version: 0.1.0
Requires at least: 6.6
Requires PHP: 8.0
Text Domain: the-dev-point
*/

/*
 * Minimal, scoped layout/responsive rules that native block settings and
 * theme.json presets cannot express. No new colours, shadows or gradients —
 * every value below is either an approved theme.json colour/spacing preset
 * or a structural flexbox rule. See docs/design-tokens.md for the approved
 * palette and spacing scale this file must stay within.
 */

/* Capability, industry and NetSuite chips: prevent long labels from
   overflowing the viewport at narrow widths (320-375px) by letting the
   flex item shrink and its text wrap, instead of forcing horizontal scroll. */
.tdp-chip {
	max-width: 100%;
	min-width: 0;
}

.tdp-chip p {
	overflow-wrap: break-word;
	word-break: break-word;
}

/* Capability strip: a very light brand-soft band around the chip row,
   restrained to a border + tint (no shadow) so it reads as a container,
   not a card. */
.tdp-capability-band {
	background-color: var(--wp--preset--color--primary-soft);
	border: 1px solid var(--wp--preset--color--primary-subtle);
	border-radius: var(--tdp-radius-card);
	padding: var(--wp--preset--spacing--40);
}

/* Services grid cards: native Columns blocks already stretch columns to an
   equal row height, but the card group inside each column has to be told to
   fill that height itself for the borders/backgrounds to align, and the CTA
   button needs to pin to the bottom so all three cards in a row read as the
   same size regardless of copy length. */
.tdp-card {
	display: flex;
	flex-direction: column;
	height: 100%;
}

.tdp-card .wp-block-buttons {
	margin-top: auto;
}

/* FAQ items: native core/details blocks have no built-in divider or
   summary/content padding controls, so this adds a subtle border-light
   divider between questions and comfortable click/tap padding on the
   summary, using only approved colour and spacing tokens. */
.tdp-faq-item {
	border-bottom: 1px solid var(--wp--preset--color--border-light);
	padding-bottom: var(--wp--preset--spacing--40);
}

.tdp-faq-item summary {
	padding-top: var(--wp--preset--spacing--30);
	padding-bottom: var(--wp--preset--spacing--30);
	cursor: pointer;
}

.tdp-faq-item[open] summary {
	padding-bottom: var(--wp--preset--spacing--20);
}

.tdp-faq-item p {
	margin-top: 0;
}

/* Footer copyright line: a light divider separating it from the column
   content above, using the approved border-light token. */
.tdp-footer-copyright {
	border-top: 1px solid var(--wp--preset--color--border-light);
	padding-top: var(--wp--preset--spacing--50);
}

/* Process section: follows directly after Services Grid, and both render
   on the same (base) background with no colour change between them. A
   restrained top divider in the approved border-light token gives the two
   sections a deliberate seam instead of reading as one undifferentiated
   block of white space. */
.tdp-section-divider,
.tdp-footer,
.tdp-hub-support {
	border-top: 1px solid var(--wp--preset--color--border-light);
}

/* Services grid cards: native Columns blocks stack to a single column
   below 782px by default, skipping a two-column tablet layout entirely.
   isStackedOnMobile:false removes that forced stacking so this fluid
   flex-basis scale can take over: one column under 600px, two columns
   from 600px, three columns from 1024px up. */
/* Core's own .is-not-stacked-on-mobile rule sets flex-wrap: nowrap
   !important at every width, which would keep all six cards in one
   shrinking, overflowing row instead of wrapping — !important is required
   here to take back the wrap behaviour this responsive layout depends on. */
.tdp-services-columns.is-not-stacked-on-mobile {
	flex-wrap: wrap !important;
}

.tdp-services-columns.is-not-stacked-on-mobile > .wp-block-column {
	flex-basis: 100%;
}

@media (min-width: 600px) {
	.tdp-services-columns.is-not-stacked-on-mobile > .wp-block-column {
		flex-basis: calc(50% - var(--wp--preset--spacing--60) / 2);
		flex-grow: 0;
	}
}

@media (min-width: 1024px) {
	.tdp-services-columns.is-not-stacked-on-mobile > .wp-block-column {
		flex-basis: 0;
		flex-grow: 1;
	}
}

/* ==========================================================================
   Design-system foundation (Phase 1 — global design system)
   Radius, shadow, motion and layout scales, plus the reusable .tdp-*
   component classes documented in docs/design-system-foundation.md.
   Colour values are never hard-coded below this point except inside rgba()
   shadow formulas (which reference the approved `heading` colour, #0B1B26,
   at low opacity, per docs/design-system-plan.md §4) — every other colour is
   a var(--wp--preset--color--*) reference to a token defined in theme.json.
   ========================================================================== */

:root {
	/* Shape (design-system-plan §4) — ONE radius system, addressed by role.
	   Prefer the semantic aliases in all new work. */
	--tdp-radius-button: 999px;
	--tdp-radius-card: 12px;
	--tdp-radius-input: 8px;

	/* Legacy numeric scale. --tdp-radius-xl and --tdp-radius-chip were
	   removed (nothing referenced them), and the last two consumers of
	   --tdp-radius-lg moved to --tdp-radius-card, so the theme now has one
	   card radius as docs/taste-redesign-direction.md requires. */
	--tdp-radius-sm: 0.5rem;
	--tdp-radius-md: 0.75rem;
	--tdp-radius-full: 999px;

	/* Elevation — retinted from the retired slate #0b1220 to the
	   brand-biased heading hue #0B1B26. Used sparingly: the system prefers
	   border and whitespace to shadow. */
	--tdp-shadow-sm: 0 1px 2px rgba(11, 27, 38, 0.06);
	--tdp-shadow-md: 0 8px 24px -12px rgba(11, 27, 38, 0.18);
	--tdp-shadow-lg: 0 24px 48px -20px rgba(11, 27, 38, 0.22);

	/* Motion (design-system-plan §6) — MOTION_INTENSITY 3. */
	--tdp-ease: cubic-bezier(0.16, 1, 0.3, 1);
	--tdp-duration-press: 100ms;
	--tdp-duration-fast: 150ms;
	--tdp-duration-standard: 200ms;
	--tdp-duration-reveal: 400ms;

	/* Prose measure, read by the global paragraph rule. */
	--tdp-measure: 65ch;

	/* Minimum interactive target (WCAG 2.5.8). */
	--tdp-target-min: 44px;
}

/* Reduced motion: a single global override rather than per-component
   exceptions, so no new animation added anywhere in this file can ever
   ignore the user's preference. */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
		scroll-behavior: auto !important;
	}

	.tdp-reveal {
		opacity: 1 !important;
		transform: none !important;
	}
}

/* ==========================================================================
   PHASE 1 — GLOBAL FOUNDATION
   Sitewide element defaults only. Nothing here targets an individual
   homepage or service section; those remain the responsibility of their own
   rules further down this file and are unchanged by Phase 1.
   ========================================================================== */

/* --------------------------------------------------------------------
   Global reset-lite and overflow guard.
   overflow-x:clip (not hidden) on the root prevents a stray wide child
   from producing horizontal scroll WITHOUT creating a scroll container,
   so position:sticky continues to work in the header.
   -------------------------------------------------------------------- */
html {
	overflow-x: clip;
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
}

body {
	/* D6: light theme only, painted explicitly rather than inherited. */
	background-color: var(--wp--preset--color--base);
	color: var(--wp--preset--color--text);
	overflow-x: clip;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-synthesis-weight: none;
}

/* Nothing may exceed its container and force a horizontal scrollbar. */
img,
svg,
video,
canvas,
iframe,
table {
	max-width: 100%;
}

img,
svg,
video,
canvas {
	height: auto;
}

/* --------------------------------------------------------------------
   Typography foundation (design-system-plan §2).
   Sizes, weights and tracking live in theme.json; what cannot be
   expressed there lives here: measure, wrapping and rendering.
   -------------------------------------------------------------------- */
h1,
h2,
h3,
h4,
h5,
h6 {
	text-wrap: balance;
	overflow-wrap: break-word;
}

p,
li {
	text-wrap: pretty;
}

/* Body measure cap. Applied to the constrained content column only, so
   full-width and wide blocks are unaffected. Sections that deliberately
   set their own measure keep it — this is a default, not an override. */
.wp-block-post-content > p,
.entry-content > p,
/* Same constrained-layout centring problem as the paragraph rule below, but
   for anything else this stylesheet gives a max-width or a fit-content width:
   headings capped by a measure, and the eyebrow pill. Without this the service
   hero rendered its eyebrow and H1 centred while its lede and buttons sat
   left, which read as a mistake rather than a choice. */
main .is-layout-constrained > :is(h1, h2, h3, h4, h5, h6):not(.has-text-align-center):not(.aligncenter),
main .is-layout-constrained > .tdp-eyebrow {
	margin-left: 0 !important;
	margin-right: auto !important;
}

main .is-layout-constrained > p:not(.aligncenter):not(.alignleft):not(.alignright):not(.has-text-align-center) {
	max-width: var(--tdp-measure);
	/* Constrained layout centres any child narrower than the container, which
	   left every measure-capped paragraph indented 79px from its own heading.
	   Core sets `margin-left: auto !important; margin-right: auto !important`
	   in `.is-layout-constrained > :where(...)`, so !important is the only way
	   to win: no amount of specificity beats it. This is the one place in this
	   stylesheet where !important is used against core, and it is scoped to
	   constrained paragraphs that carry no alignment class of their own, so
	   deliberately centred or floated content is untouched. */
	margin-left: 0 !important;
	margin-right: auto !important;
}

/* A lede is the one paragraph allowed to run at display scale. */
.tdp-lede {
	font-size: var(--wp--preset--font-size--large);
	line-height: 1.5;
	color: var(--wp--preset--color--text);
	max-width: 60ch;
	text-wrap: pretty;
}

/* Same hazard for any long unbroken token in flowing copy: a bare URL, an
   order reference, an email. Wrap it rather than lose it. */
main p,
main li,
main dd,
main td {
	overflow-wrap: break-word;
}

/* --------------------------------------------------------------------
   Text links in flowing content. Navigation, buttons and cards opt out,
   because an underline there is noise rather than affordance.
   -------------------------------------------------------------------- */
main a:not(.wp-block-button__link):not(.wp-block-navigation-item__content):not(.tdp-chip) {
	text-decoration-thickness: 1px;
	text-underline-offset: 0.2em;
	transition: color var(--tdp-duration-fast) var(--tdp-ease),
		text-decoration-color var(--tdp-duration-fast) var(--tdp-ease);
}

main a:not(.wp-block-button__link):not(.wp-block-navigation-item__content):not(.tdp-chip):hover {
	color: var(--wp--preset--color--primary-hover);
}

/* --------------------------------------------------------------------
   Form-control foundation (design-system-plan §5).
   Contact Form 7 is retained (D2) and renders native controls, so styling
   the elements themselves covers CF7 without coupling the theme to it.
   All four states are specified: default, focus, error, success.
   -------------------------------------------------------------------- */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="search"],
input[type="date"],
select,
textarea {
	width: 100%;
	/* border-box for the same reason as .wp-block-button__link above: it keeps
	   min-height a true rendered height rather than a content-box floor. */
	box-sizing: border-box;
	min-height: var(--tdp-target-min);
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
	font-family: inherit;
	font-size: var(--wp--preset--font-size--medium);
	line-height: 1.5;
	color: var(--wp--preset--color--text);
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-input);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease);
	-webkit-appearance: none;
	appearance: none;
}

textarea {
	min-height: 8rem;
	resize: vertical;
}

input::placeholder,
textarea::placeholder {
	/* muted on base = 5.51:1, clears AA. Placeholder is never the label. */
	color: var(--wp--preset--color--muted);
	opacity: 1;
}

input:hover,
select:hover,
textarea:hover {
	border-color: var(--wp--preset--color--muted);
}

input:focus,
select:focus,
textarea:focus {
	border-color: var(--wp--preset--color--primary);
	outline: 2px solid var(--wp--preset--color--primary);
	outline-offset: 2px;
}

/* Label above input, always. */
.tdp-field {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--20);
	margin-bottom: var(--wp--preset--spacing--50);
}

.tdp-field > label,
.tdp-label {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	color: var(--wp--preset--color--heading);
}

.tdp-help {
	font-size: var(--wp--preset--font-size--x-small);
	color: var(--wp--preset--color--muted);
}

/* Error state: colour is never the only signal — the message text carries
   the meaning, and CF7 associates it with the input for assistive tech. */
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"],
.wpcf7-not-valid {
	border-color: var(--wp--preset--color--danger);
}

.tdp-error,
.wpcf7-not-valid-tip {
	display: block;
	margin-top: var(--wp--preset--spacing--20);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--danger);
}

.tdp-success,
.wpcf7 form.sent .wpcf7-response-output {
	border: 1px solid var(--wp--preset--color--success);
	border-radius: var(--tdp-radius-input);
	padding: var(--wp--preset--spacing--40);
	color: var(--wp--preset--color--success);
	background-color: var(--wp--preset--color--base);
}

.wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.failed .wpcf7-response-output {
	border: 1px solid var(--wp--preset--color--danger);
	border-radius: var(--tdp-radius-input);
	padding: var(--wp--preset--spacing--40);
	color: var(--wp--preset--color--danger);
}

/* --------------------------------------------------------------------
   Skip link. Visually hidden until focused, then pinned top-left.
   -------------------------------------------------------------------- */
.skip-link.screen-reader-text:focus {
	z-index: 100000;
	top: var(--wp--preset--spacing--40);
	left: var(--wp--preset--spacing--40);
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	color: var(--wp--preset--color--primary-foreground);
	background-color: var(--wp--preset--color--primary);
	border-radius: var(--tdp-radius-input);
	text-decoration: none;
	clip: auto !important;
	clip-path: none;
	height: auto;
	width: auto;
}

/* --------------------------------------------------------------------
   Responsive globals. Asymmetric desktop layouts collapse to a strict
   single column below 768 (design-system-plan §3).
   -------------------------------------------------------------------- */
@media (max-width: 767px) {
	.wp-block-columns:not(.is-not-stacked-on-mobile) {
		gap: var(--wp--preset--spacing--50);
	}
}

/* Long unbroken strings (URLs, IDs) must not push the layout wide. */
@media (max-width: 480px) {
	h1,
	h2,
	h3 {
		hyphens: auto;
	}
}

/* --------------------------------------------------------------------
   Focus-ring system (DS-004): one rule for every interactive element
   this theme renders. outline (not box-shadow) is used deliberately —
   it is never clipped by an ancestor's overflow:hidden and is preserved
   under Windows High Contrast Mode.
   -------------------------------------------------------------------- */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
.tdp-chip:focus-visible,
.tdp-faq-item summary:focus-visible,
.wp-block-navigation-item > a:focus-visible {
	outline: 2px solid var(--wp--preset--color--primary);
	outline-offset: 2px;
	border-radius: var(--tdp-radius-sm);
}

/* Inside the primary-coloured Consultation CTA band, a primary-coloured
   ring would be invisible against the primary-coloured section
   background, so it is swapped for the base (white) token there only. */
.tdp-section--brand :focus-visible,
.tdp-contact-close__inner :focus-visible {
	outline-color: var(--wp--preset--color--base);
}

/* --------------------------------------------------------------------
   Buttons (DS-011): the existing primary/outline variants already come
   from theme.json's button element style and core's is-style-outline —
   this adds the missing hover/active/focus states for both.
   -------------------------------------------------------------------- */
/* Phase 1 realignment to docs/design-system-plan.md §5/§6: hover transitions
   colour ONLY (the previous -1px lift plus shadow ramp was motion the system
   does not budget for), and the press affordance moves to :active as a +1px
   translate. No scale transforms on buttons, ever. */
.wp-block-button__link {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/* border-box is required: without it min-height is added ON TOP of the
	   theme.json padding, which renders a 70px button instead of ~46px. */
	box-sizing: border-box;
	min-height: var(--tdp-target-min);
	text-decoration: none;
	transition: background-color var(--tdp-duration-fast) var(--tdp-ease),
		border-color var(--tdp-duration-fast) var(--tdp-ease),
		color var(--tdp-duration-fast) var(--tdp-ease);
}

/* Primary CTA labels must never wrap at desktop (three words maximum). */
@media (min-width: 768px) {
	.wp-block-button__link {
		white-space: nowrap;
	}
}

.wp-block-button:not(.is-style-outline) > .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--primary-hover) !important;
}

.wp-block-button:not(.is-style-outline) > .wp-block-button__link:active {
	background-color: var(--wp--preset--color--primary-active) !important;
}

.wp-block-button__link:active {
	transform: translateY(1px);
	transition-duration: var(--tdp-duration-press);
}

/* Secondary variant: transparent ground, 1px primary border, primary label. */
.is-style-outline .wp-block-button__link {
	background-color: transparent;
	border: 1px solid var(--wp--preset--color--primary);
	color: var(--wp--preset--color--primary);
	box-shadow: none;
}

.is-style-outline .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--primary-soft);
	border-color: var(--wp--preset--color--primary) !important;
}

.is-style-outline .wp-block-button__link:active {
	background-color: var(--wp--preset--color--primary-subtle);
}

/* The Consultation CTA's own button is base-background/primary-text
   (an inverted primary button on a primary section) — give it its own
   hover/active pair instead of the standard primary-on-white rules
   above, which would be invisible here. The lift lives here too, so the
   button has one hover definition rather than two. */
.tdp-section--brand .wp-block-button__link:hover,
.tdp-contact-close .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--primary-soft) !important;
	transform: translateY(-2px);
	box-shadow: var(--tdp-shadow-lg);
}

.tdp-section--brand .wp-block-button__link:active {
	background-color: var(--wp--preset--color--primary-subtle) !important;
	transform: none;
}

/* --------------------------------------------------------------------
   Full-bleed sections (homepage wedge, full-width pass, DS-022/§22):
   `.tdp-full-bleed` is a documentation marker only — it carries no CSS
   rule of its own. The actual full-width behaviour comes from native
   `align:"full"` (hero, Consultation CTA) or, for header/footer, from
   simply rendering outside the constrained `<main>` wrapper. The class is
   applied to all four full-bleed surfaces (header, hero, CTA, footer) so
   they are grep-discoverable as one architectural group — see
   docs/design-system-foundation.md §22.
   -------------------------------------------------------------------- */

/* Homepage section bands must butt directly against one another.

   Core's constrained layout puts a `--wp--style--block-gap` margin (24px)
   between the children of `<main>`. While every section was white or
   near-white that gap was invisible, so it went unnoticed. The dark tier
   made it a defect: a 24px stripe of the white body background showed
   between the navy hero and the section under it, and again either side of
   the dark authority band, reading as a rendering fault rather than spacing.

   Deliberately scoped to `body.home`. This redesign is homepage-scoped, and
   an unscoped `main >` rule would change top-level block spacing on the
   service pages, About, Contact, Portfolio and Blog, none of which have the
   full-bleed coloured bands that make the gap a problem. Verified against
   the rendered markup: the homepage carries `home` on <body>, and /about/
   does not.

   Vertical rhythm here comes from each section's own padding (spacing 70 to
   90), never from the gap between them, so removing it changes no intended
   spacing. Core's selector is wrapped in `:where()` and carries no
   specificity, so this wins without `!important`. */
body.home main > .wp-block-group + .wp-block-group {
	margin-block-start: 0;
}

/* --------------------------------------------------------------------
   Header (DS-006, DS-007): surface separation from the hero, plus
   nav-link hover/focus treatment.
   -------------------------------------------------------------------- */
.tdp-header {
	position: sticky;
	top: 0;
	z-index: 100;
	background-color: var(--wp--preset--color--base);
	border-bottom: 1px solid var(--wp--preset--color--border-light);
	transition: box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		border-color var(--tdp-duration-standard) var(--tdp-ease);
}

/* Scrolled state (tdp-motion.js toggles .is-scrolled). The border swaps
   for a shadow rather than stacking both, so there is never a double
   separator over content passing underneath. */
.tdp-header.is-scrolled {
	border-bottom-color: transparent;
	box-shadow: var(--tdp-shadow-md);
	background-color: color-mix(in srgb, var(--wp--preset--color--base) 88%, transparent);
	backdrop-filter: saturate(180%) blur(10px);
	-webkit-backdrop-filter: saturate(180%) blur(10px);
}

/* Without backdrop-filter a translucent bar would show content through it,
   so it stays fully opaque there. */
@supports not (backdrop-filter: blur(1px)) {
	.tdp-header.is-scrolled {
		background-color: var(--wp--preset--color--base);
	}
}

.wp-block-navigation-item > a {
	transition: color var(--tdp-duration-fast) var(--tdp-ease);
}

.wp-block-navigation-item > a:hover {
	color: var(--wp--preset--color--primary);
}

/* Underline grows from the left and doubles as the current-page marker,
   so the active item does not need a competing weight change. */
.tdp-header .wp-block-navigation-item > a {
	position: relative;
	padding-block: 0.35rem;
}

.tdp-header .wp-block-navigation-item > a::after {
	content: "";
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 2px;
	border-radius: 2px;
	background-color: var(--wp--preset--color--primary);
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-header .wp-block-navigation-item > a:hover::after,
.tdp-header .wp-block-navigation-item > a:focus-visible::after,
.tdp-header .wp-block-navigation-item[aria-current="page"] > a::after,
.tdp-header .wp-block-navigation-item.current-menu-item > a::after {
	transform: scaleX(1);
}

/* Submenu links sit in a popover where the underline would collide with
   the panel edge, so they keep the plain colour hover. */
.tdp-header .wp-block-navigation__submenu-container .wp-block-navigation-item > a::after {
	display: none;
}

/* --------------------------------------------------------------------
   Chips (DS-012, DS-024): capability strip, NetSuite capability list,
   Industries — one shared pill treatment with a hover affordance.
   -------------------------------------------------------------------- */
.tdp-chip {
	padding: 0.3rem 0.85rem;
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-full);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease),
		background-color var(--tdp-duration-fast) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-chip:hover {
	border-color: var(--wp--preset--color--primary);
	background-color: var(--wp--preset--color--primary-soft);
	box-shadow: var(--tdp-shadow-sm);
	transform: translateY(-1px);
}

/* --------------------------------------------------------------------
   Eyebrows (homepage image-block-fix + visual-refinement pass): a
   dedicated compact pill for the "label above a heading" pattern.
   NOTE (Phase 3): `.tdp-badge`, referenced below, was retired when its
   last two consumers were removed. The history is kept because it is
   why `.tdp-eyebrow` uses `width: fit-content`, which is still load-bearing.
   (Difference, the 3 story sections, NetSuite Spotlight), separate from
   `.tdp-badge` above. Root cause of the bug this replaces: a `wp:group`
   with `layout:{"type":"flex"}` gets WordPress's own generated
   `.wp-block-group-is-layout-flex { display:flex }` class alongside
   `.tdp-badge`'s `display:inline-flex` — both are single-class selectors,
   but the block-support stylesheet loads after this theme's own
   stylesheet in some template contexts, so `display:flex` won by source
   order for the eyebrow instances (measured at 494-680px wide, filling
   the column) even though `.tdp-badge` itself never changed. A
   `display:flex` block-level box (unlike `inline-flex`) fills its
   container's available width by default, which is the "full-column blue
   bar" bug. `.tdp-eyebrow` fixes this at the source instead of trying to
   win the display-property battle: `width: fit-content` overrides
   whichever `display` wins, so the pill always shrinks to its text
   regardless of `flex` vs `inline-flex`. Same visual treatment as
   `.tdp-badge` (colour, radius, type) — this is a layout fix, not a
   restyle.
   -------------------------------------------------------------------- */
.tdp-eyebrow {
	display: inline-flex;
	align-items: center;
	width: fit-content;
	max-width: 100%;
	background-color: var(--wp--preset--color--primary-soft);
	border-radius: var(--tdp-radius-full);
	padding: 0.3rem 0.9rem;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	font-size: 0.8125rem;
}

.tdp-eyebrow p {
	margin: 0;
	overflow-wrap: break-word;
	word-break: break-word;
	color: var(--wp--preset--color--primary-active) !important;
}

/* Only for eyebrows belonging to a genuinely centered section heading
   (NetSuite Spotlight's intro is `textAlign:center` throughout) — the
   left-aligned story-section and Difference eyebrows do not get this. */
/* --------------------------------------------------------------------
   Cards (DS-019): services grid. Layout rules (flex column, height:100%)
   already existed above — this adds the visual surface.
   -------------------------------------------------------------------- */
.tdp-card {
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-card);
	box-shadow: var(--tdp-shadow-sm);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-card:hover {
	border-color: var(--wp--preset--color--primary);
	box-shadow: var(--tdp-shadow-md);
	transform: translateY(-3px);
}

/* Services grid only: a small primary accent + tightened heading-to-body
   spacing, scoped to `.tdp-card--accent` so it doesn't affect the other
   `.tdp-card` instances (Selected Work's project cards). */
.tdp-card--accent {
	border-top: 3px solid var(--wp--preset--color--primary);
}

.tdp-card--accent h3 {
	margin-bottom: var(--wp--preset--spacing--20);
}

.tdp-card:focus-within {
	outline: 2px solid var(--wp--preset--color--primary);
	outline-offset: 2px;
}

/* --------------------------------------------------------------------
   Section images (homepage image-block-fix + visual-hierarchy pass): the
   real photography in the Difference, Web & Software Development,
   SEO & Digital Growth, Business Systems & Technical Support and NetSuite
   Spotlight sections.

   Markup constraint this whole component works within: a native core/image
   block's save() output is exactly `<figure class="wp-block-image {className}">
   <img .../></figure>` — no third element is available inside it (an extra
   wrapper div around the <img> is exactly the kind of unrecognized markup
   that broke Site Editor validation the first time; see the doc comment at
   the top of difference.php for the full root-cause). So "frame" and
   "media" are necessarily the same two elements the block already gives us:
   the `<img>` itself is both frame and media (aspect-ratio, object-fit,
   border, radius, shadow and background all live on it directly — a
   border-radius on an <img> clips its own pixels with no wrapper needed),
   and the `<figure>` (`.tdp-section-image`) is the "outer" layer, used only
   as a positioning context for decorative brand shapes that sit behind or
   beside the frame. No `__frame`/`__media` child elements exist because
   core/image has no child elements to give them.
   -------------------------------------------------------------------- */
.tdp-section-image {
	position: relative;
	isolation: isolate;
	margin: 0;
}

/* One image frame shared with .tdp-figure: same radius, same elevation,
   so the two feature splits and the story sections read as one system. */
.tdp-section-image img {
	box-shadow: var(--tdp-shadow-md);
	position: relative;
	z-index: 1;
	display: block;
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	background-color: var(--wp--preset--color--primary-subtle);
	border: 1px solid var(--wp--preset--color--primary-subtle);
	border-radius: var(--tdp-radius-card);
	box-shadow: var(--tdp-shadow-sm);
}

/* Marker class for the four Difference/story-section instances (as opposed
   to the NetSuite Spotlight instance, styled separately below). Carries no
   rule of its own today — height consistency between Web & Software
   Development, SEO & Digital Growth and Business Systems & Technical
   Support comes from those three sections sharing the same 48/52 column
   split (so aspect-ratio produces the same pixel height at every
   breakpoint), not from a height cap here. The Difference section is
   intentionally 50/50 per its own "increase visual column presence"
   requirement, so its image renders very slightly taller than the other
   three by design, not by omission. A shared max-height was deliberately
   not used, since at the widths where it would engage it would stop the
   image being full-width in its column, contradicting that requirement. */
.tdp-section-image--story {
	/* Reserved grouping hook — see comment above. */
}

/* Service-page hero + supporting content images (Website Development
   visual-review fix pass, rounds 1-2): the default 4/3 ratio at these
   sections' image columns rendered photos taller than the text beside
   them, making the image compete with the copy instead of supporting it.
   A shorter ratio plus an explicit cap keeps the frame premium without it
   dominating the section. Reused across the hero and both two-column
   supporting-image sections (Problems/Outcomes, Editability) rather than
   giving each its own near-duplicate modifier. */
.tdp-section-image--hero-compact img {
	aspect-ratio: 5 / 4;
	max-height: 22rem;
}

/* Screenshots rather than illustrations. Added during the page 1070 Elementor
   migration, following the same modifier pattern as --hero-compact above.

   The default 4/3 with `object-fit: cover` is right for the homepage's
   purpose-made section illustrations, which are drawn to that ratio. It is
   wrong for a product screenshot: the migrated Website Development hero image
   is 2560x1487 (about 16:9), so a 4/3 cover crop sliced roughly a quarter off
   each side, cutting the dashboard's left navigation and part of the traffic
   panel. Those are readable UI, not decoration, so cropping them loses
   content.

   `aspect-ratio: auto` lets the image keep its own proportions while the
   frame, radius, border and shadow all still come from `.tdp-section-image`
   above. */
.tdp-section-image--wide img {
	aspect-ratio: auto;
}

/* Service-page hero copy column (visual-review fix, round 3): a plain
   max-width instead of the block's `layout.contentSize`. Setting an
   explicit contentSize on a constrained-layout group makes WordPress
   auto-center every non-full direct child narrower than that width —
   invisible on the full-width H1/paragraph/buttons, but it silently
   re-centered the eyebrow pill (deliberately `width: fit-content`) even
   though every other `.tdp-eyebrow` instance sitewide renders left-aligned
   with no such override. This narrows the column without triggering that
   side effect. */
.tdp-hero-copy {
	max-width: 580px;
}

/* Round 4: the eyebrow was still re-centered after the round-3 fix above.
   Cause is unrelated to contentSize — `.tdp-hero-copy` itself keeps
   `layout:{"type":"constrained"}`, which renders WordPress core's generic
   `.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull))
   { margin-left: auto !important; margin-right: auto !important; }` rule
   (block-library base styles, not a per-instance generated rule). That
   still targets the eyebrow directly under `.tdp-hero-copy` regardless of
   contentSize. Needs `!important` to win against `!important`; only the
   hero's own eyebrow is scoped via the `.tdp-hero-copy >` parent, so the
   homepage story-section eyebrows (plain, unclassed parent group) are
   unaffected. */
.tdp-hero-copy > .tdp-eyebrow {
	margin-left: 0 !important;
	margin-right: auto !important;
}

/* --------------------------------------------------------------------
   Process steps (DS-022): number badge + optional desktop connector.
   Visual-hierarchy pass adds a restrained bordered surface (border +
   padding + radius, no shadow, no background tint) to improve scanability
   without making the section heavy — the connector line and number badge
   are unchanged.
   -------------------------------------------------------------------- */
.tdp-process-step {
	position: relative;
	height: 100%;
	padding: var(--wp--preset--spacing--50);
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border-light);
	border-radius: var(--tdp-radius-card);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-process-step:hover {
	border-color: var(--wp--preset--color--primary-subtle);
	box-shadow: var(--tdp-shadow-sm);
	transform: translateY(-2px);
}

/* Solid brand badge rather than a soft tint: at four equal steps the
   number is the only thing carrying the sequence, so it has to read as
   deliberate. z-index lifts it over the connector rule. */
/* Double-class selector is deliberate, and it is the same collision the
   .tdp-eyebrow comment above documents: core's constrained-layout rule
   `.is-layout-constrained > :where(...)` sets margin-inline auto at (0,1,0),
   and the block-support stylesheet loads after this file in some template
   contexts, so a single-class selector loses on source order and the badge
   centres itself over left-aligned text. (0,2,0) wins on class count without
   reaching for !important. */
.tdp-process-step .tdp-process-step__number {
	position: relative;
	z-index: 1;
	margin-inline: 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	border-radius: var(--tdp-radius-full);
	background-color: var(--wp--preset--color--primary);
}

.tdp-process-step__number p {
	color: var(--wp--preset--color--primary-foreground) !important;
	margin: 0;
	font-weight: 600;
}

@media (min-width: 1024px) {
	/* Aligned to the badge centre: spacing-50 padding (1.5rem) plus half
	   the 2.5rem badge. Tied to the padding above, so if one moves the
	   other has to. */
	.wp-block-column:not(:first-child) .tdp-process-step::before {
		content: "";
		position: absolute;
		top: 2.75rem;
		right: 100%;
		width: var(--wp--preset--spacing--50);
		border-top: 1px dashed var(--wp--preset--color--border);
	}
}

/* --------------------------------------------------------------------
   FAQ (DS-028): upgraded from a bottom-only divider to a bordered item
   with hover/focus/open-state treatment. Native <details>/<summary>
   markup and behaviour are unchanged — no accordion JavaScript.
   -------------------------------------------------------------------- */
.tdp-faq-item {
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-md);
	padding-left: var(--wp--preset--spacing--40);
	padding-right: var(--wp--preset--spacing--40);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease),
		background-color var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-faq-item:hover {
	border-color: var(--wp--preset--color--primary);
}

.tdp-faq-item[open] {
	background-color: var(--wp--preset--color--primary-soft);
	border-color: var(--wp--preset--color--primary);
}

/* --------------------------------------------------------------------
   Consultation CTA (DS-029): brand gradient + decorative shapes.
   -------------------------------------------------------------------- */
.tdp-section--brand {
	position: relative;
	overflow: hidden;
	background: linear-gradient(
		135deg,
		var(--wp--preset--color--brand) 0%,
		var(--wp--preset--color--primary) 45%,
		var(--wp--preset--color--primary-active) 100%
	);
}

/* One hairline of light along the top edge. It is what separates the band
   from the section above it and gives the ground a sense of depth without
   putting decoration into a section that is carrying a single message. */
.tdp-section--brand::before {
	content: "";
	position: absolute;
	inset: 0 0 auto;
	height: 1px;
	background-color: rgba(255, 255, 255, 0.28);
	pointer-events: none;
}

/* --------------------------------------------------------------------
   Footer (DS-030): link hover and de-emphasised column
   headings.
   -------------------------------------------------------------------- */
.tdp-footer h3 {
	font-size: 0.9375rem;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--wp--preset--color--muted);
}

.tdp-footer a {
	transition: color var(--tdp-duration-fast) var(--tdp-ease);
}

.tdp-footer a:hover {
	color: var(--wp--preset--color--primary);
}

/* --------------------------------------------------------------------
   Feature-list marker refinement (DS-015): Difference-family sections
   and services-grid cards share a plain <ul> — restyle the disc marker
   colour only, no icon dependency.
   -------------------------------------------------------------------- */
.tdp-card ul,
.tdp-section .wp-block-group > .wp-block-list {
	color: var(--wp--preset--color--primary);
}

.tdp-card ul li,
.tdp-section .wp-block-group > .wp-block-list li {
	color: var(--wp--preset--color--text);
}

/* --------------------------------------------------------------------
   Scroll reveal (DS-005): visible by default; only becomes a
   progressively-enhanced reveal once tdp-motion.js adds
   .tdp-motion-ready to <html>. See docs/design-system-foundation.md §10.4.
   -------------------------------------------------------------------- */
.tdp-reveal {
	opacity: 1;
	transform: none;
}

.tdp-motion-ready .tdp-reveal {
	opacity: 0;
	transform: translateY(12px);
	transition: opacity var(--tdp-duration-reveal) var(--tdp-ease),
		transform var(--tdp-duration-reveal) var(--tdp-ease);
}

.tdp-motion-ready .tdp-reveal.is-visible {
	opacity: 1;
	transform: none;
}

/* ==========================================================================
   PHASE 2 — HOMEPAGE + HEADER
   Only what the Phase 1 primitives and the surviving component classes
   cannot already express. Service cards reuse .tdp-card, process steps reuse
   .tdp-process-step and .tdp-process-step__number, chips reuse .tdp-chip:
   no parallel card, step or chip system is introduced here.
   ========================================================================== */

/* --------------------------------------------------------------------
   Header. One flex row: logo, then nav + CTA grouped at the end so the
   CTA stays adjacent to the menu rather than drifting to the far edge.
   -------------------------------------------------------------------- */
.tdp-header__row {
	gap: var(--wp--preset--spacing--50);
}

.tdp-header__end {
	gap: var(--wp--preset--spacing--50);
}

.tdp-header .wp-block-navigation {
	font-size: var(--wp--preset--font-size--small);
}

/* Current page and current section both get the same weight bump, so the
   submenu parent stays marked while a service page is open. */
.tdp-header .wp-block-navigation-item.current-menu-item > a,
.tdp-header .wp-block-navigation-item.current-menu-ancestor > a,
.tdp-header .wp-block-navigation-item[aria-current="page"] > a {
	color: var(--wp--preset--color--primary);
	font-weight: 600;
}

.tdp-header__cta .wp-block-button__link {
	white-space: nowrap;
	transition: transform var(--tdp-duration-standard) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		background-color var(--tdp-duration-fast) var(--tdp-ease);
}

.tdp-header__cta .wp-block-button__link:hover {
	transform: translateY(-1px);
	box-shadow: var(--tdp-shadow-md);
}

.tdp-header__cta .wp-block-button__link:active {
	transform: none;
	box-shadow: none;
}

/* Core renders the submenu disclosure as a real, focusable, clickable
   <button aria-expanded> sized to its 9px chevron SVG. At 9x9 CSS px that
   fails WCAG 2.5.8 Target Size (Minimum, AA, 24x24) with no applicable
   exception: it is not inline in a sentence, and the parent "Services" link
   is not an equivalent control because it navigates to /services/ rather than
   opening the submenu. Padding it out to 24x24 keeps the chevron the same
   size while giving the control a conformant hit area; the negative margin
   absorbs the added box so the nav row's spacing is unchanged. */
.tdp-header .wp-block-navigation-submenu__toggle {
	box-sizing: content-box;
	min-width: 24px;
	min-height: 24px;
	padding: 8px;
	margin: -8px;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Nothing in the header row may be crushed. Without these the CTA (236px,
   non-shrinking text) starved the logo down to 58px and collapsed the nav
   and its overlay toggle to ZERO width at 390, leaving no way to open the
   menu at all. */
.tdp-logo,
.tdp-header .wp-block-navigation,
.tdp-header__cta {
	flex-shrink: 0;
}

/* Below core's 600px overlay breakpoint the CTA is dropped so the logo and
   the menu button own the row. The conversion path is not lost: the hero's
   own "Book a Free Consultation" button sits immediately beneath the header,
   and "Contact" remains in the menu.

   The double-class selector is deliberate: core ships `body .is-layout-flex
   { display: flex }` at specificity (0,1,1), which beats a single class.
   Raising to (0,2,0) wins on class count without resorting to !important. */
@media (max-width: 599px) {
	.tdp-header .tdp-header__cta {
		display: none;
	}
}

/* 600-781px: core has switched the overlay off and shows every item, which
   wrapped the nav internally and pushed the header to 106px tall. Letting
   the ROW wrap instead gives a clean two-line header: logo, then nav + CTA. */
@media (min-width: 600px) and (max-width: 781px) {
	.tdp-header__row {
		flex-wrap: wrap;
	}

	.tdp-header__end {
		flex-basis: 100%;
		justify-content: space-between;
	}
}

/* --------------------------------------------------------------------
   Hero. Asymmetric split, capped top padding so it does not float down
   the viewport. No image: see patterns/hero.php.
   -------------------------------------------------------------------- */
.tdp-hero {
	background-color: var(--wp--preset--color--primary-soft);
	border-bottom: 1px solid var(--wp--preset--color--primary-subtle);
}

/* Two lines at desktop, breaking at the sentence boundary.

   The constraint was never max-width, it was the 689px column: at the global
   h1 size the phrase "Digital growth made real." measures 717px, so no
   two-line split fit and it broke to three, against the two-line cap this
   project set for itself in docs/taste-redesign-direction.md. The third line
   was also what left the right-hand column short.

   Scoped to the hero rather than raised in theme.json, because every other
   h1 on the site is a single shorter phrase that does not have this problem
   and should keep the full display size. Weight and colour carry the
   hierarchy here, not raw scale. */
.tdp-hero__title {
	font-size: clamp(2.25rem, 1.6rem + 2.6vw, 3.375rem);
	max-width: 22ch;
}

.tdp-hero .tdp-lede {
	max-width: 46ch;
}

/* A real panel, not a border rule with a list beside it. The white surface
   separates it from the tinted hero ground and gives the right column
   enough weight to hold its half of the composition. */
.tdp-hero__aside {
	padding: var(--wp--preset--spacing--60) var(--wp--preset--spacing--50);
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--primary-subtle);
	border-radius: var(--tdp-radius-card);
	box-shadow: var(--tdp-shadow-md);
}

.tdp-hero__aside-label,
.tdp-work__panel-label {
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--primary-active);
	margin: 0 0 var(--wp--preset--spacing--40);
}

/* Markers reuse the DS-015 technique (colour the list, reset the item) that
   the services cards and Difference sections already use, rather than a
   second parallel system built from ::before dots. */
.tdp-hero__list,
.tdp-work__list {
	margin: 0;
	padding-left: 1.15rem;
	display: grid;
	gap: var(--wp--preset--spacing--30);
	color: var(--wp--preset--color--primary);
}

.tdp-hero__list li,
.tdp-work__list li {
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--text);
	line-height: 1.45;
}

.tdp-hero__aside-foot,
.tdp-work__panel-foot {
	margin: var(--wp--preset--spacing--40) 0 0;
	padding-top: var(--wp--preset--spacing--40);
	border-top: 1px solid var(--wp--preset--color--border-light);
	font-size: var(--wp--preset--font-size--small);
}

.tdp-hero__actions {
	margin-top: var(--wp--preset--spacing--50);
	align-items: center;
	flex-wrap: wrap;
}

/* --------------------------------------------------------------------
   Positioning and Why: three equal prose columns, no card chrome. The
   design system prefers whitespace to boxes where nothing is clickable.
   -------------------------------------------------------------------- */
.tdp-pos__grid h3,
.tdp-why__grid h3 {
	margin-bottom: var(--wp--preset--spacing--30);
}

.tdp-pos__grid > .wp-block-column {
	padding: var(--wp--preset--spacing--50);
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-card);
	/* Accent as an inset shadow rather than border-top, so the hover
	   border-colour change cannot wipe it out. */
	box-shadow: inset 0 3px 0 var(--wp--preset--color--primary);
	transition: border-color var(--tdp-duration-fast) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease),
		transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-pos__grid > .wp-block-column:hover {
	border-color: var(--wp--preset--color--primary-subtle);
	box-shadow: inset 0 3px 0 var(--wp--preset--color--primary), var(--tdp-shadow-md);
	transform: translateY(-2px);
}

.tdp-pos__grid > .wp-block-column > :first-child {
	margin-top: 0;
}

.tdp-pos__grid > .wp-block-column > :last-child {
	margin-bottom: 0;
}

/* Platform strip: a labelled block rather than loose chips under the grid. */
.tdp-pos__band-label {
	margin: var(--wp--preset--spacing--60) 0 var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

/* --------------------------------------------------------------------
   Difference: a tinted full-width band so it reads as a different kind of
   section from the white panel grid above it. Columns are separated by
   hairlines rather than boxed, and each heading carries a short brand rule.
   Two different answers to "three columns" instead of the same one twice.
   -------------------------------------------------------------------- */
.tdp-why__grid h3::before {
	content: "";
	display: block;
	width: 2rem;
	height: 3px;
	margin-bottom: var(--wp--preset--spacing--40);
	border-radius: 2px;
	background-color: var(--wp--preset--color--primary);
}

@media (min-width: 782px) {
	.tdp-why__grid > .wp-block-column + .wp-block-column {
		padding-left: var(--wp--preset--spacing--60);
		border-left: 1px solid var(--wp--preset--color--border);
	}
}

.tdp-pos .tdp-capability-band {
	margin-top: var(--wp--preset--spacing--60);
}

/* --------------------------------------------------------------------
   Service cards: .tdp-card supplies the surface; this adds only the
   read-more affordance and the equal-height column behaviour.
   -------------------------------------------------------------------- */
.tdp-svc-card {
	padding: var(--wp--preset--spacing--50);
}

/* Tightens title-to-body so the card reads title / description / action
   rather than three evenly spaced paragraphs. */
.tdp-svc-card h3 {
	margin-bottom: var(--wp--preset--spacing--30);
}

.tdp-card__link {
	margin-top: auto;
	padding-top: var(--wp--preset--spacing--50);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
}

.tdp-card__link a {
	text-decoration: none;
}

.tdp-card__link a:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* The arrow is the moving part so the label stays put and the motion reads
   as direction rather than text drifting. Drawn from two borders: no icon
   font, no SVG request, and it inherits the link colour. */
.tdp-card__link a::after {
	content: "";
	display: inline-block;
	width: 0.4em;
	height: 0.4em;
	margin-left: 0.45em;
	border-right: 2px solid currentColor;
	border-top: 2px solid currentColor;
	transform: rotate(45deg) translateY(0.05em);
	transition: transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-card:hover .tdp-card__link a::after,
.tdp-card:focus-within .tdp-card__link a::after {
	transform: rotate(45deg) translate(0.15em, -0.1em);
}

/* --------------------------------------------------------------------
   Feature splits. Two only, alternating side, per the taste direction's
   two-zigzag cap. The tinted band separates them from the prose sections.
   -------------------------------------------------------------------- */
.tdp-feature--growth {
	background-color: var(--wp--preset--color--surface);
}

.tdp-figure img {
	border-radius: var(--tdp-radius-card);
	box-shadow: var(--tdp-shadow-md);
	width: 100%;
	height: auto;
	display: block;
}

/* --------------------------------------------------------------------
   Insights. Native query loop; the grid is core's, this only supplies
   the card surface and strips the default list margins.
   -------------------------------------------------------------------- */
.tdp-insights__grid {
	gap: var(--wp--preset--spacing--50);
	margin-top: var(--wp--preset--spacing--60);
}

.tdp-insight-card {
	padding: var(--wp--preset--spacing--50);
	height: 100%;
}

/* The auto-margin trap, and where it moved to.

   `.tdp-card` makes the card a flex column, and core's constrained-layout
   rule sets `margin-left/right: auto !important` on its children. The title
   and excerpt fill the width, so those margins have no free space to absorb
   and they sit flush left. A short element is different: the auto margins
   take the slack and centre it under left-aligned text. The margins cannot
   be overridden - `!important` in a stylesheet beats even an inline style,
   which was confirmed by testing.

   The date used to carry `width: 100%` for exactly that reason. It is now a
   child of `.tdp-insight-card__meta`, a flex row, so the constrained-layout
   rule no longer reaches it and the full width would instead push the
   category onto its own line. The fix moves up to the meta row itself, which
   is the short child now. */
.tdp-insight-card__date {
	color: var(--wp--preset--color--muted);
	margin: 0;
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

.tdp-insight-card .wp-block-post-title {
	margin: 0 0 var(--wp--preset--spacing--30);
}

.tdp-insight-card .wp-block-post-title a {
	text-decoration: none;
	transition: color var(--tdp-duration-fast) var(--tdp-ease);
}

.tdp-insight-card:hover .wp-block-post-title a {
	color: var(--wp--preset--color--primary);
}

.tdp-insight-card .wp-block-post-title a:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* Fallback when nothing is published yet. The query is real and dynamic, so
   this is what a visitor sees only while the post count is zero: a designed
   state rather than a stray sentence under a heading. No invented posts. */
.tdp-insights__query .wp-block-query-no-results p {
	margin: 0;
	padding: var(--wp--preset--spacing--70) var(--wp--preset--spacing--50);
	border: 1px dashed var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-card);
	background-color: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--muted);
	text-align: center;
}

.tdp-insights .wp-block-buttons {
	margin-top: var(--wp--preset--spacing--60);
}

.tdp-insight-card .wp-block-post-excerpt {
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	margin-bottom: 0;
}

/* Featured image. Pulled out to the card edges by cancelling the card's own
   spacing-50 padding, so the media reads as the top of an editorial card
   rather than an inset thumbnail. The radius matches the card and is applied
   to the top corners only, since the bottom edge is now an interior edge.

   The 16/9 crop comes from the block's `aspectRatio` attribute, not from CSS,
   so every card holds the same ratio whatever the source image dimensions.
   `object-fit: cover` keeps the subject filling that frame without
   distortion. */
.tdp-insight-card__media {
	margin: calc(-1 * var(--wp--preset--spacing--50))
		calc(-1 * var(--wp--preset--spacing--50))
		var(--wp--preset--spacing--50);
}

.tdp-insight-card__media img {
	width: 100%;
	border-radius: var(--tdp-radius-card) var(--tdp-radius-card) 0 0;
	object-fit: cover;
}

/* The meta row is the short child the auto-margin comment above describes, so
   it is the one that needs the full width now. */
.tdp-insight-card__meta {
	width: 100%;
	align-items: baseline;
	margin-bottom: var(--wp--preset--spacing--30);
}

/* Category. The one accent-coloured element in the card, which is what marks
   it as a filter rather than body text; the date beside it stays muted so the
   two do not compete. */
.tdp-insight-card__cat,
.tdp-insight-card__cat a {
	margin: 0;
	color: var(--wp--preset--color--primary);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
}

.tdp-insight-card__cat a:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

/* Read article. `margin-top: auto` is what keeps the CTA on a common baseline
   across the three cards when excerpt lengths differ - the same mechanism the
   service cards use for `.tdp-card__link`. It reuses that chevron treatment
   rather than defining a second arrow. */
.tdp-insight-card__more {
	margin: auto 0 0;
	padding-top: var(--wp--preset--spacing--50);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
}

.tdp-insight-card__more a {
	text-decoration: none;
}

.tdp-insight-card__more a:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.tdp-insight-card__more a::after {
	content: "";
	display: inline-block;
	width: 0.4em;
	height: 0.4em;
	margin-left: 0.45em;
	border-right: 2px solid currentColor;
	border-top: 2px solid currentColor;
	transform: rotate(45deg) translateY(0.05em);
	transition: transform var(--tdp-duration-standard) var(--tdp-ease);
}

.tdp-insight-card:hover .tdp-insight-card__more a::after,
.tdp-insight-card:focus-within .tdp-insight-card__more a::after {
	transform: rotate(45deg) translate(0.15em, -0.1em);
}

/* --------------------------------------------------------------------
   Closing CTA band: primary ground, so its copy and button invert.
   -------------------------------------------------------------------- */
/* Copy is held to a readable measure instead of running the full content
   width, so the band reads as a composed closing statement. The button then
   sits against real space rather than trailing a full-width paragraph. */
/* The copy now sits in its own column, so the prose measure is set by the
   column width and the earlier --tdp-measure override here became inert.
   The heading keeps an explicit cap so it breaks to two lines rather than
   running the full column. */
.tdp-section--brand h2 {
	max-width: 30rem;
}

.tdp-section--brand h2,
.tdp-section--brand p {
	color: var(--wp--preset--color--primary-foreground) !important;
}

/* Stacked under the copy the action needs its spacing back, and it has to
   return to the left edge: the right-justify that balances the desktop band
   reads as a stray indent once the columns collapse. */
@media (max-width: 781px) {
	.tdp-section--brand .wp-block-buttons,
	.tdp-contact-close .wp-block-buttons {
		margin-top: var(--wp--preset--spacing--50);
		justify-content: flex-start;
	}

	/* Stacked, the action carries the full measure so it reads as an
	   unmissable target rather than a small pill on a large band. */
	.tdp-contact-close .wp-block-button,
	.tdp-contact-close .wp-block-button__link {
		width: 100%;
	}
}

.tdp-section--brand .wp-block-button__link {
	background-color: var(--wp--preset--color--base) !important;
	color: var(--wp--preset--color--primary) !important;
	transition: transform var(--tdp-duration-standard) var(--tdp-ease),
		box-shadow var(--tdp-duration-standard) var(--tdp-ease);
}

/* --------------------------------------------------------------------
   Selected Work panel. A tinted surface rather than the white card used
   elsewhere: it is supporting information beside the explanation, not a
   peer of the service cards, and the tint keeps that relationship clear.
   The list and foot link share the hero panel's rules above.
   -------------------------------------------------------------------- */
.tdp-work__panel {
	padding: var(--wp--preset--spacing--60) var(--wp--preset--spacing--50);
	background-color: var(--wp--preset--color--surface);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-card);
}

/* --------------------------------------------------------------------
   Mobile menu target sizes. Core ships a 24x24 toggle and ~25px menu
   rows, which clears WCAG 2.5.8 AA but not the 44x44 floor this project
   set for itself in docs/design-system-plan.md section 7.
   -------------------------------------------------------------------- */
.wp-block-navigation__responsive-container-open,
.wp-block-navigation__responsive-container-close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: var(--tdp-target-min);
	min-height: var(--tdp-target-min);
	border-radius: var(--tdp-radius-input);
}

.wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
	display: flex;
	align-items: center;
	min-height: var(--tdp-target-min);
}

/* ==========================================================================
   PHASE 3 — SERVICE PAGES + SERVICES HUB
   Section shells only. Cards, chips, steps, FAQ items, eyebrows, buttons and
   the closing band all reuse the Phase 1/2 primitives unchanged: there is no
   per-service CSS system and no six-page duplication.
   ========================================================================== */

/* Service and hub hero. Same tinted band as the homepage hero so the site
   reads as one system, but its own class so the two can diverge later. */
/* The shared page hero. Used by the services hub, every block-built service
   page, Portfolio, About, Contact, the blog archive, single posts and 404 -
   9 page types from one class. It joined the dark group above in the
   site-wide colour rollout, so the ground and every text, link, card and
   button treatment inside it now come from that single tier. Only the
   bottom border lives here, and it follows the dark hairline rather than
   the light-tier `primary-subtle`, which was invisible on navy.

   This is deliberately NOT a new `tdp-page-hero` system. One shared hero
   already existed; duplicating it would have created two to maintain. */
.tdp-svc-hero {
	border-bottom: 1px solid var(--wp--preset--color--dark-line);
}

.tdp-svc-hero__title {
	max-width: 20ch;
}

.tdp-svc-hero .tdp-lede {
	max-width: 56ch;
}

/* Alternating surfaces give a long service page rhythm without adding
   decoration. Applied by role, not per page, so all six stay related.

   The two `tdp-work-*` entries were added during the site-wide colour
   rollout. Portfolio previously ran five consecutive light bands, the
   flattest sequence on the site; with the dark anchor on
   `tdp-work-approach` it now reads
   light / soft / light / DARK / soft / light.

   `tdp-hub-support` is deliberately NOT in this list. It was added during
   the rollout and then removed after a read-only inventory of the live
   pages: production's services hub carries two sections the local database
   does not have, and both `tdp-svc-why-hub` and `tdp-svc-faq` already take
   the surface through their own `has-surface-background-color` attribute.
   Making `tdp-hub-support` soft as well produced three consecutive soft
   bands there. Left light, the live sequence alternates correctly:
   soft / light / soft. */
.tdp-svc-fit,
.tdp-svc-process,
.tdp-hub-picker,
.tdp-work-types,
.tdp-work-existing {
	background-color: var(--wp--preset--color--surface);
}

/* Callouts: the "what we will not promise" and "why us" blocks read as an
   aside rather than another body section. A left rule, no card chrome. */
.tdp-svc-honest,
.tdp-svc-why {
	border-left: 3px solid var(--wp--preset--color--primary-subtle);
	padding-left: var(--wp--preset--spacing--50);
}

.tdp-svc-honest > *,
.tdp-svc-why > * {
	max-width: 62ch;
}

/* Bulleted capability lists in "what we provide" need room to breathe. */
.tdp-svc-provide ul {
	display: grid;
	gap: var(--wp--preset--spacing--30);
	max-width: 68ch;
	padding-left: var(--wp--preset--spacing--50);
}

.tdp-svc-provide li {
	padding-left: var(--wp--preset--spacing--20);
}

/* Related services: two cards, so stop them stretching to full width. */
.tdp-svc-related .wp-block-columns {
	max-width: 60rem;
}

/* Hub: the problem-led picker cards carry links inside body copy, so they
   need the same hover affordance as the service cards without being one. */
.tdp-hub-picker .tdp-card,
.tdp-hub-grid .tdp-card {
	height: 100%;
}

/* FAQ blocks appear on three of the six pages. Constrain the answer measure
   so a long answer does not run the full container width. */
.tdp-svc-faq .tdp-faq-item p {
	max-width: 68ch;
}

/* --------------------------------------------------------------------
   Full-bleed sections: align the padding edge to the content column.

   A section with `alignfull` spans the viewport, so its padding edge sits
   at spacing-50 from the screen edge, while any child WITHOUT a max-width
   is centred by the constrained layout at contentSize. That put the hero's
   eyebrow, heading and lede at 24px and its buttons at 100px on a 1440
   viewport: the same block of content starting at two different left edges.

   Growing the side padding to (100% - contentSize) / 2 PLUS spacing-50
   makes the padding edge match where a constrained section's content
   actually starts: those sections are centred at contentSize and THEN add
   their own spacing-50 inside it. Without the extra term, full-bleed
   sections started 24px left of every other section on the page.
   max() keeps spacing-50 as the floor on narrow screens.
   -------------------------------------------------------------------- */
.alignfull.tdp-section,
.alignfull.tdp-hero {
	padding-left: max(var(--wp--preset--spacing--50), calc((100% - 1240px) / 2 + var(--wp--preset--spacing--50))) !important;
	padding-right: max(var(--wp--preset--spacing--50), calc((100% - 1240px) / 2 + var(--wp--preset--spacing--50))) !important;
}

/* ==========================================================================
   PHASE 4 — CONTACT + ABOUT
   The form controls, labels, helper text, error and success states already
   exist from Phase 1, including the Contact Form 7 class mappings. This adds
   only the page layout and the two markers CF7 introduces.
   ========================================================================== */

/* Contact: form on the left, the human details beside it. */
.tdp-contact__grid {
	gap: var(--wp--preset--spacing--80);
}

@media (min-width: 782px) {
	.tdp-contact__grid > .wp-block-column:first-child {
		flex-basis: 40%;
	}
	.tdp-contact__grid > .wp-block-column:last-child {
		flex-basis: 60%;
	}
}

/* "(required)" is stated in words as well as being enforced, so the cue does
   not depend on colour or on noticing an asterisk. */
.tdp-req,
.tdp-optional {
	font-weight: 400;
	color: var(--wp--preset--color--muted);
}

/* CF7 wraps each control in its own <p>. Neutralise that spacing so the
   .tdp-field rhythm from Phase 1 is what actually governs the form. */
.tdp-form p {
	margin: 0;
}

/* CF7 emits label, helper span and control as inline siblings inside one <p>,
   which put the helper text on the same line as the label. Force the block
   flow the Phase 1 field pattern assumes: label, then helper, then control. */
.tdp-form label,
.tdp-form .tdp-help {
	display: block;
}

.tdp-form .tdp-help {
	margin-top: var(--wp--preset--spacing--20);
}

.tdp-form .wpcf7-form-control-wrap {
	display: block;
	margin-top: var(--wp--preset--spacing--20);
}

.tdp-form br {
	display: none;
}

.tdp-field--submit {
	margin-top: var(--wp--preset--spacing--50);
}

/* CF7 renders its submit as a bare input, so give it the button treatment
   rather than shipping a second button system. */
.tdp-form input[type="submit"] {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	box-sizing: border-box;
	min-height: var(--tdp-target-min);
	width: auto;
	padding: 0.8125rem 1.75rem;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	line-height: 1.2;
	color: var(--wp--preset--color--primary-foreground);
	background-color: var(--wp--preset--color--primary);
	border: 0;
	border-radius: var(--tdp-radius-button);
	cursor: pointer;
	transition: background-color var(--tdp-duration-fast) var(--tdp-ease);
}

.tdp-form input[type="submit"]:hover {
	background-color: var(--wp--preset--color--primary-hover);
}

.tdp-form input[type="submit"]:active {
	background-color: var(--wp--preset--color--primary-active);
	transform: translateY(1px);
}

/* CF7's own spinner and inline validation text. */
.tdp-form .wpcf7-spinner {
	margin-left: var(--wp--preset--spacing--30);
}

/* The form is the page's primary surface, so it gets the card treatment
   rather than floating on the page background. Radius, border, shadow and
   padding all come from the existing card scale. */
.tdp-form {
	padding: var(--wp--preset--spacing--60);
	background-color: var(--wp--preset--color--base);
	border: 1px solid var(--wp--preset--color--border);
	border-radius: var(--tdp-radius-card);
	box-shadow: var(--tdp-shadow-md);
}

/* Name/email and phone/company pair up once there is room; below the columns
   breakpoint every field is full width, which is what the .tdp-field rhythm
   already assumes. */
@media (min-width: 782px) {
	.tdp-form__grid {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 0 var(--wp--preset--spacing--50);
	}
}

/* CF7 sizes text inputs with a `size` attribute, which would otherwise leave
   them short of their container and inconsistent with the select/textarea. */
.tdp-form input[type="text"],
.tdp-form input[type="email"],
.tdp-form input[type="tel"],
.tdp-form select,
.tdp-form textarea {
	width: 100%;
}

.tdp-form textarea {
	resize: vertical;
}

.tdp-form__fallback {
	margin: 0;
}

.tdp-contact__email {
	font-size: var(--wp--preset--font-size--large);
	font-weight: 600;
}

.tdp-contact__routes-label {
	margin-bottom: var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--x-small);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

/* Routing chips are links, so the chip is the affordance and the underline
   inside it would be noise. The whole chip is the target. */
.tdp-contact-routes,
.tdp-contact-assure {
	margin-top: var(--wp--preset--spacing--40);
}

.tdp-contact-routes .tdp-chip,
.tdp-contact-assure .tdp-chip {
	margin: 0;
}

.tdp-contact-routes .tdp-chip a {
	text-decoration: none;
	color: var(--wp--preset--color--text);
}

.tdp-contact-routes .tdp-chip:hover a {
	color: var(--wp--preset--color--primary);
}

/* Contact closing band. The navy ground comes from the `heading` token via a
   native backgroundColor attribute, so only the shape and the accent live
   here. The blue wash is a z-index:-1 pseudo-element: it lifts the corner the
   heading sits in and can never sit over text. At 0.30 it reads as depth
   rather than glow. */
.tdp-contact-close__inner {
	position: relative;
	isolation: isolate;
	overflow: hidden;
	border-radius: var(--tdp-radius-card);
}

.tdp-contact-close__inner::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: radial-gradient(70% 140% at 6% 0%, rgba(0, 123, 199, 0.30), transparent 60%);
	pointer-events: none;
}

/* About: alternating surface, same rhythm as the service pages. */
.tdp-about-how,
.tdp-about-where {
	background-color: var(--wp--preset--color--surface);
}

.tdp-about-team > * {
	max-width: 62ch;
}

/* ==========================================================================
   MOTION (extends the DS-005 scroll-reveal above)

   One language for the whole site: fade plus a short rise, siblings
   staggered by position, images settling rather than sliding. Durations sit
   inside the 400-700ms reveal / 180-250ms hover band, no bounce, no
   parallax, no library.

   Stagger reads position from :nth-child() on the grid children that already
   exist, so no pattern markup carries index classes and a card added or
   removed re-numbers itself. The targets are `.wp-block-column` and post
   template items because every staggered grid on the site is built from one
   or the other, which keeps this to two selectors rather than a list of
   every card class.
   ========================================================================== */

:root {
	--tdp-duration-reveal-lg: 620ms;
	--tdp-stagger: 70ms;
}

.tdp-motion-ready .tdp-reveal {
	transform: translateY(16px);
	transition-duration: var(--tdp-duration-reveal-lg);
}

.tdp-motion-ready .tdp-reveal.is-visible {
	transform: none;
}

/* `will-change` deliberately absent: these run once, and pinning a
   compositor layer per card costs more than the animation does. */
.tdp-motion-ready .tdp-reveal :is(.wp-block-column, .wp-block-post-template > li) {
	opacity: 0;
	transform: translateY(14px);
	transition: opacity var(--tdp-duration-reveal-lg) var(--tdp-ease),
		transform var(--tdp-duration-reveal-lg) var(--tdp-ease);
	transition-delay: calc(var(--tdp-stagger) * var(--tdp-i, 0));
}

.tdp-motion-ready .tdp-reveal.is-visible :is(.wp-block-column, .wp-block-post-template > li) {
	opacity: 1;
	transform: none;
}

:is(.wp-block-column, .wp-block-post-template > li):nth-child(1) { --tdp-i: 1; }
:is(.wp-block-column, .wp-block-post-template > li):nth-child(2) { --tdp-i: 2; }
:is(.wp-block-column, .wp-block-post-template > li):nth-child(3) { --tdp-i: 3; }
:is(.wp-block-column, .wp-block-post-template > li):nth-child(4) { --tdp-i: 4; }

/* Transform only, no opacity: the column above is already fading, and
   fading the image inside it as well would compound to a double dip. */
.tdp-motion-ready .tdp-reveal :is(.tdp-figure, .tdp-section-image) img {
	transform: scale(1.015);
	transition: transform var(--tdp-duration-reveal-lg) var(--tdp-ease);
}

.tdp-motion-ready .tdp-reveal.is-visible :is(.tdp-figure, .tdp-section-image) img {
	transform: none;
}

/* The hero is above the fold, so it animates on load rather than waiting on
   an observer that would make first paint feel late. Held inside the
   motion-safe query so it never runs against the user's preference. */
@media (prefers-reduced-motion: no-preference) {
	.tdp-hero__title,
	.tdp-hero .tdp-lede,
	.tdp-hero__actions,
	.tdp-hero__aside {
		animation: tdp-rise var(--tdp-duration-reveal-lg) var(--tdp-ease) both;
	}

	.tdp-hero__title { animation-delay: 60ms; }
	.tdp-hero .tdp-lede { animation-delay: 140ms; }
	.tdp-hero__actions { animation-delay: 220ms; }

	.tdp-hero__aside {
		animation-name: tdp-rise-left;
		animation-delay: 300ms;
	}
}

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

@keyframes tdp-rise-left {
	from { opacity: 0; transform: translateX(18px); }
	to { opacity: 1; transform: none; }
}

/* --------------------------------------------------------------------
   Dark section tier (DS-031). Supersedes the light-only decision recorded
   as D6 in docs/decisions-record.md, on the brief's explicit instruction
   that the homepage must carry a deliberate light/dark rhythm.

   The ground is the existing `heading` token, not a new colour: the
   contact closing band already proved it as a dark surface, so the tier
   extends a treatment the theme has rather than introducing a second
   navy. Only the tokens that genuinely did not exist were added to
   theme.json - a raised surface, a hairline, two text weights and a
   lifted accent.

   Every value below was measured, not estimated:
     on-dark        #E8EFF4 on #0B1B26 = 15.08:1  AA
     on-dark-muted  #9FB3C1 on #0B1B26 =  8.08:1  AA
     brand-lift     #4FB3E8 on #0B1B26 =  7.46:1  AA
     dark-soft      #17364C vs #0B1B26 =  1.39:1  reads as a distinct surface

   `brand` (#007BC7) measures 3.88:1 on this ground - large text only.
   That is why `brand-lift` exists and why no small text or link on a dark
   section may use `brand`.
   -------------------------------------------------------------------- */
/* ── THE DARK SECTION GROUP ───────────────────────────────────────────
   Single source of truth for what counts as a dark section. Every rule in
   this tier keys off this same `:is()` list, so a section is added to or
   removed from the dark system in one place rather than eighteen.

   `.tdp-section--dark` is the real class. Everything after it is a
   TEMPORARY ALIAS.

   Why the aliases exist: the sections below live in `post_content` in the
   database, not in theme files, because `page.html` and `page-service.html`
   render `wp:post-content` only. Styling them by their existing semantic
   class keeps this release file-only, with no database writes and no
   content re-stage. That was a deliberate trade (Option A) against the
   cleaner alternative of adding `tdp-section--dark` to the markup.

   MIGRATION PATH: during a planned content refactor, add
   `tdp-section--dark` to these sections in `deployment/content/*.html`,
   re-stage, then delete the alias lines below. No other rule changes.

     .tdp-svc-hero         THE SHARED PAGE HERO - 9 page types. This one is
                           a theme-level class, not a content alias, and it
                           stays in the group permanently.

     .tdp-svc-honest       service pages - "Where X is the wrong answer"
     .tdp-svc-editability  service pages - "Built to be handed over"
     .tdp-svc-why          services hub  - "Why The DEV Point"
     .tdp-about-team       about         - "The people"
     .tdp-work-approach    portfolio     - "How we run a project"

   About uses `about-team` rather than `about-how`, for two reasons.
   `about-how` already carries the soft surface a few hundred lines up, so
   putting it in this group would leave two rules fighting over the same
   ground on equal specificity, decided only by source order. And it sits
   second on the page: dark hero followed almost immediately by a dark band
   makes the page top-heavy. `about-team` sits late and carries the stronger
   trust statement, which is what a dark anchor is for.

   Each was chosen as its page's authority/trust moment, not by position.
   `tdp-svc-honest` and `tdp-svc-why` also carry a callout treatment (left
   rule, 62ch measure) defined further up this file; the rule colour is
   retuned for the dark ground below.

   Live coverage, measured by read-only inventory on 2026-09-05 (exact
   class match, not substring):

     tdp-svc-hero          10 live occurrences - the shared hero
     tdp-svc-honest         2 - paid-advertising, ai-integration-automation
     tdp-about-team         1
     tdp-work-approach      1
     tdp-svc-editability    0 - its page is Elementor on live today
     tdp-svc-why            0 - its page is Elementor on live today

   The last two are intentionally retained. They are real classes in
   `deployment/content/`, currently sitting on a page production still
   serves through Elementor. They are inert until that page is migrated to
   blocks in Phase 2, at which point they take effect with no CSS change.

   Note that live's services hub uses `tdp-svc-why-hub`, a different class
   from `tdp-svc-why`, and it is NOT in this group: it already takes the
   surface from its own background attribute, and soft is the right
   treatment for it in that page's sequence.
   Aliases that do not exist on a given page are simply inert, which also
   makes the group safe against the local/production content drift recorded
   in docs/whole-site-colour-audit.md.
   ──────────────────────────────────────────────────────────────────── */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) {
	position: relative;
	overflow: hidden;
	/* `!important` is load-bearing here, and it is the one place in this file
	   that needs it.

	   Some of these sections carry a native background attribute in their
	   stored content - the Contact hero, for instance, has
	   `has-primary-soft-background-color`. WordPress emits every
	   `.has-*-background-color` preset class with `!important`, so without a
	   matching `!important` the ground stays light while the text in this
	   tier turns `on-dark`. That produced near-white text on a near-white
	   hero, measured at 1.03:1, which is invisible rather than merely
	   low-contrast.

	   Specificity cannot win this - only `!important` beats `!important`.
	   It is scoped to the dark group alone, so no other section is affected.
	   When the aliases are retired during the planned content refactor, the
	   attribute should be removed from the markup at the same time and this
	   `!important` can go with it. */
	background-color: var(--wp--preset--color--heading) !important;
}

/* Same hairline of light the brand band uses, for the same reason: it
   separates one full-bleed ground from the section above it without
   putting decoration inside a section carrying a single message. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach)::before {
	content: "";
	position: absolute;
	inset: 0 0 auto;
	height: 1px;
	background-color: rgba(255, 255, 255, 0.16);
	pointer-events: none;
}

/* Type on dark. Headings take the full-strength value; body and lists take
   it too, because at 15:1 there is no reason to spend contrast on prose.
   Secondary copy steps down to the muted token, which still clears AA. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(h1, h2, h3, h4, p, li, strong) {
	color: var(--wp--preset--color--on-dark);
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(.tdp-eyebrow, .tdp-muted, figcaption, .tdp-hero__aside-foot) {
	color: var(--wp--preset--color--on-dark-muted);
}

/* Inline links and accents. `brand` is unusable here (3.88:1), so the
   lifted accent carries every small accent role on a dark ground. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) a:not(.wp-block-button__link) {
	color: var(--wp--preset--color--brand-lift);
	text-underline-offset: 0.2em;
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) a:not(.wp-block-button__link):hover {
	color: var(--wp--preset--color--on-dark);
}

/* Raised surfaces: cards, chips and process steps sitting on the dark
   ground need their own surface and hairline, or they dissolve into it. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(.tdp-card, .tdp-chip, .tdp-process-step, .tdp-faq-item, .tdp-svc-card) {
	background-color: var(--wp--preset--color--dark-soft);
	border-color: var(--wp--preset--color--dark-line);
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(.tdp-card, .tdp-chip, .tdp-process-step, .tdp-svc-card) :is(h2, h3, h4, p, li) {
	color: var(--wp--preset--color--on-dark);
}

/* Dividers and rules follow the hairline token rather than the light-tier
   border, which is invisible here. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(hr, .wp-block-separator) {
	border-color: var(--wp--preset--color--dark-line);
	color: var(--wp--preset--color--dark-line);
}

/* A primary-coloured focus ring is not reliably visible on this ground, so
   it swaps to the base token - the same substitution the brand band makes. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :focus-visible {
	outline-color: var(--wp--preset--color--base);
}

/* The two callout sections carry a light-tier left rule (primary-subtle) and
   a 62ch measure from their aside treatment further up this file. On the dark
   ground that rule is invisible, so it steps up to the lifted accent. The
   measure is deliberately kept: these sections stay a focused statement
   rather than becoming a full-width wall of navy text. */
:is(.tdp-svc-honest, .tdp-svc-why) {
	border-left-color: var(--wp--preset--color--brand-lift);
}

/* Buttons on dark. The primary action keeps the brand fill with white text
   (4.51:1, AA) so the conversion path stays one recognisable colour across
   light and dark sections. The secondary action is a transparent ghost with
   an on-dark label and hairline, which clears AA at 15:1 rather than relying
   on a tinted fill. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) .wp-block-button:not(.is-style-outline) .wp-block-button__link {
	background-color: var(--wp--preset--color--brand);
	color: var(--wp--preset--color--primary-foreground);
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) .wp-block-button:not(.is-style-outline) .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--brand-lift);
	color: var(--wp--preset--color--heading);
	transform: translateY(-2px);
	box-shadow: var(--tdp-shadow-lg);
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) .wp-block-button.is-style-outline .wp-block-button__link {
	background-color: transparent;
	color: var(--wp--preset--color--on-dark);
	border-color: var(--wp--preset--color--dark-line);
}

:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) .wp-block-button.is-style-outline .wp-block-button__link:hover {
	background-color: var(--wp--preset--color--dark-soft);
	color: var(--wp--preset--color--on-dark);
	border-color: var(--wp--preset--color--on-dark-muted);
	transform: translateY(-2px);
}

/* Images on a dark ground: the light-tier frame reads as a bright rectangle
   here, so the frame follows the hairline instead. */
:is(.tdp-section--dark, .tdp-svc-hero, .tdp-svc-honest, .tdp-svc-editability, .tdp-svc-why, .tdp-about-team, .tdp-work-approach) :is(.tdp-figure, .tdp-section-image) img {
	border-color: var(--wp--preset--color--dark-line);
}

/* Footer link lists.

   The generic accent rule above is right for a link inside prose, where the
   colour is what marks it as a link. It is wrong for the footer, which
   carries roughly twenty links in three columns: at accent blue the whole
   block reads as noise and nothing in it has priority. They take the calm
   on-dark value instead, and spend the accent on hover, where it actually
   communicates something.

   The bullets and 40px indent come from the underlying core list blocks and
   predate the dark tier. They are removed here because a bulleted, indented
   list is a reading list, not a navigation column. */
.tdp-footer.tdp-section--dark a:not(.wp-block-button__link) {
	color: var(--wp--preset--color--on-dark);
	text-decoration: none;
}

.tdp-footer.tdp-section--dark a:not(.wp-block-button__link):hover {
	color: var(--wp--preset--color--brand-lift);
	text-decoration: underline;
	text-underline-offset: 0.2em;
}

.tdp-footer ul {
	list-style: none;
	padding-left: 0;
	margin-left: 0;
}

/* Hero on dark.

   The hero carries several rules written for the light tier whose
   specificity beats the generic tier selectors above, so each is
   overridden explicitly rather than left to source order:
     .tdp-hero              sets the primary-soft tint  (0,1,0)
     .tdp-hero .tdp-lede    scopes the lede             (0,2,0)
     .tdp-hero__list li     sets body text              (0,2,0)
   Leaving any of these to cascade order would put dark text on a dark
   ground the moment the file is reordered. */
.tdp-hero.tdp-section--dark {
	background-color: var(--wp--preset--color--heading);
	border-bottom-color: var(--wp--preset--color--dark-line);
}

/* A soft brand wash lifting the corner the H1 sits in. Same technique and
   same restraint as the contact closing band: a z-index:-1 pseudo-element,
   so it can never sit over text, and no image or script to load. ::before
   is already the tier's top hairline, so this takes ::after. */
.tdp-hero.tdp-section--dark::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: radial-gradient(62% 130% at 12% 0%, rgba(0, 123, 199, 0.28), transparent 62%);
	pointer-events: none;
}

.tdp-hero.tdp-section--dark {
	isolation: isolate;
}

/* The aside stops being a white card and becomes a raised dark surface. The
   shadow goes: elevation on a dark ground reads as dirt, not lift. */
.tdp-section--dark .tdp-hero__aside {
	background-color: var(--wp--preset--color--dark-soft);
	border-color: var(--wp--preset--color--dark-line);
	box-shadow: none;
}

.tdp-section--dark .tdp-hero__aside-label {
	color: var(--wp--preset--color--brand-lift);
}

.tdp-section--dark .tdp-hero__list {
	color: var(--wp--preset--color--brand-lift);
}

.tdp-section--dark .tdp-hero__list li {
	color: var(--wp--preset--color--on-dark);
}

.tdp-section--dark .tdp-hero__aside-foot {
	border-top-color: var(--wp--preset--color--dark-line);
}

/* Why The DEV Point on dark. The accent rule above each heading and the
   column dividers both use light-tier values that disappear on the dark
   ground: `primary` measures 3.4:1 here and `border` is invisible. */
.tdp-section--dark .tdp-why__grid h3::before {
	background-color: var(--wp--preset--color--brand-lift);
}

@media (min-width: 782px) {
	.tdp-section--dark .tdp-why__grid > .wp-block-column + .wp-block-column {
		border-left-color: var(--wp--preset--color--dark-line);
	}
}

/* Footer on dark. This template part renders on every route, so the dark
   ground here is a site-wide change, not a homepage one - that is the
   intent, since a footer that changed tone per page would read as a bug.

   Both borders and the column headings use light-tier values that fail on
   this ground: `border-light` is invisible and `muted` measures 2.94:1. */
.tdp-footer.tdp-section--dark,
.tdp-footer.tdp-section--dark .tdp-footer-copyright {
	border-top-color: var(--wp--preset--color--dark-line);
}

.tdp-section--dark.tdp-footer h3 {
	color: var(--wp--preset--color--on-dark-muted);
}

/* The global reduced-motion block near the top of this file flattens
   `.tdp-reveal` itself. These are the properties this section introduced on
   its descendants, neutralised explicitly rather than trusting that earlier
   block to reach selectors written after it. */
@media (prefers-reduced-motion: reduce) {
	.tdp-motion-ready .tdp-reveal :is(.wp-block-column, .wp-block-post-template > li),
	.tdp-motion-ready .tdp-reveal :is(.tdp-figure, .tdp-section-image) img {
		opacity: 1 !important;
		transform: none !important;
		transition: none !important;
		transition-delay: 0ms !important;
	}

	.tdp-card:hover,
	.tdp-chip:hover,
	.tdp-process-step:hover,
	.tdp-header__cta .wp-block-button__link:hover,
	.tdp-section--brand .wp-block-button__link:hover,
	.tdp-contact-close .wp-block-button__link:hover,
	.tdp-section--dark .wp-block-button__link:hover {
		transform: none !important;
	}

	.tdp-header {
		transition: none;
	}
}
