/*
Theme Name: Electrician Pro
Description: Lead-gen block theme for electrical contractors. Client details come
  from factory_* options set by site-factory's new-site.sh.
Version: 1.14.18
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 8.1
Text Domain: electrician
*/

/* Every brand-derived shade uses color-mix against --wp--preset--color--brand,
   which is overridden per client at runtime. Nothing here hardcodes a hue, so a
   client with an orange brand gets a coherent orange system, not blue with
   orange buttons. */

:root {
	--r-sm: 8px;
	--r: 12px;
	--r-lg: 16px;

	--tap: 44px;
	--measure: 68ch;

	/* Header height. Derived, not measured: .brand__logo (56px) plus the
	   header's 0.85rem block padding, since the logo is the tallest thing in
	   the bar. The mobile nav panel already offsets itself by this, and the
	   full-bleed hero pads its content clear of it, so change the logo height and
	   this has to follow. */
	--header-h: 84px;

	--line: var(--wp--preset--color--line);
	--line-strong: #CBD3E0;

	/* Restrained elevation. Depth reads as considered; heavy shadow reads cheap. */
	--sh-1: 0 1px 2px rgba(11, 18, 32, .05);
	--sh-2: 0 4px 12px -2px rgba(11, 18, 32, .08), 0 2px 4px -2px rgba(11, 18, 32, .04);
	--sh-3: 0 18px 40px -12px rgba(11, 18, 32, .18), 0 4px 10px -4px rgba(11, 18, 32, .07);

	--brand: var(--wp--preset--color--brand);
	--accent: var(--wp--preset--color--accent);
	--brand-hi: color-mix(in srgb, var(--brand) 88%, black);
	--brand-lo: color-mix(in srgb, var(--brand) 8%, white);
	--brand-ring: color-mix(in srgb, var(--brand) 35%, transparent);

	--ease: cubic-bezier(.2, .7, .3, 1);
	--z-header: 30;
	--z-mega: 35;
	--z-callbar: 40;
	--z-chat: 42;
	--z-cookie: 45;
}

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

body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }

body.nav-locked { overflow: hidden; }

/* theme.json's root padding lands on <body>, which insets every section by
   20px, so no coloured band ever reached the screen edge and the hero photo
   ran with a strip of page background down both sides. The gutter belongs to
   the sections, not the page. `html body` rather than `body` so this wins on
   specificity regardless of where global styles land in the cascade.

   The content axis is unchanged: each section's children still hit their
   1180px max-width with auto margins, just measured against the full viewport
   minus this padding instead of the body's. */
html body { padding-inline: 0; }

.wp-site-blocks > *,
.wp-site-blocks > .wp-block-template-part > * { padding-inline: 1.25rem; }

/* Template parts wrap the real <header>/<footer> in a div. The gutter has to go
   on the element itself, not the wrapper: over a full-bleed hero the header is
   taken out of flow and escapes the wrapper's padding box entirely, which put
   the logo hard against the screen edge on narrow viewports. */
.wp-site-blocks > .wp-block-template-part { padding-inline: 0; }

/* ------------------------------------------------------------- readability */

.wp-block-post-content > p,
.wp-block-post-content > ul,
.wp-block-post-content > ol,
.wp-block-post-content > h2,
.wp-block-post-content > h3 { max-width: var(--measure); }

.wp-block-post-content > h2 { margin-top: 2.75em; }
.wp-block-post-content > h3 { margin-top: 2em; }
.wp-block-post-content li { margin-bottom: .55rem; }

.wp-block-post-content > ol { counter-reset: step; list-style: none; padding-left: 0; }
.wp-block-post-content > ol > li {
	position: relative;
	padding-left: 2.75rem;
	margin-bottom: 1rem;
}
.wp-block-post-content > ol > li::before {
	counter-increment: step;
	content: counter(step);
	position: absolute;
	left: 0;
	top: -.1em;
	width: 1.9rem;
	height: 1.9rem;
	display: grid;
	place-items: center;
	border-radius: 50%;
	background: var(--brand-lo);
	color: var(--brand-hi);
	font-size: .85rem;
	font-weight: 700;
}

/* ------------------------------------------------------------------ focus */

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
.btn:focus-visible {
	outline: 2px solid var(--brand);
	outline-offset: 2px;
	border-radius: var(--r-sm);
}

/* ---------------------------------------------------------------- buttons */

.btn,
.wp-element-button,
.wp-block-button__link {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: .55rem;
	min-height: var(--tap);
	border: 1px solid transparent;
	border-radius: var(--r-sm);
	font-weight: 650;
	line-height: 1;
	letter-spacing: -.01em;
	text-decoration: none;
	cursor: pointer;
}

.btn { padding: .95rem 1.6rem; font-size: 1.0625rem; }

.btn--primary,
.wp-block-button:not(.is-style-outline) .wp-block-button__link {
	background: var(--brand);
	color: #fff;
	box-shadow: var(--sh-1);
}

.btn--primary:hover,
.wp-block-button:not(.is-style-outline) .wp-block-button__link:hover {
	background: var(--brand-hi);
	box-shadow: var(--sh-2);
	color: #fff;
	text-decoration: none;
}

.btn--primary:active,
.wp-block-button:not(.is-style-outline) .wp-block-button__link:active {
	box-shadow: none;
}

.btn--secondary,
.wp-block-button.is-style-outline .wp-block-button__link {
	background: var(--wp--preset--color--base);
	color: var(--wp--preset--color--ink);
	border-color: var(--line-strong);
	box-shadow: var(--sh-1);
}

.btn--secondary:hover,
.wp-block-button.is-style-outline .wp-block-button__link:hover {
	border-color: var(--brand);
	color: var(--brand-hi);
	text-decoration: none;
}

.btn svg { flex: 0 0 auto; }

@media (prefers-reduced-motion: no-preference) {
	.btn, .wp-element-button, .wp-block-button__link {
		transition: background-color 180ms var(--ease), box-shadow 180ms var(--ease), border-color 180ms var(--ease), color 180ms var(--ease);
	}
}

/* ----------------------------------------------------------------- header */

/* The wp:template-part wrapper (a bare <header>, not .site-header itself)
   is what actually needs position:sticky — that wrapper's own height hugs
   its one child exactly, so sticky on the child alone has no room to travel
   within its containing block and just scrolls away with the page. Stickying
   the wrapper instead gives it the full remaining page as its containing
   block. Only the header template part renders as a <header> tag (the footer
   part renders as <footer>), so the bare tag selector is safe here. */
header.wp-block-template-part {
	position: sticky;
	top: 0;
	z-index: var(--z-header);
}

/* Stays a positioning context of its own so ::before below still anchors to
   its own box rather than the sticky wrapper's. */
.site-header {
	position: relative;
}

/* The bar's background lives on a pseudo-element rather than the header itself
   so it can fade. `backdrop-filter` does not interpolate cleanly between a blur
   and none, and animating the header's own background would drag the border
   with it; opacity on a separate layer transitions perfectly. */
.site-header::before {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	background: color-mix(in srgb, var(--wp--preset--color--base) 88%, transparent);
	backdrop-filter: saturate(1.6) blur(12px);
	border-bottom: 1px solid var(--line);
}

@media (prefers-reduced-motion: no-preference) {
	.site-header::before { transition: opacity 280ms var(--ease); }
}

.site-header .wp-block-site-title a { text-decoration: none; letter-spacing: -.03em; }

/* ------------------------------------------------------------------ brand */

.brand {
	display: inline-flex;
	align-items: center;
	min-height: var(--tap);
	text-decoration: none;
	color: inherit;
	flex: 0 0 auto;
}

/* Height-constrained, width auto: trade logos arrive in every aspect ratio and
   a fixed width squashes the tall ones. */
.brand__logo {
	display: block;
	height: 56px;
	width: auto;
	max-width: 220px;
	object-fit: contain;
}

/* Circular badge logos ship on a square canvas. Against a dark header the
   leftover corners read as a box around the mark; clipping to a circle when the
   asset is square removes them without needing a transparent PNG. */
.brand__logo { border-radius: 4px; }
.brand--round .brand__logo { border-radius: 50%; }

.brand__word { font-size: 1.2rem; font-weight: 800; letter-spacing: -.03em; }

.footer__brand { margin-bottom: .5rem; }

@media (max-width: 900px) {
	.brand__logo { height: 40px; }
	:root { --header-h: 68px; }
}

/* -------------------------------------------------------------------- nav */

.nav { display: flex; align-items: center; }

.nav__panel {
	display: flex;
	align-items: center;
	gap: .35rem;
}

.nav__item { position: relative; }

.nav__link,
.nav__trigger {
	display: inline-flex;
	align-items: center;
	gap: .3rem;
	min-height: var(--tap);
	padding: 0 .8rem;
	border: 0;
	border-radius: var(--r-sm);
	background: none;
	color: var(--wp--preset--color--ink);
	font: inherit;
	font-size: .9375rem;
	font-weight: 600;
	letter-spacing: -.01em;
	text-decoration: none;
	cursor: pointer;
	white-space: nowrap;
}

.nav__link:hover,
.nav__trigger:hover,
.nav__trigger[aria-expanded="true"] {
	background: var(--wp--preset--color--surface);
	color: var(--brand-hi);
	text-decoration: none;
}

.nav__chev { transition: transform 180ms var(--ease); }
.nav__trigger[aria-expanded="true"] .nav__chev { transform: rotate(180deg); }

.nav__cta { margin-left: .5rem; padding: .7rem 1.15rem; font-size: .9375rem; }

.nav__burger { display: none; }

/* ---- mega menu ---- */

.nav__mega {
	position: absolute;
	top: calc(100% + .55rem);
	left: 50%;
	transform: translateX(-50%);
	z-index: var(--z-mega);
	/* Explicit width, not max-content: auto-fit columns have no intrinsic max,
	   so max-content collapses the panel to a single narrow column. */
	width: min(92vw, 900px);
	padding: 1.5rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-3);
}

.nav__mega[hidden] { display: none; }

/* Invisible bridge across the gap between trigger and panel. Without it the
   pointer leaves the item on the way down, mouseleave fires, and the menu
   vanishes before you can reach it. */
.nav__mega::before {
	content: '';
	position: absolute;
	top: -0.65rem;
	left: 0;
	right: 0;
	height: 0.7rem;
}

/* A single column of towns doesn't need the full services panel width. */
.nav__mega--narrow { width: min(92vw, 300px); }

/* Editorial flag for a page still missing the client's local detail. Loud on
   purpose, and it must not survive to launch. */
.editor-note {
	max-width: var(--measure);
	padding: 1rem 1.15rem;
	border: 1px dashed #C2410C;
	border-radius: var(--r-sm);
	background: #FFF7ED;
	color: #7C2D12;
	font-size: .9375rem;
}

.nav__mega-inner {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
	gap: 1.5rem 1.75rem;
}

.nav__mega-head {
	display: block;
	margin-bottom: .6rem;
	padding-bottom: .55rem;
	border-bottom: 1px solid var(--line);
	color: var(--wp--preset--color--ink);
	font-size: .8125rem;
	font-weight: 700;
	letter-spacing: .06em;
	text-transform: uppercase;
	text-decoration: none;
}

.nav__mega-head:hover { color: var(--brand-hi); }

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

.nav__mega-list a {
	display: flex;
	align-items: center;
	min-height: 2.35rem;
	padding: .25rem .5rem;
	margin-left: -.5rem;
	border-radius: var(--r-sm);
	color: var(--wp--preset--color--muted);
	font-size: .9375rem;
	text-decoration: none;
}

.nav__mega-list a:hover {
	background: var(--wp--preset--color--surface);
	color: var(--brand-hi);
}

/* ---- mobile ---- */

@media (max-width: 900px) {
	.nav__burger {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: var(--tap);
		height: var(--tap);
		margin-left: auto;
		border: 1px solid var(--line);
		border-radius: var(--r-sm);
		background: var(--wp--preset--color--base);
		cursor: pointer;
	}

	.nav__burger-bars,
	.nav__burger-bars::before,
	.nav__burger-bars::after {
		display: block;
		width: 18px;
		height: 2px;
		border-radius: 2px;
		background: var(--wp--preset--color--ink);
		transition: transform 200ms var(--ease), opacity 150ms var(--ease);
	}
	.nav__burger-bars { position: relative; }
	.nav__burger-bars::before,
	.nav__burger-bars::after { content: ''; position: absolute; left: 0; }
	.nav__burger-bars::before { top: -6px; }
	.nav__burger-bars::after { top: 6px; }

	.nav__burger[aria-expanded="true"] .nav__burger-bars { background: transparent; }
	.nav__burger[aria-expanded="true"] .nav__burger-bars::before { transform: translateY(6px) rotate(45deg); }
	.nav__burger[aria-expanded="true"] .nav__burger-bars::after { transform: translateY(-6px) rotate(-45deg); }

	.nav__panel {
		position: fixed;
		inset: 0 0 auto;
		top: var(--header-h, 68px);
		z-index: var(--z-mega);
		display: none;
		flex-direction: column;
		align-items: stretch;
		gap: 0;
		max-height: calc(100dvh - var(--header-h, 68px));
		overflow-y: auto;
		padding: 1rem 1.25rem 2rem;
		background: var(--wp--preset--color--base);
		border-bottom: 1px solid var(--line);
		box-shadow: var(--sh-3);
	}

	.nav__panel.is-open { display: flex; }

	.nav__link,
	.nav__trigger {
		width: 100%;
		justify-content: space-between;
		min-height: 3.25rem;
		padding: 0 .25rem;
		border-bottom: 1px solid var(--line);
		border-radius: 0;
		font-size: 1.0625rem;
	}

	.nav__link:hover, .nav__trigger:hover { background: none; }

	/* Accordion rather than a floating panel, since a dropdown pinned to a trigger
	   is unusable at this width. */
	.nav__mega {
		position: static;
		transform: none;
		width: auto;
		max-width: none;
		padding: .5rem 0 1rem;
		border: 0;
		border-bottom: 1px solid var(--line);
		border-radius: 0;
		box-shadow: none;
	}

	.nav__mega-inner { grid-template-columns: 1fr; gap: 1.25rem; }

	/* Desktop can afford tighter rows; on a phone every one of these is a
	   thumb target. */
	.nav__mega-head { min-height: var(--tap); display: flex; align-items: center; }
	.nav__mega-list a { min-height: var(--tap); font-size: 1rem; }

	.nav__cta { margin: 1.25rem 0 0; width: 100%; min-height: 3.25rem; font-size: 1.0625rem; }
}

/* ------------------------------------------------------------------- hero */

/* Padding lives here rather than inline on the block, so the full-bleed variant
   below can override the top value without an !important. */
.hero { padding-block: var(--wp--preset--spacing--60); }

/* ---- full-bleed inside constrained page content ---- */

/* page.html wraps ordinary page content in a constrained 1180px column so
   body text reads at a sane measure. The homepage hero never needs this — it
   sits outside any constrained wrapper — but a page-content section such as
   the contact hero still wants its own background to reach the viewport
   edges, with only its inner text staying centred. Standard viewport-relative
   bleed; overflow-x:hidden on body guards against the sub-pixel scrollbar-
   width rounding this trick is prone to. */
body { overflow-x: hidden; }

.is-layout-constrained > .alignfull {
	width: auto;
	max-width: none;
	margin-left: calc(50% - 50vw);
	margin-right: calc(50% - 50vw);
}

/* ---- full-bleed hero ---- */

/* Set by the theme only when the hero is dark enough to carry a transparent bar
   over it (a photo which always ships under a dark scrim) or the dark theme's
   gradient. Over a light hero this is off, because white nav text on a pale
   surface is unreadable and a transparent bar over a flat colour buys nothing.

   The header goes fixed rather than sticky so the hero can start at y=0 without
   a negative margin that would have to match the header height exactly. Only
   the front page is affected; every other page keeps the header in flow. */
[data-hero="full"] .site-header { position: fixed; top: 0; left: 0; right: 0; }

.header-fade[data-hero="full"] .site-header::before { opacity: 0; }
.header-fade[data-hero="full"] .site-header.is-stuck::before { opacity: 1; }

/* Over the photo the bar has no surface of its own, so the controls have to
   carry their own contrast. The scrim behind them is rgba(4,6,11,.88) at this
   corner, which white clears comfortably.

   The `.nav a` form is deliberate: the dark theme's blanket link rule is
   `[data-theme="dark"] a:not(.wp-block-button__link):not(.btn):not(.callbar)`,
   which scores (0,4,1) and otherwise repaints these links its own blue. */
.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav a.nav__link,
.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav .nav__trigger,
.header-fade[data-hero="full"] .site-header:not(.is-stuck) .brand .brand__word {
	color: #fff;
}

.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav a.nav__link:hover,
.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav .nav__trigger:hover,
.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav .nav__trigger[aria-expanded="true"] {
	background: rgba(255, 255, 255, .14);
	color: #fff;
}

/* svh, not vh: mobile browsers report vh against the chrome-collapsed viewport,
   so a 100vh hero is taller than the screen on first paint and jumps as the
   address bar retracts. svh is the stable small-viewport unit. */
[data-hero="full"] .hero {
	min-height: 100vh;
	min-height: 100svh;
	display: grid;
	align-content: center;
	/* Clear the floating bar, then a little more, since content centred in the space
	   below the bar reads better than content centred in the whole screen. */
	padding-top: calc(var(--header-h) + var(--wp--preset--spacing--40));
	/* The header is still the first child in the DOM, so WordPress's flow layout
	   gives the hero a 24px top margin against it. With the header taken out of
	   flow that margin has nothing above it and shows as a band of page colour
	   above the photo. */
	margin-block-start: 0;
}

/* On a page-template page (Contact) the hero sits inside page.html's <main> —
   which is itself the element carrying both the flow-layout top margin
   against the now-fixed header (the same 24px margin the comment above
   already zeroes on .hero for the front page, but here it lands on <main>,
   one level further out) and its own inline top padding for ordinary page
   titles/content. Both sit above an alignfull hero, so both need zeroing —
   only relevant here, since the front page's hero has no such wrapper to
   begin with. !important on padding because it is inline on the element,
   not just a stylesheet default. */
[data-hero="full"] main.wp-block-group {
	margin-top: 0;
	padding-top: 0 !important;
}

/* The panel drops from under a fixed bar, so it measures against the viewport. */
@media (max-width: 900px) {
	[data-hero="full"] .nav__burger { background: transparent; }
	.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav__burger { border-color: rgba(255, 255, 255, .45); }
	.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav__burger-bars,
	.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav__burger-bars::before,
	.header-fade[data-hero="full"] .site-header:not(.is-stuck) .nav__burger-bars::after { background: #fff; }
}

/* ---- hero photo ---- */

.hero--photo {
	position: relative;
	background-image: var(--hero-img);
	background-size: cover;
	background-position: center 62%;
	isolation: isolate;
}

/* A landscape hero photo crops awkwardly in a narrow portrait viewport, so a
   client can optionally supply a second, mobile-only photo (factory_hero_
   image_mobile) via --hero-img-mobile. Falls back to the desktop photo when
   only one is set. */
@media (max-width: 781px) {
	.hero--photo { background-image: var(--hero-img-mobile, var(--hero-img)); }
}

/* Scrim. Heaviest behind the headline, lifting toward the form side so the
   photo still reads. Opaque enough that contrast holds regardless of what the
   photo does underneath, such as a dusk shot with bright windows and pale sky. */
.hero--photo::before {
	content: '';
	position: absolute;
	inset: 0;
	z-index: -1;
	/* Measured, not eyeballed: at 0.82 over the brightest pixel the photo could
	   contain, the muted lede still clears 5.3:1. Below that it fails, so the
	   text zone holds at 0.86 and only the right side (where the form card
	   covers the photo) lightens enough to let the work show.

	   The first two layers dissolve the bottom edge into the section below
	   instead of letting the photo stop on a hard line. A flat-then-ramp curve
	   still read as a seam when a plateau followed by a sudden slope is itself a
	   visible kink, so this is one continuous ramp, 0% opacity at the top of
	   the box to fully opaque at the very bottom, landing on --d-bg exactly:
	   the same colour the section underneath is already painted in (body's
	   fixed background, see the --d-glow rules below), so hero's last pixel
	   row and the next section's first are the same value, not just close. */
	background:
		radial-gradient(55% 45% at 22% 100%, color-mix(in srgb, var(--accent) 32%, transparent), transparent 72%),
		linear-gradient(to bottom, color-mix(in srgb, var(--d-bg, #041330) 0%, transparent) 0%, var(--d-bg, #041330) 100%),
		linear-gradient(100deg,
			rgba(4, 6, 11, .88) 0%,
			rgba(4, 6, 11, .86) 40%,
			rgba(4, 6, 11, .62) 72%,
			rgba(4, 6, 11, .44) 100%),
		radial-gradient(120% 90% at 12% -10%, color-mix(in srgb, var(--brand) 20%, transparent), transparent 62%);
}

@media (max-width: 781px) {
	/* Stacked, text runs the full width, so the scrim has to be even throughout. */
	.hero--photo::before {
		background:
			radial-gradient(70% 35% at 30% 100%, color-mix(in srgb, var(--accent) 30%, transparent), transparent 72%),
			linear-gradient(to bottom, color-mix(in srgb, var(--d-bg, #041330) 0%, transparent) 0%, var(--d-bg, #041330) 100%),
			linear-gradient(180deg, rgba(4, 6, 11, .86) 0%, rgba(4, 6, 11, .88) 60%, rgba(4, 6, 11, .92) 100%);
	}
}

/* Same idea as the old hero badge (now removed — "24/7 emergency" already
   leads the H1, so a duplicate pill above it was redundant), applied to the
   headline copy itself: "24/7
   emergency" gets its own colour inside the H1 rather than reading as plain
   white text identical to the rest of the sentence. */
.hero__h1-accent,
.hero__meta-247 { color: var(--brand); }

[data-theme="dark"] .hero__h1-accent,
.hero--photo .hero__h1-accent,
[data-theme="dark"] .hero__meta-247,
.hero--photo .hero__meta-247 { color: var(--accent); }

.hero__lede {
	max-width: 36ch;
	font-size: clamp(1.08rem, .95rem + .55vw, 1.3rem);
	line-height: 1.55;
}

.hero__meta { margin-top: 1.1rem; max-width: 48ch; }

/* Turns the static "Serving X, Y and 21 more suburbs" line actionable: type a
   suburb, get an instant match against the client's own real service-area
   list (same data electrician_service_area_map() plots) instead of scanning
   a wall of place names. --wp--preset--color--muted is used directly rather
   than a [data-theme="dark"] override, same as .hero__lede/.hero__meta next
   to it — the preset itself already swaps value between themes. */
.suburb-check { margin-top: .9rem; max-width: 26rem; }

.suburb-check__label {
	display: block;
	margin-bottom: .35rem;
	font-size: .8125rem;
	font-weight: 650;
	letter-spacing: .01em;
	color: var(--wp--preset--color--muted);
}

.suburb-check__input {
	width: 100%;
	min-height: var(--tap);
	padding: .65rem .85rem;
	border: 1px solid var(--line-strong);
	border-radius: var(--r-sm);
	background: var(--wp--preset--color--base);
	color: var(--wp--preset--color--ink);
	font: inherit;
	font-size: 16px;
	line-height: 1.4;
}

.suburb-check__input:focus {
	border-color: var(--brand);
	box-shadow: 0 0 0 3px var(--brand-ring);
	outline: none;
}

.suburb-check__result {
	margin: .5rem 0 0;
	font-size: .875rem;
	font-weight: 600;
	min-height: 1.2em;
}

.suburb-check__result.is-yes { color: #16A34A; }
.suburb-check__result.is-unsure { color: var(--wp--preset--color--muted); font-weight: 500; }

@media (max-width: 781px) {
	/* Desktop gets its side space for free from the 1180px content column
	   centring inside the viewport (contentSize in the hero's wp:columns).
	   Stacked on mobile that column IS the viewport, so .hero needs its own
	   inline padding here or the headline, suburb search and quote-form run
	   edge-to-edge — matches the 1.25rem root padding used everywhere else
	   (theme.json spacing.padding, style.css:71). */
	.hero { padding-inline: 1.25rem; }
	.suburb-check { margin-inline: auto; text-align: left; }
	.hero__meta { margin-top: .8rem; }
	/* Stacked, everything in the text column reads better centred under the
	   photo than pinned left with nothing to line up against — headline,
	   lede, button and meta all move together. .wp-block-buttons is a flex
	   row from core, so centering its content centres the button; the lede/
	   meta need margin-inline:auto too since their max-width otherwise keeps
	   them pinned to the left edge of the centred text. */
	.hero__grid > .wp-block-column:first-child { text-align: center; }
	.hero__lede,
	.hero__meta { margin-inline: auto; }
	.hero .wp-block-buttons { justify-content: center; }
}

/* ------------------------------------------------------------ trust strip */

.trust { display: grid; grid-template-columns: repeat(auto-fit, minmax(205px, 1fr)); gap: .9rem 1.5rem; }

/* .sec--trust sits inside .hero-group, which zeroes the 1.25rem gutter that
   `.wp-site-blocks > *` hands every other section (style.css:71). .hero re-adds
   its own at the 781px breakpoint above; its sibling never got the same fix, so
   the chips' icon circles sat hard against the left edge on mobile while every
   other block on the page was inset 20px.

   Unconditional rather than inside that media query: the constrained 1180px
   column is capped by the viewport anywhere below ~1220px, so 782-1220px wants
   the gutter too, and above that the column still fits inside the padding box
   and nothing moves. The border-bottom is on the group's border box, so it
   still runs the full width. */
.sec--trust { padding-inline: 1.25rem; }

/* Longer client-supplied points wrap to two lines while short ones don't. A
   fixed row height keeps the icons on one baseline so the strip stays a strip
   rather than a ragged run of chips. */
.trust__item {
	display: flex;
	align-items: center;
	gap: .75rem;
	min-height: 2.75rem;
	font-size: .9688rem;
	font-weight: 600;
	line-height: 1.3;
	letter-spacing: -.01em;
}

.trust__icon {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 50%;
	background: var(--brand-lo);
	color: var(--brand-hi);
}

.trust--stats {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 1rem 3rem;
	text-align: center;
}

.stat { display: flex; flex-direction: column; gap: .15rem; }

.stat__num {
	font-size: clamp(1.75rem, 1.4rem + 1.2vw, 2.25rem);
	font-weight: 800;
	letter-spacing: -.02em;
	color: var(--brand-hi);
	font-variant-numeric: tabular-nums;
}

.stat__label {
	font-size: .8438rem;
	font-weight: 600;
	color: var(--wp--preset--color--muted);
	letter-spacing: -.01em;
}

/* ---------------------------------------------------------- section rhythm */

/* WordPress builds the 1180px content column by giving every direct child of a
   constrained group `max-width: 1180px` plus auto inline margins. So a child
   that sets a *narrower* max-width of its own gets re-centred inside that
   column and drifts off the axis, measured before this fix: heading at x=50,
   lede at 289, FAQ at 215, editor note at 319, four left edges on one page.
   Zeroing the margins is not the fix; that drops the element back to the root
   padding edge at x=20. The fix is a full-width wrapper that takes the column,
   leaving anything narrower inside it aligned to the column's left edge. */
.sec-block { margin-top: 2.25rem; }
.sec-block > :first-child { margin-top: 0; }

/* Auto margins do nothing on inline-level boxes, so the inline-flex reviews
   pill ignored the column entirely and sat at x=20. Block-level flex sized to
   its content sits on the axis and still shrink-wraps. */
.reviews__meta { display: flex; width: fit-content; max-width: 100%; }

/* Every section opens the same way: rule, label, heading. The repeat is what
   makes a long page scan as one document rather than a stack of unrelated
   blocks, and it gives each section a name without spending a heading level. */
/* Inline margins deliberately untouched, as they are what puts this on the axis. */
.sec__eyebrow {
	margin-block: 0 .7rem !important;
	font-size: .8125rem;
	font-weight: 700;
	letter-spacing: .09em;
	text-transform: uppercase;
	color: var(--brand);
}

.sec__eyebrow::before {
	content: '';
	display: inline-block;
	width: 1.75rem;
	height: 2px;
	margin-right: .65rem;
	vertical-align: .3em;
	background: currentColor;
	border-radius: 2px;
}

.section__lede {
	max-width: 62ch;
	margin: 1rem 0 0;
	font-size: 1.125rem;
	line-height: 1.6;
	color: var(--wp--preset--color--muted);
}

/* --------------------------------------------------- about, photo + copy */

.about__grid {
	display: grid;
	grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
	gap: 2.5rem;
	align-items: center;
}

.about__media img,
.about__placeholder {
	display: block;
	width: 100%;
	aspect-ratio: 4 / 5;
	border-radius: var(--r-lg);
	border: 1px solid var(--line);
}

.about__media img { object-fit: cover; }

/* No about_image: the media column isn't rendered at all, so the copy runs
   full width rather than sitting beside an empty grid track. */
.about__grid--solo { grid-template-columns: minmax(0, 1fr); }

/* Dashed rather than solid so it reads as "add a photo here", not as a
   finished tile, and it's the same intent as .editor-note, sized to sit in a layout
   instead of calling out over one. Disappears the moment about_image is set. */
.about__placeholder {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: .6rem;
	border-style: dashed;
	background: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--muted);
	text-align: center;
	padding: 1.5rem;
}

.about__placeholder svg { opacity: .55; }

.about__placeholder span {
	font-size: .8125rem;
	font-weight: 600;
}

.about__copy .section__lede { margin-top: 0; }

/* A real cert_src links out to the scanned certificate (solid border, brand
   hover); the placeholder stays dashed, same "add something here" language
   as .about__placeholder, and is never a clickable link since there's
   nothing behind it yet. */
.cert-badge {
	display: flex;
	align-items: center;
	gap: .75rem;
	margin-top: 1.1rem;
	padding: .75rem .9rem;
	border: 1px solid var(--line-strong);
	border-radius: var(--r-sm);
	background: var(--wp--preset--color--surface);
	text-decoration: none;
	color: inherit;
}

.cert-badge:not(.cert-badge--placeholder):hover { border-color: var(--brand); }

.cert-badge--placeholder { border-style: dashed; }

.cert-badge__icon {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 50%;
	background: var(--brand-lo);
	color: var(--brand-hi);
}

.cert-badge__label { display: block; font-size: .875rem; font-weight: 700; }

.cert-badge__note {
	display: block;
	font-size: .8125rem;
	color: var(--wp--preset--color--muted);
}

@media (max-width: 720px) {
	.about__grid { grid-template-columns: 1fr; gap: 1.5rem; }
	.about__media img,
	.about__placeholder { aspect-ratio: 16 / 10; }
}

/* ------------------------------------------------------- before/after slider */

/* The split is drawn with clip-path, not a shrinking width, so the "before"
   layer's own <img> never resizes — only the visible portion of it changes —
   and the frame's --ba-pos custom property (set inline, matching the range
   input's own default value) is what both the no-JS initial render and
   assets/motion.js's drag handler agree on. */
.ba-slider { max-width: 46rem; margin-inline: auto; }

.ba-slider__frame {
	position: relative;
	aspect-ratio: 4 / 3;
	border-radius: var(--r-lg);
	overflow: hidden;
	border: 1px solid var(--line);
	isolation: isolate;
}

.ba-slider__img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

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

.ba-slider__img--after { z-index: 1; }

.ba-slider__img--before {
	z-index: 2;
	clip-path: inset(0 calc(100% - var(--ba-pos, 50%)) 0 0);
	border-right: 2px solid #fff;
}

.ba-slider__placeholder {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: .6rem;
	background: var(--wp--preset--color--surface);
	color: var(--wp--preset--color--muted);
	text-align: center;
	padding: 1.5rem;
}

.ba-slider__img--after .ba-slider__placeholder { background: color-mix(in srgb, var(--brand) 10%, var(--wp--preset--color--surface)); }
.ba-slider__placeholder svg { opacity: .55; }
.ba-slider__placeholder span { font-size: .8125rem; font-weight: 600; }

.ba-slider__tag {
	position: absolute;
	top: .85rem;
	z-index: 3;
	padding: .3rem .7rem;
	border-radius: 999px;
	font-size: .75rem;
	font-weight: 700;
	letter-spacing: .02em;
	color: #fff;
	background: rgba(10, 14, 22, .68);
	backdrop-filter: blur(3px);
}

.ba-slider__tag--before { left: .85rem; }
.ba-slider__tag--after { right: .85rem; }

/* Overlaid full-bleed, appearance:none, transparent track — only the native
   thumb is visible, riding the divider line since a horizontal range input
   centres its track vertically in its own box by default. */
.ba-slider__range {
	position: absolute;
	inset: 0;
	z-index: 4;
	width: 100%;
	height: 100%;
	margin: 0;
	cursor: ew-resize;
	appearance: none;
	-webkit-appearance: none;
	background: transparent;
}

.ba-slider__range::-webkit-slider-runnable-track { appearance: none; background: transparent; }
.ba-slider__range::-moz-range-track { background: transparent; border: none; }

.ba-slider__range::-webkit-slider-thumb {
	-webkit-appearance: none;
	width: 2.75rem;
	height: 2.75rem;
	border-radius: 50%;
	background: #fff;
	border: 3px solid var(--brand);
	box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
	cursor: ew-resize;
}

.ba-slider__range::-moz-range-thumb {
	width: 2.75rem;
	height: 2.75rem;
	border-radius: 50%;
	background: #fff;
	border: 3px solid var(--brand);
	box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
	cursor: ew-resize;
}

/* Rest of the portfolio, below the one dramatic before/after comparison —
   ordinary job photos, fixed six-slot grid regardless of how many the
   client has actually supplied (see our-work.php), so it never reads as a
   finished, closed set. */
.portfolio-grid {
	margin-top: 1.75rem;
	display: grid;
	grid-template-columns: repeat(3, 1fr);
	gap: 1rem;
}

.portfolio-grid__item {
	position: relative;
	aspect-ratio: 4 / 3;
	border-radius: var(--r-sm);
	overflow: hidden;
	border: 1px solid var(--line);
}

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

.portfolio-grid__item .ba-slider__placeholder { padding: 1rem; }
.portfolio-grid__item .ba-slider__placeholder svg { width: 24px; height: 24px; }
.portfolio-grid__item .ba-slider__placeholder span { font-size: .75rem; }

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

/* Alternating bands. Not alternating *themes*, which is what once made this
   read as two half-finished sites stitched together. One ground colour, one
   band lifted a few percent off it, so the eye gets a section boundary without
   the page changing character. */
.sec--alt {
	background-color: var(--wp--preset--color--surface);
	border-top: 1px solid var(--line);
	border-bottom: 1px solid var(--line);
}

/* Both grid variants are emitted as a `wp:group` block (el_cards() in
   inc/pages.php) so they pick up editor chrome for free, but that means
   WordPress's default block-gap CSS also applies: `.is-layout-flow > *`
   gives every child except the first a margin-block-start. Combined with
   align-items: stretch, that shows up as the first card rendering ~24px
   taller than its siblings instead of an even row. Not layout spacing we
   want — the grid's own `gap` already handles that — so it's zeroed here. */
.cardgrid > * { margin-block-start: 0; }

/* -------------------------------------------------- balanced three-up grid */

/* An auto-fit grid strands the remainder: five service categories render as a
   row of three, then two cards and a hole. Twelve columns with last-row
   balancing means a trailing pair splits the full width and a trailing single
   centres, so every row is complete and the block reads as a designed grid.
   Rows stretch, so card bottoms align instead of ending wherever the copy
   actually stops. */
.grid3 {
	display: grid;
	grid-template-columns: repeat(12, 1fr);
	gap: 1.25rem;
	align-items: stretch;
	margin-top: 2.25rem;
}

.grid3 > * { grid-column: span 4; }

.grid3 > :nth-child(3n+1):nth-last-child(2),
.grid3 > :nth-child(3n+1):nth-last-child(2) ~ * { grid-column: span 6; }

.grid3 > :nth-child(3n+1):last-child { grid-column: 5 / span 4; }

@media (max-width: 1000px) {
	.grid3 > *,
	.grid3 > :nth-child(3n+1):nth-last-child(2),
	.grid3 > :nth-child(3n+1):nth-last-child(2) ~ *,
	.grid3 > :nth-child(3n+1):last-child { grid-column: span 6; }

	.grid3 > :nth-child(2n+1):last-child { grid-column: span 12; }
}

@media (max-width: 640px) {
	.grid3 > *,
	.grid3 > :nth-child(3n+1):nth-last-child(2),
	.grid3 > :nth-child(3n+1):nth-last-child(2) ~ *,
	.grid3 > :nth-child(3n+1):last-child,
	.grid3 > :nth-child(2n+1):last-child { grid-column: span 12; }
}

/* ------------------------------------------------------- related-services */

/* A related-services set is at most four cards (up to three category
   siblings plus one "all services" card), never the five-or-six .grid3 is
   balanced for. auto-fit divides that small, predictable range evenly on
   its own — 2, 3 or 4 cards each stretch to fill the row — so there's no
   need for .grid3's nth-child balancing math here, and no risk of the
   orphaned-single-card row it produces on an even split it wasn't tuned for. */
.cardgrid--related {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
	gap: 1.25rem;
	align-items: stretch;
	margin-top: 2.25rem;
}

/* ----------------------------------------------------------------- process */

/* Same twelve-column last-row balancing as .grid3 (cardgrid section, above):
   a step count that isn't a multiple of four used to strand a fourth-row
   tile on its own at the left edge. A trailing pair now splits the row width
   between them and a trailing single centres, so every row reads as a
   finished design instead of a count that didn't divide evenly. */
.steps {
	display: grid;
	grid-template-columns: repeat(12, minmax(0, 1fr));
	gap: 1.25rem;
	margin: 2.25rem 0 0;
	padding: 0;
	list-style: none;
	counter-reset: none;
}

/* Tiles, not floating text. Four unbordered columns of ragged-length copy is
   the single strongest source of the "random boxes" read: an equal-height
   panel makes the sequence legible as a sequence. */
.steps__item {
	position: relative;
	grid-column: span 3;
	margin: 0;
	padding: 1.5rem 1.35rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-1);
}

.steps__item:nth-child(4n+1):nth-last-child(3),
.steps__item:nth-child(4n+1):nth-last-child(3) ~ * { grid-column: span 4; }

.steps__item:nth-child(4n+1):nth-last-child(2),
.steps__item:nth-child(4n+1):nth-last-child(2) ~ * { grid-column: span 6; }

.steps__item:nth-child(4n+1):last-child { grid-column: 5 / span 4; }

@media (max-width: 1000px) {
	.steps__item,
	.steps__item:nth-child(4n+1):nth-last-child(3),
	.steps__item:nth-child(4n+1):nth-last-child(3) ~ *,
	.steps__item:nth-child(4n+1):nth-last-child(2),
	.steps__item:nth-child(4n+1):nth-last-child(2) ~ *,
	.steps__item:nth-child(4n+1):last-child { grid-column: span 6; }

	.steps__item:nth-child(2n+1):last-child { grid-column: span 12; }
}

@media (max-width: 560px) {
	.steps__item,
	.steps__item:nth-child(4n+1):nth-last-child(3),
	.steps__item:nth-child(4n+1):nth-last-child(3) ~ *,
	.steps__item:nth-child(4n+1):nth-last-child(2),
	.steps__item:nth-child(4n+1):nth-last-child(2) ~ *,
	.steps__item:nth-child(4n+1):last-child,
	.steps__item:nth-child(2n+1):last-child { grid-column: span 12; }
}

.steps__item:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); box-shadow: var(--sh-2); }

/* Hairline connector between consecutive steps, drawn into the gutter. It has
   to be suppressed on whichever tile starts a row, or it draws a stray dash off
   the left edge of the container, which is why the column counts above are
   explicit rather than auto-fit. */
.steps__item::before {
	content: '';
	position: absolute;
	top: 2.6rem;
	right: 100%;
	width: 1.25rem;
	height: 1px;
	background: var(--line-strong);
}

/* Suppress on every tile that starts a row, not just the very first — with a
   step count that isn't a multiple of the column count (e.g. 5 steps in a
   4-column grid), the first tile of the wrapped row is still a "row start"
   and would otherwise draw a stray dash into the left margin. */
.steps__item:nth-child(4n+1)::before { display: none; }

@media (max-width: 1000px) { .steps__item:nth-child(2n+1)::before { display: none; } }
@media (max-width: 560px)  { .steps__item::before { display: none; } }

.steps__num {
	display: grid;
	place-items: center;
	width: 2.4rem;
	height: 2.4rem;
	margin-bottom: .9rem;
	border-radius: 50%;
	background: color-mix(in srgb, var(--accent) 18%, transparent);
	color: var(--accent);
	font-weight: 700;
	font-size: 1rem;
}

.steps__title {
	margin: 0 0 .4rem;
	font-size: 1.0625rem;
	font-weight: 700;
	letter-spacing: -.015em;
}

.steps__text {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: .9688rem;
	line-height: 1.55;
}

/* ----------------------------------------------------------------- gallery */

.gallery {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
	gap: 1rem;
	margin-top: 2rem;
}

.gallery__item { margin: 0; }

.gallery__item img {
	display: block;
	width: 100%;
	height: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	border-radius: var(--r);
	border: 1px solid var(--line);
}

/* ------------------------------------------------------------ testimonials */

.reviews__meta {
	display: inline-flex;
	align-items: center;
	flex-wrap: wrap;
	gap: .5rem .7rem;
	margin-top: 1.5rem;
	padding: .7rem 1.1rem;
	background: #fff;
	border: 1px solid #DADCE0;
	border-radius: 999px;
	font-size: .9375rem;
	color: #5F6368;
}

.reviews__meta strong { font-size: 1.15rem; color: #202124; }

.reviews__brand { font-weight: 650; color: #202124; }

.reviews__rule {
	width: 1px;
	height: 1.1rem;
	background: #DADCE0;
}

.reviews__link { color: #1A73E8 !important; font-weight: 600; }

.glogo, .revlogo { flex: 0 0 auto; }

.review__src {
	display: flex;
	align-items: center;
	gap: .45rem;
	margin-top: 1.1rem;
	padding-top: .85rem;
	border-top: 1px solid #E8EAED;
	font-size: .8125rem;
	color: #5F6368;
}

.stars { display: inline-flex; gap: 1px; color: #F5B301; }

/* Masonry, not a grid. Review length is wildly uneven and outside our control,
   a row grid either stretches a two-line review to match a ten-line one or
   leaves a ragged bottom edge, and five reviews in a three-up row strands two.
   Columns pack by height, so the wall stays flush on both edges at any count. */
/* Two columns, not three. Fewer columns balance far better against unbreakable
   blocks of wildly different length: measured on this client's five reviews,
   three columns left the last one 574px short of the first, two columns came
   out within 64px. Wider cards read better as quotations, too. */
.reviews {
	columns: 2;
	column-gap: 1.25rem;
	margin-top: 1.25rem;
}

@media (max-width: 720px) { .reviews { columns: 1; } }

/* Under ~530px the rating line and the "See all on Google" link no longer
   fit on one row, so the flex-wrap pill breaks onto two. A 999px pill radius
   sized to the widest (first) line leaves the shorter second line stranded
   with a wide dead gap to its right — exactly the "empty space" a stadium
   shape only works for single-line content. Once it can wrap, it reads
   better as a plain rounded card at full width than as an oversized pill.
   Flex-wrap justifies each wrapped line independently, so centering also
   pulls the link into the middle of its own line instead of hugging left. */
@media (max-width: 720px) {
	.reviews__meta {
		width: 100%;
		justify-content: center;
		border-radius: var(--r-lg);
	}
}

/* Review cards stay white in both themes. On a dark site that reads as a
   quoted artefact lifted from somewhere else, and that's exactly what it is. */
.review {
	display: inline-block;
	width: 100%;
	break-inside: avoid;
	-webkit-column-break-inside: avoid;
	margin: 0 0 1.25rem;
	padding: 1.5rem;
	background: #fff;
	border: 1px solid #DADCE0;
	border-radius: var(--r);
	box-shadow: 0 1px 3px rgba(60, 64, 67, .12);
	color: #202124;
}

.review__name { color: #202124; }
.review__text { color: #3C4043; }
.review__price { border-color: #DADCE0; color: #5F6368; }

.review__head { display: flex; align-items: center; gap: .75rem; }

.review__avatar {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.5rem;
	height: 2.5rem;
	border-radius: 50%;
	color: #fff;
	font-weight: 700;
	font-size: 1.05rem;
}

.review__name {
	display: block;
	font-style: normal;
	font-weight: 650;
	letter-spacing: -.01em;
}


.review__rating {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: .5rem .75rem;
	margin: .85rem 0 .65rem;
}

.review__price {
	padding: .1rem .5rem;
	border: 1px solid var(--line);
	border-radius: 999px;
	font-size: .75rem;
	color: var(--wp--preset--color--muted);
}

.review__text {
	margin: 0;
	font-size: .9688rem;
	line-height: 1.6;
	color: var(--wp--preset--color--ink);
}

.reviews__source {
	margin-top: 1.25rem;
	font-size: .8125rem;
	color: var(--wp--preset--color--muted);
}

/* --------------------------------------------------------------------- faq */

/* Contained panel rather than bare rules floating in the band: an accordion
   with no edges reads as loose text, and at 1180px wide the questions were
   stranded miles from their answers' right edge. */
.faq {
	max-width: 920px;
	margin-top: 2.25rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	overflow: hidden;
}

.faq__item { border-bottom: 1px solid var(--line); }
.faq__item:last-child { border-bottom: 0; }

.faq__q {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	min-height: var(--tap);
	padding: 1.15rem 1.4rem;
	font-size: 1.0313rem;
	font-weight: 650;
	letter-spacing: -.015em;
	cursor: pointer;
	list-style: none;
}

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

.faq__q::after {
	content: '';
	flex: 0 0 auto;
	width: .6rem;
	height: .6rem;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: rotate(45deg) translateY(-.15rem);
}

.faq__item[open] .faq__q::after { transform: rotate(-135deg) translateY(-.15rem); }
.faq__item[open] .faq__q { color: var(--brand); }

/* Set by the script for the length of the collapse, while [open] is still on. */
.faq__item.is-closing .faq__q::after { transform: rotate(45deg) translateY(-.15rem); }
.faq__item.is-closing .faq__q { color: inherit; }

.faq__a {
	max-width: var(--measure);
	margin: -.25rem 1.4rem 1.35rem;
	color: var(--wp--preset--color--muted);
	line-height: 1.65;
}

@media (prefers-reduced-motion: no-preference) {
	.faq__q::after { transition: transform 200ms var(--ease); }
}

/* ------------------------------------------------------------- closing CTA */

/* The closing block was a full-width stack: giant heading, giant lede, button,
   then a form far below the fold of the section. Copy left, form right puts the
   ask and the means of answering it in the same eyeline. */
.cta__grid {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 440px);
	gap: 3.5rem;
	align-items: start;
}

.cta__title { margin: 0 0 .9rem; }

.cta__lede {
	max-width: 44ch;
	margin: 0 0 1.75rem;
	font-size: 1.125rem;
	line-height: 1.6;
}

.cta__actions { display: flex; flex-wrap: wrap; align-items: center; gap: .9rem 1.25rem; }

.cta__note { margin: 1rem 0 0; font-size: .875rem; opacity: .8; }

.cta__points {
	display: grid;
	gap: .6rem;
	margin: 2rem 0 0;
	padding: 1.5rem 0 0;
	border-top: 1px solid rgba(255, 255, 255, .12);
	list-style: none;
}

.cta__points li { display: flex; align-items: center; gap: .6rem; font-size: .9375rem; }

.cta__points svg { flex: 0 0 auto; color: var(--accent); }

@media (max-width: 900px) {
	.cta__grid { grid-template-columns: 1fr; gap: 2.25rem; }
	.cta__lede { max-width: none; }
}

/* -------------------------------------------------------------- why choose */

/* Services, process and reviews already read as bordered-tile grids; a fourth
   one back-to-back with the first is what makes the page feel like nothing
   but boxes. Flex-wrap instead of a shared grid row means an item whose title
   wraps to two lines no longer has to drag its neighbours' row height with
   it, so this can drop the border/shadow/background a tile needed and read
   as a plain checklist. The top rule is the only separator, the same
   one-line-not-a-box treatment .cta__points uses to break from the copy above.
   justify-content: center is doing real work here, not just tidiness: a grid
   would have left an odd leftover item (5 points = 4 + 1) stranded at the
   left edge of its own row with three empty tracks beside it. Flex justifies
   each wrapped line independently, so that last item centres under the row
   above instead. */
.why {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 1.6rem 2.5rem;
	margin-top: 2.25rem;
	padding-top: 1.75rem;
	border-top: 1px solid var(--line);
}

.why__item {
	display: flex;
	flex: 0 1 260px;
	max-width: 320px;
	gap: .9rem;
	align-items: flex-start;
}

.why__bolt {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 8px;
	background: color-mix(in srgb, var(--accent) 16%, transparent);
	color: var(--accent);
}

.why__title {
	margin: .15rem 0 .35rem;
	font-size: 1.0625rem;
	font-weight: 700;
	letter-spacing: -.015em;
}

.why__text {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: .9688rem;
	line-height: 1.55;
}

/* ------------------------------------------------------------------- card */

/* Layout comes from .grid3. Cards stretch to the row height and the sub-service
   list is pinned to the bottom, so the row closes on one line. Letting each
   card size to its own content (the previous behaviour) is what produced the
   ragged bottom edge where one category happened to have fewer services. */
.card {
	display: flex;
	flex-direction: column;
	position: relative;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	padding: 1.65rem 1.5rem;
	box-shadow: var(--sh-1);
	cursor: pointer;
}

.card:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); box-shadow: var(--sh-2); }

/* Whole-card click target. Two different markups render `.card` as the
   homepage services grid (.card__title, .card__links) and the generated
   category/sub-service pages (el_cards() in inc/pages.php: a bare heading
   and list). Both put the card's own link in a direct-child heading and any
   secondary links in a list, so keying off that structure covers both
   without duplicating the rule per markup.
   The heading link's ::after is stretched over the whole box, so clicking
   the intro copy or the empty padding navigates like clicking the title.
   Anything with its own href (the sub-service list) is raised above that
   layer so it still goes to its own page rather than hijacking the click. */
.card > h3 a::after {
	content: '';
	position: absolute;
	inset: 0;
	z-index: 1;
}

.card ul a { position: relative; z-index: 2; }

.card:has(> h3 a:focus-visible) { outline: 2px solid var(--brand); outline-offset: 3px; }

.card__title,
.card h3 {
	margin: 0 0 .55rem;
	font-size: 1.2rem;
	line-height: 1.25;
	letter-spacing: -.02em;
}

.card__title a,
.card h3 a { color: inherit; text-decoration: none; }
.card__title a:hover,
.card h3 a:hover { color: var(--brand-hi); }

.card__text,
.card p {
	margin: 0 0 1.4rem;
	color: var(--wp--preset--color--muted);
	font-size: .9688rem;
	line-height: 1.55;
}

.card__links,
.card ul {
	margin: auto 0 0;
	padding: .9rem 0 0;
	border-top: 1px solid var(--line);
	list-style: none;
	display: grid;
	gap: .1rem;
}

.card__links li,
.card li { margin: 0; }

.card__links a,
.card li a {
	display: flex;
	align-items: center;
	min-height: 2.5rem;
	color: var(--brand);
	font-size: .9375rem;
	font-weight: 550;
	text-decoration: none;
}

.card__links a:hover,
.card li a:hover { color: var(--brand-hi); text-decoration: underline; }

/* Decorative click affordance for a card with no link sub-list of its own
   (the related-services cards) — the whole card is already a click target
   via .card > h3 a::after, so this is a <span>, not a real link, and the
   arrow nudge on hover is purely visual. */
.card__cta {
	display: inline-flex;
	align-items: center;
	gap: .35rem;
	margin: auto 0 0;
	padding-top: .9rem;
	color: var(--brand);
	font-size: .9375rem;
	font-weight: 600;
}

.card__cta svg { transition: transform 200ms var(--ease); }

.card:hover .card__cta { color: var(--brand-hi); }
.card:hover .card__cta svg { transform: translateX(3px); }

/* ------------------------------------------------------ emergency highlight */

/* Same "24/7 emergency" callout as the hero, surfaced wherever someone might
   actually be scanning a list for it: the homepage card's link list and the
   nav dropdown share this exact li > a shape, so one rule covers both. Server
   -gated on electrician_is_24_7() — never rendered for a client whose hours
   aren't round-the-clock, so there's nothing here to keep in sync by hand. */
.card__links li.is-urgent a,
.nav__mega-list li.is-urgent a {
	color: var(--brand);
	font-weight: 700;
}

.tag-247 {
	display: inline-flex;
	align-items: center;
	margin-left: .5rem;
	padding: .1rem .45rem;
	border-radius: 999px;
	font-size: .625rem;
	font-weight: 800;
	letter-spacing: .03em;
	color: #fff;
	background: var(--brand);
}

/* Sub-service card variant (el_cards() output on category pages): a ring and
   a small pill riding the top edge, same idea as the list tag but sized for a
   full card rather than an inline link. */
.card--urgent {
	border-color: color-mix(in srgb, var(--brand) 55%, var(--line));
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand) 28%, transparent), var(--sh-1);
}

.card__badge {
	position: absolute;
	top: -.65rem;
	left: 1.4rem;
	padding: .25rem .65rem;
	border-radius: 999px;
	font-size: .6875rem;
	font-weight: 800;
	letter-spacing: .04em;
	text-transform: uppercase;
	color: #fff;
	background: var(--brand);
	box-shadow: var(--sh-1);
}

/* ---------------------------------------------------- reactive card hover */

/* Every tile grid on the page (services, why-us, process) reads as a set,
   so hovering one member should acknowledge the group, not just the tile
   under the cursor. The hovered card grows; its siblings ease back a touch,
   so the row breathes rather than one box silently popping over the rest.
   :has() lets the container react to which child is hovered without a
   mouseenter/mouseleave listener per tile.
   Gated on real hover, since a touch tap latches :hover until the next tap
   elsewhere, and a stuck "everything but one card shrunk" state would read
   as a bug on mobile, not a feature. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
	.grid3 > .card,
	.cardgrid--related > .card,
	.steps > .steps__item,
	.causes > .causes__item {
		transition: transform 260ms var(--ease), border-color 200ms var(--ease), box-shadow 200ms var(--ease);
	}

	/* :not(:hover) matters here, not just for clarity: without it this selector
	   also matches the hovered card itself (:has() doesn't exclude the element
	   that satisfies its own condition), and it out-specifies the plain :hover
	   grow rule below, so every card including the one under the cursor
	   was shrinking. */
	.grid3:has(> .card:hover) > .card:not(:hover),
	.cardgrid--related:has(> .card:hover) > .card:not(:hover),
	.steps:has(> .steps__item:hover) > .steps__item:not(:hover),
	.causes:has(> .causes__item:hover) > .causes__item:not(:hover) {
		transform: scale(.965);
	}

	/* `html` in front is a specificity bump, not a scoping change, and it still
	   matches exactly the same elements. Without it this ties with the reveal
	   system's `.motion-ready .reveal.is-revealed { transform: none }` (same
	   three class-selectors), and because that rule is declared later in the
	   file, the tie went to "none": every already-revealed card sat there
	   inert on hover, never growing, while its siblings still shrank. */
	html .grid3 > .card:hover,
	html .cardgrid--related > .card:hover,
	html .steps > .steps__item:hover,
	html .causes > .causes__item:hover {
		position: relative;
		z-index: 2;
		transform: scale(1.045);
	}
}

/* ---------------------------------------------------------- area listing */

/* 27 suburbs as a bullet list is a wall. Columnar chips scan in seconds. */
.arealist {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
	gap: .4rem .75rem;
	margin-top: 1.25rem;
	max-width: none;
}

.arealist p,
.arealist a,
.arealist span {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: var(--tap);
	margin: 0;
	padding: .5rem .9rem;
	border: 1px solid var(--line);
	border-radius: 999px;
	background: var(--wp--preset--color--base);
	color: var(--wp--preset--color--ink);
	font-size: .9375rem;
	text-align: center;
	text-decoration: none;
}

.arealist a { color: var(--brand); font-weight: 600; }
.arealist a:hover { border-color: var(--brand); background: var(--brand-lo); }

.arealist__note {
	margin-top: 1.5rem;
	color: var(--wp--preset--color--muted);
}

/* -------------------------------------------------- service page layout */

/* "How we handle it", as numbered tiles rather than a plain ordered list —
   same component as the homepage process steps, just without a title line,
   since each step here is already a single sentence, not a title/body pair. */
.steps--compact .steps__item { padding-bottom: 1.35rem; }
.steps--compact .steps__text { margin: 0; }

/* "What to expect" and "what it costs" used to be two full-width H2
   sections apiece. Two answers this short don't need that much page, so
   they sit side by side as self-labelled tiles instead — same card
   language as why-choose, half the scroll. */
.quickfacts {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 1.25rem;
	margin-top: 2.25rem;
}

.quickfacts__item {
	display: flex;
	gap: .9rem;
	align-items: flex-start;
	padding: 1.4rem 1.35rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-1);
}

.quickfacts__icon {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 8px;
	background: color-mix(in srgb, var(--accent) 16%, transparent);
	color: var(--accent);
}

.quickfacts__title {
	margin: .15rem 0 .35rem;
	font-size: 1.0625rem;
	font-weight: 700;
	letter-spacing: -.015em;
}

.quickfacts__text {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: .9688rem;
	line-height: 1.55;
}

@media (max-width: 640px) {
	.quickfacts { grid-template-columns: 1fr; }
}

/* -------------------------------------------------------------- causes */

/* "Why this comes up" as icon tiles instead of a bullet list. Item counts
   vary per service (5-9), so this uses the same twelve-column last-row
   balancing as .grid3 and .steps above rather than .grid3's fixed three-up
   math or a plain auto-fill grid — auto-fill stretched a trailing 4+2 split
   into two visibly mismatched row widths instead of a deliberate one. Wired
   into the same reactive hover-grow treatment as .card / .steps__item below
   so it reads as part of the same design language, not a bolted-on list. */
.causes {
	display: grid;
	grid-template-columns: repeat(12, minmax(0, 1fr));
	gap: 1rem;
	margin-top: 2.25rem;
}

.causes__item {
	display: flex;
	grid-column: span 3;
	gap: .75rem;
	align-items: flex-start;
	padding: 1.1rem 1.2rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-1);
}

.causes__item:nth-child(4n+1):nth-last-child(3),
.causes__item:nth-child(4n+1):nth-last-child(3) ~ * { grid-column: span 4; }

.causes__item:nth-child(4n+1):nth-last-child(2),
.causes__item:nth-child(4n+1):nth-last-child(2) ~ * { grid-column: span 6; }

.causes__item:nth-child(4n+1):last-child { grid-column: 5 / span 4; }

.causes__item:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); box-shadow: var(--sh-2); }

.causes__icon {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 1.9rem;
	height: 1.9rem;
	margin-top: .05rem;
	border-radius: 8px;
	background: color-mix(in srgb, var(--accent) 16%, transparent);
	color: var(--accent);
}

.causes__text {
	margin: 0;
	color: var(--wp--preset--color--muted);
	font-size: .9375rem;
	line-height: 1.5;
}

@media (max-width: 640px) {
	.causes__item,
	.causes__item:nth-child(4n+1):nth-last-child(3),
	.causes__item:nth-child(4n+1):nth-last-child(3) ~ *,
	.causes__item:nth-child(4n+1):nth-last-child(2),
	.causes__item:nth-child(4n+1):nth-last-child(2) ~ *,
	.causes__item:nth-child(4n+1):last-child { grid-column: span 12; }
}

/* ---------------------------------------------------------------- map */

.map-embed {
	position: relative;
	width: 100%;
	aspect-ratio: 21 / 8;
	border-radius: var(--r-lg);
	overflow: hidden;
	border: 1px solid var(--line);
}

.map-embed iframe {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
}

@media (max-width: 640px) {
	.map-embed { aspect-ratio: 4 / 3; }
}

/* -------------------------------------------------------------- tel links */

.tel-link {
	display: inline-flex;
	align-items: center;
	min-height: var(--tap);
	font-weight: 700;
	white-space: nowrap;
}

/* ------------------------------------------------------------- quote form */

.quote-form {
	max-width: 34rem;
	padding: 1.75rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-2);
	color: var(--wp--preset--color--ink);
}

.quote-form__title {
	margin: 0 0 1.1rem;
	font-size: 1.2rem;
	font-weight: 700;
	letter-spacing: -.02em;
}

.quote-form p { margin: 0 0 .95rem; }

.quote-form label {
	display: block;
	margin-bottom: .35rem;
	font-size: .8125rem;
	font-weight: 650;
	letter-spacing: .01em;
	color: var(--wp--preset--color--ink);
}

.quote-form__opt { font-weight: 400; color: var(--wp--preset--color--muted); }
.quote-form__req { color: var(--brand); }

.quote-form input,
.quote-form textarea {
	width: 100%;
	min-height: var(--tap);
	padding: .75rem .9rem;
	border: 1px solid var(--line-strong);
	border-radius: var(--r-sm);
	background: var(--wp--preset--color--base);
	color: var(--wp--preset--color--ink);
	/* Under 16px makes iOS Safari zoom the page on focus, which reads as a bug. */
	font: inherit;
	font-size: 16px;
	line-height: 1.4;
}

.quote-form input:focus,
.quote-form textarea:focus {
	border-color: var(--brand);
	box-shadow: 0 0 0 3px var(--brand-ring);
	outline: none;
}

.quote-form textarea { min-height: 6.5rem; resize: vertical; }

.quote-form button { width: 100%; }

.quote-form__note {
	margin: .85rem 0 0;
	font-size: .8125rem;
	line-height: 1.5;
	color: var(--wp--preset--color--muted);
}

/* Off-canvas rather than display:none, which some bots check for. */
.quote-form__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.quote-form__ok,
.quote-form__err {
	margin: 0;
	padding: 1rem 1.15rem;
	border-radius: var(--r-sm);
	font-weight: 600;
}

/* Contrast checked: both exceed 4.5:1 on their own background. */
.quote-form__ok { background: #DCFCE7; color: #14532D; }
.quote-form__err { background: #FEE2E2; color: #7F1D1D; }

.has-ink-background-color .quote-form { border-color: transparent; }

@media (prefers-reduced-motion: no-preference) {
	.quote-form input, .quote-form textarea {
		transition: border-color 160ms var(--ease), box-shadow 160ms var(--ease);
	}
}

/* -------------------------------------------------------------- dark mode */

/* One ground colour for the whole document. Panels lift off it with a hair of
   white, never with a different hue, so alternating light and dark bands is what
   made this read as two half-finished sites stitched together. */

[data-theme="dark"] {
	--d-bg:      var(--wp--preset--color--night);
	/* Alternating band. Three percent off the ground, enough to register as a
	   section boundary, not enough to read as a second background colour. */
	--d-band:    #080D15;
	--d-panel:   #0D141F;
	--d-panel-2: #131C29;
	--d-line:    rgba(255, 255, 255, .10);
	--d-line-2:  rgba(255, 255, 255, .18);
	--d-text:    #E9EEF6;
	--d-muted:   #9BA9C0;
	/* The brand blue is ~3.4:1 on near-black, which is under the 4.5:1 floor for text.
	   Links on dark get a lifted tint of the same hue instead. */
	--d-link:    color-mix(in srgb, var(--brand) 55%, white);

	--line: var(--d-line);
	--line-strong: var(--d-line-2);
	--sh-1: none;
	--sh-2: 0 6px 18px -6px rgba(0, 0, 0, .6);
	--sh-3: 0 24px 50px -18px rgba(0, 0, 0, .75);

	/* Echo of the logo's lightning: brand blue over one corner, accent gold
	   over the other. One definition, reused with `background-attachment:
	   fixed` on body and on every opaque band below: fixed anchors it to the
	   viewport rather than to whichever box paints it, so the same two corner
	   glows line up across a section boundary instead of each band drawing its
	   own copy and visibly repeating one under the other. */
	--d-glow:
		radial-gradient(85% 130% at 100% 0%, color-mix(in srgb, var(--brand) 26%, transparent), transparent 62%),
		radial-gradient(75% 120% at 0% 100%, color-mix(in srgb, var(--accent) 24%, transparent), transparent 65%);
}

[data-theme="dark"] body {
	background-color: var(--d-bg);
	color: var(--d-text);
}

/* The glow itself moved off body's own background and onto this fixed,
   fadeable pseudo-element so it can be delayed the same way the hero's own
   glow divs are (assets/motion.js) — background-image can't be transitioned
   directly, so a plain opacity fade needs something with its own paint
   layer. Only body's copy: .sec--alt/.sec--cta/etc. keep painting their own
   instant copy (see the --d-glow comment above on why every opaque band
   needs one) since they're never in view during the brief pre-scroll window
   this delay covers, so there's nothing to desync. z-index: -1 keeps it
   behind body's actual content, same stacking spot the background-image
   version occupied. */
[data-theme="dark"] body::before {
	content: '';
	position: fixed;
	inset: 0;
	z-index: -1;
	background-image: var(--d-glow);
	pointer-events: none;
}

/* Gating the hidden state on .motion-ready (added by JS a beat after first
   paint) rather than making it the plain default was the bug: body::before
   started at its unset default (opacity 1, fully visible) for that first
   beat, then motion-ready landed and the rule started matching — a genuine
   style change after initial render, which is exactly what a transition
   fires on. So instead of starting hidden, it played a quick 900ms fade
   from visible down to 0 the moment the page loaded, then wouldn't fade
   back in until the scroll threshold — the opposite of delayed, and
   probably what read as "appears immediately".
   Unconditional opacity: 0 here means the pseudo-element is hidden from its
   very first computed style, before any JS has run, so there's no prior
   rendered state for a transition to fire from. .is-visible (scroll-gated,
   assets/motion.js) is the only thing that ever changes it after that,
   which is the one transition actually wanted. Reduced motion still sees it
   at all times, since none of this block applies then. */
@media (prefers-reduced-motion: no-preference) {
	[data-theme="dark"] body::before {
		opacity: 0;
		transition: opacity 900ms var(--ease);
	}
	html.is-visible[data-theme="dark"] body::before { opacity: 1; }
}

/* Every band the theme paints (surface, ink, base) collapses to the one
   ground colour, with the same fixed glow layered on top so it reads as one
   continuous wash behind the page rather than stopping at each section. */
[data-theme="dark"] .has-surface-background-color,
[data-theme="dark"] .has-ink-background-color,
[data-theme="dark"] .has-base-background-color,
[data-theme="dark"] .hero {
	background-color: var(--d-bg) !important;
	background-image: var(--d-glow);
	background-attachment: fixed;
	color: var(--d-text);
}

[data-theme="dark"] .hero--photo { background-image: var(--hero-img) !important; }

@media (max-width: 781px) {
	[data-theme="dark"] .hero--photo { background-image: var(--hero-img-mobile, var(--hero-img)) !important; }
}

/* Fade entirely on hero's own side, contained inside its own box — no
   reaching into the trust strip, no change to hero's height/padding/content.
   The ramp ends on --d-bg itself, the exact colour the (backgroundless,
   transparent) trust strip already shows through to, so hero's bottom edge
   lands on a flat match of what's already there rather than introducing a new
   colour. It must stay a token reference: this was once written out as
   rgba(4, 6, 11), which silently stopped matching when --d-bg moved to
   #041330 and left a black band against the deep blue below.
   .hero--photo sets isolation: isolate (style.css:600), which makes .hero
   its own stacking context. That means this z-index was never actually
   comparable to .glow's — a sibling of .hero under .hero-group — in the
   first place: raising it to 5 only reordered paint among .hero's own
   descendants, it couldn't reach past hero's isolation boundary to sit
   above an external sibling. .hero itself carries no z-index of its own, so
   as a whole unit it already paints below the glows (explicit positive
   z-index) in .hero-group's stacking context regardless of what this
   value is — which is also why raising it earlier produced no visible
   change. Set back to 1, its only real job now is staying above hero's own
   photo/scrim, not the glows. */
[data-theme="dark"] .hero { position: relative; }
/* :not(.glow) matters now that the glows live inside .hero (moved from
   .hero-group): they're also direct children this selector would otherwise
   match, and at (0,2,0) it outranks .glow's own (0,1,0) rule — without the
   exclusion this was silently forcing the glows to position: relative and
   z-index: 6, which stacked them into normal document flow as ~600px block
   elements each, inflating hero's own height, and stomped their z-index: 2
   entirely. */
[data-theme="dark"] .hero > *:not(.glow) { position: relative; z-index: 6; }

[data-theme="dark"] .hero::after {
	content: '';
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	height: 260px;
	z-index: 1;
	pointer-events: none;
	/* Lands on --d-bg, never a literal. The trust strip below paints no
	   background of its own, so what shows there is body's --d-bg; a hardcoded
	   rgba(4, 6, 11) was a near-black that no longer matched once --d-bg became
	   #041330, and it read as a black band butting into deep blue. Driving both
	   ends off the same token means the seam stays invisible if the palette
	   moves again. color-mix rather than `transparent` for the upper stops:
	   `transparent` is rgba(0,0,0,0), so interpolating toward it in sRGB drags
	   the ramp through black — the exact artefact being fixed. */
	background: linear-gradient(
		to bottom,
		color-mix(in srgb, var(--d-bg, #041330) 0%, transparent) 0%,
		color-mix(in srgb, var(--d-bg, #041330) 60%, transparent) 55%,
		var(--d-bg, #041330) 100%
	);
}

/* .hero-group is a plain (non-alignfull) wp:group, so the theme's default
   block padding/gap applied to it directly — the alignfull escape trick that
   .hero and .sec--trust each carry only bleeds themselves individually, and
   doesn't help their new parent. Zeroing this out is the fix, not another
   layer of alignfull/vw math on top of what they're already doing.
   overflow: hidden here is the outer backstop — it clips at the combined
   hero+trust-strip boundary so nothing bleeds into services — not what
   scopes the glows' own stacking (that's .hero's isolation, below). */
.hero-group { position: relative; isolation: isolate; overflow: hidden; width: 100%; margin: 0; padding: 0; }

/* Ambient corner glow, contained to hero's own upper area only — no seam
   crossing, trust strip untouched. Markup lives inside
   electrician_hero_block() (inc/pages.php) rather than as template-level
   children of .hero-group: .hero--photo sets isolation: isolate, which
   makes .hero its own stacking context, so living inside .hero puts these
   in the same local context as .hero::after and .hero's content, and
   z-index: 2 is a real, effective position between them (1 and 6) rather
   than a number that only looks like one from outside that context. */
.glow {
	position: absolute;
	border-radius: 50%;
	pointer-events: none;
	filter: blur(120px);
	mix-blend-mode: screen;
	z-index: 2; /* between .hero::after (1) and .hero > * (6) */
}

.glow--blue {
	right: -8%;
	top: -80px;
	width: 800px;
	height: 600px;
	background: radial-gradient(closest-side, rgba(60, 120, 240, .22), rgba(60, 120, 240, 0));
}

[data-theme="dark"] .wp-block-heading,
[data-theme="dark"] h1, [data-theme="dark"] h2,
[data-theme="dark"] h3, [data-theme="dark"] h4 { color: var(--d-text); }

[data-theme="dark"] p,
[data-theme="dark"] li,
[data-theme="dark"] .has-muted-color { color: var(--d-muted) !important; }

/* Body copy links only. Components that style their own links (the nav, the
   buttons, the call bar) are excluded here rather than fighting this rule from
   further down the file: it scores (0,4,1), which quietly outranks most
   component selectors and repainted the nav items link-blue while the dropdown
   triggers, being buttons, stayed near-white. */
[data-theme="dark"] a:not(.wp-block-button__link):not(.btn):not(.callbar):not(.nav__link) { color: var(--d-link); }

/* ---- panels ---- */

[data-theme="dark"] .card,
[data-theme="dark"] .steps__item,
[data-theme="dark"] .quickfacts__item,
[data-theme="dark"] .causes__item,
[data-theme="dark"] .faq,
[data-theme="dark"] .quote-form,
[data-theme="dark"] .nav__mega,
[data-theme="dark"] .about__placeholder,
[data-theme="dark"] .arealist a,
[data-theme="dark"] .arealist span,
[data-theme="dark"] .arealist p,
[data-theme="dark"] .cert-badge {
	background: var(--d-panel);
	border-color: var(--d-line);
	color: var(--d-text);
}

[data-theme="dark"] .about__placeholder { color: var(--d-muted); }
[data-theme="dark"] .cert-badge__note { color: var(--d-muted); }

/* Panels sit on the band, so the band cannot also be panel-coloured or the
   tiles vanish into it. */
[data-theme="dark"] .sec--alt {
	background-color: var(--d-band);
	background-image: var(--d-glow);
	background-attachment: fixed;
	border-color: var(--d-line);
}

[data-theme="dark"] .sec--alt .card,
[data-theme="dark"] .sec--alt .steps__item,
[data-theme="dark"] .sec--alt .faq,
[data-theme="dark"] .sec--alt .about__placeholder,
[data-theme="dark"] .sec--alt .cert-badge { background: var(--d-panel-2); }

[data-theme="dark"] .sec--alt .card:hover { background: #18222F; }

[data-theme="dark"] .steps__item::before { background: var(--d-line-2); }

/* The trust strip's separator is a real block-level border, coloured with the
   light-theme preset token directly rather than the --line custom property, so
   dark mode never gets a chance to repaint it and it renders as a stark pale
   hairline cutting across the glow wash. Hide it, and don't try to replace it
   with an overlay of our own either.
   This section has never had a background colour of its own — it's
   transparent, showing the same fixed body --d-glow wash every non-hero
   section already shows through, which is what makes the page read as one
   continuous backdrop rather than stacked bands in the first place. Hero is
   the one exception, opaque out of necessity for the photo, and its own
   fade already targets --d-bg to land on that same wash. The only real bug
   was the 24px default block-spacing margin between the two sections — a
   gap neither box painted anything into — so closing that is the whole fix.
   No overlay needed once the gap's gone: this section going back to doing
   nothing is what lets it disappear into the same background hero is
   already fading toward. */
[data-theme="dark"] .sec--trust {
	border-bottom-color: transparent !important;
	margin-top: 0;
}

[data-theme="dark"] .sec--trust::after {
	content: '';
	position: absolute;
	inset: auto 0 -28px 0;
	height: 56px;
	background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--brand) 55%, transparent), transparent);
	filter: blur(18px);
	pointer-events: none;
	display: none;
}

[data-theme="dark"] .sec__eyebrow { color: var(--accent); }

/* This was the one place the glow used to be allowed to show at all, but now
   it's the site-wide wash, so the closing band just needs the seam. */
[data-theme="dark"] .sec--cta {
	background-image: var(--d-glow);
	background-attachment: fixed;
	border-top: 1px solid var(--d-line);
}

[data-theme="dark"] .cta__points { border-top-color: var(--d-line-2); }
[data-theme="dark"] .cta__points li { color: var(--d-text) !important; }
[data-theme="dark"] .cta__lede { color: var(--d-muted) !important; }
[data-theme="dark"] .cta__note { color: var(--d-muted); }

/* Gold, matching the hero and the form (the brand blue is under 4.5:1 here). */
[data-theme="dark"] .sec--cta .btn--primary { background: var(--accent); color: #12202E; }
[data-theme="dark"] .sec--cta .btn--primary:hover { background: color-mix(in srgb, var(--accent) 88%, white); }

[data-theme="dark"] .card:hover {
	background: var(--d-panel-2);
	border-color: color-mix(in srgb, var(--brand) 55%, var(--d-line));
}

[data-theme="dark"] .card__title a,
[data-theme="dark"] .card h3 a { color: var(--d-text); }
[data-theme="dark"] .card__links,
[data-theme="dark"] .card ul { border-top-color: var(--d-line); }
[data-theme="dark"] .card__links a,
[data-theme="dark"] .card li a { color: var(--d-link); }

[data-theme="dark"] .card__links li.is-urgent a,
[data-theme="dark"] .nav__mega-list li.is-urgent a { color: var(--accent); }
[data-theme="dark"] .tag-247 { background: var(--accent); color: #12202E; }
[data-theme="dark"] .card--urgent {
	border-color: color-mix(in srgb, var(--accent) 55%, var(--d-line));
	box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 32%, transparent), var(--sh-1);
}
[data-theme="dark"] .card__badge { background: var(--accent); color: #12202E; }

[data-theme="dark"] .arealist a { color: var(--d-link); }
[data-theme="dark"] .arealist a:hover {
	background: var(--d-panel-2);
	border-color: color-mix(in srgb, var(--brand) 55%, var(--d-line));
}

/* ---- header and nav ---- */

/* ::before, not the header itself, since the bar's surface moved to the pseudo so it
   could fade, and leaving this on the parent meant the dark theme kept painting
   an invisible layer while the visible one fell back to the white `base`
   colour. A dark site must not flash a white bar on scroll. */
[data-theme="dark"] .site-header::before {
	background: color-mix(in srgb, var(--d-bg) 92%, transparent);
	border-bottom-color: var(--d-line);
}

/* `.nav a` deliberately: the blanket dark link rule above scores (0,4,1) and
   was repainting the plain nav items link-blue, and the dropdown triggers
   which are buttons, so that rule never touched them and stayed near-white. The
   bar rendered "Services" white next to "About" in blue. */
[data-theme="dark"] .nav a.nav__link,
[data-theme="dark"] .nav .nav__trigger { color: var(--d-text); }

[data-theme="dark"] .nav__link:hover,
[data-theme="dark"] .nav__trigger:hover,
[data-theme="dark"] .nav__trigger[aria-expanded="true"] {
	background: rgba(255, 255, 255, .08);
	color: var(--accent);
}

[data-theme="dark"] .nav__mega-head { color: var(--d-text); border-bottom-color: var(--d-line); }
[data-theme="dark"] .nav__mega-list a { color: var(--d-muted); }
[data-theme="dark"] .nav__mega-list a:hover { background: rgba(255, 255, 255, .07); color: var(--accent); }

[data-theme="dark"] .nav__burger { background: transparent; border-color: var(--d-line-2); }
[data-theme="dark"] .nav__burger-bars,
[data-theme="dark"] .nav__burger-bars::before,
[data-theme="dark"] .nav__burger-bars::after { background: var(--d-text); }

@media (max-width: 900px) {
	[data-theme="dark"] .nav__panel { background: var(--d-bg); border-bottom-color: var(--d-line); }
	[data-theme="dark"] .nav__link,
	[data-theme="dark"] .nav__trigger { border-bottom-color: var(--d-line); }
	[data-theme="dark"] .nav__mega { background: transparent; border-bottom-color: var(--d-line); }
}

/* ---- buttons ---- */

/* Gold on near-black is the logo's own strongest pairing and clears contrast
   easily; the brand blue does not at this lightness. */
[data-theme="dark"] .hero .wp-block-button__link,
[data-theme="dark"] .quote-form button {
	background: var(--accent);
	color: #12202E;
}

[data-theme="dark"] .hero .wp-block-button__link:hover,
[data-theme="dark"] .quote-form button:hover {
	background: color-mix(in srgb, var(--accent) 88%, white);
	color: #12202E;
}

/* ---- form ---- */

[data-theme="dark"] .quote-form label { color: var(--d-text); }
[data-theme="dark"] .quote-form__opt,
[data-theme="dark"] .quote-form__note { color: var(--d-muted); }
[data-theme="dark"] .quote-form__req { color: var(--accent); }

[data-theme="dark"] .quote-form input,
[data-theme="dark"] .quote-form textarea {
	background: rgba(255, 255, 255, .04);
	border-color: var(--d-line-2);
	color: var(--d-text);
}

[data-theme="dark"] .quote-form input::placeholder,
[data-theme="dark"] .quote-form textarea::placeholder { color: var(--d-muted); }

[data-theme="dark"] .suburb-check__input {
	background: rgba(255, 255, 255, .04);
	border-color: var(--d-line-2);
	color: var(--d-text);
}
[data-theme="dark"] .suburb-check__input::placeholder { color: var(--d-muted); }
[data-theme="dark"] .suburb-check__result.is-yes { color: #4ADE80; }

[data-theme="dark"] .chip span {
	background: rgba(255, 255, 255, .04);
	border-color: var(--d-line-2);
	color: var(--d-text);
}

[data-theme="dark"] .chip input:checked + span {
	background: var(--accent);
	border-color: var(--accent);
	color: #12202E;
}

[data-theme="dark"] .chips legend { color: var(--d-text); }

/* ---- trust strip and misc ---- */

[data-theme="dark"] .trust__item { color: var(--d-text); }
[data-theme="dark"] .trust__icon {
	background: color-mix(in srgb, var(--accent) 18%, transparent);
	color: var(--accent);
}

[data-theme="dark"] .stat__num { color: var(--accent); }
[data-theme="dark"] .stat__label { color: var(--d-text); opacity: .7; }

[data-theme="dark"] .wp-block-post-content > ol > li::before {
	background: color-mix(in srgb, var(--brand) 26%, transparent);
	color: var(--d-link);
}

[data-theme="dark"] .editor-note {
	background: rgba(194, 65, 12, .14);
	border-color: rgba(251, 146, 60, .55);
	color: #FDBA74;
}

[data-theme="dark"] .callbar { color: #12202E; background: var(--accent); }

[data-theme="dark"] .steps__title,
[data-theme="dark"] .faq__q,
[data-theme="dark"] .quote__text { color: var(--d-text); }

[data-theme="dark"] .faq__item { border-bottom-color: var(--d-line); }
[data-theme="dark"] .faq__item[open] .faq__q { color: var(--accent); }
[data-theme="dark"] .faq__item.is-closing .faq__q { color: var(--d-text); }
[data-theme="dark"] .gallery__item img { border-color: var(--d-line); }
[data-theme="dark"] .map-embed { border-color: var(--d-line); }
/* Deliberately left light: the card is a quotation from another surface, and
   restyling it to match ours would blur where it came from. */
[data-theme="dark"] .review { background: #fff; color: #202124; }
[data-theme="dark"] .review__name { color: #202124; }
[data-theme="dark"] .review__text { color: #3C4043; }
[data-theme="dark"] .review__price,
[data-theme="dark"] .review__src { color: #5F6368; }
[data-theme="dark"] .review p { color: #3C4043 !important; }
[data-theme="dark"] .reviews__meta { color: #5F6368; }
[data-theme="dark"] .reviews__meta strong,
[data-theme="dark"] .reviews__brand { color: #202124; }
[data-theme="dark"] .reviews__source { color: var(--d-muted); }

[data-theme="dark"] footer.wp-block-group { border-top: 1px solid var(--d-line); }
[data-theme="dark"] footer .wp-block-site-title a { color: var(--accent); }

/* ------------------------------------------------------------------ chips */

.chips {
	margin: 0 0 1.1rem;
	padding: 0;
	border: 0;
}

.chips legend {
	padding: 0;
	margin-bottom: .5rem;
	font-size: .8125rem;
	font-weight: 650;
}

.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chips legend { float: left; width: 100%; }

.chip { position: relative; }

/* Visually hidden, not display:none, so the radio must stay focusable and
   announced, and the visible chip is its label. */
.chip input {
	position: absolute;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

.chip span {
	display: inline-flex;
	align-items: center;
	min-height: var(--tap);
	padding: 0 1rem;
	border: 1px solid var(--line-strong);
	border-radius: 999px;
	background: var(--wp--preset--color--base);
	font-size: .9375rem;
	font-weight: 550;
	line-height: 1;
	cursor: pointer;
	user-select: none;
}

.chip input:checked + span {
	background: var(--brand);
	border-color: var(--brand);
	color: #fff;
}

.chip input:focus-visible + span {
	outline: 2px solid var(--brand);
	outline-offset: 2px;
}

.chip:hover span { border-color: var(--brand); }

@media (prefers-reduced-motion: no-preference) {
	.chip span { transition: background-color 160ms var(--ease), border-color 160ms var(--ease), color 160ms var(--ease); }
	.chip input:checked + span { animation: chip-pop 220ms var(--ease); }
	@keyframes chip-pop {
		0%   { transform: scale(1); }
		45%  { transform: scale(1.06); }
		100% { transform: scale(1); }
	}
}

/* --------------------------------------------------------------- reveals */

/* Only applied once JS confirms it can animate, so a failed script leaves the
   page readable rather than invisible. */
@media (prefers-reduced-motion: no-preference) {
	.motion-ready .reveal {
		opacity: 0;
		transform: translateY(14px);
		transition:
			opacity 520ms var(--ease) var(--reveal-delay, 0ms),
			transform 520ms var(--ease) var(--reveal-delay, 0ms);
		will-change: opacity, transform;
	}

	.motion-ready .reveal.is-revealed {
		opacity: 1;
		transform: none;
		will-change: auto;
	}

	/* ---- dropdowns ---- */

	/* Transitions rather than a keyframe: a keyframe only plays on open, so the
	   panel animated in and then vanished on a frame when it closed. Hiding is
	   `display:none` by default, which cannot transition, so while motion is
	   allowed the panel keeps its box and hides via visibility instead, so the
	   delayed `visibility` step is what holds it on screen for the fade out.

	   The keyframe also carried `translateX(-50%)` unscoped, and on mobile
	   where the panel is a static accordion, not a centred dropdown, which yanked it
	   half its width to the left for the duration of the animation. */
	.nav__mega {
		opacity: 1;
		visibility: visible;
		transform: translateX(-50%) translateY(0);
		transition:
			opacity 190ms var(--ease),
			transform 190ms var(--ease),
			visibility 0s;
	}

	.nav__mega[hidden] {
		display: block;
		opacity: 0;
		visibility: hidden;
		transform: translateX(-50%) translateY(-8px);
		pointer-events: none;
		transition:
			opacity 150ms var(--ease),
			transform 150ms var(--ease),
			visibility 0s linear 150ms;
	}

	/* Press feedback. Scale on the button only, so nothing around it shifts. */
	.btn:active,
	.wp-block-button__link:active,
	.quote-form button:active { transform: scale(.985); }

	.card:hover { transform: translateY(-2px); }
	.card { transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease), transform 200ms var(--ease); }

	.nav__cta svg { transition: transform 200ms var(--ease); }
	.nav__cta:hover svg { transform: rotate(-8deg) scale(1.08); }

	/* Answers slide open. Height is animated in JS because a <details> has no
	   measurable height until it is open; this just supplies the easing and the
	   clip. */
	.faq__body { overflow: hidden; }
	.faq__body.is-animating { transition: height 280ms var(--ease); }
}

/* The mobile panel is an in-flow accordion, so it cannot hold its box while
   closed the way the desktop dropdown does, and it would reserve the open height
   permanently. A 0fr/1fr grid row collapses to nothing and still animates. */
@media (prefers-reduced-motion: no-preference) and (max-width: 900px) {
	.nav__mega {
		display: grid;
		grid-template-rows: 1fr;
		transform: none;
		transition:
			grid-template-rows 260ms var(--ease),
			opacity 200ms var(--ease),
			padding 260ms var(--ease),
			visibility 0s;
	}

	.nav__mega[hidden] {
		display: grid;
		grid-template-rows: 0fr;
		padding-top: 0;
		padding-bottom: 0;
		/* The trigger above already draws a rule; keeping this one while the
		   panel is collapsed stacks two hairlines and the closed items with a
		   dropdown read heavier than the ones without. */
		border-bottom-width: 0;
		opacity: 0;
		transform: none;
		transition:
			grid-template-rows 260ms var(--ease),
			opacity 200ms var(--ease),
			padding 260ms var(--ease),
			visibility 0s linear 260ms;
	}

	/* A 0fr row only collapses if its content can be crushed. */
	.nav__mega > * { min-height: 0; overflow: hidden; }

	/* The hover bridge is a pointer affordance; there is no hover here, and as a
	   grid item it would occupy a row of its own. */
	.nav__mega::before { content: none; }

	/* Same treatment for the burger panel itself, and it is fixed, so keeping its
	   box costs no layout. */
	.nav__panel {
		display: flex;
		visibility: hidden;
		opacity: 0;
		transform: translateY(-10px);
		pointer-events: none;
		transition:
			opacity 220ms var(--ease),
			transform 220ms var(--ease),
			visibility 0s linear 220ms;
	}

	.nav__panel.is-open {
		visibility: visible;
		opacity: 1;
		transform: none;
		pointer-events: auto;
		transition:
			opacity 220ms var(--ease),
			transform 220ms var(--ease),
			visibility 0s;
	}
}

/* --------------------------------------------------------------- call bar */

.callbar {
	position: fixed;
	inset: auto 0 0 0;
	z-index: var(--z-callbar);
	display: none;
	min-height: var(--tap);
	align-items: center;
	justify-content: center;
	gap: .5rem;
	padding: .95rem;
	background: var(--brand);
	color: #fff;
	font-weight: 700;
	font-size: 1.05rem;
	letter-spacing: -.01em;
	text-decoration: none;
	box-shadow: 0 -4px 16px -6px rgba(11, 18, 32, .25);
}

/* Phone only: the highest-converting element on a trade site. Body gets
   matching padding so it never covers the footer. */
@media (max-width: 781px) {
	.callbar { display: flex; }
	body { padding-bottom: calc(var(--tap) + 1.25rem); }

	/* Hidden until the hero's own call button scrolls away, so it isn't
	   duplicating a control already on screen. Scoped to .motion-ready: without
	   JS the bar simply stays put rather than never appearing. */
	@media (prefers-reduced-motion: no-preference) {
		.motion-ready .callbar {
			transform: translateY(100%);
			transition: transform 260ms var(--ease);
		}
		.motion-ready .callbar.is-visible { transform: translateY(0); }
	}
}

/* ---------------------------------------------------------- footer sitemap */

.footer__sitemap {
	display: flex;
	flex-wrap: wrap;
	gap: 1.75rem 2.5rem;
	margin-top: 2.5rem;
	padding-top: 1.75rem;
	border-top: 1px solid rgba(255, 255, 255, .12);
}

.footer__sitemap-group { flex: 1 1 9rem; min-width: 9rem; }

.footer__sitemap-heading {
	margin: 0 0 .6rem;
	font-size: .8125rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: .04em;
	opacity: .7;
}

.footer__sitemap-heading a { color: inherit; text-decoration: none; }
.footer__sitemap-heading a:hover { text-decoration: underline; }

.footer__sitemap-group ul {
	display: flex;
	flex-direction: column;
	gap: .4rem;
	margin: 0;
	padding: 0;
	list-style: none;
}

.footer__sitemap-group a { font-size: .8125rem; color: inherit; text-decoration: none; opacity: .82; }
.footer__sitemap-group a:hover { opacity: 1; text-decoration: underline; }

.footer__social-link { display: inline-flex; align-items: center; gap: .4em; }
.footer__social-icon { flex: none; }

/* ------------------------------------------------------------ footer legal */

.footer__legal {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: .5rem 1.5rem;
	margin-top: 2.5rem;
	padding-top: 1.5rem;
	border-top: 1px solid rgba(255, 255, 255, .12);
	font-size: .8125rem;
	opacity: .8;
}

.footer__legal-links {
	display: flex;
	flex-wrap: wrap;
	gap: .35rem 1.25rem;
}

.footer__legal-links a { color: inherit; text-decoration: none; }
.footer__legal-links a:hover { text-decoration: underline; }

/* ------------------------------------------------------------ cookie banner */

.cookie-banner {
	position: fixed;
	left: 1rem;
	right: 1rem;
	bottom: 1rem;
	z-index: var(--z-cookie);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: .85rem 1.25rem;
	max-width: 640px;
	margin-inline: auto;
	padding: 1.1rem 1.25rem;
	background: var(--wp--preset--color--ink);
	color: var(--wp--preset--color--base);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-2);
}

.cookie-banner[hidden] { display: none; }

.cookie-banner__text {
	margin: 0;
	flex: 1 1 240px;
	font-size: .875rem;
	line-height: 1.5;
}

.cookie-banner__text a { color: inherit; text-decoration: underline; }

.cookie-banner__actions { display: flex; flex: none; gap: .6rem; }

.cookie-banner__actions .btn {
	min-height: 0;
	padding: .55rem 1rem;
	font-size: .875rem;
}

/* Clear of the phone call bar, which owns the full width of the bottom edge
   on the same breakpoint (see .callbar above) rather than stacking on it. */
@media (max-width: 781px) {
	.cookie-banner { left: .5rem; right: .5rem; bottom: calc(var(--tap) + 1.75rem); }
}

@media (max-width: 480px) {
	.cookie-banner { flex-direction: column; align-items: stretch; }
	/* .cookie-banner__text's flex-grow is for the row layout above, sharing
	   width with the buttons. In a column it grows along the vertical axis
	   instead — the text itself stays its natural height, but the box around
	   it stretches to fill the container, leaving a blank gap before the
	   buttons. */
	.cookie-banner__text { flex: none; }
	.cookie-banner__actions { justify-content: flex-end; }
}

/* ------------------------------------------------------------ chat widget */

/* Bottom-right stack: the bubble, the one-line teaser above it, and the panel
   that replaces both when open. Dark-mode overrides sit at the end of this
   section rather than up in the dark-mode block: the widget landed long after
   that block was written, and its handful of overrides are easier to keep
   honest next to the rules they override than 600 lines away from them. */

.chatw {
	position: fixed;
	right: 1.25rem;
	bottom: 1.25rem;
	z-index: var(--z-chat);
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: .75rem;
}

.chatw[hidden] { display: none; }

.chatw__sr {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* ---- bubble ---- */

.chatw__bubble {
	display: grid;
	place-items: center;
	width: 60px;
	height: 60px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: var(--brand);
	color: #fff;
	box-shadow: var(--sh-3);
	cursor: pointer;
}

.chatw__bubble:hover { background: var(--brand-hi); }

.chatw__ico--close { display: none; }
.chatw.is-open .chatw__ico--open { display: none; }
.chatw.is-open .chatw__ico--close { display: block; }

/* ---- teaser ---- */

.chatw__teaser {
	position: relative;
	display: flex;
	align-items: center;
	gap: .7rem;
	max-width: min(19rem, calc(100vw - 2rem));
	padding: .8rem 2.1rem .8rem .8rem;
	background: var(--wp--preset--color--base);
	border: 1px solid var(--line);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-3);
	cursor: pointer;
}

.chatw__teaser[hidden] { display: none; }

/* Tail pointing down at the bubble. Border-less by design: a one-pixel line
   around a 9px triangle reads as a rendering artefact, not as a speech tail. */
.chatw__teaser::after {
	content: '';
	position: absolute;
	right: 20px;
	bottom: -9px;
	width: 16px;
	height: 10px;
	background: var(--wp--preset--color--base);
	clip-path: polygon(0 0, 100% 0, 50% 100%);
}

.chatw__teaser-text {
	margin: 0;
	font-size: .9rem;
	font-weight: 650;
	line-height: 1.4;
	color: var(--wp--preset--color--ink);
}

.chatw__x {
	position: absolute;
	top: 6px;
	right: 6px;
	display: grid;
	place-items: center;
	width: 26px;
	height: 26px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: transparent;
	color: var(--wp--preset--color--muted);
	cursor: pointer;
}

.chatw__x:hover {
	background: var(--brand-lo);
	color: var(--wp--preset--color--ink);
}

/* ---- avatar ---- */

.chatw__avatar {
	flex: none;
	display: grid;
	place-items: center;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	background: var(--brand);
	color: #fff;
}

.chatw__avatar--word {
	font-size: .9rem;
	font-weight: 700;
	letter-spacing: .02em;
}

.chatw__avatar--sm { width: 30px; height: 30px; }
.chatw__avatar--sm svg { width: 17px; height: 17px; }
.chatw__avatar--sm.chatw__avatar--word { font-size: .75rem; }

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

.chatw__panel {
	display: flex;
	flex-direction: column;
	width: min(22.5rem, calc(100vw - 2.5rem));
	max-height: min(38rem, calc(100vh - 8rem));
	/* svh, not vh: mobile browsers measure vh against the chrome-collapsed
	   viewport, so a vh-capped panel is taller than the screen on first paint. */
	max-height: min(38rem, calc(100svh - 8rem));
	overflow: hidden;
	background: var(--wp--preset--color--base);
	border-radius: var(--r-lg);
	box-shadow: var(--sh-3);
}

.chatw__panel[hidden] { display: none; }

.chatw__head {
	display: flex;
	align-items: center;
	gap: .75rem;
	flex: none;
	padding: .85rem 1rem;
	background: var(--brand);
	color: #fff;
}

.chatw__title {
	flex: 1;
	margin: 0;
	font-size: 1.05rem;
	font-weight: 700;
	letter-spacing: -.01em;
}

.chatw__min {
	display: grid;
	place-items: center;
	width: 32px;
	height: 32px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: transparent;
	color: inherit;
	cursor: pointer;
}

.chatw__min:hover { background: rgba(255, 255, 255, .18); }

.chatw__body {
	flex: 1;
	overflow-y: auto;
	padding: 1rem;
}

.chatw__said {
	display: flex;
	align-items: flex-start;
	gap: .6rem;
	margin-bottom: .9rem;
}

.chatw__msg {
	margin: 0;
	padding: .7rem .9rem;
	background: color-mix(in srgb, var(--brand) 7%, white);
	/* Square corner top-left, against the avatar: the same shorthand every
	   messaging app uses to say which side of the conversation this is. */
	border-radius: 4px var(--r) var(--r) var(--r);
	font-size: .9rem;
	line-height: 1.5;
	color: var(--wp--preset--color--ink);
}

/* The panel reuses .quote-form so the fields, focus rings, honeypot and dark
   mode all stay in one place. Only the chrome around them changes: inside a
   360px panel the card's own padding, shadow and 34rem cap are wrong. */
.quote-form--chat {
	max-width: none;
	padding: .85rem;
	border-radius: var(--r);
	background: transparent;
	box-shadow: none;
}

.quote-form--chat p { margin-bottom: .6rem; }
.quote-form--chat textarea { min-height: 4.5rem; }

.chatw__send {
	display: flex;
	justify-content: center;
	margin: 0;
}

.chatw__send button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: .5rem;
	width: auto;
	min-width: 8.5rem;
}

.chatw__fine {
	margin: .8rem 0 0;
	font-size: .75rem;
	line-height: 1.5;
	color: var(--wp--preset--color--muted);
}

.chatw__fine a { color: inherit; }

.chatw__done { font-size: .9rem; }

/* The page form's error box, sized for a 360px panel. */
.chatw__err {
	margin-bottom: .7rem;
	padding: .6rem .75rem;
	font-size: .8125rem;
}

.chatw__foot {
	flex: none;
	padding: .7rem 1rem;
	border-top: 1px solid var(--line);
}

.chatw__call {
	display: inline-flex;
	align-items: center;
	gap: .5rem;
	font-size: .85rem;
	font-weight: 650;
	color: var(--brand);
	text-decoration: none;
}

.chatw__call:hover { text-decoration: underline; }

/* Clear of the phone call bar, which owns the bottom edge on this breakpoint
   (see .callbar above) rather than stacking with it. */
@media (max-width: 781px) {
	.chatw {
		right: .75rem;
		bottom: calc(var(--tap) + 1.5rem);
	}

	.chatw__panel {
		max-height: calc(100vh - var(--tap) - 5.5rem);
		max-height: calc(100svh - var(--tap) - 5.5rem);
	}
}

@media (prefers-reduced-motion: no-preference) {
	.chatw__bubble { transition: transform 180ms var(--ease), background-color 180ms var(--ease); }
	.chatw__bubble:hover { transform: translateY(-2px); }

	.chatw__panel,
	.chatw__teaser { animation: chatw-in 220ms var(--ease) both; }

	@keyframes chatw-in {
		from { opacity: 0; transform: translateY(10px); }
		to   { opacity: 1; transform: none; }
	}
}

/* ---- dark mode ---- */

[data-theme="dark"] .chatw__panel,
[data-theme="dark"] .chatw__teaser {
	background: var(--d-panel);
	border-color: var(--d-line);
}

[data-theme="dark"] .chatw__teaser::after { background: var(--d-panel); }

[data-theme="dark"] .chatw__teaser-text,
[data-theme="dark"] .chatw__msg { color: var(--d-text); }

[data-theme="dark"] .chatw__msg { background: var(--d-panel-2); }

[data-theme="dark"] .chatw__x { color: var(--d-muted); }
[data-theme="dark"] .chatw__x:hover { background: rgba(255, 255, 255, .08); color: var(--d-text); }

[data-theme="dark"] .quote-form--chat { border-color: var(--d-line); }

[data-theme="dark"] .chatw__fine { color: var(--d-muted); }
[data-theme="dark"] .chatw__call { color: var(--d-link); }
[data-theme="dark"] .chatw__foot { border-top-color: var(--d-line); }
