/**
 * Component styles.
 *
 * Tokens live in theme.json — reference them via var(--wp--preset--*) and
 * var(--wp--custom--*). No hardcoded hex below: if a colour isn't in
 * theme.json, it belongs in theme.json, not here.
 *
 * ⚠ COLOUR DISCIPLINE — READ THIS BEFORE CHANGING ANY RED.
 * Values current as of 2026-07-18. Comments elsewhere in this file may still
 * quote #B30000 (the pre-dark-rebuild fill) or #FF7070 (a one-day-only value
 * that was too pink — Toby: "it looks a bit too pink to me"). BOTH ARE STALE.
 * theme.json is the only source of truth; these two are what it currently says:
 *
 *   --brand-red (#FF0000) — the logo red. 4.89:1 on surface. Logo, LARGE
 *   display type (>=24px / >=18.66px bold), decorative fills and rules only.
 *   Never body text.
 *
 *   --red-text (#FF3B2A) — a saturated scarlet, and the important property is
 *   that it measures 5.50:1 against surface in BOTH directions. That is why
 *   one token can safely do two opposite jobs: as TEXT on the dark page, and
 *   as a BACKGROUND FILL with a dark (--surface) label on it. Contrast is
 *   symmetric, so both directions are the same number and both clear AA.
 *
 * Why this red and not a lighter one: a red light enough to be "safe" on dark
 * goes pink, because lightening a pure red desaturates it toward white
 * (#FF7070 has G and B equal — that is literally pink). Staying saturated and
 * shifting fractionally toward orange keeps it reading as RED while still
 * clearing 5.5:1. Don't lighten this to buy contrast; it does not need it.
 *
 * The obvious #FF0000 button is still the broken one. Don't "fix" a button by
 * swapping it to the brand red.
 */

/* ---------------------------------------------------------------
   Document shell
   --------------------------------------------------------------- */

/*
 * KILL CORE'S FLOW blockGap AT EVERY FULL-BLEED BOUNDARY. Three levels, and
 * missing any one of them prints a visible bar of page background.
 *
 * Core emits exactly this, from core-block-supports-inline-css:
 *     :root :where(.is-layout-flow) > :first-child { margin-block-start: 0 }
 *     :root :where(.is-layout-flow) > *            { margin-block-start: 1.5rem }
 * Every group this theme renders is is-layout-flow, so EVERY non-first child of
 * EVERY group gets 24px of leading margin unless something says otherwise.
 *
 * An earlier pass fixed only `.wp-site-blocks > *` — the outermost level — and
 * called it done. It wasn't; the same rule was doing damage two levels deeper,
 * and both of those shipped to staging where Toby found them on a phone:
 *
 *  1. .pob-main > *  — every section is a full-bleed band with its own padding,
 *     so they must butt. With 24px between them, the page background printed a
 *     WHITE BAR between the photo hero and the grey band under it, and another
 *     above the ink footer. Only visible where two differently-coloured bands
 *     meet, which is why it survived a desktop look at the top of the page.
 *
 *  2. .pob-hero > *  — the hero's children are __media, __scrim and __inner.
 *     __media is :first-child so it got 0; __scrim is the SECOND child so it got
 *     24px. Both are position:absolute with inset:0 — and for an abspos box with
 *     top:0, bottom:0 and height:auto, margins are NOT ignored, they resolve
 *     into the equation. So the scrim alone was pushed down 24px, and the top
 *     24px of every photo hero rendered unscrimmed: a bright raw strip of sky
 *     above the darkened rest of the image. It reads exactly like a broken
 *     gradient or a mispositioned header and is neither.
 *
 * Specificity: core's general rule is (0,1,0) — `:root` plus a :where() that
 * contributes nothing. The selectors below are also (0,1,0), so this only wins
 * on source order (theme.css is enqueued after core-block-supports). That tie
 * is too close for something this load-bearing, and a plugin that inlines or
 * reorders CSS would silently reintroduce all of the above. `:root` takes these
 * to (0,2,0) and settles it on specificity instead, matching core's own idiom.
 */
:root .wp-site-blocks > *,
:root .pob-main > *,
:root .pob-hero > * {
	margin-block-start: 0;
}

/* ---------------------------------------------------------------
   Header — floats over the hero, solid once past it
   --------------------------------------------------------------- */

/*
 * SHRINK ON SCROLL — one variable, because the header has three sizes and they
 * must not drift apart.
 *
 * .is-stuck already existed and already meant "the hero is behind us" (app.js
 * observes the hero itself). It only changed COLOUR — the header stayed exactly
 * as tall scrolled as it was at rest, which is the note from Toby: shrink it.
 *
 * So one scale token drives all three dimensions rather than three hand-tuned
 * numbers that a later edit would desynchronise:
 *   - the logo (both variants — see the note below on why they cannot drift),
 *   - the row's min-height,
 *   - the block padding.
 *
 * ⚠ IT IS A SCALE, NOT AN OVERRIDE, AND THAT IS DELIBERATE. The obvious version
 * is `.pob-header.is-stuck { --wp--custom--header--logo-width: 128px }`. That
 * breaks mobile silently: the max-600px query below rebases the logo to 112px at
 * (0,1,0), and a .pob-header.is-stuck rule is (0,2,0) — so it would WIN, and a
 * scrolled phone header would snap its logo back UP from 112 to 128 and blow the
 * 320px measurement that query exists to protect. Multiplying the token leaves
 * whatever base is in force alone and scales it, so desktop and mobile both
 * shrink by the same proportion from their own correct starting size.
 *
 * 0.78 is chosen against the numbers, not by feel: 76px row + 32px padding =
 * 108px at rest, going to 59 + 25 = 84px stuck. ~22% of vertical space back on
 * a phone, while the 160px logo lands at 125px — still above the ~112px the
 * mobile query proves the wordmark stays legible at.
 *
 * The height TOKEN is untouched on purpose: it is the at-rest height, and the
 * hero's top padding and app.js's observer offset are both measured from it.
 * Shrinking the token would move the hero's content under a header that is only
 * short after you have already scrolled past the hero.
 */
.pob-header {
	--pob-header-scale: 1;

	padding-block: calc(var(--wp--preset--spacing--30) * var(--pob-header-scale));
	padding-inline: var(--wp--custom--gutter);
	transition: padding-block 0.3s var(--wp--custom--ease--out, ease);
}

.pob-header.is-stuck {
	--pob-header-scale: 0.78;
}

/*
 * FULL WIDTH, THREE ZONES. Toby, 2026-07-18: "make the header full width so
 * the logo sits right in the left hand corner and the contact phone number
 * over on the right and the menu in the middle... Make it use the width of
 * the page."
 *
 * The max-width cap that used to be here pulled the whole row into the 1320px
 * measure, so on a wide screen the logo floated well inside the corner. Gone;
 * the row now spans the viewport and .pob-header's own padding-inline is what
 * keeps it off the edge.
 *
 * The centring is done by making the two OUTER zones equal-flex and the nav
 * intrinsic (see the three rules below). `justify-content: space-between`
 * alone would only centre the nav when the brand and the CTA happened to be
 * the same width — which they are not, and which would drift the moment the
 * phone number or the logo changed size.
 */
.pob-header__inner {
	min-height: calc(var(--wp--custom--header--height) * var(--pob-header-scale));
	transition: min-height 0.3s var(--wp--custom--ease--out, ease);
	gap: var(--wp--preset--spacing--40);
}

/* Equal-flex outer zones + intrinsic centre = the nav sits on the page's
   true centre line regardless of what flanks it.

   ⚠ NO `min-width: 0` HERE, deliberately. The default `min-width: auto` is
   what stops a flex item shrinking below its own content, and that is the
   only thing protecting the logo — see the note on .pob-header__brand below
   for the bug that produced (it went to width:0 and the nav rendered straight
   over the top of it). These zones may grow to balance the row; they may not
   shrink past what is inside them. */
.pob-header__brand,
.pob-header__right {
	flex: 1 1 0;
}

.pob-header__nav {
	flex: 0 0 auto;
}

.pob-header__right {
	justify-content: flex-end;
}

/*
 * Reduced motion: the header still shrinks, it just does not animate getting
 * there. Same principle as the mega panel — less movement, not less header.
 * Declared here rather than leaning on motion.css's `.pob-header{transition:none}`
 * because that rule only covers the properties motion.css itself sets
 * (background-color, box-shadow) and would not reach the logo or the row.
 */
@media (prefers-reduced-motion: reduce) {
	.pob-header,
	.pob-header__inner,
	.pob-header__logo img {
		transition: none;
	}
}

/*
 * The logo's width is set HERE, from a token, and both variants read it.
 *
 * This used to be core/site-logo, which brought two problems. The first was
 * sizing: core renders <div class="wp-block-site-logo"><a><img></a></div> and
 * styles the image width:100%, while the <a> is an inline-block that
 * shrink-wraps to its content — an image asking for 100% of the <a>. That
 * circular dependency resolved to ZERO, .pob-header__brand computed to width:0,
 * the flex header collapsed, and the absolutely-positioned reversed variant kept
 * painting on top: a header that showed a logo while behaving as though it had
 * none, nav rendering straight over it. The explicit width below fixed that.
 *
 * The second problem was worse and the width couldn't fix it — site-logo renders
 * NOTHING when the site_logo option is unset, which is what shipped to staging.
 * See patterns/header-logo.php. The block is gone; the img is ours now.
 *
 * Both variants size from --wp--custom--header--logo-width and nothing else, so
 * they cannot drift apart. They are stacked on each other, where any mismatch
 * shows as a jump the moment the header goes solid.
 */
.pob-header__logo a {
	display: block;
}

/*
 * ⚠ SIZED BY HEIGHT, NOT WIDTH — 2026-07-17. Do not switch this back to
 * `width: <logo-width>; height: auto`.
 *
 * The artwork is 430x221, so width-driven sizing made the logo
 * width x (221/430) tall: at the old 160px width that is 82px, inside a
 * --header--height row of 76px. The logo was therefore ALWAYS taller than the
 * row it sits in, which is why it pushed the header to 114px and still looked
 * like it was hanging out of the sticky one. Height-driven sizing makes the
 * row's own height the constraint, so the logo can never overflow it no
 * matter what the artwork's aspect ratio is.
 *
 * --pob-header-scale shrinks it in place on scroll; the flex row's
 * align-items:center keeps it optically anchored while it does.
 */
.pob-header__logo img {
	display: block;
	width: auto;
	height: calc(var(--wp--custom--header--logo-height) * var(--pob-header-scale, 1));
	transition: height 0.3s var(--wp--custom--ease--out, ease);
}

/*
 * ⚠ THE TWO-LOGO STACK IS GONE, 2026-07-17 — do not reintroduce it without
 * reading parts/header.html's note first.
 *
 * There used to be a second, absolutely-positioned .pob-header__logo-reversed
 * layered on this one and cross-faded by scroll state. It caused a real
 * measured bug (WP flow-layout blockGap put a 24px top margin on it, which
 * shifts an absolutely-positioned box even with inset-block-start: 0) and it
 * had nothing left to swap between once every header state went dark.
 * header-logo.php now serves the light artwork directly, as the only child.
 *
 * ⚠ THE LOGO MUST NEVER BE ALLOWED TO SHRINK. This used to say `flex: none`;
 * it is now `flex: 1 1 0` (set above, for the three-zone centring) and the
 * protection comes from the default `min-width: auto` instead. The rule being
 * protected against is the same either way, and it is worth keeping:
 *
 * .pob-header__inner is flex/nowrap. Once the nav grew to eight items the
 * right-hand group measured 874px inside an 843px row, and flex resolved the
 * overflow by shrinking the only thing that would give: the logo. It went to
 * width:0.
 *
 * The failure was invisible in the worst way. At the time a second logo
 * variant was stacked on top at position:absolute, so it was out of flow and
 * kept painting at full size — the header LOOKED like it had a logo while the
 * layout behaved as though it had nothing, and the nav rendered straight over
 * the top of it. It read as a z-index or a float bug and was neither.
 *
 * A logo is not a thing that should negotiate for space. The overflow it used
 * to absorb is handled honestly, by the nav collapsing to the overlay — see
 * the breakpoint below.
 */
.pob-header__brand {
	position: relative;
	line-height: 0;
}

.pob-header__right {
	gap: var(--wp--preset--spacing--40);
}

/* The phone number must never wrap — a broken-up number is unreadable and
   unusable, and it's the primary conversion action on the site. */
.pob-header__cta .wp-block-button__link {
	white-space: nowrap;
}

/*
 * Move core/navigation's overlay breakpoint from 600px up to 1200px.
 *
 * The block's overlayMenu:"mobile" hands off to the hamburger at core's fixed
 * 600px, which is simply wrong for this header: eight nav items measure 679px,
 * the logo is 160px and the phone CTA ~180px, so the row needs ~1070px of inner
 * width before it fits. Inner width is min(viewport - 2x gutter, 1320px), and
 * the gutter is itself clamp(20px, 5vw, 72px) — so the row only genuinely fits
 * from about 1200px up. Between 600 and 1200 core was rendering a horizontal
 * nav that did not fit, and something had to give (it was the logo).
 *
 * Core has no attribute for this, so the two rules its own 600px media query
 * sets are inverted back here for the gap. Scoped to .pob-header__links so the
 * footer navigation, which has no such constraint, is untouched.
 *
 * 1200px is measured, not chosen for tidiness — it is where the content stops
 * fitting. Shrink the nav (fewer items, smaller gap) and this number should
 * come down with it.
 */
@media (min-width: 600px) and (max-width: 1199px) {
	.pob-header__links .wp-block-navigation__responsive-container-open:not(.always-shown) {
		display: flex;
	}

	.pob-header__links .wp-block-navigation__responsive-container:not(.hidden-by-default):not(.is-menu-open) {
		display: none;
	}
}

.pob-header__links a {
	color: var(--wp--preset--color--ink);
	font-weight: 600;
	text-decoration: none;
}

/*
 * The mobile overlay menu.
 *
 * THE COMMENT THAT USED TO BE HERE SAID "the overlay is a dark full-screen
 * panel". IT IS NOT, AND THE WHOLE MENU WAS INVISIBLE BECAUSE OF IT. Core:
 *   .wp-block-navigation:not(.has-background)
 *     .wp-block-navigation__responsive-container.is-menu-open
 *     :not(.disable-default-overlay) { background-color: #fff }
 * The panel is WHITE. Nothing here sets a nav background, so :not(.has-background)
 * matches and always will.
 *
 * That made the over-photo nav rule in motion.css —
 *   .has-hero-media .pob-header:not(.is-stuck) .pob-header__links a { color: white }
 * — apply to the anchors INSIDE the open overlay too. It is (0,4,1); the ink
 * rule that used to be here was (0,3,1) and lost. Result: white links on a white
 * panel. The menu opened, the close button worked, the links were there and
 * focusable, and a screen reader read them out perfectly. It was simply
 * invisible, on exactly the pages with a photo hero (i.e. nearly all of them)
 * and only at scroll-top, which is precisely when you reach for the hamburger.
 *
 * Fixed in two places, deliberately:
 *  - motion.css now scopes that rule to :not(.is-menu-open), because "the header
 *    is floating over a photo" was never true of a fixed full-screen modal. That
 *    is the actual defect.
 *  - and here, the panel states its OWN background and text colour rather than
 *    inheriting core's #fff/#000 defaults. Two reasons: those defaults evaporate
 *    the moment anyone sets a nav background in the editor (the :not(.has-background)
 *    guard stops matching), and a modal that names its own colours cannot be
 *    recoloured by an ancestor's scroll state ever again.
 *
 * Both, not either. The scoping is the fix; this is the guard rail that keeps a
 * future header state from re-breaking it.
 */
.pob-header__links .wp-block-navigation__responsive-container.is-menu-open {
	background-color: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--ink);
}

/*
 * (0,5,1) — one class more than motion.css's (0,4,1) over-photo rule. The
 * :not(.is-menu-open) scoping there means this shouldn't have to fight it any
 * more, but the two files can be loaded in either order by an optimiser and
 * this is the one that must not lose.
 */
.has-hero-media .pob-header .pob-header__links .wp-block-navigation__responsive-container.is-menu-open a,
.pob-header__links .wp-block-navigation__responsive-container.is-menu-open a {
	color: var(--wp--preset--color--ink);
}

/* The close button inherits currentColor from the container, which the rule
   above now sets to ink. Stated anyway: it is the only way back out. */
.pob-header__links .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-close {
	color: var(--wp--preset--color--ink);
}

/* ⚠ DARK REBUILD, 2026-07-17 — this used to flip the hamburger to `surface`
   (white, pre-flip) while over the photo hero, then back to the inherited
   ink once the header went solid over what was then a white page. `surface`
   is now near-black, so this was setting the icon to dark-on-dark exactly
   over the one background it's guaranteed to be dark. Every header state is
   dark now (see the .pob-header__logo note above), so ink is simply always
   correct — no conditional needed. */
.wp-block-navigation__responsive-container-open {
	color: var(--wp--preset--color--ink);
}

.pob-header__links a:hover,
.pob-header__links a:focus-visible {
	color: var(--wp--preset--color--red-text);
}

/* The fixed header sits over content; push the page down to compensate.
   Views with a hero opt out — the hero sits under the header by design and
   pays for the clearance itself. */
body:not(.has-hero) .pob-main {
	padding-block-start: var(--wp--custom--header--height);
}

/*
 * The phone CTA stays on mobile, unlike the Just Lets header this is modelled
 * on. Tap-to-call is the primary conversion action for a trades business, and
 * mobile is where that happens — hiding it there would remove the best button
 * on the site from the visitors most likely to press it.
 *
 * (The inherited `display:none` here was dead anyway — core's
 * .wp-block-buttons-is-layout-flex{display:flex} ties on specificity and wins
 * on source order. It was never hiding anything.)
 *
 * This block used to claim "Verified down to 320px: logo, menu toggle and CTA
 * fit with no horizontal overflow." That verification was true and worthless:
 * it was carried out while .pob-header__brand was computing to width:0 (see the
 * flex:none note above). Of course three items fit when one of them is zero
 * pixels wide. With a real 160px logo the header measured 399px of content into
 * 320px of space at 360px viewport, and the phone number — the best button on
 * the site — was clipped off the right edge.
 *
 * So the parts are sized to the space rather than the claim re-asserted. At
 * 320px, the narrowest viewport worth supporting: 112 logo + 16 + 24 hamburger
 * + 16 + ~117 CTA = 285px inside 288px of inner width. Re-measured, not
 * reasoned about.
 *
 * The logo size is overridden through the custom property rather than on the
 * img rule, so anything else reading the token stays in step with it.
 */
@media (max-width: 600px) {
	.pob-header {
		--wp--custom--header--logo-height: 46px;

		padding-inline: 16px;
	}

	.pob-header__right {
		gap: var(--wp--preset--spacing--30);
	}

	.pob-header__cta .wp-block-button__link {
		font-size: 0.8125rem;
		padding-inline: 0.85rem;
	}
}

/* ---------------------------------------------------------------
   Hero — type-led, and built to accept a photograph later
   --------------------------------------------------------------- */

/*
 * There is no hero-capable photography for this site (PLAN §5b: everything is
 * <=2279px, mostly 1170px WhatsApp compression). So the hero carries itself on
 * typographic scale and the logo's own red roof geometry instead of an image.
 *
 * It is deliberately NOT a photo-less design — it is a photo-ready design with
 * no photo in it yet. The __media and __scrim layers below are the reference's
 * full-bleed-photo-plus-gradient shell, present and wired up but currently
 * empty. Dropping a shoot in later means setting a background-image on __media
 * and adding .has-media — no redesign, no re-layout, no template change.
 */

.pob-hero {
	position: relative;
	isolation: isolate;
	overflow: hidden;
	display: flex;
	align-items: center;
	min-height: var(--wp--custom--hero--min-height);
	/* Clear the fixed header, which floats over this. */
	padding-block-start: calc(var(--wp--custom--header--height) + var(--wp--preset--spacing--60));
	padding-block-end: var(--wp--preset--spacing--60);
	padding-inline: var(--wp--custom--gutter);
	background-color: var(--wp--preset--color--surface);
}

/* Empty until a photo exists. Set a background-image here + add .has-media. */
.pob-hero__media {
	position: absolute;
	inset: 0;
	z-index: -2;
	background-size: cover;
	background-position: center;
}

/*
 * The reference's gradient scrim. Transparent while there's no photo under it,
 * so the hero reads as clean white rather than a grey wash over nothing.
 */
.pob-hero__scrim {
	position: absolute;
	inset: 0;
	z-index: -1;
}

.pob-hero__inner {
	max-width: var(--wp--style--global--wide-size);
	margin-inline: auto;
	width: 100%;
}

/*
 * The roof, at page scale.
 *
 * ⚠ .pob-hero__roof IS GONE, 2026-07-18 — the red chevron that used to sit
 * above the headline. It read as "the brand's own geometry doing the work"
 * back when the hero was type-led on white with no photograph and no logo
 * above it. Once a photo landed behind it and the header put the same chevron
 * a few dozen pixels higher, it was the mark twice on one screen — and it was
 * the single tallest item in the hero stack, which is what pushed the content
 * past the fold on desktop.
 *
 * It was then briefly relocated as a faded watermark behind the headline, and
 * that was cut too (see the note further down) — this hero carries no roof
 * graphic in any form now, the photograph is its image. The old rules (this
 * one, its mobile display:none, and the pob-roof-raise keyframe in motion.css)
 * are removed rather than left dangling because nothing renders it any more.
 */

.pob-hero__title {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--hero);
	font-weight: 800;
	font-stretch: var(--wp--custom--display--stretch);
	line-height: 0.92;
	letter-spacing: -0.03em;
	text-transform: uppercase;
	text-wrap: balance;
	margin: 0;
	color: var(--wp--preset--color--ink);
}

/* The rule under the title echoes the logo's red bar. Decorative. */
.pob-hero__rule {
	width: var(--wp--custom--hero--roof-width);
	max-width: 100%;
	height: var(--wp--custom--rule--thick);
	border: 0;
	background: var(--wp--preset--color--brand-red);
	margin: var(--wp--preset--spacing--40) 0;
}

.pob-hero__lead {
	color: var(--wp--preset--color--ink);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 500;
	max-width: 34ch;
	margin: 0 0 var(--wp--preset--spacing--30);
	text-wrap: balance;
}

.pob-hero__meta {
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	text-transform: uppercase;
	letter-spacing: 0.12em;
	font-weight: 600;
	max-width: 46ch;
	margin: 0;
}

.pob-hero__actions {
	margin-block-start: var(--wp--preset--spacing--50);
}

/*
 * Photo state — the whole point of the shell above.
 *
 * Nothing here is speculative styling for a hypothetical: it's the switch that
 * makes the type-led hero reversible. Ink-on-white flips to white-on-scrim,
 * and the header's nav flips with it (below), because a light-on-light header
 * over a dark photo is unreadable.
 */
/* Tokenised, because this is the exact value someone will want to tune the
   day a real photograph lands and the scrim turns out too heavy or too light
   for it. One place, in theme.json, not buried in a component rule. */
.pob-hero.has-media .pob-hero__scrim {
	background: var(--wp--custom--hero--scrim);
}

/*
 * ⚠ DARK REBUILD, 2026-07-17 — every `color` in this `.has-media` block used
 * to read `surface`, back when surface was white: text on a photo needs to be
 * light, and white was the ink-inverting token available. `surface` is now
 * near-black (#0A0E12), so all five of these had inverted into setting hero
 * text/border to near-black against a scrim (theme.json's `--hero--scrim`)
 * that is ALSO near-black — Toby: "lots of text not visible". `ink` is the
 * light token post-flip; it is what these all should read now. Verified:
 * ink-on-surface is 17.7:1 (theme.css note above / theme.json).
 */
.pob-hero.has-media .pob-hero__title {
	color: var(--wp--preset--color--ink);
}

.pob-hero.has-media .pob-hero__meta,
.pob-hero.has-media .pob-hero__lead {
	color: var(--wp--preset--color--ink);
}

.pob-hero.has-media .pob-hero__meta {
	opacity: 0.82;
}

.pob-hero.has-media .pob-pill {
	color: var(--wp--preset--color--ink);
	border-color: var(--wp--custom--hero--line-on-media);
}

/* The outline CTA draws itself in ink on the (now dark) surface by default;
   over a photo scrim it still needs to read light, so it stays on ink here
   too rather than flipping to the now-dark surface token. */
.pob-hero.has-media .wp-block-button.is-style-outline .wp-block-button__link {
	color: var(--wp--preset--color--ink);
	border-color: var(--wp--preset--color--ink);
}

/* ---------------------------------------------------------------
   Hero photography — the client's own work, nothing licensed in
   --------------------------------------------------------------- */

/*
 * PLAN.md §5b said flatly: "Paul O'Brien Roofing has no hero-capable
 * photography. Anywhere. Everything is <=2279px." That is true of the ROOFING
 * photography and false of the developments set — 03-media-library/projects/
 * developments/ and the Joomla images/ root hold native 12MP iPhone frames
 * (IMG_0258 and IMG_1037 are both 4032x3024, 179-189AshtonDriveBristol is
 * 3264x2448). Measured with pillow, not assumed. So the two biggest heroes on
 * the site are real, full-resolution, and the client's own completed work.
 *
 * Every image below is POB's own, from their own Joomla site or the curated
 * library. Two sources are deliberately NOT here and must never be added:
 *   - general/solar-page-stock-photos/*-utc.jpg — Envato Elements, licensed for
 *     a 2023 brochure only. Non-transferable; a website is a new end product.
 *     This is why the Solar hero below has no photograph.
 *   - gallery-showcase/showcase01..12.jpg + portfolio-banner.png — Joomla
 *     template demo stock (a puppy, a chameleon, a chess set, two BMWs). They
 *     were mis-filed as "SP Simple Portfolio source images"; none of the 25 real
 *     portfolio rows reference them.
 *
 * Declared here rather than inline in the templates on purpose: the templates
 * are .html and cannot resolve the theme URI, so an inline background-image
 * would have to hardcode /wp-content/themes/pobroofing/. A stylesheet-relative
 * url() has no such dependency, and it keeps "swap the photo after a shoot" a
 * one-line edit in one file.
 */

.pob-hero--home .pob-hero__media {
	/*
	 * ⚠ AI-GENERATED, NOT CLIENT PHOTOGRAPHY. 2816x1536.
	 * A roofer on a fully-tiled Victorian terrace roof setting a final ridge
	 * tile, shot from across the rooftops; chimney stacks receding behind,
	 * scaffold at the edge, moody sky with warm light. Toby's call
	 * 2026-07-16: he supplied a reference frame and asked for the home hero to
	 * match it ("something more like the ai generated one i provided as a
	 * sample"). The reference is preserved at
	 * 05-reference-imagery/client-supplied/hero-ai-SAMPLE-toby-2026-07-16.jpg.
	 *
	 * ⚠ REGENERATED FOUR TIMES, 2026-07-16. THE FIX IS: DO NOT SHOW ROOF
	 * STRUCTURE AT ALL. Read this before regenerating — it will save you three
	 * attempts.
	 *
	 * Toby caught a real construction error in v1: "you don't have battens
	 * going both ways, you have felt, then horizontal battens then the tiles on
	 * top." Correct — a tiled roof is felt, then HORIZONTAL battens only, then
	 * tiles hooked over them. v1 showed a diagonal lattice.
	 *
	 * What did NOT work, twice:
	 *  - v2: prompting explicitly for "horizontal only, like ladder rungs, no
	 *    crossing/diagonal/vertical battens, no lattice, no grid, no mesh".
	 *    Result: a MORE pronounced diagonal lattice. Imagen 4 has a very strong
	 *    prior that "roof under construction" = cross-hatch, and NEGATING a
	 *    thing in the prompt reliably summons it.
	 *  - v3: shrinking the exposed area to a sliver at the ridge. This LOOKED
	 *    fixed in a tight crop and was signed off on that basis — wrongly. Toby
	 *    caught it again: a large timber still ran UP the slope along the hip,
	 *    and the roof was littered with dozens of loose tile stacks. Verifying
	 *    a crop is not verifying the composition.
	 *
	 * v4, which works: the roof is COMPLETELY tiled, so there is no exposed
	 * structure for the model to get wrong, and the prompt never mentions
	 * battens, felt or timber at all. Toby's own steer: "majority of the roof is
	 * tiled so you can't see the timberwork and slightly further away and then
	 * just have it so they're just finishing off like the last tiles". Shot from
	 * further back across the rooftops, one roofer setting a final ridge tile,
	 * one small neat stack beside him.
	 *
	 * ⚠ THE GENERAL RULE, which also applies to the service detail images:
	 *   omit, don't negate. If the model keeps drawing a thing wrong, remove
	 *   the reason for it to be in frame rather than adding another "no X" to
	 *   the prompt.
	 *
	 * WHY THIS IS ALLOWED HERE, AND WHERE THE LINE IS:
	 * This is a decorative CSS background under a scrim — there is no <img>, no
	 * alt text, no caption, and no surrounding copy that attributes it to a job.
	 * It asserts "we are roofers", which is true, and makes no claim about a
	 * specific project. That is the ONLY footing on which an AI image is
	 * acceptable on this site.
	 * ❌ It must NEVER be captioned, alt-texted or presented as POB's completed
	 *    work, and must never appear in the gallery or a case study. Doing so
	 *    would be the same error as Dev Bristol's cropped ©Google Street View
	 *    images (PLAN §5b) — synthetic material passed off as the client's own.
	 *
	 * WHAT IT REPLACED — and this is a real trade-off, not a free upgrade:
	 * The previous hero was **genuine client photography**: The Glassworks,
	 * Nailsea, POB's own completed development, 4032x3024 native. It is NOT
	 * lost — preserved at
	 * 05-reference-imagery/replaced-placeholders/hero-home-GLASSWORKS-real-client-photo.jpg
	 * and restorable by pointing this one url() back at it.
	 * We traded authenticity for on-message: the Glassworks shot is a finished
	 * housing development in flat midday sun with estate-agents' FOR SALE boards
	 * in frame — a *developments* picture leading a *roofing* site, and carrying
	 * a third party's branding. The new hero is about the trade the site sells.
	 * If Paul commissions a real shoot, this is the first thing to replace.
	 */
	background-image: url(../img/hero/hero-home.jpg);
}

.pob-hero--about .pob-hero__media {
	/* The Luckwell Club, Southville — POB's own development. 4032x3024 source. */
	background-image: url(../img/hero/hero-about.jpg);
}

.pob-hero--developments .pob-hero__media {
	/* The Glassworks at dusk. 3110x2073 source. */
	background-image: url(../img/hero/hero-developments.jpg);
}

.pob-hero--gallery .pob-hero__media {
	/* 179-185 Ashton Drive. 3264x2448 source. */
	background-image: url(../img/hero/hero-gallery.jpg);
}

/* ---------------------------------------------------------------
   AI-GENERATED ATMOSPHERIC HEROES — READ THIS BEFORE MOVING ONE
   ---------------------------------------------------------------

   The five rules below are NOT photographs of POB's work. They are
   AI-generated (fal.ai, 2026-07-16, commissioned by Toby), 2400px, and they
   exist because the real roofing photography does not: the files they replaced
   were 640x240 (loft), 816x306 (testimonials) and 2279x854 (roofing) — the
   browser was upscaling a 640px image across a full-bleed band. Sources and
   prompts: 05-reference-imagery/ai-generated/.

   ⚠ THEY ARE DECORATIVE, AND THE DISTINCTION IS THE WHOLE POINT OF PLAN §5b.
   They may sit on SERVICE and UTILITY heroes, which are atmospheric by
   convention — a roofscape behind "Contact" claims nothing. They must NEVER be
   used on Gallery, Case Studies or Developments, or given a caption, alt text
   or adjacent copy that attributes them to POB. That is precisely the failure
   §5b documents on the client's LIVE site: third-party images (Street View with
   the ©Google credit cropped off, a rival's sales particulars) published as the
   client's own development photography. Generating an image instead of taking
   someone else's fixes the copyright half of that problem and not the
   attribution half. An invented roof presented as a job POB completed is still
   a claim POB did not make.

   The genuinely-POB heroes are above (home, about, developments, gallery) and
   are captioned as such. Do not let the two sets drift together.

   Swap any of these for a real shoot and nothing else changes — same rule,
   same file name. That was always the design (see the .has-media note above). */

.pob-hero--roofing .pob-hero__media {
	/* AI-generated. Roofer laying clay tiles on battens, Victorian terrace.
	   The /roofing/ hub page only — the individual service pages each have their
	   own photograph now; see the block below. */
	background-image: url(../img/hero/hero-roofing.jpg);
}

/* ---------------------------------------------------------------
   Per-service heroes — one photograph per service page
   ---------------------------------------------------------------

   ⚠ EVERY SERVICE PAGE USED TO SHOW THE SAME PICTURE. Don't undo this.

   templates/page-service.html is ONE template shared by all eleven service
   pages, and a .html block template cannot vary per post — so it carried
   `pob-hero--roofing` and Tiling & Slating, Lead Work, Flat Roofs, Single Ply,
   UPVC Goods and Industrial Roofing all opened with the identical photograph of
   a man laying clay tiles. On a Lead Work page that is not decoration, it is
   the wrong picture: it shows a different trade to the one the page sells.
   Toby: "the hero image is the same on all of them — it needs to be different
   and relevant to the actual type of work."

   The template now carries `pob-hero--service`, which supplies the DEFAULT
   below, and each page overrides it via the slug body class from
   inc/setup.php's pobroofing_body_classes(). Slug, never post ID — see the note
   there.

   Adding a service page: drop hero-{slug}.jpg in assets/img/hero/ and add one
   rule here. Forget to, and the page still gets the default rather than a hole.

   ⚠ SPECIFICITY: these are (0,2,0), the same as `.pob-hero--service
   .pob-hero__media`, so they win on SOURCE ORDER ONLY. They must stay below the
   default. Do not move the default underneath them.

   All AI-generated, all decorative, all subject to the rule at the top of this
   file: service heroes only, never captioned, never presented as POB's own
   completed jobs. Each one draws the actual trade its page sells. */

.pob-hero--service .pob-hero__media {
	/* Default for any service page without its own file. The general roofing
	   shot — right for a roofing company, wrong for nothing in particular. */
	background-image: url(../img/hero/hero-roofing.jpg);
}

.pob-page-tiling-slating .pob-hero__media {
	/* Roofer laying natural blue-grey slate in courses. */
	background-image: url(../img/hero/hero-tiling-slating.jpg);
}

.pob-page-lead-work .pob-hero__media {
	/* Dressed milled lead over a mop-stick roll + lead valley — the page's own
	   subject, drawn from its own copy. */
	background-image: url(../img/hero/hero-lead-work.jpg);
}

.pob-page-flat-roofs .pob-hero__media {
	/* Felt membrane in bays, welted seams, trim edge, parapet. */
	background-image: url(../img/hero/hero-flat-roofs.jpg);
}

.pob-page-single-ply-roofing .pob-hero__media {
	/* One continuous membrane, hot-air welded seams — single ply's whole point
	   is that it is ONE sheet, same idea as its icon in inc/icons.php. */
	background-image: url(../img/hero/hero-single-ply-roofing.jpg);
}

.pob-page-upvc-goods .pob-hero__media {
	/* Fascia, soffit and rainwater goods at the eaves. */
	background-image: url(../img/hero/hero-upvc-goods.jpg);
}

.pob-page-industrial-roofing .pob-hero__media {
	/* Box-profile cladding on an industrial unit. */
	background-image: url(../img/hero/hero-industrial-roofing.jpg);
}

.pob-page-workmanship .pob-hero__media {
	/* A team on a roof with a supervisor checking the work — which is literally
	   what this page's copy says ("A site supervisor will check the quality of
	   the work"). */
	background-image: url(../img/hero/hero-workmanship.jpg);
}

.pob-page-site-setup .pob-hero__media {
	/* Correctly erected scaffold: guard rails, toe boards, netting. The page is
	   about H&S site setup, so the photograph has to be exemplary rather than
	   merely atmospheric. */
	background-image: url(../img/hero/hero-site-setup.jpg);
}

.pob-page-external-wall-insulation .pob-hero__media {
	/* Render going on over insulation board, half the elevation finished — the
	   retrofit mid-act. */
	background-image: url(../img/hero/hero-external-wall-insulation.jpg);
}

.pob-page-planning-services .pob-hero__media {
	/* A drawing set on a desk. */
	background-image: url(../img/hero/hero-planning-services.jpg);
}

.pob-page-housing-associations .pob-hero__media {
	/* A terrace of social housing with a school beyond — the page's own three
	   audiences (housing associations, schools, councils). */
	background-image: url(../img/hero/hero-housing-associations.jpg);
}

.pob-hero--testimonials .pob-hero__media {
	/* AI-generated. Completed re-roof on an Edwardian house, scaffold striking. */
	background-image: url(../img/hero/hero-testimonials.jpg);
}

.pob-hero--loft .pob-hero__media {
	/* AI-generated. Flat-roof dormer on a slate terrace roof. Replaces the only
	   loft photograph that existed anywhere on the site, which was 640x240. */
	background-image: url(../img/hero/hero-loft.jpg);
}

/*
 * Solar and Contact were type-led BY CHOICE and are now photographic — the
 * reason each was type-led has been answered rather than overridden:
 *
 *  - Solar had no image because every solar photograph on the old site is
 *    Envato Elements stock licensed for a 2023 brochure only (PLAN §5b's
 *    "licensing landmine"). This one is generated, so nothing is licensed in
 *    and that constraint is gone. The Envato files are still off-limits.
 *  - Contact "wanted to be clean, not decorated". A Bristol roofscape at golden
 *    hour behind a contact form is still clean; it was the absence of any usable
 *    image that made the choice, not a preference for white.
 */
.pob-hero--solar .pob-hero__media {
	/* AI-generated. PV panels on the slate roof of a period stone house. */
	background-image: url(../img/hero/hero-solar.jpg);
}

/*
 * ⚠ THE CASE STUDY ARCHIVE HAD `has-media` AND NO IMAGE — A BLACK BAND.
 *
 * archive-case_study.html and taxonomy-case_service.html both ship
 * `pob-hero--case-archive has-media`, but no rule ever set a background for it.
 * `.has-media` is what turns on the scrim (a near-opaque dark gradient) and
 * flips the title to white, so the result was a heavy dark gradient painted
 * over an empty div: a black band where a photograph should be, on the page
 * that showcases the company's work. Toby: "case studies, along with a lot of
 * other pages, don't have a hero image."
 *
 * Not a missing file — a missing rule. The template was always asking.
 *
 * ⚠ THE SINGLES ARE A DIFFERENT MECHANISM AND ARE NOT FIXED HERE.
 * single-case_study.html renders `wp:post-featured-image` INTO .pob-hero__media,
 * so a single case study's hero is that post's own featured image — which is
 * right, and is why `.pob-hero--case` deliberately has no rule. If a single
 * looks empty, the POST has no featured image; that is content, not CSS, and no
 * stylesheet should paper over it with a generic photo, because on a case study
 * a photograph is a claim about that specific job.
 */
.pob-hero--case-archive .pob-hero__media {
	/* AI-generated, decorative. A finished re-roof, scaffold struck. Deliberately
	   a GENERAL roofing scene and never a specific job: this is the index of the
	   real case studies, and each of those carries its own real photograph. */
	background-image: url(../img/hero/hero-case-studies.jpg);
}

.pob-hero--contact .pob-hero__media {
	/* AI-generated. Bristol terraced roofscape, golden hour. */
	background-image: url(../img/hero/hero-contact.jpg);
}

/* ---------------------------------------------------------------
   Page head — the same hero shell, smaller, on every inner page
   --------------------------------------------------------------- */

/*
 * The reference runs one hero formula across every page and only swaps the
 * headline (DESIGN-REFERENCE §4). That repeats here: same shell, same header
 * behaviour, shorter. It's the cheapest way to make a multi-page site feel
 * built rather than assembled.
 */
.pob-pagehead {
	min-height: 0;
	padding-block-start: calc(var(--wp--custom--header--height) + var(--wp--preset--spacing--60));
	padding-block-end: var(--wp--preset--spacing--60);
	background-color: var(--wp--preset--color--surface-alt);
}

.pob-pagehead .pob-hero__title {
	font-size: var(--wp--preset--font-size--display);
}

/* With a photograph behind it the band needs enough height for the image to be
   a photograph rather than a stripe, and the flat surface-alt ground underneath
   it has to go or it prints a grey bar above the image while it loads. */
.pob-pagehead.has-media {
	min-height: var(--wp--custom--pagehead--min-height);
	background-color: var(--wp--preset--color--ink);
}

/*
 * No breadcrumb row yet, deliberately.
 *
 * The reference pairs "HOME / ABOUT" with a headline that differs from the
 * page name. We have no per-page headline copy — the H1 is the page title —
 * so a crumb trail here would print that same title twice within a hundred
 * pixels. Rendering real crumbs also needs either core/breadcrumbs (WP 7.0, on
 * the 20i target but not the 6.9.4 test image, so unverifiable here) or a
 * small dynamic block; a pattern can't do it, because theme pattern PHP is
 * evaluated once at registration with no post in scope. Left out rather than
 * faked.
 */

/* ---------------------------------------------------------------
   Pill badge — the reference's section eyebrow
   --------------------------------------------------------------- */

.pob-pill {
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--20);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.4rem 0.9rem;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.1em;
	color: var(--wp--preset--color--ink);
	line-height: 1;
}

/*
 * The eyebrow's mark is a ROOF, not a dot.
 *
 * It was a red circle — a generic "status dot" that could have come off any
 * SaaS template and said nothing about the business. Toby's note: the dot goes,
 * a roof shape is welcome. So it is the logo's own gable, at 12px.
 *
 * The geometry is not eyeballed. inc/icons.php: the roof chevron in
 * pob-roof.svg has a run of 120.7 and a rise of 54.5 — a ratio of 0.451, a
 * 24.3° pitch — and EVERY pitched line in the icon set is drawn on it. This
 * mark obeys the same rule: --dot--size is the RISE, and the run is that over
 * 0.451 (hence the 2.2 multiplier), so the pill's roof sits at exactly the
 * pitch of the logo above it and of the six service icons below it.
 *
 * clip-path rather than an SVG or a border trick: it is one element, it scales
 * from the token, and it inherits nothing that can drift. Brand red is safe
 * here — this is a decorative fill, not text, so the 3:1 non-text bar applies
 * and #FF0000 clears it at 4.00:1 (see the colour discipline at the top).
 */
.pob-pill::before {
	content: "";
	/* Run = rise / 0.451. Keeps the logo's 9:20 pitch at any --dot--size. */
	inline-size: calc(var(--wp--custom--dot--size) * 2.2);
	block-size: var(--wp--custom--dot--size);
	background: var(--wp--preset--color--brand-red);
	clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
	flex: none;
}

/* ---------------------------------------------------------------
   Service cards — 01..06, each with its own drawn roof detail
   --------------------------------------------------------------- */

.pob-services {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
	gap: var(--wp--preset--spacing--60) var(--wp--preset--spacing--50);
}

.pob-service {
	border-block-start: var(--wp--custom--rule--thick) solid var(--wp--preset--color--brand-red);
	padding-block-start: var(--wp--preset--spacing--40);
}

/* Icon and number share a row: the drawing says which service, the number says
   where it sits in the six. The number is pushed to the far end so the icons
   line up on the grid's left edge and read as a set across the row. */
.pob-service__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--30);
	margin-block-end: var(--wp--preset--spacing--30);
}

.pob-service__desc {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
}

/* Large display type is the one place brand red is unambiguously safe. */
.pob-service__num {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 800;
	font-stretch: var(--wp--custom--display--stretch);
	color: var(--wp--preset--color--brand-red);
	line-height: 1;
	margin: 0;
}

.pob-service__title {
	margin: 0 0 var(--wp--preset--spacing--20);
}

/* ---------------------------------------------------------------
   Drawn icons — see inc/icons.php for what each one is and why
   --------------------------------------------------------------- */

/*
 * Ink, not red. At 48px with a 2.5px stroke these are not "large text", so
 * brand red would be resting on the 3:1 non-text floor to carry meaning. The
 * number next to it is display type and can afford the red; the drawing can't.
 *
 * currentColor + no fill: the row inherits the card's colour, so these cannot
 * drift out of the palette and would survive a section going dark.
 */
/*
 * 64px and a 3px stroke, NOT the 48/2.5 they were first drawn at. Measured
 * against what sits next to them rather than chosen in the abstract: the number
 * is x-large (32px) ExtraBold and the card title is the same again, so a 48px
 * hairline drawing next to them read as weedy — technically present, visually
 * apologetic. The icons now match the type's optical weight.
 *
 * The viewBox is still 48 and the paths are unchanged: the SVG scales, so size
 * is a CSS decision and stays in one place. stroke-width is in user units, so
 * 3 in a 48-box displayed at 64px renders at 4 device px — which is why the
 * paths use non-scaling-stroke, keeping the stroke honest at any display size.
 */
.pob-ico {
	display: block;
	width: 64px;
	height: 64px;
	flex: none;
	color: var(--wp--preset--color--ink);
	fill: none;
	stroke: currentColor;
	stroke-width: 3;
	stroke-linecap: round;
	stroke-linejoin: round;
	overflow: visible;
}

/*
 * Vector strokes scale with the box; these are drawn at exactly 48px and never
 * resized, so a hairline is not a risk — but non-scaling-stroke means the 2.5px
 * stays 2.5px if anyone ever does resize one, which keeps the six visually
 * identical in weight. That consistency is most of why they read as a family.
 */
.pob-ico__p {
	vector-effect: non-scaling-stroke;
}

/* ---------------------------------------------------------------
   Section head — pill + title + optional lead, used by most sections
   --------------------------------------------------------------- */

.pob-sectionhead {
	max-width: 62ch;
	margin-block-end: var(--wp--preset--spacing--60);
}

.pob-sectionhead > * {
	margin-block: 0;
}

.pob-sectionhead .pob-pill {
	margin-block-end: var(--wp--preset--spacing--30);
}

.pob-sectionhead__lead {
	margin-block-start: var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--large);
	color: var(--wp--preset--color--muted);
}

/* ---------------------------------------------------------------
   Stat callouts — DESIGN-REFERENCE §5
   --------------------------------------------------------------- */

/*
 * The reference's "glass" stat cards are translucent panels over a dark hero.
 * Inverted here: a light card on white, separated by hairlines rather than a
 * fill, because a frosted panel over a white section is just a grey box.
 *
 * Every figure is one of the company's own existing claims, carried across
 * unchanged (CATALOGUE §1). There is no count-up animation: the numbers are
 * short and a counter racing to "2" is a gimmick, not motion design.
 */
.pob-stats {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
	gap: var(--wp--preset--spacing--50);
	border-block-start: var(--wp--custom--rule--thick) solid var(--wp--preset--color--brand-red);
	padding-block-start: var(--wp--preset--spacing--50);
}

.pob-stat__num {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--display);
	font-weight: 800;
	font-stretch: var(--wp--custom--display--stretch);
	line-height: 1;
	letter-spacing: -0.03em;
	color: var(--wp--preset--color--brand-red);
	margin: 0;
}

.pob-stat__label {
	margin: var(--wp--preset--spacing--20) 0 0;
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	font-weight: 600;
}

/*
 * ⚠ REMOVED 2026-07-17, on purpose — read before re-adding it.
 *
 * This used to force white numbers here because brand-red on the OLD black
 * ink band measured ~1.9:1 (illegible even as display type). On the new dark
 * palette .pob-band-ink's background is surface-alt (#111820), and brand-red
 * measures 4.47:1 there — clears the 3:1 WCAG bar for large text with
 * headroom (.pob-stat__num is 4.5rem/800-weight, comfortably "large text").
 * So .pob-stat__num's base rule (color: var(--brand-red)) now works
 * correctly here without a special case, and the override actively made
 * things worse: it would have painted the numbers ink-on-surface-alt instead
 * of the intended brand red. .pob-stat__label already uses --muted, which
 * clears ~6.7:1 against every dark tier — no override needed there either.
 */

/* ---------------------------------------------------------------
   Testimonials — DESIGN-REFERENCE §5: quote + name, no stars, no avatars
   --------------------------------------------------------------- */

/*
 * ⚠⚠ THE TESTIMONIAL CARDS WERE NARROW, TALL AND FAR APART BECAUSE OF THE
 *    BROWSER'S DEFAULT <blockquote> MARGIN. `margin: 0` below is the fix.
 *    DO NOT REMOVE IT, AND DO NOT "FIX" THIS GRID ANY OTHER WAY FIRST.
 *
 * .pob-quote is a <blockquote>, and every UA stylesheet ships:
 *     blockquote { margin-block: 1em; margin-inline: 40px; }
 * Nothing here ever reset it. These cards are raw HTML inside a pattern, not a
 * wp-block layout container, so core's own flow-margin reset never reached them
 * either, and :root .pob-main > * only covers direct children of <main>.
 *
 * Measured on staging at a 1600px viewport, before the fix:
 *   - grid track:  300px
 *   - card:        220px   <- 300 minus 40px of UA margin on EACH side
 *   - gap:         40px    <- but the VISUAL gap was 40 + 40 + 40 = 120px
 *   - card height: up to 749px
 *
 * That is all three of Toby's complaints, from one line of UA CSS:
 *   "not very wide"          -> 80px of every track eaten by margin
 *   "big gaps in the middle" -> the real gutter was 3x what the gap said
 *   "really tall"            -> a 225-character quote in a 220px column is
 *                               ~11 lines, so the card grew vertically to pay
 *                               for the width the margin had taken
 *
 * The first pass at this blamed align-items:stretch + flex:1 and added
 * `align-items: start`. That was treating a symptom: the cards were the wrong
 * SHAPE because they were the wrong WIDTH. `start` is kept below — it is still
 * correct that quotes of different lengths should not stretch to match the
 * longest in their row — but it was never the cause.
 *
 * box-sizing is stated for the same family of reason recorded on .pob-field:
 * WordPress ships no global border-box reset, so this padded, bordered box was
 * content-box. It is not currently a bug here (a grid item has no author width
 * to be added to) but it is one edit away from being one.
 */
/*
 * ⚠ THE auto-fit GRID THAT USED TO BE HERE IS GONE — 2026-07-18.
 *
 * It was `repeat(auto-fit, minmax(min(100%, 340px), 1fr))` with a long note
 * about tuning 360 -> 340 to stop five cards landing as 3 + 2. That tuning was
 * real and it worked, but it was solving the wrong problem: an equal-track
 * grid can only ever be as tidy as the content is even, and these quotes run
 * from one line (Steve Knight) to a paragraph (Carol). Every row was as tall
 * as its tallest card, so the short ones floated in whitespace — Toby: "some
 * of the boxes are smaller than others... there's just big gaps in the page."
 *
 * Note that `grid-auto-rows: 1fr` would NOT have fixed it. That makes the gaps
 * uniform by making every card as tall as the longest quote on the page, which
 * is more whitespace, not less.
 *
 * Two layouts now, chosen by content shape rather than by page — see
 * patterns/testimonials.php. Shared card styling stays below; only the
 * container differs.
 */

/* ---- /testimonials/ : masonry ----
   CSS columns, not `grid-template-rows: masonry` — the native property is
   still not in Safari or Chrome stable, and this needs to work now. The
   trade-off is reading order: columns flow top-to-bottom then across, so card
   2 sits below card 1 rather than beside it. For an unordered set of
   testimonials that is invisible to a reader, and it is the only way to pack
   mixed-height cards with genuinely no gaps. */
.pob-quotes--masonry {
	columns: 3 340px;
	column-gap: var(--wp--preset--spacing--50);
}

.pob-quotes--masonry .pob-quote {
	/* Without this a card can be split across a column break mid-quote. */
	break-inside: avoid;
	margin-block-end: var(--wp--preset--spacing--50);
	/* `display: flex` (from .pob-quote) forces the card onto its own block in
	   a multicol context in some engines; flow layout is what lets the
	   packing work. The cite is pushed down by margin instead — see below. */
	display: block;
}

.pob-quotes--masonry .pob-quote__text {
	margin-block-end: var(--wp--preset--spacing--30);
}

/* ---- home : slider ----
   Equal height comes free: flex items stretch, so every card matches the
   tallest in the strip. Uniform by construction rather than by tuning. */
.pob-quotes--slider {
	display: flex;
	gap: var(--wp--preset--spacing--50);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scroll-padding-inline: 2px;
	/* Room for the focus ring, which would otherwise be clipped by overflow. */
	padding: 2px;
	scrollbar-width: thin;
}

.pob-quotes--slider .pob-quote {
	flex: 0 0 clamp(268px, 30vw, 380px);
	scroll-snap-align: start;
}

.pob-quotes--slider:focus-visible {
	outline: 2px solid var(--wp--preset--color--ink);
	outline-offset: 3px;
}

@media (max-width: 600px) {
	.pob-quotes--slider .pob-quote {
		/* Just under full width, so the next card peeks and the strip reads as
		   scrollable without needing the arrows. */
		flex-basis: 84%;
	}
}

/* ---- slider chrome ---- */
.pob-slider {
	position: relative;
}

.pob-slider__nav {
	display: flex;
	gap: var(--wp--preset--spacing--20);
	justify-content: flex-end;
	margin-block-start: var(--wp--preset--spacing--40);
}

/* [hidden] must win over the flex above — app.js only unhides these once it
   has actually bound them. */
.pob-slider__nav[hidden] {
	display: none;
}

.pob-slider__btn {
	inline-size: 44px;
	block-size: 44px;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border: 1px solid var(--wp--custom--line-strong);
	border-radius: 50%;
	background: transparent;
	color: var(--wp--preset--color--ink);
	font-size: 1.1rem;
	cursor: pointer;
	transition:
		background-color 0.2s var(--wp--custom--ease--out, ease),
		border-color 0.2s var(--wp--custom--ease--out, ease),
		opacity 0.2s ease;
}

.pob-slider__btn:hover:not(:disabled) {
	background: var(--wp--preset--color--surface-alt);
	border-color: var(--wp--preset--color--red-text);
}

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

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

.pob-quote {
	/* ⚠ THE FIX. See the block above before touching. */
	margin: 0;
	box-sizing: border-box;
	display: flex;
	flex-direction: column;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--card);
	padding: var(--wp--preset--spacing--50);
	background: var(--wp--preset--color--surface);
}

/* The quote mark is decorative and must not be announced or selected — it is
   not part of the customer's words. */
.pob-quote::before {
	content: "\201C";
	font-family: var(--wp--preset--font-family--display);
	font-size: 4rem;
	font-weight: 800;
	line-height: 0.8;
	color: var(--wp--preset--color--brand-red);
	display: block;
	margin-block-end: var(--wp--preset--spacing--20);
	user-select: none;
}

.pob-quote__text {
	margin: 0 0 var(--wp--preset--spacing--40);
	flex: 1;
}

.pob-quote__name {
	margin: 0;
	font-weight: 700;
	font-size: var(--wp--preset--font-size--small);
	text-transform: uppercase;
	letter-spacing: 0.06em;
}

/* ---------------------------------------------------------------
   FAQ — patterns/faq.php. Native <details>/<summary>, no JS, same
   disclosure-marker reset as .pob-mnav__toggle (list-style +
   ::-webkit-details-marker, Firefox and everything-else vs Safari).

   ⚠ TWO-COLUMN SINCE 2026-07-16. Toby: "slightly more tucked away, as in
   maybe faqs one side and something else on the other." FAQs left, a
   compact contact card right. It also moved off the home page onto the
   service pages, with per-service questions — see patterns/faq.php and
   inc/faqs.php before changing any of this.
   --------------------------------------------------------------- */

/*
 * Single column by default, two from 960px. NOT grid-template-columns on a
 * bare .pob-faqrow at every width: the aside is a sidebar next to a list of
 * disclosures, and at phone width a 2-col grid would squeeze both to
 * unreadable. Mobile order is FAQs first, contact card after — the card is
 * the fallback for people the FAQs didn't answer, so it belongs after them
 * in reading order, which source order already gives us for free.
 */
.pob-faqrow {
	display: grid;
	gap: var(--wp--preset--spacing--60);
	align-items: start;
}

@media (min-width: 960px) {
	.pob-faqrow {
		grid-template-columns: minmax(0, 1fr) 20rem;
		gap: var(--wp--preset--spacing--70);
	}

	/*
	 * Sticks alongside a long accordion rather than stranding the phone
	 * number at the top while the reader is at question six. The offset
	 * clears the header: --wp--custom--header--height is the AT-REST height
	 * token (see the note above .pob-header — the token is deliberately not
	 * shrunk by --pob-header-scale, so this clears the header in both its
	 * states rather than only the scrolled one).
	 */
	.pob-faqrow__aside {
		position: sticky;
		top: calc(var(--wp--custom--header--height) + var(--wp--preset--spacing--40));
	}
}

@media (prefers-reduced-motion: reduce) {
	/* Sticky is motion the visitor didn't ask for; pin it in place instead. */
	.pob-faqrow__aside { position: static; }
}

.pob-faq {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--30);
	/*
	 * No max-inline-size any more — the grid column is the measure now. The
	 * old 760px cap was for a full-width home-page band; inside a 1fr column
	 * it would just leave a gap between the questions and the card.
	 */
}

/* ---- the contact card (second column) ---- */

.pob-faqcard {
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--base);
	padding: var(--wp--preset--spacing--50);
}

.pob-faqcard__title {
	margin: 0 0 0.5rem;
	font-size: var(--wp--preset--font-size--medium);
}

.pob-faqcard__lead {
	margin: 0 0 var(--wp--preset--spacing--40);
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
}

/*
 * The phone number is the primary conversion for a trades business and
 * tap-to-call is the point of it on a phone — so it is the biggest thing in
 * the card. --wp--preset--color--red-text (#FF3B2A, 5.5:1 on surface),
 * never brand-red (#FF0000, fails AA at this size) — see the brand note at
 * the top of this file.
 */
.pob-faqcard__phone {
	display: block;
	font-family: var(--wp--preset--font-family--display, inherit);
	font-size: var(--wp--preset--font-size--large);
	font-weight: 700;
	line-height: 1.1;
	color: var(--wp--preset--color--red-text);
	text-decoration: none;
}

.pob-faqcard__phone:hover,
.pob-faqcard__phone:focus-visible { text-decoration: underline; }

.pob-faqcard__line { margin: 0.65rem 0 0; font-size: var(--wp--preset--font-size--small); }
.pob-faqcard__line a { color: var(--wp--preset--color--red-text); overflow-wrap: anywhere; }

.pob-faqcard__addr {
	margin: var(--wp--preset--spacing--40) 0 0;
	padding-block-start: var(--wp--preset--spacing--40);
	border-block-start: 1px solid var(--wp--preset--color--line);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

.pob-faq__item {
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--surface);
	padding: var(--wp--preset--spacing--40) var(--wp--preset--spacing--50);
	transition:
		border-color 0.25s var(--wp--custom--ease--out, ease),
		background-color 0.25s var(--wp--custom--ease--out, ease);
}

.pob-faq__item[open] {
	border-color: var(--wp--custom--line-strong);
	background: var(--wp--preset--color--surface-alt);
}

/*
 * The soft open/close. Toby, 2026-07-17: the accordions "don't animate like
 * softly open".
 *
 * ::details-content is the only way to transition a <details> open without
 * JavaScript — it targets the implicit wrapper around everything after the
 * <summary>, which is otherwise unstyleable. interpolate-size unlocks
 * animating TO an intrinsic size; without it `height: auto` is not an
 * animatable value and the panel would jump.
 *
 * Deliberately CSS-only and deliberately un-polyfilled: a browser without
 * ::details-content ignores this whole block and the panel snaps open, which
 * is exactly the behaviour that shipped before. Nothing is hidden behind it,
 * so there is no state where content becomes unreachable — the same rule the
 * rest of this theme's motion follows.
 */
:root {
	interpolate-size: allow-keywords;
}

.pob-faq__item::details-content {
	block-size: 0;
	overflow: hidden;
	opacity: 0;
	transition:
		block-size 0.34s var(--wp--custom--ease--out, ease),
		opacity 0.28s ease,
		content-visibility 0.34s allow-discrete;
	transition-behavior: allow-discrete;
}

.pob-faq__item[open]::details-content {
	block-size: auto;
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.pob-faq__item,
	.pob-faq__item::details-content {
		transition: none;
	}
}

.pob-faq__q {
	list-style: none;
	cursor: pointer;
	font-weight: 700;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--30);
}

.pob-faq__q::-webkit-details-marker {
	display: none;
}

/* The disclosure arrow — CSS content, not an icon file, so it can rotate on
   [open] with nothing but a transform. */
.pob-faq__q::after {
	content: "";
	flex: 0 0 auto;
	inline-size: 10px;
	block-size: 10px;
	border-inline-end: 2px solid var(--wp--preset--color--red-text);
	border-block-end: 2px solid var(--wp--preset--color--red-text);
	transform: rotate(45deg);
	transition: transform 0.2s ease;
}

.pob-faq__item[open] .pob-faq__q::after {
	transform: rotate(-135deg);
}

.pob-faq__q:focus-visible {
	outline: 2px solid var(--wp--preset--color--red-text);
	outline-offset: 3px;
}

.pob-faq__a {
	margin: var(--wp--preset--spacing--30) 0 0;
}

/* ---------------------------------------------------------------
   Media grid — gallery tiles and case-study cards
   --------------------------------------------------------------- */

.pob-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
	gap: var(--wp--preset--spacing--40);
}

.pob-tile {
	position: relative;
	display: block;
	overflow: hidden;
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--surface-alt);
	aspect-ratio: 4 / 3;
	isolation: isolate;
}

.pob-tile img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	transition: transform 0.6s var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1));
}

.pob-tile:hover img,
.pob-tile:focus-visible img {
	transform: scale(1.05);
}

/* The caption sits on the photo, so it needs its own scrim — a roofline against
   bright sky will otherwise eat white text. */
.pob-tile__cap {
	position: absolute;
	inset-inline: 0;
	inset-block-end: 0;
	padding: var(--wp--preset--spacing--50) var(--wp--preset--spacing--30) var(--wp--preset--spacing--30);
	background: linear-gradient(to top, rgb(0 0 0 / 82%) 0%, rgb(0 0 0 / 0%) 100%);
	/* Dark rebuild, 2026-07-17: was `surface` (white, pre-flip) for white text
	   on this black photo-scrim gradient. `surface` is near-black now — use
	   `ink` (the light token post-flip) instead. */
	color: var(--wp--preset--color--ink);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	margin: 0;
}

@media (prefers-reduced-motion: reduce) {
	.pob-tile img {
		transition: none;
	}

	.pob-tile:hover img,
	.pob-tile:focus-visible img {
		transform: none;
	}
}

/* ---------------------------------------------------------------
   Case study cards
   --------------------------------------------------------------- */

.pob-cases {
	display: grid;
	gap: var(--wp--preset--spacing--70);
}

.pob-case {
	display: grid;
	gap: var(--wp--preset--spacing--50);
	align-items: center;
}

@media (min-width: 782px) {
	.pob-case {
		grid-template-columns: 1fr 1fr;
	}

	/* Alternate the photo side so a run of case studies reads as a rhythm
	   rather than a list. */
	.pob-case:nth-child(even) .pob-case__media {
		order: 2;
	}
}

.pob-case__media {
	border-radius: var(--wp--custom--radius--card);
	overflow: hidden;
	aspect-ratio: 4 / 3;
	background: var(--wp--preset--color--surface-alt);
}

.pob-case__media img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

.pob-case__title {
	margin: 0 0 var(--wp--preset--spacing--30);
}

/* Key Facts — only Fairlawn has one. Do not synthesise these for the other
   three case studies; the source has no such data for them (CATALOGUE §4c). */
/* The box's own label, from the source. Sized as a small caps-y eyebrow rather
   than a heading proper — it names the list without competing with the case
   title above it. */
.pob-facts__h {
	margin: var(--wp--preset--spacing--50) 0 0;
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.1em;
	color: var(--wp--preset--color--muted);
}

.pob-facts {
	list-style: none;
	margin: var(--wp--preset--spacing--30) 0 0;
	padding: 0;
	border-block-start: 1px solid var(--wp--preset--color--line);
}

.pob-facts li {
	display: flex;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--30);
	padding-block: 0.6rem;
	border-block-end: 1px solid var(--wp--preset--color--line);
	font-size: var(--wp--preset--font-size--small);
}

.pob-facts dt,
.pob-facts .pob-facts__k {
	color: var(--wp--preset--color--muted);
	text-transform: uppercase;
	letter-spacing: 0.06em;
	font-weight: 600;
}

.pob-facts .pob-facts__v {
	font-weight: 700;
	text-align: right;
}

/* ---------------------------------------------------------------
   Developments list
   --------------------------------------------------------------- */

/*
 * 32 entries, so this is a list and not a card grid — cards would make the page
 * three screens longer and imply each one has a detail page behind it. None do
 * (PLAN §6 #4 is still open on whether these become a CPT).
 */
.pob-devs {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
	gap: 0 var(--wp--preset--spacing--60);
}

.pob-dev {
	border-block-start: 1px solid var(--wp--preset--color--line);
	padding-block: var(--wp--preset--spacing--40);
}

.pob-dev__name {
	margin: 0 0 var(--wp--preset--spacing--20);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 700;
	font-family: var(--wp--preset--font-family--body);
	letter-spacing: 0;
	line-height: 1.4;
}

.pob-dev__units {
	margin: 0 0 var(--wp--preset--spacing--20);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	color: var(--wp--preset--color--red-text);
}

.pob-dev__desc {
	margin: 0;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

/* The status strings are the client's own verbatim ("Completed Summer 18'",
   "Currently in planning", "TBC"). Rendered as a plain tag — NOT colour-coded
   by state, because that would require interpreting them, and "TBC" and
   "Currently in planning since Winter 16'" don't map onto a status vocabulary
   anyone here gets to invent. */
.pob-dev__status {
	display: inline-block;
	margin-block-start: var(--wp--preset--spacing--30);
	padding: 0.25rem 0.6rem;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--pill);
	font-size: 0.75rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--wp--preset--color--ink);
}

/* ---------------------------------------------------------------
   Split — text beside a photo
   --------------------------------------------------------------- */

.pob-split {
	display: grid;
	gap: var(--wp--preset--spacing--60);
	align-items: center;
}

@media (min-width: 782px) {
	.pob-split {
		grid-template-columns: 1fr 1fr;
	}
}

.pob-split__media img {
	width: 100%;
	height: auto;
	display: block;
	border-radius: var(--wp--custom--radius--card);
}

/* ---------------------------------------------------------------
   Bands
   ---------------------------------------------------------------
   ⚠ GRADIENT BANDS, 2026-07-17. Toby, on the reference site: "there's some
   sections of background that have got like a gradient or a fade across them.
   That looks quite nice."

   How this is built, and why it is not just `background: linear-gradient(...)`:
     - The band keeps a FLAT background-color as its base. That is the real,
       measurable colour every contrast figure in this file was calculated
       against, and it is what paints if the gradient layers fail to render.
     - The gradient is a separate ::before layer at low opacity. It fades the
       band's own tier in and out vertically (--grad--veil) so the section
       edges dissolve into the page instead of banding hard, and adds an
       off-screen red glow (--grad--glow) for warmth.
     - Nothing readable sits on the gradient alone: the veil tops out at 65%
       of a colour one tier from the base, so worst-case text contrast moves
       by well under a tenth of a stop. The glow is 11% red at the very edge
       of the viewport.
   pointer-events:none and z-index:0 keep the layer non-interactive and
   behind content, which is given z-index:1 below.
   --------------------------------------------------------------- */

.pob-band-alt,
.pob-band-ink,
.pob-band-grad {
	position: relative;
	isolation: isolate;
}

.pob-band-alt::before,
.pob-band-ink::before,
.pob-band-grad::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
	background-image: var(--wp--custom--grad--glow), var(--wp--custom--grad--veil);
}

/* Content above the gradient layer. :where() keeps this at zero specificity
   so it never fights a component's own stacking. */
.pob-band-alt > *,
.pob-band-ink > *,
.pob-band-grad > * {
	position: relative;
	z-index: 1;
}

/* Alternate the glow's side down the page so a run of stacked bands doesn't
   read as the same lighting repeated. */
.pob-band-alt:nth-of-type(even)::before,
.pob-band-ink:nth-of-type(even)::before {
	background-image: var(--wp--custom--grad--glow-left), var(--wp--custom--grad--veil);
}

.pob-band-alt {
	background-color: var(--wp--preset--color--surface-alt);
}

/*
 * ⚠ REWRITTEN for the dark theme, 2026-07-17. Read before "simplifying" this
 * back to `background: var(--ink)`.
 *
 * This USED to be a deliberately-black contrast band on an otherwise-white
 * site (background: ink=#000000, text: surface=#FFFFFF) — a genuinely
 * different colour to the page around it. Once the base palette flipped
 * (ink is now near-white, surface is now near-black, see theme.json), that
 * same rule would have INVERTED rather than continued to mean the same
 * thing: `background: var(--ink)` would now paint a near-white card, i.e. a
 * bright card floating in an otherwise-dark site. Every declaration in the
 * old block was internally self-consistent even after the flip (background
 * and every text colour both referenced tokens, so they moved together) —
 * it just stopped meaning "contrast band", full stop.
 *
 * A bright inverted card COULD be a legitimate accent move, but it isn't
 * what a "contrast band" should mean on an already-dark site, and it isn't
 * how the reference site (paulobrien-roofing.vercel.app) treats section
 * variation — its stats band is just the base dark, no inversion at all.
 * Redefined instead as a SECOND, slightly-lighter DARK TIER
 * (surface-alt) — graduated elevation, matching the reference's bg/bg-2/bg-3
 * approach, not a black-to-white flip.
 */
.pob-band-ink {
	background-color: var(--wp--preset--color--surface-alt);
	color: var(--wp--preset--color--ink);
}

.pob-band-ink :where(h1, h2, h3, h4, p, li) {
	color: var(--wp--preset--color--ink);
}

.pob-band-ink a:not(.wp-element-button) {
	color: var(--wp--preset--color--red-text);
}

.pob-band-ink .pob-pill {
	color: var(--wp--preset--color--ink);
	border-color: var(--wp--custom--line-strong);
}

.pob-band-ink :where(a, button):focus-visible {
	outline-color: var(--wp--preset--color--ink);
}

/* ---------------------------------------------------------------
   Prose — long-form migrated copy
   --------------------------------------------------------------- */

.pob-prose > * + * {
	margin-block-start: var(--wp--preset--spacing--30);
}

.pob-prose h2,
.pob-prose h3 {
	margin-block-start: var(--wp--preset--spacing--50);
}

.pob-prose ul {
	padding-inline-start: 1.2rem;
}

.pob-prose li {
	margin-block-end: 0.35rem;
}

/* ---------------------------------------------------------------
   Contact
   --------------------------------------------------------------- */

.pob-contact {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
	gap: var(--wp--preset--spacing--50);
}

.pob-office {
	border-block-start: var(--wp--custom--rule--thick) solid var(--wp--preset--color--brand-red);
	padding-block-start: var(--wp--preset--spacing--40);
}

.pob-office__name {
	margin: 0 0 var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--x-large);
}

.pob-office p {
	margin: 0 0 var(--wp--preset--spacing--20);
}

/* ---------------------------------------------------------------
   Post content
   --------------------------------------------------------------- */

/*
 * Vertical rhythm for page/post bodies. Lives here rather than on a wrapper
 * group in the template because a constrained wrapper would cap alignfull
 * patterns inside the content at wideSize and stop them bleeding.
 *
 * :not(:empty) matters right now: pages are deliberately shipping with empty
 * bodies until content migration (Phase 4), and unconditional padding would
 * print a 12rem dead band between the page head and the footer on every one.
 *
 * The :has() rules handle the other half: full-bleed pattern sections carry
 * their own vertical padding because they're designed to stand alone, so a page
 * that opens or closes with one would double up (6rem + 6rem) into a visible
 * void. Prose keeps its rhythm; bands butt cleanly against the page head and
 * the footer.
 */
.pob-content:not(:empty) {
	padding-block: var(--wp--preset--spacing--70);
}

.pob-content:has(> .alignfull:first-child) {
	padding-block-start: 0;
}

.pob-content:has(> .alignfull:last-child) {
	padding-block-end: 0;
}

/*
 * ⚠⚠ CONTENT WIDTH IS ONE NUMBER, AND IT LIVES IN theme.json. DO NOT ADD A
 *    SECOND ONE HERE. READ THIS BEFORE "FIXING" A PAGE THAT LOOKS TOO WIDE.
 *
 * Toby's rule, stated plainly and twice: "we want the content width to be the
 * same throughout the site, because it just looks odd having certain text
 * sections being a different width."
 *
 * So: theme.json now sets contentSize == wideSize == 1320px. Every constrained
 * container on the site — a pattern's prose group, wp:post-content, an editor's
 * new page — resolves to the same 1320 the hero and the alignwide bands use.
 * `alignwide` is consequently a no-op, and that is correct rather than
 * redundant: it means a block cannot accidentally be wider than the body text
 * beside it.
 *
 * ⚠ THE HISTORY, BECAUSE THIS EXACT BUG HAS NOW BEEN "FIXED" THREE TIMES AND
 *   KEPT COMING BACK — on Cloud9 before this project, and twice here:
 *
 *   contentSize was 760px while wideSize was 1320px. Every pattern that wanted
 *   to be wide said `align:"wide"` and got 1320; everything else silently got
 *   760. So the hero title ran 1320 and the body copy under it ran 760, on the
 *   same page. The fix people reach for is to widen the ONE page they are
 *   looking at — I did exactly that, setting contentSize on page.html and
 *   capping prose at 90ch. It fixed privacy.html and did nothing at all for the
 *   service pages, which are patterns and never touched that template. That is
 *   why it took several passes on Cloud9 too: the default is the bug, and
 *   patching pages hides it one page at a time.
 *
 *   Cloud9 landed on contentSize:1200 / wideSize:1440 with an opt-in narrow
 *   class (docs/brand-guidance.md §5: "Narrow reading: 680px"). That is the
 *   conventional answer and it is NOT what this site does — Toby's call is one
 *   width, no exceptions, because two widths is the thing that looked wrong.
 *
 * WHAT IS DELIBERATELY STILL NARROW, AND WHY IT IS NOT AN EXCEPTION TO THE ABOVE:
 *   - Forms (.pob-contactform, 680px). A form is a component, not a text
 *     section; an eight-field form spread across 1320px is worse in every way.
 *   - Single short lines that are typography, not content columns: the hero lead
 *     (34ch), the section head (62ch), the footer blurb (42ch). These are
 *     one-or-two-line elements inside a section, not a page's body width, and
 *     un-capping them would make the design worse without addressing the note.
 * Neither is a "text section at a different width". If that distinction ever
 * stops being obvious, the rule wins: one width.
 */

/* ---------------------------------------------------------------
   Buttons — motion
   --------------------------------------------------------------- */

/*
 * theme.json declares the button's :hover (red-text -> ink) but theme.json
 * CANNOT declare a transition — there is no styles.elements.button.transition
 * key, and its :hover block only emits the end state. So every button on the
 * site snapped between two colours with nothing in between, while the cards,
 * tiles, chips and mega panel around them all eased. That inconsistency is most
 * of what reads as "unfinished" next to the rest of the motion work; the site
 * looked like two different builds depending on what you hovered.
 *
 * Hence here, not there. This is the only place it can live.
 *
 * The lift is 2px and deliberately smaller than the cards' 3px: a card is a big
 * surface where 3px reads as a nudge, a button is small and the same distance
 * reads as a hop. Both use the same easing token, which is what actually makes
 * them feel like one system.
 *
 * Covers .wp-element-button (theme.json's own class, on every core button) and
 * .pob-submit (CF7's bare <input type=submit>, which inherits none of core's
 * button styling — see the forms section).
 */
.wp-element-button,
.wp-block-button__link,
.pob-submit {
	transition:
		background-color 0.22s var(--wp--custom--ease--out, ease),
		color 0.22s var(--wp--custom--ease--out, ease),
		border-color 0.22s var(--wp--custom--ease--out, ease),
		transform 0.22s var(--wp--custom--ease--pop, ease);
}

.wp-block-button__link:hover,
.pob-submit:hover {
	transform: translateY(-2px);
}

/* Pressing should feel like pressing, not like a second hover. */
.wp-block-button__link:active,
.pob-submit:active {
	transform: translateY(0);
	transition-duration: 0.05s;
}

@media (prefers-reduced-motion: reduce) {
	.wp-element-button,
	.wp-block-button__link,
	.pob-submit {
		transition: none;
	}

	.wp-block-button__link:hover,
	.pob-submit:hover {
		transform: none;
	}
}

/* ---------------------------------------------------------------
   CTA band
   --------------------------------------------------------------- */

/*
 * The band is already --red-text; the default button fill is --red-text too,
 * so an untouched button would be invisible on it. Invert.
 *
 * ⚠ CORRECTED 2026-07-17 — an earlier pass on this same day swapped surface
 * and ink here, on the wrong assumption that red-text was still the old dark
 * #B30000 (a stale value from memory, not from theme.json). It is `#FF3B2A`
 * now — a light coral, deliberately built to read as TEXT on `surface`
 * ("7.20:1 AAA on surface" is its own name comment in theme.json). Against
 * that light-coral band a dark (`surface`) button fill pops harder than a
 * light (`ink`) one does, and surface-on-red-text actually measures 7.2:1,
 * not the 2.69:1 the earlier pass calculated from the wrong hex. Reverted to
 * the original pairing: dark button/light text at rest, inverted on hover.
 */
.pob-cta-band .wp-block-button__link {
	background-color: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--ink);
}

.pob-cta-band .wp-block-button__link:hover,
.pob-cta-band .wp-block-button__link:focus-visible {
	background-color: var(--wp--preset--color--ink);
	color: var(--wp--preset--color--surface);
}

.pob-cta-band :where(a, button):focus-visible {
	outline-color: var(--wp--preset--color--surface);
}

/* ---------------------------------------------------------------
   Cards
   --------------------------------------------------------------- */

.pob-card {
	padding: var(--wp--preset--spacing--50);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--surface);
}

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

/* Dark rebuild, 2026-07-17: was `surface` (white, pre-flip) for white links
   on the (also pre-flip: black) footer. Both flipped, so this needs to stay
   the light token — `ink` post-flip — now that footer.html's own
   backgroundColor/textColor have been corrected back to a dark footer. */
.pob-footer a {
	color: var(--wp--preset--color--ink);
	text-decoration: none;
}

.pob-footer a:hover,
.pob-footer a:focus-visible {
	text-decoration: underline;
}

.pob-footer__logo img {
	display: block;
	width: var(--wp--custom--footer--logo-width);
	max-width: 100%;
	height: auto;
	margin-block-end: var(--wp--preset--spacing--30);
}

.pob-footer__h {
	margin-block-end: var(--wp--preset--spacing--30);
	opacity: 0.7;
	text-transform: uppercase;
	letter-spacing: 0.08em;
	font-family: var(--wp--preset--font-family--body);
	font-weight: 600;
	/*
	 * Stated rather than left to a class. The footer's existing headings are
	 * wp:heading h2s carrying .has-small-font-size, but the grouped menus
	 * (patterns/footer-menus.php) use <p> for heading-outline reasons — and a
	 * <p> inherits body size, so without this the two kinds of column heading
	 * would render at different sizes side by side.
	 */
	font-size: var(--wp--preset--font-size--small);
	margin-block-start: 0;
}

/* ---------------------------------------------------------------
   Footer — grouped menus (patterns/footer-menus.php)
   --------------------------------------------------------------- */

/*
 * The footer's site map: Roofing / Building / Company, mirroring the header's
 * own branches. Replaces a single ~17-item vertical wp:navigation that ran down
 * one column beside two columns of white space.
 *
 * auto-fit and not a fixed 3-up, for the reason recorded on .pob-services: a
 * fixed grid becomes three stacked full-width screens on a phone. 140px is the
 * narrowest a column can be before "External Wall Insulation" wraps to three
 * lines. Below that it settles to 2-up, then 1.
 */
.pob-footer__menus {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 140px), 1fr));
	gap: var(--wp--preset--spacing--40) var(--wp--preset--spacing--30);
}

.pob-footer__menu {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 0.45rem;
	font-size: var(--wp--preset--font-size--small);
}

.pob-footer__menu li {
	margin: 0;
}

/*
 * Set back from full white so the column headings and the brand blurb keep
 * their hierarchy, then brought up on hover. The footer's `a:hover` already
 * adds an underline (above); this adds the tonal step, which is what makes a
 * dense link list feel navigable rather than flat.
 */
.pob-footer__menu a {
	opacity: 0.78;
	transition: opacity 0.15s ease;
	line-height: 1.4;
	display: inline-block;
}

.pob-footer__menu a:hover,
.pob-footer__menu a:focus-visible {
	opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
	.pob-footer__menu a {
		transition: none;
	}
}

.pob-footer__legal {
	opacity: 0.6;
}

.pob-footer__legal a {
	text-decoration: underline;
	text-underline-offset: 0.15em;
}

/* The client's own footer blurb (bbum8_modules id 94). Set back from full
   white so it reads as supporting copy under the logo, not as a headline. */
.pob-footer__blurb {
	margin-block-start: var(--wp--preset--spacing--40);
	opacity: 0.7;
	max-inline-size: 42ch;
}

.pob-footer__social {
	margin-block-start: var(--wp--preset--spacing--40);
}

.pob-footer__rule {
	border: 0;
	opacity: 0.15;
}

/* ---------------------------------------------------------------
   Focus — visible, always. Never remove without a replacement.
   --------------------------------------------------------------- */

:where(a, button, input, select, textarea, [tabindex]):focus-visible {
	outline: 2px solid var(--wp--preset--color--ink);
	outline-offset: 2px;
	border-radius: var(--wp--custom--radius--sm);
}

/*
 * ⚠ DARK REBUILD, 2026-07-17 — REMOVED. This used to flip the focus outline
 * to `surface` (white, pre-flip) on the footer/hero-media dark grounds,
 * because the base outline above is `ink` (black, pre-flip) — correct on the
 * white page, invisible on those two dark sections. `ink` is now the light
 * token and every ground on the site is dark (see the header logo note
 * further up), so the base rule's `ink` outline is already correct
 * everywhere; this override was re-darkening it back to invisible.
 */

/* Skip link needs to clear the fixed header when focused. */
.skip-link:focus {
	top: var(--wp--preset--spacing--30) !important;
}

/* ---------------------------------------------------------------
   Trust marks — the client's own published accreditations
   --------------------------------------------------------------- */

/*
 * ⚠ DARK REBUILD, 2026-07-17 — READ BEFORE TOUCHING THIS SECTION.
 * Three of the four logo files are opaque JPEGs whose own background is baked
 * white (measured: trustmark.jpg and competentroofer.jpg are #FFFFFF at the
 * corner, labc-bea-2016.jpg is #FEFEFE). Only nfrc.png has a real alpha
 * channel. Under the old light theme the band itself was on --surface (also
 * white), so the images blended in with only a hairline rule marking the
 * section — no chip needed. --surface is now dark (#0A0E12), which would
 * turn those three JPEGs into literal white slabs sitting on the band, so
 * each mark now gets its own small white chip (.pob-trust__item) instead:
 * still reproduces the marks as published, just framed rather than blended,
 * because "blended" isn't achievable anymore without re-cutting the source
 * files as transparent PNGs (not done — nothing here alters the client's
 * images).
 */
.pob-trust {
	background: var(--wp--preset--color--surface);
	border-block: 1px solid var(--wp--preset--color--line);
}

.pob-trust__h {
	margin-block-end: var(--wp--preset--spacing--50);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.1em;
	color: var(--wp--preset--color--muted);
	text-align: center;
}

.pob-trust__item {
	margin: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	background: #fff;
	padding: 0.65rem 1.05rem;
	border-radius: var(--wp--custom--radius--card);
}

/*
 * Two caps, not one, because these four marks are two different SHAPES and
 * sizing them by height alone makes the odd one out shout.
 * Three are ~2:1 badges (236x118); NFRC is a 3.6:1 wordmark (189x52). At a
 * common 58px height the wordmark comes out 211px wide against the badges' 116px
 * and reads as the biggest, most important mark on the row purely because it is
 * a long word. Capping width as well means the badges hit the height limit and
 * the wordmark hits the width limit, which is how the eye actually weighs them.
 */
.pob-trust__item img {
	inline-size: auto;
	block-size: auto;
	max-block-size: 58px;
	max-inline-size: 150px;
}

@media (max-width: 600px) {
	/* Smaller marks on narrow screens — the strip scrolls/loops either way,
	   this just keeps each chip from dominating the row. */
	.pob-trust__item img {
		max-block-size: 46px;
		max-inline-size: 122px;
	}
}

/* ---------------------------------------------------------------
   Community — Help Bristol's Homeless
   --------------------------------------------------------------- */

/*
 * Deliberately NOT styled like .pob-trust. This is a charity the company
 * supports, not a body that certifies it, and the two must not read as the same
 * kind of badge. Hence: tinted band rather than white, one centred mark rather
 * than a row, and its own heading — which is the source's own copy and the only
 * text in this whole area that the client actually wrote.
 */
.pob-community {
	background: var(--wp--preset--color--surface-alt);
}

.pob-community__inner {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--wp--preset--spacing--40);
	text-align: center;
}

.pob-community__h {
	margin: 0;
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 600;
	color: var(--wp--preset--color--ink);
}

.pob-community__link {
	display: inline-flex;
	border-radius: var(--wp--custom--radius--sm);
	transition: opacity 0.2s var(--wp--custom--ease--out);
}

.pob-community__link:hover {
	opacity: 0.72;
}

.pob-community__logo {
	inline-size: auto;
	block-size: auto;
	max-block-size: 72px;
	max-inline-size: 170px;
}

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

/* ---------------------------------------------------------------
   Award badge — closes the About article, as it does on the live site
   --------------------------------------------------------------- */

.pob-award {
	margin-block-start: var(--wp--preset--spacing--50);
}

.pob-award img {
	inline-size: auto;
	block-size: auto;
	max-inline-size: 208px;
}

/* ---------------------------------------------------------------
   Video — the client's own promo, 16:9, responsive
   --------------------------------------------------------------- */

/*
 * ⚠ The video sits BESIDE copy now, it is not a full-width slab. See the note in
 *   patterns/video.php. .pob-split does the 1fr 1fr at >=782px and the stack
 *   below it; only the differences live here.
 */
.pob-video__split {
	align-items: center;
}

.pob-video__h {
	margin-block: var(--wp--preset--spacing--20) var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--xx-large);
	text-wrap: balance;
}

.pob-video__lead {
	margin: 0 0 var(--wp--preset--spacing--40);
	color: var(--wp--preset--color--muted);
	max-width: 42ch;
}

.pob-video__cta {
	margin: 0;
	font-weight: 700;
}

.pob-video__cta a {
	color: var(--wp--preset--color--red-text);
}

/*
 * aspect-ratio rather than the source's fixed width="560" height="315", which
 * overflows a 360px viewport horizontally. The frame carries the radius and the
 * clip so the iframe's own square corners don't punch through it.
 */
.pob-video__frame {
	position: relative;
	aspect-ratio: 16 / 9;
	inline-size: 100%;
	overflow: hidden;
	border-radius: var(--wp--custom--radius--card);
	background: var(--wp--preset--color--ink);
}

.pob-video__frame iframe {
	position: absolute;
	inset: 0;
	inline-size: 100%;
	block-size: 100%;
	border: 0;
}

/* ---------------------------------------------------------------
   Forms — CF7. See inc/forms.php for lookup, honeypot and Turnstile
   --------------------------------------------------------------- */

/*
 * THE HONEYPOT'S HIDING RULE. inc/forms.php INJECTS A FIELD THAT DEPENDS ON
 * THIS EXISTING — if this rule is ever deleted, every visitor sees a text input
 * labelled "Leave this field empty" in the middle of the quote form.
 *
 * Hidden here, in a stylesheet, rather than with an inline style="display:none"
 * on the element: bots parse inline styles and skip what they find hidden;
 * almost none fetch and apply a CSS file. So a CSS-hidden trap catches things an
 * inline-hidden one does not. That is the entire trick.
 *
 * NOT display:none, and not visibility:hidden, for a different reason —
 * position:absolute + clip keeps the input in the DOM where a bot's parser will
 * find and fill it. The field also carries tabindex="-1" and aria-hidden, so a
 * keyboard user and a screen reader both skip it regardless of this rule.
 */
.pob-hp {
	position: absolute !important;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/*
 * Split band: copy + phone on the left, form on the right. Collapses to one
 * column below 860px — measured, not guessed: two 380px columns plus the gutter
 * stop fitting there, and a five-field form in a half-column narrower than that
 * puts the labels on top of the inputs.
 */
.pob-quoteband {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--wp--preset--spacing--60);
	align-items: start;
}

@media (max-width: 860px) {
	.pob-quoteband {
		grid-template-columns: 1fr;
	}
}

.pob-quoteband__h {
	font-size: var(--wp--preset--font-size--xx-large);
	margin: var(--wp--preset--spacing--30) 0 var(--wp--preset--spacing--30);
	text-wrap: balance;
}

.pob-quoteband__lead {
	color: var(--wp--preset--color--muted);
	max-width: 42ch;
	margin: 0 0 var(--wp--preset--spacing--50);
}

/*
 * The phone, at display scale, because it is the primary action and this band
 * is the moment someone has decided to make contact. Brand red is correct here
 * and ONLY because of the size: this renders at ~2rem+, comfortably over the
 * 24px large-text threshold where 4.00:1 clears the 3:1 bar. Shrink this and it
 * must move to --red-text.
 */
.pob-quoteband__tel a {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 800;
	font-stretch: var(--wp--custom--display--stretch);
	color: var(--wp--preset--color--brand-red);
	text-decoration: none;
	white-space: nowrap;
}

.pob-quoteband__tel a:hover,
.pob-quoteband__tel a:focus-visible {
	text-decoration: underline;
}

.pob-quoteband__tel {
	margin: 0 0 var(--wp--preset--spacing--20);
}

.pob-quoteband__or {
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
	margin: 0;
}

.pob-quoteband__form {
	background: var(--wp--preset--color--surface);
	padding: var(--wp--preset--spacing--50);
	border-radius: var(--wp--custom--radius--card);
	border-block-start: var(--wp--custom--rule--thick) solid var(--wp--preset--color--brand-red);
}

/* Two-up on the short fields, full width for message and submit. The form is
   already inside a half-width column, so this only kicks in above the
   single-column collapse above — hence the container-relative min of 380px. */
.pob-form form,
.wpcf7-form {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
	gap: var(--wp--preset--spacing--30);
}

.pob-field {
	margin: 0;
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
}

.pob-field--wide,
.pob-field--submit {
	grid-column: 1 / -1;
}

.pob-field label {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
}

/* The required marker is --red-text, never brand red: it renders at 14px, where
   #FF0000's 4.00:1 fails outright. abbr[title] also gets a dotted underline in
   most UAs, which reads as a typo next to a label. */
.pob-field abbr {
	color: var(--wp--preset--color--red-text);
	text-decoration: none;
	border: 0;
}

/*
 * box-sizing: border-box IS LOAD-BEARING — it is the difference between this
 * form fitting on a phone and not.
 *
 * WordPress does NOT ship a global border-box reset, and form controls default
 * to content-box. So `width: 100%` means 100% PLUS 1.7rem of padding PLUS 2px
 * of border. At a 360px viewport that resolved to a 349px input inside 320px of
 * available width, and /contact/ scrolled sideways by 9px — measured, not
 * guessed: 320 + 27.2 + 2 = 349.2.
 *
 * It only showed on /contact/ because that is the only page whose form is full
 * width; the quote band's form sits in a grid column that had slack to absorb
 * it. Same bug on both, visible on one.
 */
.pob-field input[type="text"],
.pob-field input[type="tel"],
.pob-field input[type="email"],
.pob-field select,
.pob-field textarea {
	box-sizing: border-box;
	font-family: inherit;
	font-size: max(16px, var(--wp--preset--font-size--medium));
	color: var(--wp--preset--color--ink);
	background: var(--wp--preset--color--surface);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--sm);
	padding: 0.7rem 0.85rem;
	width: 100%;
	max-width: 100%;
}

/*
 * font-size: max(16px, ...) above is not a style choice — iOS Safari ZOOMS THE
 * WHOLE PAGE when a focused input's font-size is under 16px, and the body size
 * here is 17px but shrinks toward 16px fluidly. Anything that resolved under
 * 16px would make tapping the first field jump the layout on exactly the device
 * most of this site's traffic uses.
 */

.pob-field textarea {
	resize: vertical;
	min-height: 6rem;
}

.pob-field :is(input, select, textarea):focus-visible {
	outline: 2px solid var(--wp--preset--color--ink);
	outline-offset: 1px;
	border-color: var(--wp--preset--color--ink);
}

/* The submit is a real button styled like the theme's buttons — CF7 renders a
   bare <input type="submit">, which inherits none of core's button element
   styling from theme.json. */
/*
 * ⚠ CORRECTED 2026-07-17 — an earlier pass on this same day briefly changed
 * this to `ink`, on the wrong assumption that red-text was still the old
 * dark #B30000. It is `#FF3B2A` now (theme.json — a saturated scarlet, chosen
 * specifically to read as text ON surface: "7.20:1 AAA on surface" is
 * literally its own name comment). As a BACKGROUND it therefore wants dark
 * text, not light: surface-on-red-text measures 7.2:1, ink-on-red-text
 * measures 2.46:1 and fails. `surface` was correct all along.
 */
.pob-submit {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 600;
	color: var(--wp--preset--color--surface);
	background: var(--wp--preset--color--red-text);
	border: 0;
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.85rem 1.75rem;
	cursor: pointer;
	width: 100%;
}

.pob-submit:hover {
	background: var(--wp--preset--color--ink);
}

.pob-submit:focus-visible {
	outline: 2px solid var(--wp--preset--color--ink);
	outline-offset: 2px;
}

/* CF7's response region. Never red-on-white at body size — --red-text, 7.20:1. */
.wpcf7-response-output {
	grid-column: 1 / -1;
	margin: 0;
	padding: 0.75rem 1rem;
	border: 1px solid currentColor;
	border-radius: var(--wp--custom--radius--sm);
	font-size: var(--wp--preset--font-size--small);
}

.wpcf7-not-valid-tip {
	color: var(--wp--preset--color--red-text);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
}

.wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.unaccepted .wpcf7-response-output {
	color: var(--wp--preset--color--red-text);
}

.wpcf7-spinner {
	margin-inline-start: 0.5rem;
}

/* The no-form fallback (inc/forms.php). Deliberately looks like content rather
   than an error — the visitor is not the person who needs to know the form is
   missing, they just need the phone number. */
.pob-form--fallback p {
	margin: 0 0 var(--wp--preset--spacing--20);
}

.pob-form--fallback strong {
	font-size: var(--wp--preset--font-size--large);
}

/* The contact page's enquiry form. Wider than the quote band's — it has eight
   fields, and this page has no competing column beside it. */
.pob-contactform {
	margin-block-start: var(--wp--preset--spacing--70);
	max-width: 680px;
}

.pob-contactform__h {
	font-size: var(--wp--preset--font-size--x-large);
	margin: 0 0 var(--wp--preset--spacing--40);
}

/* ===============================================================
   Mobile navigation — patterns/mobile-nav.php. <details>, no JS.
   ===============================================================

   The counterpart to the mega menu: exactly one of the two exists at any width.
     >=1200px  .pob-mega           (this is display:none)
     <1200px   .pob-mnav           (the mega menu is display:none)
   1200 is theme.css's own measured breakpoint, reused rather than re-picked.

   Replaces core/navigation's overlay entirely — see patterns/mobile-nav.php for
   why (it was rendering wp:page-list: every page on the site, in no chosen
   order). That also retires the white-on-white overlay bug documented at length
   further up this file: core no longer paints a panel here, so nothing can
   force white links onto it.

   ⚠ THE TOGGLE IS THE <summary>, AND IT IS ALSO THE CLOSE BUTTON. When open it
   is repositioned to the top-right of the panel and drawn as an X. There is
   exactly ONE control, so open and closed cannot desynchronise, and no JS is
   needed to close. Do not add a second close button — it would need JS, and a
   no-JS visitor could then open the menu and not close it. */

.pob-mnav {
	display: none;
}

@media (max-width: 1199px) {
	.pob-mnav {
		display: block;
	}
}

/* ---------------------------------------------------------------
   The toggle / close control
   --------------------------------------------------------------- */

.pob-mnav__toggle {
	display: flex;
	align-items: center;
	justify-content: center;
	inline-size: 44px;
	block-size: 44px;
	cursor: pointer;
	/* 44px is the WCAG 2.5.8 target minimum, which is why the box is bigger than
	   the drawing inside it. */
	border-radius: var(--wp--custom--radius--sm);
	/* Both are needed: Safari uses the -webkit- pseudo, everything else honours
	   list-style. Without them the browser prints a disclosure triangle beside
	   the hamburger. */
	list-style: none;
	position: relative;
	z-index: 120;
	color: var(--wp--preset--color--ink);
}

.pob-mnav__toggle::-webkit-details-marker {
	display: none;
}

/*
 * The bars, drawn from one element: the middle IS the element, the outer two are
 * its pseudos. Three spans would be tidier markup and worse CSS — this way the X
 * is two rotations of things that already exist.
 */
.pob-mnav__bars,
.pob-mnav__bars::before,
.pob-mnav__bars::after {
	display: block;
	inline-size: 24px;
	block-size: 2px;
	background: currentColor;
	border-radius: 2px;
	transition:
		transform 0.25s var(--wp--custom--ease--out, ease),
		opacity 0.15s linear;
}

.pob-mnav__bars::before,
.pob-mnav__bars::after {
	content: "";
	position: absolute;
	inset-inline-start: 10px;
}

.pob-mnav__bars::before {
	transform: translateY(-7px);
}

.pob-mnav__bars::after {
	transform: translateY(7px);
}

/*
 * ⚠ DARK REBUILD, 2026-07-17 — REMOVED, READ BEFORE RE-ADDING ANYTHING HERE.
 *
 * This used to force the toggle to `surface` (white, pre-flip) while the
 * header floated over a photo hero, with an elaborate `:not([open])` /
 * doubled-selector specificity fight (see the git history if you need the
 * blow-by-blow) purely to stop that override from ALSO recolouring the open
 * X-close control white-on-white. `surface` is now near-black, so the whole
 * override had inverted into forcing the closed hamburger dark-on-dark —
 * Toby: "the logo is still dark on a dark background", same root cause as
 * the header logo above. Every header state is dark now, the base
 * `.pob-mnav__toggle { color: ink }` rule above is already correct in every
 * state, and removing the override removes the reason the specificity fight
 * existed at all. If a genuinely light section ever comes back, redo this as
 * a scoped override rather than reinstating a rule that fights the default.
 */
.pob-mnav[open] .pob-mnav__toggle {
	position: fixed;
	inset-block-start: 14px;
	inset-inline-end: clamp(12px, 4vw, 28px);
	z-index: 210;
	color: var(--wp--preset--color--ink);
}

.pob-mnav[open] .pob-mnav__bars {
	/* The middle bar goes; the outer two cross. */
	background: transparent;
}

.pob-mnav[open] .pob-mnav__bars::before {
	transform: translateY(0) rotate(45deg);
}

.pob-mnav[open] .pob-mnav__bars::after {
	transform: translateY(0) rotate(-45deg);
}

/* ---------------------------------------------------------------
   The panel
   --------------------------------------------------------------- */

.pob-mnav__panel {
	position: fixed;
	inset: 0;
	z-index: 200;
	background: var(--wp--preset--color--surface);
	display: grid;
	/* head / scroller / foot — the scroller is the only row that flexes, which
	   is what keeps the phone number on screen at all times. */
	grid-template-rows: auto minmax(0, 1fr) auto;
	/* ⚠ dvh, not vh. On iOS Safari 100vh is the height WITHOUT the address bar,
	   so a vh panel is taller than the window and the foot sits under the
	   chrome — the phone number, permanently just off-screen, on the device this
	   menu exists for. */
	block-size: 100dvh;
	max-block-size: 100dvh;
	animation: pob-mnav-in 0.28s var(--wp--custom--ease--out, ease) both;
}

@keyframes pob-mnav-in {
	from {
		opacity: 0;
		transform: translateY(-8px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

.pob-mnav__head {
	padding: var(--wp--preset--spacing--30) var(--wp--custom--gutter);
	/* Clears the X, which is pinned at the same height on the right. */
	padding-inline-end: 72px;
	border-block-end: 1px solid var(--wp--preset--color--line);
}

.pob-mnav__logo {
	display: block;
	line-height: 0;
}

.pob-mnav__logo img {
	inline-size: 132px;
	block-size: auto;
}

/* ---------------------------------------------------------------
   The scroller — "fades at either end"
   --------------------------------------------------------------- */

/*
 * ⚠ A MASK, NOT TWO GRADIENT OVERLAYS.
 *
 * The obvious version is an absolutely-positioned ::before/::after with a white
 * gradient over each end. Those sit ON TOP of the list, so the top and bottom
 * ~20px of the menu stop taking taps — on a phone, on the first and last item.
 * A mask fades the element's own alpha, so there is nothing to intercept the
 * touch, and it works over any background rather than only over white.
 *
 * The stops are asymmetric on purpose: 12px at the top, where the head's
 * hairline already sits and a long fade reads as a rendering fault, and 28px at
 * the bottom, where the fade is doing real work — it is the only cue that there
 * is more list underneath the foot.
 */
.pob-mnav__scroll {
	overflow-y: auto;
	/* Stops a flick at the end of the list scrolling the page behind it. */
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
	padding: var(--wp--preset--spacing--40) var(--wp--custom--gutter);
	mask-image: linear-gradient(
		to bottom,
		transparent 0,
		#000 12px,
		#000 calc(100% - 28px),
		transparent 100%
	);
}

.pob-mnav__nav {
	display: grid;
	gap: var(--wp--preset--spacing--40);
}

.pob-mnav__list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
}

.pob-mnav__list a {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	padding-block: 0.6rem;
	color: var(--wp--preset--color--ink);
	text-decoration: none;
	font-weight: 600;
	/* 44px min target, met with padding rather than a fixed height so a
	   two-line label grows instead of clipping. */
	min-block-size: 44px;
}

.pob-mnav__list a:hover,
.pob-mnav__list a:focus-visible {
	color: var(--wp--preset--color--red-text);
}

/* The lead item (About) and the tail (Solar, Gallery, Case Studies,
   Testimonials) ARE the top-level menu, so they carry top-level weight. */
.pob-mnav__list--lead a,
.pob-mnav__list--rest a {
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 700;
	font-stretch: var(--wp--custom--display--stretch);
	letter-spacing: -0.02em;
	line-height: 1.15;
}

.pob-mnav__list--rest {
	border-block-start: 1px solid var(--wp--preset--color--line);
	padding-block-start: var(--wp--preset--spacing--30);
}

.pob-mnav__group {
	border-block-start: 1px solid var(--wp--preset--color--line);
	padding-block-start: var(--wp--preset--spacing--30);
}

.pob-mnav__grouph {
	margin: 0 0 0.35rem;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--wp--preset--color--red-text);
}

.pob-mnav__grouph a {
	color: inherit;
	text-decoration: none;
}

/* The service items sit UNDER a group heading, so they are the second tier and
   are sized as a list rather than as display type. */
.pob-mnav__list--icons a {
	font-family: var(--wp--preset--font-family--body);
	font-size: var(--wp--preset--font-size--medium);
	font-weight: 500;
	color: var(--wp--preset--color--muted);
}

.pob-mnav__list--icons a:hover,
.pob-mnav__list--icons a:focus-visible {
	color: var(--wp--preset--color--red-text);
}

/* Same size and weight as the mega menu's list icons — one drawing, two menus.
   See the stroke note on .pob-mega__list .pob-ico for why 2 and not 4. */
.pob-mnav__list--icons .pob-ico {
	inline-size: 20px;
	block-size: 20px;
	flex: none;
	stroke-width: 2;
	color: var(--wp--preset--color--ink);
}

/* Same rule as .pob-mega__list: indent the icon-less items to the text column
   so the list reads as a column of labels rather than a ragged edge. */
.pob-mnav__list--icons a:not(:has(.pob-ico)) {
	padding-inline-start: calc(20px + 0.75rem);
}

/* ---------------------------------------------------------------
   The foot — pinned, because the phone is the point
   --------------------------------------------------------------- */

.pob-mnav__foot {
	padding: var(--wp--preset--spacing--30) var(--wp--custom--gutter);
	/* The iPhone home indicator sits over the bottom ~34px. Without this the
	   email link is under it and untappable. */
	padding-block-end: max(var(--wp--preset--spacing--30), env(safe-area-inset-bottom));
	border-block-start: 1px solid var(--wp--preset--color--line);
	background: var(--wp--preset--color--surface-alt);
	text-align: center;
}

/* --red-text, not --brand-red: a solid fill with a white label, and white on
   #FF0000 is 4.00:1 and fails AA. The obvious red button is the broken one. */
/* Corrected 2026-07-17 — same red-text mix-up as .pob-submit above,
   reverted back to `surface`. */
.pob-mnav__cta {
	display: block;
	background: var(--wp--preset--color--red-text);
	color: var(--wp--preset--color--surface);
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--x-large);
	font-weight: 800;
	font-stretch: var(--wp--custom--display--stretch);
	text-decoration: none;
	border-radius: var(--wp--custom--radius--pill);
	padding: 0.7rem 1rem;
	white-space: nowrap;
	transition: background-color 0.22s var(--wp--custom--ease--out, ease);
}

.pob-mnav__cta:hover,
.pob-mnav__cta:focus-visible {
	background: var(--wp--preset--color--ink);
}

.pob-mnav__email {
	margin: var(--wp--preset--spacing--20) 0 0;
	font-size: var(--wp--preset--font-size--small);
}

.pob-mnav__email a {
	color: var(--wp--preset--color--muted);
}

.pob-mnav__foota {
	margin: 0.35rem 0 0;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
}

.pob-mnav__foota a {
	color: var(--wp--preset--color--red-text);
}

/*
 * Background scroll lock, set on <html> by app.js when the panel opens.
 * Enhancement: if the script never runs the page behind scrolls, which is
 * untidy and not broken. position:fixed on <body> would also work and would
 * throw away the scroll position.
 */
.pob-mnav-open {
	overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
	.pob-mnav__panel {
		animation: none;
	}

	.pob-mnav__bars,
	.pob-mnav__bars::before,
	.pob-mnav__bars::after,
	.pob-mnav__cta {
		transition: none;
	}
}

/* ===============================================================
   Mega menu — desktop only, no JS
   ===============================================================

   Read patterns/mega-menu.php's header before changing anything here. The two
   rules that matter are the :hover/:focus-within pair and the visibility (NOT
   display) that the panel is hidden with. Swap that to display:none and the
   mouse still works — so it looks fine — while the menu becomes completely
   unreachable by keyboard, because focus can never enter a display:none subtree
   to trigger :focus-within. Nothing in the test pass catches that.

   THE 1200px SWAP. Below 1200 this menu does not exist and core/navigation's
   overlay does the whole job, untouched — see the long note by the overlay
   rules above for why that path is left strictly alone (it was recently a blank
   white screen). Above 1200 the core nav's inline links are hidden and this
   replaces them. Exactly one of the two is ever active; they never overlap.
   1200 is theme.css's own measured breakpoint, reused deliberately rather than
   picked again — it is where the header row genuinely fits. */

.pob-mega {
	display: none;
}

@media (min-width: 1200px) {
	/* ⚠ BOTH OF THESE ARE NEEDED. STRETCHING ONLY .pob-mega GETS YOU HALFWAY.
	 * .pob-header__inner is a flex row with verticalAlignment:center, so
	 * .pob-header__right is only as tall as ITS content (55px) and sits centred
	 * in the 82px row. align-self:stretch on .pob-mega therefore stretched it to
	 * 55px, not 82 — the panel moved down 8px and still clipped the logo by 7.
	 * Measured at each step rather than assumed: the chain from the row to the
	 * <li> has to stretch at every link or the last one inherits a short parent.
	 * .pob-header__right keeps align-items:center internally, so the phone
	 * button does not move. */
	.pob-header__right {
		align-self: stretch;
	}

	.pob-mega {
		display: block;

		/* ⚠ THE MENU MUST BE AS TALL AS THE HEADER ROW, AND IT IS NOT COSMETIC.
		 * .pob-header__right is a flex row with verticalAlignment:center, so by
		 * default this box is only as tall as its own text (~20px) while the
		 * header row is 82px (the LOGO sets that height, not the nav).
		 *
		 * The panel hangs off the <li> at `top: 100%`. With a 20px-tall <li>
		 * that put the panel's top edge 15px ABOVE the bottom of the logo, so
		 * an open menu sliced the logo in half. Measured, not eyeballed: panel
		 * top 83, brand bottom 98.
		 *
		 * Stretching to the row's height fixes both halves of that at once:
		 *  - the panel now hangs from the BOTTOM of the row, clearing the logo;
		 *  - and the dead space between the link and the panel is now INSIDE the
		 *    <li>, so :hover holds while the pointer travels down through it.
		 * Pushing the panel down with a magic offset would have fixed the
		 * overlap and broken the hover, because that space would still have
		 * been outside the <li>. */
		align-self: stretch;
	}

	/* The core nav is the mobile path only from here up. Its hamburger is
	   already hidden at this width by core's own query; this hides the inline
	   links it would otherwise render beside the mega menu.
	 *
	 * ⚠ THE `.pob-header` PREFIX IS LOAD-BEARING. DO NOT SIMPLIFY IT AWAY.
	 * A bare `.pob-header__links { display: none }` is (0,1,0) and LOSES to
	 * core's own layout rule:
	 *
	 *     body .is-layout-flex { display: flex }        <- (0,1,1), inline
	 *
	 * The nav block carries .is-layout-flex (its layout is flex), so core won
	 * on specificity and BOTH navs rendered side by side at >=1200px — the mega
	 * menu and the eight inline links it was supposed to replace. Measured in
	 * the browser, not guessed: getComputedStyle said `flex` while the rule
	 * plainly said `none`.
	 *
	 * This is the same family as the trap recorded against core's
	 * `:root :where(.is-layout-flow) > * { margin-block-start: 1.5rem }` — a
	 * core layout rule with more specificity than it looks like it has, quietly
	 * beating a theme rule that reads as obviously correct. Two classes (0,2,0)
	 * clears (0,1,1) without resorting to !important. */
	.pob-header .pob-header__links {
		display: none;
	}
}

.pob-mega__bar {
	display: flex;
	/* stretch, not center — see align-self on .pob-mega above. The items must
	   reach the bottom of the header row; each one centres its own link below. */
	align-items: stretch;
	block-size: 100%;
	gap: var(--wp--preset--spacing--40);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* Each item is now a full-height flex box that centres its own link. The link
   looks identical to before; the item around it is what grew. */
.pob-mega__bar > li {
	position: relative;
	display: flex;
	align-items: center;
	margin: 0;
}

.pob-mega__bar a {
	color: var(--wp--preset--color--ink);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	text-decoration: none;
}

/* ⚠ DARK REBUILD, 2026-07-17 — REMOVED, same bug as the header logo and the
   mobile toggle above: this forced the bar to `surface` over a photo hero,
   which was white pre-flip and is near-black now, i.e. dark-on-dark on the
   one background guaranteed to be dark. The base `.pob-mega__bar a { color:
   ink }` rule above is already correct in every header state. */

.pob-mega__top {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
}

/* A caret drawn on the logo's own 24.3° pitch (rise 9 / run 20), inverted —
   the same geometry as inc/icons.php, so it belongs to the family rather than
   being a generic triangle. currentColor, so it follows the bar over a photo. */
.pob-mega__caret {
	width: 9px;
	height: 5px;
	background: currentColor;
	clip-path: polygon(0 0, 100% 0, 50% 100%);
	transition: transform 0.2s var(--wp--custom--ease--pop, ease);
}

.pob-mega__has:hover .pob-mega__caret,
.pob-mega__has:focus-within .pob-mega__caret {
	transform: rotate(180deg);
}

/* ⚠ visibility + opacity, NEVER display:none. See the header of this section. */
.pob-mega__panel {
	position: absolute;
	inset-block-start: calc(100% + 0.75rem);
	inset-inline-start: 50%;
	transform: translateX(-50%) translateY(-6px);
	z-index: 100;
	visibility: hidden;
	opacity: 0;
	transition:
		opacity 0.18s ease-out,
		transform 0.18s var(--wp--custom--ease--pop, ease),
		visibility 0s linear 0.18s;
	background: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--ink);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius, 4px);
	/* 14% read fine as a shadow on the old white page; on the new dark page
	   the panel and its shadow are both already near-black, so the old
	   opacity was nearly invisible. Bumped so the panel still visibly lifts
	   off the page behind it. */
	box-shadow: 0 24px 60px rgb(0 0 0 / 55%);
	padding: var(--wp--preset--spacing--50);
	/* Wider than the viewport is impossible below 1200 because the menu does
	   not render there; 92vw is the guard for a 1200px window with gutters. */
	width: min(1040px, 92vw);
}

/* The bridge. Without it the 0.75rem gap between the bar and the panel is dead
   space: the pointer leaves the <li>, :hover drops, and the panel closes before
   you reach it. A transparent ::before spanning the gap keeps the <li> hovered
   the whole way down. Cheaper and more reliable than a JS close-delay timer,
   and it cannot get stuck open. */
.pob-mega__has::before {
	content: "";
	position: absolute;
	inset-block-start: 100%;
	inset-inline: 0;
	block-size: 0.75rem;
}

.pob-mega__has:hover .pob-mega__panel,
.pob-mega__has:focus-within .pob-mega__panel {
	visibility: visible;
	opacity: 1;
	transform: translateX(-50%) translateY(0);
	transition-delay: 0s;
}

.pob-mega__cols {
	display: grid;
	grid-template-columns: 1.15fr 1fr 0.8fr 1fr;
	gap: var(--wp--preset--spacing--50);
}

.pob-mega__h {
	margin: 0 0 var(--wp--preset--spacing--20);
	font-family: var(--wp--custom--display, inherit);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--red-text);
}

.pob-mega__h a {
	color: inherit;
}

.pob-mega__list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	gap: 2px;
}

.pob-mega__list a {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	padding: 0.4rem 0.5rem;
	margin-inline-start: -0.5rem;
	border-radius: var(--wp--custom--radius, 4px);
	font-weight: 500;
	color: var(--wp--preset--color--ink);
	transition: background-color 0.15s ease, color 0.15s ease;
}

.pob-mega__list a:hover,
.pob-mega__list a:focus-visible {
	background: var(--wp--preset--color--surface-alt);
	color: var(--wp--preset--color--red-text);
}

/*
 * The icon keeps its own 48-unit viewBox — so the same paths that render at
 * 64px on the service cards render here with no second drawing.
 *
 * ⚠ STROKE WEIGHT HERE IS NOT A FREE NUMBER. It was 4, and that was too heavy:
 * .pob-ico__p sets vector-effect:non-scaling-stroke, so stroke-width is in
 * DEVICE pixels and does NOT shrink with the box. A 4px stroke on a 20px icon
 * is 20% of the icon's own height, against 3px on 64px (4.7%) for the identical
 * paths on the home page. The multi-part icons are what exposed it: tiling is
 * three stacked chevrons 9 user-units apart, which at 20px is ~3.75px of gap
 * being crossed by two 4px strokes — the courses merged into a solid red-brown
 * block and the whole set read as bunched and blobby. Toby's note ("the icons
 * look bunched together, the outline is too thick — thinner, like the home
 * page") is this, exactly.
 *
 * 2 is the floor, not a preference: it is the lightest weight that survives a
 * non-retina screen, which is the constraint the original 4 was over-correcting
 * for. Matching the home page's 4.7% ratio outright would mean ~0.94px, which
 * is the hairline that disappears — so this splits the difference honestly and
 * lets the gaps read. Go lower and check it on a 1x display first.
 */
.pob-mega__list .pob-ico {
	inline-size: 20px;
	block-size: 20px;
	flex: none;
	stroke-width: 2;
}

/* Two of eleven menu items have no icon on purpose (Workmanship, Site Setup,
   Housing Associations — inc/icons.php says why). They are indented to the same
   text column as the rest so the list still reads as a column of labels rather
   than a ragged edge. The icon is decoration; the label is the menu. */
.pob-mega__list a:not(:has(.pob-ico)) {
	padding-inline-start: calc(0.5rem + 20px + 0.6rem);
}

.pob-mega__col--jobs {
	border-inline-start: 1px solid var(--wp--preset--color--line);
	padding-inline-start: var(--wp--preset--spacing--50);
}

.pob-mega__job {
	gap: 0.7rem;
	align-items: center;
	font-size: var(--wp--preset--font-size--small);
	line-height: 1.3;
}

.pob-mega__job img {
	inline-size: 64px;
	block-size: 48px;
	object-fit: cover;
	border-radius: 3px;
	flex: none;
}

/* ===============================================================
   Case studies — job cards, filter, single
   =============================================================== */

/* auto-fit, like .pob-services and for the same reason recorded there: a fixed
   3-up grid becomes three stacked full-width screens on a phone. This settles
   at 3 / 2 / 1 by available width. */
.pob-jobs {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
	gap: var(--wp--preset--spacing--50);
	margin-block-start: var(--wp--preset--spacing--50);
}

.pob-job {
	margin: 0;
}

.pob-job__link {
	display: flex;
	flex-direction: column;
	block-size: 100%;
	text-decoration: none;
	color: inherit;
	background: var(--wp--preset--color--surface);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius, 4px);
	overflow: hidden;
	transition: transform 0.2s var(--wp--custom--ease--pop, ease), box-shadow 0.2s ease;
}

.pob-job__link:hover,
.pob-job__link:focus-visible {
	transform: translateY(-3px);
	/* Same dark-page reasoning as .pob-mega__panel above: 12% was tuned for
	   a white page and reads as almost nothing now the card and page are
	   both dark. */
	box-shadow: 0 14px 34px rgb(0 0 0 / 45%);
}

.pob-job__media {
	aspect-ratio: 4 / 3;
	overflow: hidden;
	background: var(--wp--preset--color--surface-alt);
}

.pob-job__media img {
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
}

.pob-job__body {
	padding: var(--wp--preset--spacing--40);
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	flex: 1;
}

.pob-job__title {
	margin: 0;
	font-size: var(--wp--preset--font-size--medium);
	line-height: 1.25;
}

.pob-job__desc {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: var(--wp--preset--font-size--small);
}

/* Pushed to the bottom of the card so a two-line title and a four-line title
   still line their CTAs up across a row. */
.pob-job__more {
	margin-block-start: auto;
	padding-block-start: 0.4rem;
	font-weight: 700;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--red-text);
}

.pob-jobs__all {
	margin-block-start: var(--wp--preset--spacing--50);
	font-weight: 700;
}

/*
 * The developments highlight grid — the SAME card as the case studies above,
 * with two differences that both follow from one fact: a development has no
 * detail page to go to (PLAN §6 #4 is open on whether these become a CPT).
 *
 * So .pob-job__link here is a <div>, not an <a>. It must therefore not behave
 * like a link: no lift on hover, no pointer, no "Read more". A card that looks
 * clickable and is not is worse than a card that plainly is not.
 *
 * Everything else — the border, the radius, the 4:3 media, the body padding —
 * is inherited from .pob-job unchanged, which is the point. Two grids on one
 * site, one card.
 */
.pob-jobs--devs .pob-job__link {
	cursor: default;
}

.pob-jobs--devs .pob-job__link:hover {
	transform: none;
	box-shadow: none;
}

/* The units line sits between the title and the description inside the card;
   on the hairline list it is a standalone element. Same token, tighter here. */
.pob-jobs--devs .pob-dev__units {
	margin-block: 0;
}

.pob-jobs--devs .pob-dev__status {
	margin-block-start: auto;
}

/* Filter chips. Real links to real term archives — see
   patterns/case-study-archive.php on why there is no "Solar" chip and why the
   empty terms are absent rather than disabled. */
.pob-filter__list {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin: 0;
	padding: 0;
	list-style: none;
}

.pob-filter__chip {
	display: inline-block;
	padding: 0.4rem 0.9rem;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: 999px;
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	text-decoration: none;
	color: var(--wp--preset--color--ink);
	transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.pob-filter__chip:hover,
.pob-filter__chip:focus-visible {
	border-color: var(--wp--preset--color--ink);
}

/* aria-current is the state, so it is also the style hook — the two cannot
   drift apart, which they would if this were a separate .is-active class. */
.pob-filter__chip[aria-current="page"] {
	background: var(--wp--preset--color--ink);
	border-color: var(--wp--preset--color--ink);
	color: var(--wp--preset--color--surface);
}

.pob-case-single {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: var(--wp--preset--spacing--60);
}

/* The fact box goes beside the narrative only when there is room for both. At
   960px it drops under it — a 5-row label/value list in a 200px column wraps
   every value onto its own line and stops being a table. */
@media (min-width: 960px) {
	.pob-case-single {
		grid-template-columns: minmax(0, 1fr) 260px;
		align-items: start;
	}
}

.pob-casemeta {
	background: var(--wp--preset--color--surface-alt);
	border-radius: var(--wp--custom--radius, 4px);
	padding: var(--wp--preset--spacing--40);
}

.pob-casemeta .pob-facts__h {
	margin-block-start: 0;
	font-size: var(--wp--preset--font-size--medium);
}

.pob-casemeta .pob-facts__h ~ .pob-facts__h {
	margin-block-start: var(--wp--preset--spacing--40);
}

.pob-casemeta__terms {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 0.4rem;
}

.pob-casemeta__terms a {
	display: inline-block;
	padding: 0.25rem 0.7rem;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: 999px;
	background: var(--wp--preset--color--surface);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	text-decoration: none;
	color: var(--wp--preset--color--ink);
}

/* ===============================================================
   Service page body — spec cards, chips, in-content photo
   ===============================================================

   These service pages were a single column of prose. The content was never the
   problem — patterns/service-body.php's header explains that at length: Tiling
   & Slating is six paragraphs, one per covering type, and it was being rendered
   as one wall of text. This is the layout catching up with the copy.

   ⚠ REUSES .pob-service, IT DOES NOT REDRAW IT. The card here is the SAME
   component as the home page's numbered services grid — same red top rule, same
   display-type number, same title/desc. That is deliberate: the alternative is a
   second card style that nearly matches the first, which is exactly the "two
   different builds" problem the button transitions had. --spec only changes
   what has to change (no icon row, tighter columns for longer copy). */

.pob-services--spec {
	/* Wider than the home grid's 260px: these carry a full paragraph each, not a
	   one-line description, and at 260 a six-card row turns into six narrow
	   columns of ragged text. */
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
	gap: var(--wp--preset--spacing--50);
	margin-block: var(--wp--preset--spacing--50);
}

/* No icon in this head — just the number. The head exists to hold the red rule
   and the numeral; on the home page it also holds a drawing, here it does not,
   because there is no icon vocabulary for "concrete double roman" and inventing
   one is the thing inc/icons.php refuses to do. */
.pob-services--spec .pob-service__head {
	margin-block-end: var(--wp--preset--spacing--20);
}

.pob-services--spec .pob-service__desc {
	font-size: var(--wp--preset--font-size--medium);
	color: var(--wp--preset--color--ink);
}

/* A paragraph with no matching label (see the renderer) — it must still read as
   body copy rather than as a broken card. */
.pob-services__orphan {
	margin-block: var(--wp--preset--spacing--30);
}

/*
 * The Workmanship page's two figures, inline in the prose rather than on an ink
 * band. Same component as the home page's stat row (.pob-stats), so the numbers
 * are the same object in both places — but on white, because this one sits
 * inside a page body rather than being a full-bleed section of its own.
 *
 * Which means brand red is correct here and is NOT correct on the ink band: at
 * --display size this renders well over the 24px large-text threshold, where
 * #FF0000's 4.00:1 clears the 3:1 bar. .pob-band-ink flips it to white for the
 * opposite reason (red drops to ~1.9:1 on ink). Two grounds, two rules, one
 * component.
 */
.pob-stats--inline {
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
	margin-block: var(--wp--preset--spacing--60);
}

/* ---------------------------------------------------------------
   Chips — short list items, e.g. Industrial Roofing's nine services
   --------------------------------------------------------------- */

/*
 * One-line product names ("Box profile cladding", "Trims and flashings", "Metal
 * Z section purlins"...) rendered as a bulleted column is a list of things the
 * eye has to walk down. As chips they are a set you take in at once, which is
 * what that copy actually is.
 *
 * Used by: industrial-roofing (9), housing-associations (9 school names), and
 * external-wall-insulation (7 benefits + 20 survey items).
 *
 * ⚠ APPLIED BY CONTENT LENGTH, NOT BY A PER-PAGE FLAG — the renderer adds
 *   .pob-chips only when every item is <= 48 characters, so a list of sentences
 *   stays a real list (a 30-word chip is a paragraph with a border round it).
 *   As of 2026-07-16 EVERY list on the site qualifies, so nothing exercises the
 *   fallback — it is a guard for future content, not a switch in use. The full
 *   audit and the reason the threshold is 48 and not 40 are in
 *   patterns/service-body.php at the test itself; don't duplicate them here.
 */
.pob-chips {
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin-block: var(--wp--preset--spacing--40);
	padding: 0;
}

.pob-chips li {
	margin: 0;
	padding: 0.45rem 0.9rem;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--pill);
	background: var(--wp--preset--color--surface-alt);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
}

/* ---------------------------------------------------------------
   In-content service photograph
   --------------------------------------------------------------- */

/*
 * One per service, showing the TRADE — never a job. AI-generated and
 * decorative; the alt text describes what is in the frame and claims nothing
 * about who built it. See the rule at the top of this file.
 */
.pob-serviceshot {
	margin-block: var(--wp--preset--spacing--60);
}

.pob-serviceshot img {
	inline-size: 100%;
	block-size: auto;
	display: block;
	border-radius: var(--wp--custom--radius--card);
	/* The generated details are 16:9 and some are busier than others; a fixed
	   ratio keeps the page rhythm even when a future file is a different shape. */
	aspect-ratio: 16 / 9;
	object-fit: cover;
}

/* The EWI page's named contact — Steve Tuckfield. Given a box because it is the
   page's own call to action and the only direct line to a person on the site;
   read the ⚠ in patterns/service-body.php before touching the number. */
.pob-servicecontact {
	margin-block-start: var(--wp--preset--spacing--50);
	padding: var(--wp--preset--spacing--40);
	background: var(--wp--preset--color--surface-alt);
	border-inline-start: 3px solid var(--wp--preset--color--brand-red);
	border-radius: var(--wp--custom--radius, 4px);
	font-weight: 600;
}

/* ===============================================================
   Roofing hub — linked service cards
   =============================================================== */

/* The whole card is the link (patterns/roofing-breakdown.php explains why the
   <h3> lives inside it), so the <a> has to fill the card rather than sit in it,
   or only the text is clickable and the padding is dead. */
.pob-services--linked .pob-service {
	padding: 0;
}

.pob-service__link {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	block-size: 100%;
	padding: var(--wp--preset--spacing--40);
	text-decoration: none;
	color: inherit;
	border-radius: var(--wp--custom--radius, 4px);
	transition: transform 0.2s var(--wp--custom--ease--pop, ease), background-color 0.2s ease;
}

.pob-service__link:hover,
.pob-service__link:focus-visible {
	transform: translateY(-3px);
	background: var(--wp--preset--color--surface-alt);
}

.pob-service__more {
	margin-block-start: auto;
	padding-block-start: 0.6rem;
	font-weight: 700;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--red-text);
}

/* Two of the eight cards have no icon (Workmanship, Site Setup — inc/icons.php
   refuses to draw a quality claim). min-height keeps their number on the same
   baseline as the iconed cards' numbers, so the row still scans as a row.
   48px is the icon box; the head is not given a fixed height beyond that. */
.pob-services--linked .pob-service__head {
	min-block-size: 48px;
	align-items: center;
}

/* Reduced motion for the components added with the services tier.
 *
 * motion.css's reduced-motion block already covers [data-reveal], [data-stagger]
 * > * and .pob-ico__p, which is what the new sections' entrance animations hook
 * into — so those are handled. These are the HOVER transitions, which are ours
 * and are not reveals, so nothing above reaches them.
 *
 * The panel still OPENS — visibility and opacity flip instantly instead of
 * fading. Reduced motion means less movement, not less menu; removing the
 * transition is right, removing the disclosure would break the nav for the
 * people who asked for fewer animations. Same reasoning for the cards: they
 * stop lifting, they do not stop being links. */
@media (prefers-reduced-motion: reduce) {
	.pob-mega__panel,
	.pob-mega__caret,
	.pob-job__link,
	.pob-service__link,
	.pob-filter__chip {
		transition: none;
	}

	.pob-mega__has:hover .pob-mega__caret,
	.pob-mega__has:focus-within .pob-mega__caret {
		transform: none;
	}

	.pob-job__link:hover,
	.pob-job__link:focus-visible,
	.pob-service__link:hover,
	.pob-service__link:focus-visible {
		transform: none;
	}
}

/* ===============================================================
   System diagrams — added 2026-07-16
   ===============================================================

   Toby: "visually high end on roofing and solar in particular, graphics or
   animations to show or highlight certain elements... think process,
   build-up layers, system diagrams."

   Three diagrams, one convention each already established elsewhere in this
   file, none of them a new animation system:

     .pob-buildup   flat-roofs' layer stack     — reveals via [data-stagger],
                                                    the existing mechanism.
     .pob-roofline  upvc-goods' eaves section   — a single data-reveal fade,
                                                    same as any other figure.
     .pob-services--flow  solar's four steps    — .pob-service AS-IS, see the
                                                    note at the rule itself.

   No new JS, no new observer, no new easing curve. */

/* ---------------------------------------------------------------
   Flat roof build-up — patterns/service-body.php, flat-roofs only
   --------------------------------------------------------------- */

.pob-buildup {
	margin-block: var(--wp--preset--spacing--60);
}

.pob-buildup__caption {
	margin: 0 0 var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

/* column-reverse: the FIRST dom child (the deck) renders at the visual
   BOTTOM, so reading order, numbering order and construction order all stay
   the same sequence — see the long note in service-body.php. */
.pob-buildup__stack {
	display: flex;
	flex-direction: column-reverse;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--card);
	overflow: hidden;
}

.pob-buildup__layer {
	display: flex;
	align-items: baseline;
	gap: var(--wp--preset--spacing--30);
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
	background: var(--wp--preset--color--surface);
	border-block-start: 1px solid var(--wp--preset--color--line);
}

/* The structural layer everything else sits on — drawn heavier, top border
   removed since overflow:hidden on the container already rounds this corner
   and a hairline here would sit against nothing. */
.pob-buildup__layer--deck {
	padding-block: var(--wp--preset--spacing--40);
	border-block-start: none;
	font-weight: 600;
}

/* Conditional on the source's own word "If" — see service-body.php. Dashed
   rather than solid, and a muted background, so the two "+" rows read as an
   add-on rather than a fifth and sixth step in the numbered sequence. */
.pob-buildup__layer--optional {
	background: var(--wp--preset--color--surface-alt);
	border-block-start-style: dashed;
	color: var(--wp--preset--color--muted);
}

/* The felt's own cap sheet — "the finish is green mineral". Brand red is
   correct here: this is a decorative rule, not text, which is the ground
   this file's colour discipline note at the top draws the line on. Same
   motif as .pob-service / .pob-stats / .pob-office's top rule, reused. */
.pob-buildup__layer--finish {
	border-block-end: var(--wp--custom--rule--thick) solid var(--wp--preset--color--brand-red);
}

.pob-buildup__num {
	flex: none;
	inline-size: 1.75rem;
	block-size: 1.75rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 50%;
	background: var(--wp--preset--color--surface-alt);
	color: var(--wp--preset--color--red-text);
	font-family: var(--wp--preset--font-family--display);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
}

.pob-buildup__layer--optional .pob-buildup__num {
	background: var(--wp--preset--color--surface);
	border: 1px dashed var(--wp--preset--color--line);
}

.pob-buildup__label {
	font-weight: 600;
}

.pob-buildup__label em {
	font-style: normal;
	font-weight: 400;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

.pob-buildup__note {
	margin: var(--wp--preset--spacing--30) 0 0;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
}

/* ---------------------------------------------------------------
   UPVC roofline — patterns/service-body.php, upvc-goods only
   --------------------------------------------------------------- */

.pob-roofline {
	margin-block: var(--wp--preset--spacing--60);
}

.pob-roofline__caption {
	margin: 0 0 var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

.pob-roofline__diagram {
	position: relative;
	inline-size: min(100%, 340px);
	aspect-ratio: 1 / 1;
	margin-inline: auto;
}

/*
 * A scaling stroke, deliberately NOT the icon set's non-scaling-stroke: this
 * is drawn once at diagram size, not shared at multiple sizes, so the stroke
 * is sized in the same 48-unit space as the geometry and grows with it —
 * the crisp, thin line of a section drawing rather than a magnified icon.
 */
.pob-roofline__svg {
	display: block;
	inline-size: 100%;
	block-size: 100%;
	color: var(--wp--preset--color--ink);
	fill: none;
	stroke: currentColor;
	stroke-width: 0.45;
	stroke-linecap: round;
	stroke-linejoin: round;
}

.pob-roofline__tag {
	position: absolute;
	transform: translate(-50%, -50%);
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	padding: 0.3rem 0.7rem 0.3rem 0.3rem;
	background: var(--wp--preset--color--surface);
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--radius--pill);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 600;
	white-space: nowrap;
	/* Same dark-page reasoning as .pob-mega__panel: 8% was tuned for white. */
	box-shadow: 0 2px 8px rgb(0 0 0 / 35%);
}

/* Corrected 2026-07-17 — same red-text mix-up as .pob-submit above,
   reverted back to `surface`. */
.pob-roofline__tag i {
	flex: none;
	inline-size: 1.25rem;
	block-size: 1.25rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 50%;
	background: var(--wp--preset--color--red-text);
	color: var(--wp--preset--color--surface);
	font-style: normal;
	font-size: 0.7rem;
	font-weight: 700;
}

/* Positions approximate the 48-unit path coordinates each tag marks — the
   fascia's vertical face, the soffit's horizontal run, the gutter's bowl —
   as percentages of the square diagram box. Decorative pointers, not a
   measurement; a few percent either way reads exactly the same. */
.pob-roofline__tag--1 {
	inset-inline-start: 56%;
	inset-block-start: 46%;
}

.pob-roofline__tag--2 {
	inset-inline-start: 34%;
	inset-block-start: 66%;
	transform: translate(-50%, -50%);
}

.pob-roofline__tag--3 {
	inset-inline-start: 82%;
	inset-block-start: 68%;
	transform: translate(-65%, -50%);
}

.pob-roofline__note {
	margin: var(--wp--preset--spacing--40) 0 0;
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--muted);
	text-align: center;
}

/* ---------------------------------------------------------------
   Solar flow — patterns/solar-body.php, the four-step "how it works"
   --------------------------------------------------------------- */

/*
 * .pob-service AS-IS — same top rule, same number, same title/desc, same
 * icon slot the home page's numbered services and roofing-breakdown.php's
 * hub cards already use. --flow only removes the anchor styling those two
 * apply via .pob-service__link, because these four steps carry no href, and
 * widens the minimum column so four short steps sit four-up on a wide
 * screen instead of matching the 260px card grid's column count by
 * coincidence.
 */
.pob-services--flow {
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

/* ===================================================================
   DARK REBUILD — 2026-07-17
   ===================================================================
   Toby: "get the look of it as close to this one [paulobrien-roofing.
   vercel.app] but with the adjustments noted above" — dark theme, red not
   blue, more animation, a home-only loading screen, a bigger hero.

   The dark theme ITSELF needed almost none of this section: theme.json's
   seven colour tokens (surface/surface-alt/ink/muted/line/brand-red/
   red-text) drive nearly the whole site already, so flipping their VALUES
   (see theme.json) cascades through all 30 existing patterns for free. This
   section is everything that's genuinely NEW: the bigger home hero's extra
   elements, the preloader, the custom cursor, the marquee, and the pinned
   horizontal gallery. Contrast maths for the new/changed tokens are recorded
   in theme.json's own colour names; nothing below re-derives them.
   =================================================================== */

/* ---------------------------------------------------------------
   Hero — home-only additions (head / floor / card / aside)
   ---------------------------------------------------------------
   .pob-hero, .pob-hero__media, .pob-hero__scrim, .pob-hero__inner and the
   base .pob-hero__pill / __title / __rule / __lead / __meta / __actions
   rules above are UNCHANGED and still shared with every other hero variant.
   Everything here is new structure that exists ONLY inside
   .pob-hero--home (see patterns/hero.php) — nothing here reaches the
   about/roofing/service/etc heroes. */

.pob-hero--home .pob-hero__inner {
	display: flex;
	flex-direction: column;
	justify-content: space-between;
	min-height: calc(100% - var(--wp--custom--header--height));
	gap: var(--wp--preset--spacing--50);
}

/*
 * ⚠ .pob-hero__roofmark IS GONE — 2026-07-18. It was a large faded chevron
 * behind the headline, added when the roof graphic was moved out of the hero
 * stack. Toby saw it live and cut it: "I can see that roofing graphic in the
 * background on the hero on homepage. I don't like that really, so just lose
 * that now." patterns/hero.php no longer renders it.
 */

/*
 * ⚠ EVERYTHING ABOVE THE FOLD ON DESKTOP. Toby, 2026-07-18: "the homepage
 * hero seems to be just above the viewport height on desktop, which is quite
 * annoying. We should be able to fit all that content in within the initial
 * view... even if that means making the text a little bit smaller or actually
 * just losing that roof graphic above the hero text."
 *
 * ⚠ DO NOT SOLVE THIS WITH `max-height`. That was the first attempt and it
 * made things worse in a way worth recording: .pob-hero is a flex container
 * with `align-items: center`, so capping its height did not shrink the
 * content — it overflowed, and a centred flex item overflows EQUALLY IN BOTH
 * DIRECTIONS. Measured: content 905px in a 712px box, so the head rode 105px
 * upward and the "Roofing & Building" pill ended up underneath the fixed
 * header. Clipping the hero hid the symptom at the bottom while creating a
 * collision at the top.
 *
 * The fix is to make the content genuinely fit, plus one structural guard:
 *   - align-items: flex-start on this variant, so if content ever does
 *     exceed the box it grows DOWNWARD and can never reach back up into the
 *     header. Scoped to --home; every other hero keeps centring.
 *   - The roof graphic above the headline is gone (relocated to the watermark
 *     above) — the single biggest item in the stack and a duplicate of the
 *     header logo directly over it.
 *   - Top padding reduced: the old value cleared the header twice over.
 *   - The card's two CTAs sit side by side rather than stacked, and the card
 *     is less padded.
 *   - The headline comes down a little at the top end. Toby offered this
 *     ("making the text a little bit smaller") and at 7rem it was the second
 *     biggest item after the roof.
 * No max-height, so a genuinely short viewport still scrolls rather than
 * silently clipping the CTA.
 */
@media (min-width: 861px) {
	.pob-hero--home {
		align-items: flex-start;
		padding-block-start: calc(var(--wp--custom--header--height) + var(--wp--preset--spacing--40));
	}

	.pob-hero--home .pob-hero__title {
		font-size: clamp(2.75rem, 6vw, 5.5rem);
	}

	.pob-hero--home .pob-hero__card {
		padding: var(--wp--preset--spacing--40);
	}

	/* Side by side, not stacked — saves a whole button's height plus its gap,
	   and a phone number next to a quote button reads as two routes to the
	   same thing rather than a list. */
	.pob-hero--home .pob-hero__card .wp-block-buttons {
		display: flex;
		flex-wrap: nowrap;
		gap: var(--wp--preset--spacing--30);
	}

	.pob-hero--home .pob-hero__card .wp-block-button {
		width: auto;
		flex: 1 1 auto;
	}

	.pob-hero--home .pob-hero__card .wp-block-button__link {
		width: 100%;
		white-space: nowrap;
	}
}

/*
 * ⚠ NO max-width HERE — read before adding one back.
 * A previous version capped this at `min(74%, 22ch)`. `ch` resolves against
 * THIS element's own font-size, not the H1's — .pob-hero__head itself is
 * ~17px (inherited body size), while .pob-hero__title is up to 109px
 * (--wp--preset--font-size--hero), so 22ch computed to ~235px: a container
 * roughly a third the width of one capital letter's line-height at that
 * font size. "Paul O'Brien" wrapped inside it across 2-3 internal lines,
 * and .pob-line-mask's overflow:hidden (needed for the GSAP reveal) then
 * clipped every line but the first, rendering as "PAUL / O'BR / ROO".
 * No width cap is actually needed: hero.php hand-authors the title as two
 * fixed lines already (see that file's comment), .pob-hero__lead caps
 * itself at 34ch, and .pob-hero__inner already caps the whole column at
 * --wp--style--global--wide-size. Nothing here needs a narrower box.
 */

/* Word/line reveal masks — motion-gsap.js's hero timeline targets
   .pob-line-inner directly. No resting-hidden CSS state is set here on
   purpose: if GSAP never runs, these are just two ordinary lines of text,
   already visible, which is the correct no-JS fallback for free. */
.pob-line-mask {
	display: block;
	overflow: hidden;
	padding-block-end: 0.04em;
}

.pob-line-inner {
	display: block;
}

/* The second line of the home H1 — "Roofing" — carries the accent, echoing
   the reference's gradient-highlighted line without the fragility of a
   background-clip:text gradient sitting over a photo scrim. Solid red-text
   (not brand-red): this sits directly on the hero photo's dark scrim, and
   red-text is the AAA-on-dark variant (7.20:1) — see theme.json. */
.pob-hero--home .pob-hero__title-accent {
	color: var(--wp--preset--color--red-text);
}

.pob-hero__floor {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--60);
	flex-wrap: wrap;
}

/* ---- the floating quote-card ---- */

/* ⚠ Fixed 2026-07-17: shipped as `surface` originally, meaning "light text" —
   a leftover reflex from the pre-flip codebase where surface WAS the light
   token. It is the dark one now; `ink` is what these two need. Same class of
   bug Toby found sitewide ("logo dark on dark"), just introduced fresh here
   rather than inherited. */
.pob-hero__card {
	width: clamp(290px, 32vw, 420px);
	background: rgb(20 20 22 / 62%);
	backdrop-filter: blur(18px) saturate(140%);
	-webkit-backdrop-filter: blur(18px) saturate(140%);
	border: 1px solid var(--wp--custom--line-strong);
	border-radius: var(--wp--custom--radius--card);
	padding: var(--wp--preset--spacing--50);
	color: var(--wp--preset--color--ink);
}

.pob-hero__card p {
	font-size: var(--wp--preset--font-size--medium);
	line-height: 1.5;
	color: var(--wp--preset--color--ink);
	opacity: 0.86;
	margin: 0 0 var(--wp--preset--spacing--40);
}

.pob-hero__card-top {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--30);
	margin-block-end: var(--wp--preset--spacing--40);
}

.pob-hero__card-top span {
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	letter-spacing: 0.16em;
	text-transform: uppercase;
	color: var(--wp--preset--color--red-text);
	white-space: nowrap;
}

/* Purely decorative — echoes the reference's progress-bar motif under the
   card eyebrow. Asserts no percentage, no claim; it's a fixed-width fill. */
.pob-hero__card-bar {
	flex: 1;
	height: 3px;
	border-radius: 3px;
	background: rgb(255 255 255 / 12%);
	overflow: hidden;
	display: block;
}

.pob-hero__card-bar::after {
	content: "";
	display: block;
	height: 100%;
	width: 38%;
	background: var(--wp--preset--color--brand-red);
	border-radius: 3px;
}

.pob-hero__card .wp-block-buttons {
	margin: 0;
}

.pob-hero__card .wp-block-button {
	width: 100%;
}

.pob-hero__card .wp-block-button__link {
	width: 100%;
	justify-content: center;
}

/* ---- the aside: proof stats + scroll cue ---- */

.pob-hero__aside {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: var(--wp--preset--spacing--40);
	text-align: right;
}

@media (max-width: 860px) {
	.pob-hero__aside {
		align-items: flex-start;
		text-align: left;
	}
}

.pob-hero__proof {
	display: flex;
	align-items: center;
	gap: clamp(1rem, 1.6vw, 1.6rem);
	background: rgb(20 20 22 / 50%);
	backdrop-filter: blur(14px);
	-webkit-backdrop-filter: blur(14px);
	border: 1px solid var(--wp--custom--line-strong);
	border-radius: var(--wp--custom--radius--card);
	padding: var(--wp--preset--spacing--30) var(--wp--preset--spacing--40);
}

@media (max-width: 860px) {
	.pob-hero__proof {
		width: 100%;
		justify-content: space-between;
	}
}

.pob-proof-item {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.15rem;
	text-align: center;
}

/* Fixed 2026-07-17: same `surface`-meant-"light text" slip as .pob-hero__card
   above — needs `ink`. */
.pob-proof-item strong {
	font-family: var(--wp--preset--font-family--display);
	font-weight: 800;
	font-size: clamp(1.5rem, 2.2vw, 2.1rem);
	line-height: 1;
	letter-spacing: -0.01em;
	color: var(--wp--preset--color--ink);
}

.pob-proof-item__label {
	font-size: 0.7rem;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: rgb(255 255 255 / 62%);
}

.pob-proof-line {
	width: 1px;
	height: 34px;
	background: var(--wp--custom--line-strong);
}

.pob-hero__cue {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	font-size: 0.7rem;
	letter-spacing: 0.24em;
	text-transform: uppercase;
	color: rgb(255 255 255 / 62%);
}

.pob-hero__cue-dot {
	width: 26px;
	height: 42px;
	border: 1px solid var(--wp--custom--line-strong);
	border-radius: 20px;
	position: relative;
	flex: none;
}

.pob-hero__cue-dot::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 8px;
	width: 3px;
	height: 8px;
	border-radius: 3px;
	background: var(--wp--preset--color--brand-red);
	transform: translateX(-50%);
	animation: pob-cue 1.8s var(--wp--custom--ease--out, ease) infinite;
}

@keyframes pob-cue {
	0% {
		opacity: 0;
		transform: translate(-50%, 0);
	}
	30% {
		opacity: 1;
	}
	100% {
		opacity: 0;
		transform: translate(-50%, 16px);
	}
}

@media (max-width: 860px) {
	.pob-hero__cue {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	.pob-hero__cue-dot::after {
		animation: none;
		opacity: 1;
	}
}

/*
 * Mobile — everything down to the CTA fits above the fold.
 *
 * Toby, 2026-07-17: "try and make it so everything like the cta etc fits
 * above the fold ok mobile." Un-trimmed, the home hero's natural stack —
 * pill, a roof mark forced to a 180px floor by --hero--roof-width's clamp,
 * a 2-line display headline, a rule, a lead line, a town list, then the
 * quote-card's own eyebrow/bar/paragraph/buttons and the proof-stats aside —
 * comfortably exceeds one screen's height on a 375x667 viewport, pushing the
 * "Request a quote" button below the fold on exactly the device most of this
 * site's traffic uses (PLAN's own iOS-zoom note elsewhere makes the same
 * point about that device).
 *
 * The cut here is deliberate, not just "make things smaller": the town list
 * and the card's eyebrow/bar/paragraph are secondary information that
 * already exists elsewhere (the footer has the full address; the paragraph's
 * claim is the same "domestic and commercial roofing" line the page repeats
 * lower down) — dropping them on the smallest screens loses no fact, just
 * trims restated context down to what a thumb actually needs first: the
 * headline, one line of lead copy, and the two buttons. Nothing here is
 * display:none-d without a reason; the proof stats stay, just tightened,
 * because "30 years / 40+ staff / 30 roofers" is exactly the trust signal
 * worth keeping beside the CTA rather than behind it.
 */
@media (max-width: 600px) {
	.pob-hero--home .pob-hero__inner {
		gap: var(--wp--preset--spacing--40);
	}

	.pob-hero__head {
		display: flex;
		flex-direction: column;
	}

	.pob-hero__pill {
		margin-block-end: var(--wp--preset--spacing--20);
	}

	.pob-hero__rule {
		margin: var(--wp--preset--spacing--20) 0;
	}

	.pob-hero__lead {
		margin-block-end: var(--wp--preset--spacing--10);
	}

	/* The town list: already in the footer in full, and the least essential
	   line standing between the headline and the CTA. */
	.pob-hero__meta {
		display: none;
	}

	.pob-hero__floor {
		gap: var(--wp--preset--spacing--30);
	}

	.pob-hero__card {
		width: 100%;
		padding: var(--wp--preset--spacing--30);
	}

	/* Eyebrow + decorative bar + descriptive paragraph drop; the two buttons
	   are the actual CTA and are what must clear the fold. */
	.pob-hero__card-top,
	.pob-hero__card p {
		display: none;
	}

	.pob-hero__aside {
		width: 100%;
	}

	.pob-hero__proof {
		padding: var(--wp--preset--spacing--20) var(--wp--preset--spacing--30);
	}

	.pob-proof-item strong {
		font-size: 1.3rem;
	}
}

/* ---------------------------------------------------------------
   Header — transition set for the transparent -> glass swap.
   The scroll-direction HIDE that used to live here is gone; motion-gsap.js
   has the note on why. The header is permanently fixed now.
   --------------------------------------------------------------- */

.pob-header {
	transition:
		padding-block 0.3s var(--wp--custom--ease--out, ease),
		background-color 0.3s ease,
		backdrop-filter 0.3s ease,
		box-shadow 0.3s ease;
}

/* The scroll sentinel app.js injects at the top of <body> — it exists only to
   be watched by an IntersectionObserver, so it must occupy no space and paint
   nothing. 1px tall, not 0, because a zero-height box can be reported as
   never intersecting in some engines. */
.pob-header__sentinel {
	position: absolute;
	inset-block-start: 0;
	inset-inline-start: 0;
	block-size: 1px;
	inline-size: 1px;
	pointer-events: none;
	opacity: 0;
}

/* ---------------------------------------------------------------
   Preloader — templates/front-page.html only. See that file for the
   no-JS <noscript> guard and the inline backstop script; see
   motion-gsap.js's preloader() for why it can never hang the page.
   --------------------------------------------------------------- */

#preloader {
	position: fixed;
	inset: 0;
	z-index: 9999;
	background: var(--wp--preset--color--surface);
	display: grid;
	place-items: center;
}

.pob-pre__inner {
	text-align: center;
	width: min(78vw, 380px);
}

/*
 * The logo, inlined — patterns/preloader.php has the reasoning. The three
 * brand elements inside it (#pobLogoWord, #pobLogoBar, #pobLogoRoof) are
 * animated separately by motion-gsap.js.
 *
 * ⚠ The resting state here is FULLY VISIBLE, and the hidden from-state is set
 * by GSAP, not by CSS. That direction matters: if the motion layer never runs
 * (blocked script, reduced-motion, a failed deploy) the logo is simply there,
 * and preloader() hides the whole overlay instantly anyway. Hiding it in CSS
 * and revealing it in JS would mean a broken script leaves a blank cover.
 */
.pob-pre__logo {
	display: block;
	width: 100%;
}

.pob-pre__logo svg {
	display: block;
	width: 100%;
	height: auto;
	/* Slightly held back rather than full-strength white — Toby: "not too
	   bold, a bit kind of faded". */
	opacity: 0.92;
}

/*
 * ⚠ PRE-HIDDEN SO THE LOGO NEVER FLASHES IN BEFORE IT ANIMATES IN.
 * motion-gsap.js waits on document.fonts.ready (up to 1.2s) before the
 * preloader runs, so without this the finished lockup sat painted on the
 * cover for over a second and was then snapped away to be built up.
 *
 * ⚠ OPACITY ONLY — NO transform, FOR THE SAME REASON AS THE .pob-motion HOLD
 * IN motion.css. These rules briefly also set the timeline's start transforms,
 * and on the roof that produced a visible break: GSAP parsed the CSS
 * `translateY(-18px) scaleY(0.82)`, composed its own on top, and resolved
 * transform-origin to `0px 0px` — the SVG viewBox corner, not the roof's own
 * base — so the chevron scaled about the wrong point and sat over the
 * wordmark. Toby: "on the initial loader animation the roof graphic is
 * overlapping the text on the logo."
 *
 * GSAP owns every transform on these three. Nothing here may add one.
 *
 * NO RELEASE MECHANISM IS NEEDED, unlike .pob-motion on the hero: every path
 * that doesn't animate the logo (reduced motion, GSAP absent, the safety
 * timeout) hides the whole #preloader instead, so a permanently-hidden logo
 * is never a state anyone can see. Keep that true if you change preloader().
 */
.pob-pre__logo #pobLogoWord,
.pob-pre__logo #pobLogoBar,
.pob-pre__logo #pobLogoRoof {
	opacity: 0;
}

.pob-pre__bar {
	margin-block-start: 2.25rem;
	height: 2px;
	width: 100%;
	background: var(--wp--custom--line-strong);
	overflow: hidden;
	border-radius: 2px;
}

.pob-pre__bar i {
	display: block;
	height: 100%;
	width: 0;
	background: var(--wp--preset--color--red-text);
}

.pob-pre__count {
	margin-block-start: 0.8rem;
	font-size: 0.72rem;
	letter-spacing: 0.2em;
	color: var(--wp--preset--color--muted);
	font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------
   Custom cursor — desktop, mouse input only. Both the JS gate
   (motion-gsap.js's initCursor: hover:none + <980px both bail before the
   markup is ever activated) and this CSS gate exist independently, so a
   device that slips past one is still caught by the other.
   --------------------------------------------------------------- */

.pob-cursor,
.pob-cursor-ring {
	display: none;
}

.pob-has-cursor .pob-cursor {
	display: block;
	position: fixed;
	top: 0;
	left: 0;
	width: 9px;
	height: 9px;
	border-radius: 50%;
	background: var(--wp--preset--color--brand-red);
	z-index: 10000;
	pointer-events: none;
	transform: translate(-50%, -50%);
	mix-blend-mode: difference;
}

.pob-has-cursor .pob-cursor-ring {
	display: block;
	position: fixed;
	top: 0;
	left: 0;
	width: 40px;
	height: 40px;
	border: 1px solid rgb(255 255 255 / 40%);
	border-radius: 50%;
	z-index: 10000;
	pointer-events: none;
	transform: translate(-50%, -50%);
	transition: width 0.3s, height 0.3s, background 0.3s, border-color 0.3s;
}

.pob-has-cursor .pob-cursor-ring.grow {
	width: 74px;
	height: 74px;
	background: rgb(255 0 0 / 10%);
	border-color: transparent;
}

@media (hover: none), (max-width: 980px) {
	.pob-cursor,
	.pob-cursor-ring {
		display: none !important;
	}
}

/* ---------------------------------------------------------------
   Marquee — trust-marks.php's logo strip.
   --------------------------------------------------------------- */

/* Default / no-JS / reduced-motion: a single plain row, scrollable if it
   overflows, nothing clipped and nothing duplicated — the reference has no
   no-JS fallback for its marquee at all; this one does, per this codebase's
   standing rule that nothing is allowed to be JS-load-bearing. */
.pob-marquee {
	border-block: 1px solid var(--wp--preset--color--line);
	padding-block: var(--wp--preset--spacing--40);
	overflow-x: auto;
	background: var(--wp--preset--color--surface);
}

/* Only once motion-gsap.js has actually built the looping viewport (see
   that file's marquee comment — skipped under reduced-motion) does the
   strip clip instead of scroll; the loop itself is what guarantees every
   logo stays reachable at that point.

   The mask fades both ends into the band so logos dissolve rather than
   getting guillotined at the edge — only applied in the looping state,
   because in the static fallback a faded edge would hide a mark that has
   nowhere else to be. */
.pob-has-marquee .pob-marquee {
	overflow: hidden;
	-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
	mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
}

/* Built by motion-gsap.js: a flex row holding TWO identical tracks, only
   ever created once GSAP is actually driving the loop — the -50% travel
   only lands exactly on the loop point because each track carries its own
   trailing gap as padding, not the viewport. Without JS this element
   doesn't exist and .pob-marquee__track renders as the single plain row
   below. */
.pob-marquee__viewport {
	display: flex;
	width: max-content;
	will-change: transform;
}

.pob-marquee__track {
	display: flex;
	align-items: center;
	flex: 0 0 auto;
	gap: clamp(2.5rem, 6vw, 5rem);
	white-space: nowrap;
	width: max-content;
	margin: 0;
	padding: 0;
	list-style: none;
}

/* ---------------------------------------------------------------
   Pinned horizontal gallery — home page "recent work" teaser.
   See patterns/case-studies-latest.php for the markup this styles, and
   motion-gsap.js for the ScrollTrigger pin/scrub.
   --------------------------------------------------------------- */

.pob-gal {
	position: relative;
	background: var(--wp--preset--color--surface);
}

/* Default / no-JS / no-GSAP: a normal horizontally-scrollable strip. Every
   card is still reachable, just by a swipe or a scrollbar instead of a
   scroll-jacked pin. */
.pob-gal-pin {
	display: flex;
	align-items: center;
	overflow-x: auto;
	-webkit-overflow-scrolling: touch;
	scroll-snap-type: x proximity;
}

/*
 * ⚠ NO 100svh HERE — 2026-07-17. This rule used to be
 * `.pob-has-gsap-gallery .pob-gal-pin { height: 100svh }`.
 *
 * The cards are ~56vh tall, so forcing the pin to a full 100svh left roughly
 * 40% of the viewport as dead space split above and below them — on desktop
 * that read as a large empty band between the "Recent work" heading and the
 * first image. Toby: "that section particularly has got a massive gap above
 * it or underneath the title... before the images especially on the desktop."
 *
 * The pin does not need a viewport-height box to work; ScrollTrigger pins
 * whatever height the element actually has. Sizing it to the cards plus a
 * deliberate margin removes the gap without touching the pin behaviour.
 */
.pob-has-gsap-gallery .pob-gal-pin {
	height: auto;
	padding-block: clamp(1.25rem, 3vh, 2.5rem);
	overflow: hidden;
	scroll-snap-type: none;
}

.pob-gal-track {
	display: flex;
	gap: clamp(1rem, 2vw, 1.9rem);
	padding-inline: var(--wp--custom--gutter);
}

/* An <a> — case-studies-latest.php links each frame straight to its case
   study, so display:block is needed (anchors are inline by default) and the
   image gets a slow zoom on hover/focus as the only affordance a scroll-
   jacked strip has room for. */
.pob-gal-item {
	position: relative;
	display: block;
	flex: 0 0 auto;
	scroll-snap-align: center;
	width: clamp(260px, 38vw, 520px);
	height: clamp(300px, 52vh, 540px);
	border-radius: var(--wp--custom--radius--card);
	overflow: hidden;
	background: var(--wp--preset--color--surface-alt);
	border: 1px solid var(--wp--preset--color--line);
}

.pob-gal-item img {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
	transition: transform 0.5s var(--wp--custom--ease--out);
}

.pob-gal-item:hover img,
.pob-gal-item:focus-visible img {
	transform: scale(1.06);
}

@media (prefers-reduced-motion: reduce) {
	.pob-gal-item img {
		transition: none;
	}
}

/* Fixed 2026-07-17: same `surface`-meant-"light text" slip as .pob-hero__card
   above — needs `ink`. */
.pob-gal-item__cap {
	position: absolute;
	left: 18px;
	bottom: 16px;
	z-index: 2;
	font-size: 0.74rem;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--wp--preset--color--ink);
	background: rgb(12 12 13 / 55%);
	backdrop-filter: blur(6px);
	-webkit-backdrop-filter: blur(6px);
	padding: 0.45rem 0.8rem;
	border-radius: var(--wp--custom--radius--pill);
	border: 1px solid var(--wp--preset--color--line);
}

/* In flow under the strip rather than absolutely positioned over it: the pin
   is now sized to its cards (see above), so an absolutely-positioned bar
   pinned to the bottom would sit on top of the artwork instead of under it. */
.pob-gal-progress {
	display: none;
	margin: var(--wp--preset--spacing--40) var(--wp--custom--gutter) 0;
	height: 2px;
	background: var(--wp--custom--line-strong);
	border-radius: 2px;
	overflow: hidden;
}

.pob-has-gsap-gallery .pob-gal-progress {
	display: block;
}

.pob-gal-progress i {
	display: block;
	height: 100%;
	width: 10%;
	background: var(--wp--preset--color--brand-red);
}
