/* ==========================================================================
   Car Accident Claim Lawyer — single stylesheet
   Loaded on the front end and, via add_editor_style(), inside the editor.

   No @layer. WordPress prints its global styles and layout support CSS
   unlayered, and an unlayered rule beats a layered one regardless of
   specificity, so layers here would lose every conflict.

   Contents
     00  Font faces (metric-matched fallbacks only)
     01  Reset and base
     02  Token bridge
     03  Typography
     04  Layout
     05  Components                         [phase 3]
     06  Chrome — header, footer, call bar
     07  Page-specific                      [phase 3-4]
     08  Motion                             [phase 6]
     09  Editor corrections
     10  Print

   Palette rules proven by measurement, not taste. Do not break these:
     - Text on the signal band is ink (4.71:1). Paper on signal is 3.27:1
       and fails for body text.
     - Links on sand are ink with a signal underline. Signal-deep on sand is
       4.35:1 and fails for body text.
     - The signal accent never sits on sand (2.94:1, fails even as large
       text). On sand the italic accent word uses signal-deep.
     - The hairline colour is decorative at 1.36:1. It must never be the only
       indicator of anything, and never a focus ring.
   ========================================================================== */


/* 00  Font faces ==========================================================
   Fraunces and Public Sans themselves are declared in theme.json, which
   emits their @font-face rules and the matching editor UI.

   These two are the fallbacks named in those font stacks, remapped to local
   system fonts with corrected metrics. Without them, swapping from Georgia
   to Fraunces on load shifts every line of text. The numbers are measured
   from the shipped woff2 files against macOS Georgia and Arial:
   size-adjust is the ratio of average lowercase advance width, and the
   ascent and descent overrides are the target's own metrics divided by that
   ratio. See the note in bin/fetch-vendor.sh for how to recompute them.
   ------------------------------------------------------------------------ */

@font-face {
	font-family: "Fraunces Fallback";
	src: local("Georgia"), local("Times New Roman");
	size-adjust: 119.78%;
	ascent-override: 81.65%;
	descent-override: 21.29%;
	line-gap-override: 0%;
}

@font-face {
	font-family: "Public Sans Fallback";
	src: local("Arial"), local("Helvetica Neue"), local("Liberation Sans");
	size-adjust: 104.78%;
	ascent-override: 90.67%;
	descent-override: 21.47%;
	line-gap-override: 0%;
}


/* 01  Reset and base ======================================================
   :where() keeps specificity at zero so anything later, including a block's
   own inline styles, wins without !important.
   ------------------------------------------------------------------------ */

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

/*
 * The hidden attribute is only a UA style, so any class that sets display
 * overrides it. Several components here do, so make the attribute win.
 */
[hidden] {
	display: none !important;
}

:where(html) {
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
	scroll-behavior: smooth;
	/* Anchored headings must clear the sticky header. */
	scroll-padding-top: calc(var(--header-h) + 1.5rem);
	tab-size: 4;
}

@media (prefers-reduced-motion: reduce) {
	:where(html) {
		scroll-behavior: auto;
	}
}

:where(body) {
	margin: 0;
	min-height: 100vh;
	background-color: var(--paper);
	color: var(--ink);
	font-synthesis-weight: none;
	text-rendering: optimizeLegibility;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

:where(h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {
	margin: 0;
}

:where(ul, ol) {
	margin: 0;
}

:where(img, picture, video, canvas, svg) {
	display: block;
	max-width: 100%;
}

:where(img) {
	height: auto;
}

:where(input, button, textarea, select) {
	font: inherit;
	color: inherit;
}

:where(textarea) {
	resize: vertical;
}

:where(a) {
	color: inherit;
}

:where(svg) {
	fill: none;
}

/* Icons scale with the text they sit beside. */
.icon {
	inline-size: 1em;
	block-size: 1em;
	flex: none;
	stroke: currentColor;
	vertical-align: -0.125em;
}

/*
 * A visible focus ring on keyboard navigation only. The ring colour is
 * signal-deep, which clears 3:1 against paper, sand and white; on dark
 * bands it switches to paper.
 */
:where(:focus-visible) {
	outline: 2px solid var(--signal-deep);
	outline-offset: 3px;
	border-radius: 2px;
}

.section--dark :where(:focus-visible),
.site-footer :where(:focus-visible) {
	outline-color: var(--paper);
}

/* Never remove the ring outright, only the non-keyboard one. */
:where(:focus:not(:focus-visible)) {
	outline: none;
}

::selection {
	background-color: var(--signal);
	color: var(--ink);
}

/* Visually hidden but readable by assistive technology. */
.sr-only {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/*
 * Off-screen until focused.
 *
 * Moved horizontally rather than with a negative translate. The header is a
 * sticky, positioned ancestor, so `top: 0` plus a percentage translate
 * resolved against the header's own box and left the link peeking out at the
 * top of the page. Parking it off to the side has no such dependency.
 */
.skip-link {
	position: absolute;
	top: 0;
	left: -100vw;
	z-index: 999;
	padding: 0.75rem 1.25rem;
	background-color: var(--ink);
	color: var(--paper);
	font-size: var(--fs-sm);
	font-weight: 500;
	text-decoration: none;
	border-end-end-radius: var(--radius-md);
}

.skip-link:focus {
	left: 0;
}

/*
 * core/columns, reproduced verbatim from wp-includes/blocks/columns/style.css.
 *
 * Every pattern with a two-column layout (the hero grids, the "why us" and
 * process grids, the about/contact/bio grids) depends on this for mobile
 * stacking and vertical alignment. WordPress normally loads that stylesheet
 * automatically for any page using core/columns, but the detection it runs
 * scans a real parsed block tree — this theme's patterns are PHP echoing
 * already-built HTML comments at render time, not blocks WordPress ever
 * parses, so core/columns is never seen as "used" and its stylesheet never
 * loads. Without it, `.wp-block-columns` and `.wp-block-column` carried no
 * rules at all: `flex-wrap: nowrap` from the block's own inline layout style
 * stood unchallenged, every two-column section stayed side-by-side and
 * overflowed the viewport on a phone (the header bar, every hero, every
 * other columns pattern all clipped at the same right edge), and the
 * `are-vertically-aligned-*` / `is-vertically-aligned-*` classes every
 * pattern already writes did nothing.
 */
.wp-block-columns {
	align-items: normal !important;
	box-sizing: border-box;
	display: flex;
	flex-wrap: wrap !important;
}

@media (min-width: 48.875rem) {
	.wp-block-columns {
		flex-wrap: nowrap !important;
	}
}

.wp-block-columns.are-vertically-aligned-top {
	align-items: flex-start;
}

.wp-block-columns.are-vertically-aligned-center {
	align-items: center;
}

.wp-block-columns.are-vertically-aligned-bottom {
	align-items: flex-end;
}

@media (max-width: 48.8125rem) {
	.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column {
		flex-basis: 100% !important;
	}
}

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

	.wp-block-columns:not(.is-not-stacked-on-mobile) > .wp-block-column[style*="flex-basis"] {
		flex-grow: 0;
	}
}

.wp-block-columns.is-not-stacked-on-mobile {
	flex-wrap: nowrap !important;
}

.wp-block-columns.is-not-stacked-on-mobile > .wp-block-column {
	flex-basis: 0;
	flex-grow: 1;
}

.wp-block-columns.is-not-stacked-on-mobile > .wp-block-column[style*="flex-basis"] {
	flex-grow: 0;
}

.wp-block-column {
	flex-grow: 1;
	min-width: 0;
	overflow-wrap: break-word;
	word-break: break-word;
}

.wp-block-column.is-vertically-aligned-top {
	align-self: flex-start;
}

.wp-block-column.is-vertically-aligned-center {
	align-self: center;
}

.wp-block-column.is-vertically-aligned-bottom {
	align-self: flex-end;
}

.wp-block-column.is-vertically-aligned-stretch {
	align-self: stretch;
}

.wp-block-column.is-vertically-aligned-bottom,
.wp-block-column.is-vertically-aligned-center,
.wp-block-column.is-vertically-aligned-top {
	width: 100%;
}


/* 02  Token bridge ========================================================
   theme.json is the source of truth. This maps its long custom property
   names to short ones for everything below.

   Declared on both selectors because WordPress scopes its presets to :root
   on the front end and to .editor-styles-wrapper inside the editor iframe.
   Each has a literal fallback so a missing preset degrades to the intended
   colour rather than to nothing.
   ------------------------------------------------------------------------ */

:root,
.editor-styles-wrapper {
	/* Colour */
	--paper:        var(--wp--preset--color--paper, #f4f1ec);
	--sand:         var(--wp--preset--color--sand, #ebe5da);
	--ink:          var(--wp--preset--color--ink, #1c1a17);
	--ink-2:        var(--wp--preset--color--ink-2, #26231f);
	--graphite:     var(--wp--preset--color--graphite, #3d3934);
	--stone:        var(--wp--preset--color--stone, #6b665e);
	--stone-light:  var(--wp--preset--color--stone-light, #b5aea3);
	--line:         var(--wp--preset--color--line, #d8d0c3);
	--signal:       var(--wp--preset--color--signal, #e4572e);
	--signal-deep:  var(--wp--preset--color--signal-deep, #b8430f);
	--signal-tint:  var(--wp--preset--color--signal-tint, #fbe9df);
	--moss:         var(--wp--preset--color--moss, #1f5f4a);
	--moss-tint:    var(--wp--preset--color--moss-tint, #e3eee7);
	--gold:         var(--wp--preset--color--gold, #e4b04a);
	--white:        var(--wp--preset--color--white, #fff);

	/* Type */
	--font-display: var(--wp--preset--font-family--display, Georgia, serif);
	--font-body:    var(--wp--preset--font-family--body, Arial, sans-serif);

	--fs-xs:        var(--wp--preset--font-size--xs, 0.8125rem);
	--fs-sm:        var(--wp--preset--font-size--sm, 0.9375rem);
	--fs-md:        var(--wp--preset--font-size--md, 1.125rem);
	--fs-lg:        var(--wp--preset--font-size--lg, 1.25rem);
	--fs-xl:        var(--wp--preset--font-size--xl, 1.5rem);
	--fs-2xl:       var(--wp--preset--font-size--xl-2, 1.875rem);
	--fs-3xl:       var(--wp--preset--font-size--xl-3, 2.5rem);
	--fs-4xl:       var(--wp--preset--font-size--xl-4, 3.5rem);
	--fs-5xl:       var(--wp--preset--font-size--xl-5, 4.75rem);
	--fs-display:   var(--wp--preset--font-size--display, 6.5rem);

	/* Space */
	--sp-10:        var(--wp--preset--spacing--10, 0.25rem);
	--sp-20:        var(--wp--preset--spacing--20, 0.5rem);
	--sp-30:        var(--wp--preset--spacing--30, 1rem);
	--sp-40:        var(--wp--preset--spacing--40, 2rem);
	--sp-50:        var(--wp--preset--spacing--50, 3rem);
	--sp-60:        var(--wp--preset--spacing--60, 5rem);
	--sp-70:        var(--wp--preset--spacing--70, 7.5rem);
	--sp-80:        var(--wp--preset--spacing--80, 10rem);

	/* Measure and rhythm */
	--gutter:       var(--wp--custom--gutter, clamp(1.25rem, 4vw, 3.75rem));
	--measure:      var(--wp--custom--measure, 66ch);
	--content:      var(--wp--style--global--content-size, 44rem);
	--wide:         var(--wp--style--global--wide-size, 80rem);

	/* Chrome */
	--header-h:         var(--wp--custom--header--height, 5rem);
	--header-h-compact: var(--wp--custom--header--height-compact, 3.5rem);

	/* Shape */
	--radius-sm:    var(--wp--custom--radius--sm, 6px);
	--radius-md:    var(--wp--custom--radius--md, 12px);
	--radius-lg:    var(--wp--custom--radius--lg, 20px);
	--radius-xl:    var(--wp--custom--radius--xl, 28px);
	--radius-pill:  var(--wp--custom--radius--pill, 999px);

	/* Elevation */
	--shadow-sm:    var(--wp--preset--shadow--sm, 0 1px 2px rgba(28, 26, 23, 0.06));
	--shadow-md:    var(--wp--preset--shadow--md, 0 8px 24px -8px rgba(28, 26, 23, 0.14));
	--shadow-lg:    var(--wp--preset--shadow--lg, 0 24px 48px -16px rgba(28, 26, 23, 0.18));
	--shadow-glow:  var(--wp--custom--shadow--glow, 0 0 0 1px rgba(228, 87, 46, 0.35), 0 12px 40px -12px rgba(228, 87, 46, 0.3));

	/* The FAQ plus sign, drawn as a mask so it needs no icon request. */
	--plus-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.75' stroke-linecap='round'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E");

	/* Motion */
	--ease-out:     var(--wp--custom--ease--out, cubic-bezier(0.22, 1, 0.36, 1));
	--ease-in-out:  var(--wp--custom--ease--in-out, cubic-bezier(0.65, 0, 0.35, 1));
	--dur-fast:     var(--wp--custom--duration--fast, 160ms);
	--dur-base:     var(--wp--custom--duration--base, 320ms);
	--dur-slow:     var(--wp--custom--duration--slow, 640ms);
}


/* 03  Typography ==========================================================
   Sizes come from theme.json's fluid scale, so nothing here sets a font
   size in a media query.
   ------------------------------------------------------------------------ */

body {
	font-family: var(--font-body);
	font-size: var(--fs-md);
	font-weight: 400;
	line-height: 1.6;
	font-optical-sizing: auto;
}

h1, h2, h3,
.wp-block-heading {
	font-optical-sizing: auto;
	text-wrap: balance;
}

p {
	text-wrap: pretty;
}

/* Long-form copy is capped by character count, not by the layout. */
.prose > p,
.prose > ul,
.prose > ol,
p.lede,
.is-style-lede {
	max-width: var(--measure);
}

/*
 * The accent word device: one italic word per major heading, carrying the
 * outcome rather than a legal term. Parentheses are pseudo-elements so the
 * text node stays exactly as the client wrote it, which keeps the copy
 * intact for search engines and screen readers.
 */
.wonk {
	font-style: italic;
	font-weight: 400;
	color: var(--signal);
}

.wonk--paren::before {
	content: "(";
}

.wonk--paren::after {
	content: ")";
}

/* Signal is 2.94:1 on sand and fails even as large text, so step it down. */
.section--sand .wonk {
	color: var(--signal-deep);
}

/*
 * On the signal band the accent cannot be signal, or it disappears into the
 * background. Paper against signal is 3.27:1, which clears the large-text
 * threshold, and the accent word is only ever set at display sizes.
 */
.section--signal .wonk {
	color: var(--paper);
}

/* On the dark band signal is 4.71:1 against ink, so it stands as it is. */
.section--dark .wonk,
.site-footer .wonk {
	color: var(--signal);
}

.eyebrow,
.is-style-eyebrow {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0.4rem;
	margin-block-end: var(--sp-30);
	color: var(--stone);
	font-family: var(--font-body);
	font-size: var(--fs-xs);
	font-weight: 500;
	line-height: 1.2;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

.eyebrow--num {
	display: flex;
	align-items: center;
	gap: 0.75rem;
}

.eyebrow__num {
	color: var(--signal-deep);
	font-family: var(--font-display);
	font-variant-numeric: lining-nums tabular-nums;
	letter-spacing: 0;
}

.eyebrow__num::after {
	content: "";
	display: inline-block;
	inline-size: 1.5rem;
	block-size: 1px;
	margin-inline-start: 0.75rem;
	background-color: var(--line);
	vertical-align: 0.25em;
}

.lede,
.is-style-lede {
	color: var(--graphite);
	font-size: var(--fs-xl);
	line-height: 1.45;
}

.note,
.is-style-note {
	padding-inline-start: var(--sp-30);
	border-inline-start: 2px solid var(--signal);
	color: var(--stone);
	font-size: var(--fs-sm);
}

.meta {
	color: var(--stone);
	font-size: var(--fs-sm);
}

/*
 * Numerals that animate must not reflow as they count. Tabular figures give
 * every digit the same advance width, and the JS reserves a min-width in ch.
 */
.stat__num,
.tel-display {
	font-family: var(--font-display);
	font-variant-numeric: lining-nums tabular-nums;
	letter-spacing: -0.02em;
	line-height: 1;
}

/* A phone number split across two lines is hard to read and hard to dial. */
.tel-display {
	white-space: nowrap;
}

/* Animated underline that grows from the left, CSS only. */
.link-line {
	display: inline;
	color: inherit;
	text-decoration: none;
	background-image: linear-gradient(currentColor, currentColor);
	background-position: 0 100%;
	background-repeat: no-repeat;
	background-size: 0% 1px;
	transition: background-size var(--dur-base) var(--ease-out);
}

.link-line:hover,
.link-line:focus-visible {
	background-size: 100% 1px;
}

/*
 * On sand, signal-deep as body-size link text is 4.35:1 and fails AA, so
 * links there are ink with a signal underline instead of coloured text.
 */
.section--sand a:where(:not(.wp-block-button__link):not(.btn)) {
	color: var(--ink);
	text-decoration-color: var(--signal);
	text-underline-offset: 0.2em;
}


/* 04  Layout ==============================================================
   Sections are full-bleed groups with a constrained inner. Root padding is
   the responsive gutter, set in theme.json, so content never touches the
   viewport edge.
   ------------------------------------------------------------------------ */

.section {
	padding-block: var(--sp-70);
}

/*
 * Every section works to the wide measure, not the reading measure.
 *
 * WordPress's constrained layout caps direct children at contentSize and
 * centres them, which left neighbouring sections with different left edges
 * depending on how wide their content happened to be. Promoting section
 * children to the wide measure gives the whole page one left edge, and text
 * inside is capped to a readable measure by its own component rule.
 */
.section.is-layout-constrained > :where(:not(.alignfull)) {
	max-width: var(--wide);
	margin-inline: auto;
}

.section--flush-top {
	padding-block-start: 0;
}

.section--flush-bottom {
	padding-block-end: 0;
}

.section--paper {
	background-color: var(--paper);
}

.section--sand {
	background-color: var(--sand);
}

.section--dark,
.is-style-band-dark {
	position: relative;
	background-color: var(--ink);
	color: var(--paper);
	isolation: isolate;
}

/*
 * Lane markings running across the dark band.
 *
 * Repeating gradients rather than an image or a canvas: three layers at
 * different angles and speeds give a sense of depth, the whole thing is a
 * few hundred bytes, and it composites on the GPU because only the
 * background position moves.
 *
 * Very low contrast on purpose. It should register as texture when the eye
 * passes over it, not as a pattern competing with the copy.
 */
.section--dark::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image:
		repeating-linear-gradient(
			100deg,
			transparent 0 118px,
			rgba(244, 241, 236, 0.028) 118px 120px,
			transparent 120px 240px
		),
		repeating-linear-gradient(
			100deg,
			transparent 0 330px,
			rgba(228, 87, 46, 0.05) 330px 332px,
			transparent 332px 660px
		);
	background-size: 480px 100%, 1320px 100%;
	pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
	.section--dark::before {
		animation: cacl-lanes 24s linear infinite;
	}
}

@keyframes cacl-lanes {
	to {
		/* One full tile of each layer, so the loop has no seam. */
		background-position: 480px 0, 1320px 0;
	}
}

.section--dark h1,
.section--dark h2,
.section--dark h3,
.section--dark .wp-block-heading,
.is-style-band-dark .wp-block-heading {
	color: var(--paper);
}

.section--dark .lede,
.section--dark p {
	color: var(--stone-light);
}

.section--signal {
	background-color: var(--signal);
	color: var(--ink);
}

.section--signal h1,
.section--signal h2,
.section--signal .wp-block-heading,
.section--signal p {
	/* Paper on signal is 3.27:1 and fails for text. Ink is 4.71:1. */
	color: var(--ink);
}

.section--signal .eyebrow {
	color: var(--ink);
	opacity: 0.75;
}

/*
 * Section header: eyebrow, heading, then an optional intro, stacked.
 *
 * This was a two-column grid, which placed the eyebrow and heading in
 * separate columns and crushed long headings into a narrow measure. Patterns
 * emit the three parts as siblings, so capping each one's measure gives the
 * intended result without asking every pattern to add wrapper markup.
 */
.section__head {
	margin-block-end: var(--sp-50);
}

/*
 * Left-align anything whose width is capped.
 *
 * WordPress's constrained layout centres every child, and it does so with
 * `margin-left: auto !important`. No amount of specificity beats that, so a
 * capped element would sit centred against its wider neighbours and read as a
 * random indent — the narrower the element, the bigger the apparent indent.
 * Matching the !important is the only way to reset it without wrapping every
 * capped element in extra markup, which would mean restructuring every
 * pattern.
 *
 * This is the one place in the stylesheet that uses !important, and it is
 * here to cancel someone else's.
 */
.section__head > *,
.hero__narrow > *,
.prose-section > *,
.included__prose > *,
.prose > *,
.hero__title,
.hero__lede,
.hero__trust,
.hero__actions,
.stats__notes,
.section .faq,
.lede {
	margin-inline: 0 !important;
}

.section__head > h2,
.section__head > h3 {
	max-width: 24ch;
}

.section__head > p:not(.eyebrow) {
	max-width: 52ch;
	margin-block-start: var(--sp-30);
}

.section__head--narrow > h2 {
	max-width: 20ch;
}

/* Tables and wide diagrams scroll on their own rather than the page. */
.table-scroll {
	overflow-x: auto;
	overscroll-behavior-x: contain;
	-webkit-overflow-scrolling: touch;
}

/* Utilities, deliberately few. */
.u-center {
	text-align: center;
}

.u-measure {
	max-width: var(--measure);
}

.u-no-wrap {
	white-space: nowrap;
}


/* 05  Components =========================================================
   Cards, stats, marquee, process, results and the rest of the section-level
   components arrive in phase 3 with the Home page patterns.
   ------------------------------------------------------------------------ */

/* --- Buttons ------------------------------------------------------------
   theme.json styles core's own button element. These add the icon slot and
   the variants registered in inc/block-styles.php, plus .btn for the two
   places a button is a plain link rather than a core/button block: the
   header phone link and the mobile call bar.
   -------------------------------------------------------------------- */

.btn,
.wp-block-button__link {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 0.6em;
	text-decoration: none;
}

/* Match theme.json's button element styling for the hand-written .btn. */
.btn {
	padding: 0.95em 1.5em;
	border: 0;
	border-radius: var(--radius-pill);
	background-color: var(--ink);
	color: var(--paper);
	font-family: var(--font-body);
	font-size: var(--fs-md);
	font-weight: 500;
	line-height: 1;
	letter-spacing: 0.01em;
	cursor: pointer;
	transition:
		background-color var(--dur-fast) var(--ease-out),
		color var(--dur-fast) var(--ease-out),
		border-color var(--dur-fast) var(--ease-out);
}

.btn:hover {
	background-color: var(--signal);
	color: var(--ink);
}

.btn .icon,
.wp-block-button__link .icon {
	inline-size: 1.15em;
	block-size: 1.15em;
}

/* Ghost: a bordered button in the same shape. */
.btn--ghost,
.is-style-ghost .wp-block-button__link {
	background-color: transparent;
	color: var(--ink);
	box-shadow: inset 0 0 0 1px var(--ink);
}

/* The fill above provides the hover surface, so only the rim changes here. */
.btn--ghost:hover,
.is-style-ghost .wp-block-button__link:hover {
	box-shadow: inset 0 0 0 1px var(--ink);
}

/* On dark and signal bands the ghost outline has to invert. */
.section--dark .is-style-ghost .wp-block-button__link,
.site-footer .is-style-ghost .wp-block-button__link {
	color: var(--paper);
	box-shadow: inset 0 0 0 1px var(--stone-light);
}

.section--dark .is-style-ghost .wp-block-button__link:hover,
.site-footer .is-style-ghost .wp-block-button__link:hover {
	box-shadow: inset 0 0 0 1px var(--paper);
}

/*
 * On the signal band both buttons sit on orange. Ink on signal is 4.71:1;
 * paper would be 3.27:1 and fail, so the ghost variant outlines in ink.
 */
.section--signal .is-style-ghost .wp-block-button__link {
	color: var(--ink);
	box-shadow: inset 0 0 0 1px var(--ink);
}

.section--signal .is-style-ghost .wp-block-button__link:hover {
	box-shadow: inset 0 0 0 1px var(--ink);
}

.section--signal .is-style-primary .wp-block-button__link:hover {
	/* Signal on signal would vanish, so hover goes to paper-on-ink instead. */
	background-color: var(--paper);
	color: var(--ink);
}

/* --- Button interactions ----------------------------------------------
   Four effects, layered so they read as one gesture rather than four:

     1. The label swaps. The visible copy slides up and out while a duplicate
        rises into its place, which is what makes the press feel mechanical
        and intentional.
     2. The icon travels with it.
     3. A sheen crosses the surface.
     4. Ghost buttons fill from wherever the pointer entered, so the fill
        feels caused by the cursor rather than by the element.

   The label structure is added by a render filter, not written into the
   pattern; see inc/blocks.php for why.
   -------------------------------------------------------------------- */

.btn__swap {
	display: inline-grid;
	position: relative;
	overflow: hidden;
	/* Room for descenders, which a tight overflow box would shear off. */
	padding-block-end: 0.12em;
	margin-block-end: -0.12em;
}

.btn__swap-in,
.btn__swap-out {
	display: block;
	grid-area: 1 / 1;
	transition: transform var(--dur-base) var(--ease-out);
}

.btn__swap-out {
	transform: translateY(110%);
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
	:is(.wp-block-button__link, .btn):hover .btn__swap-in {
		transform: translateY(-110%);
	}

	:is(.wp-block-button__link, .btn):hover .btn__swap-out {
		transform: translateY(0);
	}

	/* The icon leads the movement by a fraction. */
	:is(.wp-block-button__link, .btn) .icon {
		transition: transform var(--dur-base) var(--ease-out);
	}

	:is(.wp-block-button__link, .btn):hover .icon {
		transform: translateX(2px);
	}

	/* A phone icon should ring, not walk. */
	:is(.btn--phone, .is-style-phone .wp-block-button__link):hover .icon {
		transform: rotate(-14deg) scale(1.06);
	}
}

/* Keyboard focus gets the same treatment, so it is not a lesser state. */
:is(.wp-block-button__link, .btn):focus-visible .btn__swap-in {
	transform: translateY(-110%);
}

:is(.wp-block-button__link, .btn):focus-visible .btn__swap-out {
	transform: translateY(0);
}

/*
 * Ghost buttons fill from the pointer.
 *
 * The circle is scaled rather than grown, so the whole thing composites on
 * the GPU; animating width or radius would relayout on every frame. The
 * default position is the centre, so a keyboard user gets a clean fill from
 * the middle.
 */
.btn--ghost,
.is-style-ghost .wp-block-button__link {
	position: relative;
	overflow: hidden;
	isolation: isolate;
	transition: color var(--dur-base) var(--ease-out);
}

.btn--ghost::before,
.is-style-ghost .wp-block-button__link::before {
	content: "";
	position: absolute;
	z-index: -1;
	top: var(--spot-y, 50%);
	left: var(--spot-x, 50%);
	inline-size: 220%;
	aspect-ratio: 1;
	border-radius: 50%;
	background-color: var(--ink);
	transform: translate(-50%, -50%) scale(0);
	transition: transform var(--dur-slow) var(--ease-out);
	pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
	.btn--ghost:hover,
	.btn--ghost:focus-visible,
	.is-style-ghost .wp-block-button__link:hover,
	.is-style-ghost .wp-block-button__link:focus-visible {
		color: var(--paper);
	}

	.btn--ghost:hover::before,
	.btn--ghost:focus-visible::before,
	.is-style-ghost .wp-block-button__link:hover::before,
	.is-style-ghost .wp-block-button__link:focus-visible::before {
		transform: translate(-50%, -50%) scale(1);
	}
}

/* On the dark and signal bands the fill has to invert to stay visible. */
.section--dark .is-style-ghost .wp-block-button__link::before,
.site-footer .is-style-ghost .wp-block-button__link::before,
.section--signal .is-style-ghost .wp-block-button__link::before,
.cta-inline .btn--ghost::before {
	background-color: var(--paper);
}

@media (prefers-reduced-motion: no-preference) {
	.section--dark .is-style-ghost .wp-block-button__link:hover,
	.site-footer .is-style-ghost .wp-block-button__link:hover,
	.section--signal .is-style-ghost .wp-block-button__link:hover,
	.cta-inline .btn--ghost:hover {
		color: var(--ink);
	}
}

/*
 * A sheen sweeping across the primary actions on hover.
 *
 * The gradient sits in a clipped overlay rather than on the button itself, so
 * it cannot disturb the label, and it is translated rather than animated on
 * background-position, which would repaint the whole button every frame.
 */
.btn--primary,
.is-style-primary .wp-block-button__link,
.btn--phone {
	position: relative;
	overflow: hidden;
	isolation: isolate;
}

.btn--primary::after,
.is-style-primary .wp-block-button__link::after,
.btn--phone::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background-image: linear-gradient(
		105deg,
		transparent 35%,
		rgba(255, 255, 255, 0.28) 50%,
		transparent 65%
	);
	transform: translateX(-100%);
	pointer-events: none;
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
	.btn--primary:hover::after,
	.is-style-primary .wp-block-button__link:hover::after,
	.btn--phone:hover::after {
		animation: cacl-sheen 0.75s var(--ease-out);
	}
}

@keyframes cacl-sheen {
	to {
		transform: translateX(100%);
	}
}

/* Phone button: the header's primary action. */
.btn--phone {
	flex: none;
	white-space: nowrap;
}

.btn--phone .btn__label {
	font-variant-numeric: tabular-nums;
}


/* --- Tables ------------------------------------------------------------
   Core's table stylesheet draws a full grid: a 1px border on every cell plus
   a 3px rule under the head. That reads as a spreadsheet. These strip it back
   to horizontal hairlines, which is what makes a data table look typeset
   rather than tabulated.

   Specificity matters here, and the obvious approach fails. Core's rule is
   `.wp-block-table td` — one class and one element, (0,1,1). Writing
   `.wp-block-table :where(th, td)` looks equivalent but `:where()`
   contributes nothing, so it scores (0,1,0) and loses, leaving the default
   grid visible. Naming `table` in the descendant chain gets to (0,1,2),
   which wins regardless of which stylesheet is printed first.
   -------------------------------------------------------------------- */

.wp-block-table table {
	width: 100%;
	border-collapse: collapse;
}

.wp-block-table table th,
.wp-block-table table td {
	border: 0;
	border-block-end: 1px solid var(--line);
	padding: 0.85em 1em 0.85em 0;
	text-align: start;
	vertical-align: top;
}

.wp-block-table table th:last-child,
.wp-block-table table td:last-child {
	padding-inline-end: 0;
}

/* Core puts a 3px rule under the head and over the foot. */
.wp-block-table table thead,
.wp-block-table table tfoot {
	border: 0;
}

.wp-block-table table thead th {
	border-block-end: 1px solid var(--ink);
}

.wp-block-table table th {
	color: var(--stone);
	font-family: var(--font-body);
	font-size: var(--fs-xs);
	font-weight: 500;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	white-space: nowrap;
}

.wp-block-table table tbody tr:last-child th,
.wp-block-table table tbody tr:last-child td {
	border-block-end: 0;
}

/* Opening hours: day on the left, time set in tabular figures. */
.table--hours table {
	table-layout: auto;
}

.table--hours td:first-child {
	width: 40%;
	font-weight: 600;
}

.table--hours td:last-child {
	font-family: var(--font-display);
	font-variant-numeric: lining-nums tabular-nums;
}

/* Today's row, marked by site.js in the visitor's own reckoning of the date. */
.table--hours tbody tr.is-today {
	position: relative;
	color: var(--ink);
}

.table--hours tbody tr.is-today td:first-child::before {
	content: "";
	position: absolute;
	inset-inline-start: -0.75rem;
	inset-block: 0.35em auto;
	inline-size: 3px;
	block-size: 1.2em;
	border-radius: var(--radius-pill);
	background-color: var(--signal);
}

/* Editorial tables: first column carries the row's name. */
.table--rich table {
	table-layout: auto;
}

.table--rich td:first-child {
	font-family: var(--font-display);
	font-size: var(--fs-lg);
	font-weight: 500;
	line-height: 1.25;
}

.table--rich tbody tr {
	scroll-margin-top: calc(var(--header-h) + 2rem);
}

/* The anchor target from a Home service card gets a moment of emphasis. */
.table--rich tbody tr:target td {
	background-color: var(--signal-tint);
}

/*
 * Below the stacking breakpoint a three-column editorial table has to scroll
 * rather than crush its columns. Scoped to .table--rich deliberately: the
 * two-column hours table fits a phone and must not be given a minimum width,
 * or it would scroll for no reason.
 */
@media (max-width: 48.9375rem) {
	.table-scroll .table--rich table {
		min-width: 34rem;
	}
}


/* --- Illustration slots ------------------------------------------------
   Declared before the components that modify them. Base and modifier share
   single-class specificity here, so source order decides which wins.
   -------------------------------------------------------------------- */

.illo {
	color: var(--ink);
}

.illo svg {
	inline-size: 100%;
	block-size: auto;
}

.section--dark .illo,
.section--signal .illo {
	color: currentColor;
}


/* --- Badges and chips -------------------------------------------------- */

.badge {
	display: inline-flex;
	align-items: center;
	gap: 0.4em;
	padding: 0.35em 0.75em;
	border-radius: var(--radius-pill);
	font-size: var(--fs-xs);
	font-weight: 600;
	letter-spacing: 0.02em;
	white-space: nowrap;
}

.badge--moss {
	background-color: var(--moss-tint);
	color: var(--moss);
}

.badge--moss .icon {
	color: var(--moss);
}

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

.card {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--sp-30);
	padding: var(--sp-40);
	border: 1px solid var(--line);
	border-radius: var(--radius-lg);
	background-color: var(--paper);
	overflow: hidden;
	/* Scopes card__watermark's negative z-index to this card, not the grid. */
	isolation: isolate;
}

/*
 * A large, faint version of the card's own icon, sitting behind its copy.
 * Reuses whichever icon the card already shows small at the top, so the
 * watermark is never a mismatched decoration — it is always that same card's
 * subject, just large and quiet. `.card`'s `overflow: hidden` clips it to the
 * rounded corner; `isolation: isolate` on `.card` keeps the negative z-index
 * from reaching past this one card.
 */
.card__watermark {
	position: absolute;
	inset-block-end: var(--sp-30);
	inset-inline-end: var(--sp-30);
	inline-size: 5.5rem;
	block-size: 5.5rem;
	z-index: -1;
	color: var(--line);
	pointer-events: none;
}

.card__watermark .icon {
	inline-size: 100%;
	block-size: 100%;
	stroke-width: 1.25;
}

.card--flat {
	border-color: transparent;
	background-color: var(--paper);
}

.section--sand .card--flat {
	background-color: var(--paper);
}

.card__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	inline-size: 2.75rem;
	block-size: 2.75rem;
	border-radius: var(--radius-pill);
	background-color: var(--signal-tint);
	color: var(--signal-deep);
	font-size: 1.25rem;
}

.card__title {
	font-family: var(--font-display);
	font-size: var(--fs-2xl);
	font-weight: 500;
	line-height: 1.2;
	letter-spacing: -0.005em;
	text-wrap: balance;
}

.card__body {
	color: var(--graphite);
	flex: 1 1 auto;
}

.card__link {
	display: inline-flex;
	align-items: center;
	gap: 0.4em;
	align-self: start;
	color: var(--signal-deep);
	font-weight: 500;
}

.card__link .icon {
	transition: transform var(--dur-base) var(--ease-out);
}

.card__link:hover .icon {
	transform: translate(2px, -2px);
}

/*
 * A pointer-tracked glow on the card's border. The gradient is painted into
 * the padding box and masked so only the 1px frame shows, which avoids
 * washing a tint over the text.
 */
.card--glow::before {
	content: "";
	position: absolute;
	inset: 0;
	padding: 1px;
	border-radius: inherit;
	background: radial-gradient(
		16rem circle at var(--mx, 50%) var(--my, 0%),
		var(--signal) 0%,
		transparent 60%
	);
	mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	mask-composite: exclude;
	-webkit-mask:
		linear-gradient(#000 0 0) content-box,
		linear-gradient(#000 0 0);
	-webkit-mask-composite: xor;
	opacity: 0;
	transition: opacity var(--dur-base) var(--ease-out);
	pointer-events: none;
}

@media (hover: hover) and (pointer: fine) {
	.card--glow:hover {
		transform: translateY(-4px);
		box-shadow: var(--shadow-md);
	}

	.card--glow:hover::before {
		opacity: 1;
	}
}

.card--glow {
	transition:
		transform var(--dur-base) var(--ease-out),
		box-shadow var(--dur-base) var(--ease-out);
}

.card--glow:focus-within {
	transform: translateY(-4px);
	box-shadow: var(--shadow-md);
}

/* --- Stats ------------------------------------------------------------- */

.stats {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 0;
	margin: 0;
	padding: 0;
	list-style: none;
	border-block: 1px solid var(--line);
}

@media (min-width: 48rem) {
	.stats {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.stat {
	padding: var(--sp-40) var(--sp-30);
}

/* Hairlines between, never around: the band edges already have a rule. */
.stat + .stat {
	border-inline-start: 1px solid var(--line);
}

@media (max-width: 47.9375rem) {
	.stat:nth-child(odd) {
		border-inline-start: 0;
	}

	.stat:nth-child(n + 3) {
		border-block-start: 1px solid var(--line);
	}
}

.stat__num {
	font-size: var(--fs-4xl);
	font-weight: 300;
	color: var(--ink);
}

.stat__label {
	display: flex;
	align-items: start;
	gap: 0.5rem;
	margin-block-start: var(--sp-20);
	color: var(--stone);
	font-size: var(--fs-sm);
	line-height: 1.5;
}

.stat__label .icon {
	margin-block-start: 0.2em;
}

.icon--gold {
	color: var(--gold);
	fill: var(--gold);
}

.stats__notes {
	display: grid;
	gap: var(--sp-30);
	margin: var(--sp-40) 0 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 48rem) {
	.stats__notes {
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--sp-40);
	}
}

.stats__note {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: 0.75rem;
	color: var(--graphite);
	font-size: var(--fs-sm);
}

/* Four figures across need the wide measure, not the reading measure. */
.stat__label span {
	max-width: 22ch;
}

/* --- Marquee -----------------------------------------------------------
   The track holds two copies of the list and slides by exactly half its own
   width, so the loop is seamless. Duration is set by site.js from the
   measured width, which keeps the speed constant however many items there
   are.
   -------------------------------------------------------------------- */

.marquee {
	margin-block-start: var(--sp-50);
	padding-block: var(--sp-40);
	border-block: 1px solid var(--line);
	overflow: hidden;
}

/*
 * A spotlight that follows the cursor along the strip.
 *
 * The items sit at reduced contrast and the spotlight restores them, so
 * moving across the strip reads the accreditations one by one. The whole
 * effect is one mask, so there is nothing to animate per item.
 *
 * Only where there is a pointer: on a touch screen the strip stays at full
 * contrast, because there would be nothing to reveal it.
 */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
	.marquee__track {
		--spot-x: 50%;
	}

	.marquee:hover .marquee__item {
		opacity: 0.45;
		transition: opacity var(--dur-base) var(--ease-out);
	}

	.marquee__glow {
		position: absolute;
		inset: 0;
		z-index: 2;
		background-image: radial-gradient(
			11rem circle at var(--spot-x, -100%) 50%,
			rgba(228, 87, 46, 0.16),
			rgba(228, 87, 46, 0) 70%
		);
		opacity: 0;
		transition: opacity var(--dur-base) var(--ease-out);
		pointer-events: none;
	}

	.marquee:hover .marquee__glow {
		opacity: 1;
	}
}

.marquee {
	position: relative;
}

.marquee__viewport {
	overflow: hidden;
	/* Fade the strip into the band rather than cutting it off. */
	mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
	-webkit-mask: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.marquee__track {
	display: flex;
	align-items: center;
	gap: var(--sp-40);
	width: max-content;
	margin: 0;
	padding: 0;
	list-style: none;
	animation: cacl-marquee var(--marquee-duration, 40s) linear infinite;
}

.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track {
	animation-play-state: paused;
}

.marquee__item {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	padding-inline-end: 0;
	font-size: var(--fs-sm);
	font-weight: 500;
	white-space: nowrap;
}

/* A signal dot separates the items, so the strip reads as a list. */
.marquee__item::after {
	content: "";
	inline-size: 4px;
	block-size: 4px;
	border-radius: 50%;
	background-color: var(--signal);
}

.marquee__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	inline-size: 2.25rem;
	block-size: 2.25rem;
	border-radius: var(--radius-pill);
	background-color: var(--signal-tint);
	color: var(--signal-deep);
}

@keyframes cacl-marquee {
	to {
		transform: translateX(-50%);
	}
}

/* --- Services grid ---------------------------------------------------- */

.services__grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--sp-40);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 40rem) {
	.services__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 64rem) {
	.services__grid {
		grid-template-columns: repeat(6, minmax(0, 1fr));
	}

	/* Five cards into a six-column grid: the first takes a double width. */
	.service-card {
		grid-column: span 2;
	}

	.service-card--wide {
		grid-column: span 3;
	}

	.service-card:nth-child(2) {
		grid-column: span 3;
	}
}

.service-card {
	padding-block-end: calc(var(--sp-40) + 2.5rem);
}

/* --- Selecting a service ----------------------------------------------
   Pointing at one card brings it forward and lets the others recede, so the
   grid behaves like a set of things to choose between rather than five equal
   blocks.

   Adapted from the Apple-style feature block, with one deliberate
   difference: nothing is hidden. That pattern usually collapses the detail
   of every card but the selected one. Here the copy is the reason someone is
   on the page, and it is what search engines read, so every word stays
   rendered and only emphasis moves.

   :has() lets the grid respond to its own children, so this needs no
   JavaScript. Pointer-only: on a touch screen there is no hover to recede
   from, and every card stays at full strength.
   -------------------------------------------------------------------- */

@media (hover: hover) and (pointer: fine) {
	.services__grid:has(.service-card:hover) .service-card:not(:hover),
	.services__grid:has(.service-card:focus-within) .service-card:not(:focus-within) {
		opacity: 0.58;
		transform: scale(0.985);
	}

	.service-card {
		transition:
			opacity var(--dur-base) var(--ease-out),
			transform var(--dur-base) var(--ease-out),
			box-shadow var(--dur-base) var(--ease-out);
	}

	/* The selected card's icon takes the accent, rather than just sitting in it. */
	.service-card:hover .card__icon,
	.service-card:focus-within .card__icon {
		background-color: var(--signal);
		color: var(--ink);
		transform: scale(1.06);
	}

	.card__icon {
		transition:
			background-color var(--dur-base) var(--ease-out),
			color var(--dur-base) var(--ease-out),
			transform var(--dur-base) var(--ease-out);
	}

	/* The watermark glyph comes up out of the background. */
	.service-card .card__glyph {
		transition:
			color var(--dur-slow) var(--ease-out),
			transform var(--dur-slow) var(--ease-out);
	}

	.service-card:hover .card__glyph,
	.service-card:focus-within .card__glyph {
		color: var(--signal-tint);
		transform: translate(-2px, -4px) scale(1.04);
	}
}

@media (prefers-reduced-motion: reduce) {
	/* Emphasis by colour alone, with nothing moving or fading. */
	.services__grid:has(.service-card:hover) .service-card:not(:hover),
	.services__grid:has(.service-card:focus-within) .service-card:not(:focus-within) {
		opacity: 1;
		transform: none;
	}
}

/* The vehicle glyph is a watermark, not an illustration to read. */
.service-card__glyph,
.card__glyph {
	position: absolute;
	inset-block-end: var(--sp-30);
	inset-inline-end: var(--sp-30);
	inline-size: 5.5rem;
	color: var(--line);
	pointer-events: none;
}

/* --- Checklist -------------------------------------------------------- */

.checklist {
	display: grid;
	gap: 0;
	margin: 0;
	padding: 0;
	list-style: none;
}

.checklist__item {
	display: flex;
	align-items: start;
	gap: var(--sp-30);
	padding-block: var(--sp-30);
	font-size: var(--fs-lg);
	line-height: 1.5;
}

.checklist__item + .checklist__item {
	border-block-start: 1px solid var(--ink-2);
}

.section--paper .checklist__item + .checklist__item,
.section--sand .checklist__item + .checklist__item {
	border-block-start-color: var(--line);
}

.checklist__mark {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: none;
	inline-size: 1.75rem;
	block-size: 1.75rem;
	margin-block-start: 0.15em;
	border-radius: var(--radius-pill);
	background-color: var(--moss);
	color: var(--paper);
	font-size: 0.9rem;
}

/* --- Process ---------------------------------------------------------- */

@media (min-width: 64rem) {
	.process__sticky,
	.why__sticky {
		position: sticky;
		top: calc(var(--header-h) + var(--sp-40));
	}
}

.process__steps {
	position: relative;
	padding-inline-start: var(--sp-50);
}

/*
 * The rail sits in the gutter to the left of the steps. The fill is scaled by
 * scroll progress and the marker rides along it, which gives the section a
 * sense of travel without pinning anything.
 */
.process__rail {
	position: absolute;
	inset-block: 0.5rem;
	inset-inline-start: 0;
	inline-size: 2px;
	background-color: var(--line);
	border-radius: var(--radius-pill);
}

.process__progress {
	position: absolute;
	inset: 0;
	background-color: var(--signal);
	border-radius: inherit;
	transform: scaleY(var(--progress, 0));
	transform-origin: top;
}

.process__marker {
	position: absolute;
	inset-inline-start: 50%;
	top: 0;
	inline-size: 1.25rem;
	color: var(--signal-deep);
	transform: translate(-50%, calc(var(--progress, 0) * var(--rail-height, 0px)));
}

.step {
	padding-block: var(--sp-40);
	transition: opacity var(--dur-base) var(--ease-out);
}

.step + .step {
	border-block-start: 1px solid var(--line);
}

.step__num {
	font-family: var(--font-display);
	font-variant-numeric: lining-nums tabular-nums;
	font-size: var(--fs-3xl);
	font-weight: 300;
	line-height: 1;
	color: var(--stone-light);
	transition: color var(--dur-base) var(--ease-out);
}

.step__title {
	margin-block: var(--sp-20) var(--sp-20);
	font-size: var(--fs-2xl);
}

.step.is-active .step__num {
	color: var(--signal-deep);
}

/* --- Casework timeline ------------------------------------------------ */

/*
 * The composition is one idea: set the year enormous and the matters small,
 * and let the distance between those two sizes carry the section. That is
 * the arrangement kubrick.life uses for its biography timeline.
 *
 * The numeral is `signal-deep`, not `signal`. On the sand band `signal` is
 * 2.94:1 and fails even the large-text threshold; `signal-deep` is 4.35:1 and
 * clears it with room to spare. The numeral is never below 4.5rem, so the
 * large-text allowance genuinely applies.
 */

.timeline {
	margin-block-start: var(--sp-50);
}

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

.timeline__year {
	position: relative;
	display: grid;
	gap: var(--sp-30) var(--sp-50);
	padding-block: var(--sp-50);
	border-block-start: 1px solid var(--line);
}

.timeline__year:first-child {
	border-block-start: 0;
	padding-block-start: 0;
}

/*
 * The marker holds its year while that year's matters scroll past, so the
 * reader always knows which year they are inside. Sticky, not pinned: no
 * spacer element, nothing for WordPress's layout margins to fight.
 */
.timeline__marker {
	align-self: start;
}

.timeline__num {
	margin: 0;
	font-family: var(--font-display);
	font-size: clamp(4.5rem, 13vw, 10rem);
	font-weight: 300;
	font-variant-numeric: tabular-nums;
	font-optical-sizing: auto;
	line-height: 0.82;
	letter-spacing: -0.035em;
	color: var(--signal-deep);
}

.timeline__count {
	margin-block: var(--sp-20) 0;
	font-size: var(--fs-xs);
	font-weight: 500;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var(--stone);
}

.timeline__items {
	display: grid;
	gap: var(--sp-50);
	margin: 0;
	padding: 0;
	list-style: none;
}

.timeline__item {
	position: relative;
	padding-inline-start: var(--sp-40);
	border-inline-start: 1px solid var(--line);
}

/* The node on the rail, level with the first line of the label. */
.timeline__item::before {
	content: "";
	position: absolute;
	inset-inline-start: 0;
	inset-block-start: 0.45em;
	inline-size: 0.5rem;
	block-size: 0.5rem;
	translate: -50% 0;
	border-radius: var(--radius-pill);
	background: var(--signal-deep);
}

.timeline__glyph {
	display: block;
	color: var(--stone);
}

.timeline__glyph svg {
	inline-size: auto;
	block-size: 1.75rem;
}

.timeline__meta {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.5ch;
	margin-block: var(--sp-20) 0;
	font-size: var(--fs-xs);
	font-weight: 500;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var(--ink);
}

/*
 * The model carries the ink, the city recedes to stone. Stated rather than
 * inherited, so the pairing survives an edit to the parent.
 */
.timeline__model {
	color: var(--ink);
}

.timeline__dot {
	color: var(--stone);
}

.timeline__city {
	color: var(--stone);
}

.timeline__title {
	margin-block: var(--sp-20) 0;
	font-family: var(--font-display);
	font-size: var(--fs-lg);
	font-weight: 400;
	line-height: 1.22;
	letter-spacing: -0.01em;
	color: var(--ink);
}

.timeline__body {
	margin-block: var(--sp-20) 0;
	max-inline-size: 42ch;
	font-size: var(--fs-sm);
	color: var(--graphite);
}

@media (min-width: 48em) {
	.timeline__year {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
		padding-block: var(--sp-60);
	}

	.timeline__marker {
		position: sticky;
		top: calc(var(--header-h-compact) + var(--sp-40));
	}
}

/*
 * Hover on the timeline.
 *
 * Pointing at one matter foregrounds it and lets its neighbours recede, the
 * same selection idea as the services grid: nothing is hidden, the contrast
 * between the pointed-at row and the rest just widens. `:has()` carries the
 * signal up to the list so the siblings can respond.
 *
 * The year numeral is not re-coloured on hover. It is already running a
 * scroll-driven opacity animation, and a CSS animation outranks a normal
 * declaration, so any hover state written on the same property would be
 * ignored. A rule under the numeral is honest about that and reads better
 * anyway.
 */
@media (hover: hover) and (pointer: fine) {
	.timeline__item {
		transition:
			border-color var(--dur-base) var(--ease-out),
			opacity var(--dur-base) var(--ease-out);
	}

	/*
	 * The ring, not the size. `scale` belongs to the scroll animation above,
	 * and a CSS animation outranks a normal declaration, so a hover state
	 * written on the same property would simply never appear.
	 */
	.timeline__item::before {
		transition: box-shadow var(--dur-base) var(--ease-out);
	}

	.timeline__glyph {
		transition: color var(--dur-base) var(--ease-out), scale var(--dur-base) var(--ease-out);
	}

	.timeline__title {
		transition: color var(--dur-base) var(--ease-out);
	}

	.timeline__items:has(.timeline__item:hover) .timeline__item:not(:hover) {
		opacity: 0.45;
	}

	.timeline__item:hover {
		border-inline-start-color: var(--signal-deep);
	}

	.timeline__item:hover::before {
		box-shadow: 0 0 0 5px var(--signal-tint);
	}

	.timeline__item:hover .timeline__glyph {
		color: var(--signal-deep);
		scale: 1.12;
	}

	/* A rule that draws itself under the year the reader is pointing into. */
	.timeline__marker {
		--rule: 0;
	}

	.timeline__marker::after {
		content: "";
		display: block;
		inline-size: 100%;
		block-size: 2px;
		margin-block-start: var(--sp-20);
		background: var(--signal-deep);
		transform: scaleX(var(--rule));
		transform-origin: left center;
		transition: transform var(--dur-slow) var(--ease-out);
	}

	.timeline__year:has(.timeline__item:hover) .timeline__marker {
		--rule: 1;
	}
}

.results__note {
	margin-block-start: var(--sp-50);
}

/* --- Service areas ---------------------------------------------------- */

.areas__map {
	position: relative;
	padding-block: var(--sp-40);
}

.illo--australia {
	position: absolute;
	inset: 0;
	display: grid;
	place-items: center;
	color: var(--line);
	pointer-events: none;
}

.illo--australia svg {
	/* Fit to the column's height so the outline cannot escape the section. */
	inline-size: auto;
	block-size: 100%;
	max-inline-size: 100%;
}

.areas__list {
	position: relative;
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 1px;
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 48rem) {
	.areas__list {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

.areas__item {
	position: relative;
	z-index: 1;
	padding: var(--sp-40) var(--sp-30);
	border: 1px solid var(--line);
	border-radius: var(--radius-md);
	background-color: color-mix(in srgb, var(--paper) 70%, transparent);
}

.areas__city {
	display: block;
	font-family: var(--font-display);
	font-size: var(--fs-2xl);
	font-weight: 500;
	line-height: 1.1;
}

.areas__state {
	display: block;
	margin-block-start: 0.25rem;
	color: var(--stone);
	font-size: var(--fs-xs);
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

/* A single highlight that slides behind whichever tile is hovered. */
.areas__glider {
	position: absolute;
	z-index: 0;
	inset-block-start: 0;
	inset-inline-start: 0;
	border-radius: var(--radius-md);
	background-color: var(--sand);
	opacity: 0;
	pointer-events: none;
}

/* --- FAQ -------------------------------------------------------------- */

.section .faq {
	max-width: 54rem;
	margin-inline: 0;
	border-block-start: 1px solid var(--line);
}

.faq > * {
	max-width: none;
	margin-inline: 0 !important;
}

.faq__item {
	border-block-end: 1px solid var(--line);
}

.faq__item > summary {
	display: flex;
	align-items: start;
	justify-content: space-between;
	gap: var(--sp-40);
	padding-block: var(--sp-40);
	font-family: var(--font-display);
	font-size: var(--fs-lg);
	font-weight: 500;
	line-height: 1.35;
	cursor: pointer;
	list-style: none;
}

.faq__item > summary::-webkit-details-marker {
	display: none;
}

/* The plus is drawn in CSS so there is no icon request per row. */
.faq__item > summary::after {
	content: "";
	flex: none;
	inline-size: 1.5rem;
	block-size: 1.5rem;
	margin-block-start: 0.15em;
	background-color: currentColor;
	mask: var(--plus-mask) center / contain no-repeat;
	-webkit-mask: var(--plus-mask) center / contain no-repeat;
	transition: transform var(--dur-base) var(--ease-out);
}

.faq__item[open] > summary::after {
	transform: rotate(45deg);
}

.faq__item > summary:hover {
	color: var(--signal-deep);
}

.faq__item > :not(summary) {
	padding-block-end: var(--sp-40);
}

.faq__item p {
	max-width: var(--measure);
	color: var(--graphite);
}

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

.cta-band__kicker {
	margin-block-start: var(--sp-30);
	font-weight: 600;
}

.cta-band .illo {
	color: var(--ink);
}

/* --- Media ------------------------------------------------------------ */

.media {
	position: relative;
	border-radius: var(--radius-md);
	/*
	 * `clip`, not `hidden`, and the difference is not cosmetic. `hidden`
	 * makes the box a scroll container, and a scroll container becomes the
	 * scrollport that `animation-timeline: view()` resolves against. The
	 * parallax figure inside exactly fills this box and never moves within
	 * it, so its view progress sat frozen at the midpoint and the parallax
	 * did nothing at all. `clip` crops without any scrolling mechanism, so
	 * the timeline keeps resolving against the page. `hidden` stays as the
	 * fallback for browsers that do not know `clip`.
	 */
	overflow: hidden;
	overflow: clip;
}

.media img {
	inline-size: 100%;
	block-size: auto;
}

/*
 * A parallax figure has to be taller than its frame, or drifting it just
 * exposes the band behind.
 *
 * It is taken out of flow to get that height. In flow, `block-size: 114%`
 * resolves against the frame, the frame then grows to fit the taller figure,
 * and the percentage is left resolving against a height its own result
 * changed — the two settled at the same number and the overhang vanished.
 * Absolute positioning breaks the loop: the frame keeps the height its
 * aspect ratio gives it, and the figure hangs 8% over each edge.
 *
 * Every frame that holds one therefore needs a definite aspect ratio. There
 * are three, each matching the ratio its photograph was cut to.
 */
.media .a-parallax {
	position: absolute;
	inset-block: -8%;
	inset-inline: 0;
	margin: 0;
}

.media .a-parallax img {
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
}

/* A wash of paper over photographs settles them into the palette. */
.media--photo::after {
	content: "";
	position: absolute;
	inset: 0;
	background-color: var(--paper);
	mix-blend-mode: multiply;
	opacity: 0.08;
	pointer-events: none;
}

/*
 * Pointing at a photograph lifts the wash where the cursor is, so the image
 * comes up to full strength in a soft circle and settles back as the cursor
 * leaves. The wash is what gets masked, not the photograph: punching a hole
 * in the overlay is one paint, and it cannot affect the image itself.
 *
 * `--spot-x`/`--spot-y` are written by the same pointer tracker the header
 * pill and the credentials strip use, so this needs no script of its own.
 * Parked off-element on pointerleave, which is why the resting state is a
 * circle nobody can see rather than a stuck highlight.
 */
@media (hover: hover) and (pointer: fine) {
	.media--photo::after {
		mask-image: radial-gradient(
			circle 13rem at var(--spot-x, -40%) var(--spot-y, 50%),
			transparent 0%,
			rgb(0 0 0 / 60%) 55%,
			#000 100%
		);
		-webkit-mask-image: radial-gradient(
			circle 13rem at var(--spot-x, -40%) var(--spot-y, 50%),
			transparent 0%,
			rgb(0 0 0 / 60%) 55%,
			#000 100%
		);
	}

	/* A touch of scale so the frame feels alive, not just brighter. */
	.media--photo img {
		transition: scale var(--dur-slow) var(--ease-out);
	}

	.media--photo:hover img {
		scale: 1.03;
	}
}

/* --- Cursor ring ------------------------------------------------------
   A soft ring that trails the pointer and opens up over anything clickable.

   It accompanies the native cursor rather than replacing it. Hiding the real
   cursor in favour of a scripted one is a genuine accessibility regression:
   it breaks the system cursor size and contrast settings people rely on, and
   if the script stalls the pointer disappears. So `cursor` is never touched,
   and this is purely an accent on top.

   The element is created by script and only where it can work, so a touch
   device has nothing extra in its DOM at all.
   ------------------------------------------------------------------------ */

.cursor {
	position: fixed;
	inset-block-start: 0;
	inset-inline-start: 0;
	z-index: 90;
	inline-size: 1.75rem;
	block-size: 1.75rem;
	border: 1px solid var(--signal-deep);
	border-radius: var(--radius-pill);
	opacity: 0;
	pointer-events: none;
	/* Written by script each frame; the browser composites the rest. */
	translate: calc(var(--cursor-x, -99rem) - 50%) calc(var(--cursor-y, -99rem) - 50%);
	transition:
		opacity var(--dur-base) var(--ease-out),
		scale var(--dur-base) var(--ease-out),
		background-color var(--dur-base) var(--ease-out),
		border-color var(--dur-base) var(--ease-out);
	will-change: translate;
}

.cursor.is-live {
	opacity: 0.75;
}

/* Over something clickable it opens and fills faintly. */
.cursor.is-over {
	scale: 2.1;
	border-color: transparent;
	background-color: color-mix(in srgb, var(--signal) 14%, transparent);
}

/* Over the dark bands the ring has to invert to stay visible. */
.cursor.is-dark {
	border-color: var(--paper);
}

.cursor.is-dark.is-over {
	border-color: transparent;
	background-color: color-mix(in srgb, var(--paper) 18%, transparent);
}

/* --- Showcase stage ---------------------------------------------------
   A product-zoom stage, the way Apple builds them: a section taller than the
   viewport, a frame inside it that sticks while the section passes, and a
   subject that scales across exactly the window in which the frame is held.

   The base state here is the finished photograph at its natural size. All of
   the staging — the extra height, the sticky frame, the zoom — is added lower
   down, inside a support query and a reduced-motion guard. A browser that
   cannot scrub it therefore does not pay for the scroll room either, which
   matters: 190vh of held image is a long way to scroll past a still.

   It is `showcase`, not `feature`, because `feature` was already taken: the
   Services "why choose us" cards are `.card.card--flat.feature`. A stage rule
   named `.feature` gave all six of them a 190vh minimum height and stretched
   that section to 3958px. Grep before naming a block class.
   ------------------------------------------------------------------------ */

.showcase__frame {
	inline-size: 100%;
}

.showcase__media {
	inline-size: 100%;
	max-inline-size: var(--wide);
	margin-inline: auto;
}

/* --- Feature grid (Services "why choose us") --------------------------- */

.features__grid,
.info__list {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--sp-40);
	margin: 0;
	padding: 0;
	list-style: none;
}

@media (min-width: 48rem) {
	.features__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 64rem) {
	.features__grid {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

.feature__body {
	font-size: var(--fs-lg);
	line-height: 1.5;
}

/* --- Inline stats (Services hero) ------------------------------------- */

.stats--inline {
	grid-template-columns: 1fr;
	border-block: 0;
}

.stats--inline .stat {
	padding-inline: 0;
	padding-block: var(--sp-30);
}

.stats--inline .stat + .stat {
	border-inline-start: 0;
	border-block-start: 1px solid var(--line);
}

@media (max-width: 47.9375rem) {
	.stats--inline .stat:nth-child(n + 3) {
		border-block-start: 1px solid var(--line);
	}
}

.stats--inline .stat__num {
	font-size: var(--fs-3xl);
}

/* --- Table of contents (About) ----------------------------------------
   Three things move here, and each answers a different question.

   The rail's faint fill answers "how far through am I", growing as sections
   are read. The solid marker answers "which one am I in", sliding to the
   current entry. The tally answers "how many are left", counting up in the
   header. None of them is decoration for its own sake.

   The geometry comes from four custom properties written by the spy in
   site.js. CSS cannot measure a list item, so the positions have to arrive
   from script; everything else, including all the easing, stays here.
   ------------------------------------------------------------------------ */

.toc {
	--marker-y: 0px;
	--marker-h: 0px;
	--read: 0px;
}

@media (min-width: 64rem) {
	.toc {
		position: sticky;
		top: calc(var(--header-h) + var(--sp-40));
	}
}

.toc__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--sp-30);
}

.toc__tally {
	margin: 0;
	font-family: var(--font-display);
	font-size: var(--fs-sm);
	font-variant-numeric: lining-nums tabular-nums;
	color: var(--stone);
}

.toc__at {
	color: var(--signal-deep);
}

.toc__slash {
	margin-inline: 0.15em;
	opacity: 0.5;
}

.toc__of {
	color: var(--stone);
}

.toc__list {
	display: grid;
	gap: 0;
	margin: 0;
	padding: 0;
	list-style: none;
	counter-reset: none;
}

.toc__item a {
	display: flex;
	gap: 0.75rem;
	padding-block: 0.6rem;
	color: var(--stone);
	font-size: var(--fs-sm);
	line-height: 1.4;
	text-decoration: none;
	transition:
		color var(--dur-fast) var(--ease-out),
		opacity var(--dur-base) var(--ease-out),
		translate var(--dur-base) var(--ease-out);
}

.toc__item a:hover {
	color: var(--ink);
}

.toc__num {
	flex: none;
	font-family: var(--font-display);
	font-variant-numeric: lining-nums tabular-nums;
	transition:
		color var(--dur-base) var(--ease-out),
		scale var(--dur-base) var(--ease-out);
}

/* The entry for the section currently being read. */
.toc__item a.is-current,
.toc__item a[aria-current] {
	color: var(--ink);
	font-weight: 500;
}

.toc__item a.is-current .toc__num,
.toc__item a[aria-current] .toc__num {
	color: var(--signal-deep);
}

@media (min-width: 64rem) {
	.toc__list {
		position: relative;
		padding-inline-start: 1.25rem;
	}

	/* The rail, and the part of it already read. */
	.toc__list::before,
	.toc__list::after {
		content: "";
		position: absolute;
		inset-inline-start: 0;
		inline-size: 1px;
	}

	.toc__list::before {
		inset-block: 0;
		background: var(--line);
	}

	.toc__list::after {
		inset-block-start: 0;
		block-size: var(--read);
		background: var(--signal-deep);
		opacity: 0.3;
		transition: block-size var(--dur-slow) var(--ease-out);
	}

	/*
	 * The marker is a pseudo-element on the nav rather than an element in the
	 * list, because `<ol>` may only contain `<li>` and a stray span there
	 * both invalidates the markup and shifts every `nth-child` in the
	 * stagger above. It is measured against the nav, so `--marker-y` carries
	 * the header's height with it; script does that arithmetic once.
	 */
	.toc::after {
		content: "";
		position: absolute;
		inset-inline-start: -1px;
		inset-block-start: 0;
		inline-size: 3px;
		block-size: var(--marker-h);
		border-radius: var(--radius-pill);
		background: var(--signal-deep);
		translate: 0 var(--marker-y);
		/* Nothing to point at until the spy reports a section. */
		opacity: 0;
		transition:
			translate var(--dur-slow) var(--ease-out),
			block-size var(--dur-slow) var(--ease-out),
			opacity var(--dur-base) var(--ease-out);
	}

	.toc.is-live::after {
		opacity: 1;
	}

	.toc__item a.is-current,
	.toc__item a[aria-current] {
		translate: 0.35rem 0;
	}

	.toc__item a.is-current .toc__num,
	.toc__item a[aria-current] .toc__num {
		scale: 1.15;
	}

	/*
	 * Pointing at one entry lets the rest recede, the same selection idea as
	 * the casework timeline and the services grid.
	 */
	@media (hover: hover) and (pointer: fine) {
		.toc__list:has(a:hover) .toc__item a:not(:hover) {
			opacity: 0.45;
		}

		.toc__item a:hover {
			translate: 0.35rem 0;
		}
	}
}

@media (max-width: 63.9375rem) {
	/* A horizontal scroller keeps the contents reachable without eating the
	   top of a phone screen. */
	.toc__list {
		display: flex;
		gap: 0.5rem;
		overflow-x: auto;
		padding-block-end: var(--sp-20);
		scrollbar-width: none;
	}

	.toc__list::-webkit-scrollbar {
		display: none;
	}

	.toc__item a {
		padding: 0.5rem 0.9rem;
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		white-space: nowrap;
	}

	.toc__label {
		max-width: 18ch;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	/* The chip for the current section fills, so the scroller shows position
	   as well as offering navigation. */
	.toc__item a {
		transition:
			background-color var(--dur-base) var(--ease-out),
			border-color var(--dur-base) var(--ease-out),
			color var(--dur-fast) var(--ease-out);
	}

	.toc__item a.is-current,
	.toc__item a[aria-current] {
		border-color: var(--signal-deep);
		background-color: var(--signal-tint);
	}
}

/* --- Long-form sections (About) --------------------------------------- */

.prose-section {
	padding-block: var(--sp-50);
	scroll-margin-top: calc(var(--header-h) + var(--sp-40));
}

.prose-section + .prose-section {
	border-block-start: 1px solid var(--line);
}

.prose-section > .prose {
	margin-block-start: var(--sp-30);
}

.prose-section .media {
	margin-block-start: var(--sp-40);
	/* 1200x800, the ratio this photograph was cut to. */
	aspect-ratio: 3 / 2;
}

.services .media {
	margin-block-start: var(--sp-50);
	/* 1200x800, the ratio this photograph was cut to. */
	aspect-ratio: 3 / 2;
}

.media--wide {
	aspect-ratio: 16 / 7;
}

.media--wide img {
	block-size: 100%;
	object-fit: cover;
}

.hero__narrow {
	max-width: 48rem;
	margin-inline: 0;
}


.illo--cone {
	max-width: 9rem;
	margin-inline: auto;
}

/* Inline call-to-action at the end of the About narrative. */
.cta-inline {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var(--sp-30) var(--sp-40);
	margin-block-start: var(--sp-50);
	padding: var(--sp-40);
	border-radius: var(--radius-lg);
	background-color: var(--ink);
	color: var(--paper);
}

.cta-inline .eyebrow {
	display: flex;
	align-items: center;
	gap: 0.4rem;
	margin: 0;
	color: var(--stone-light);
}

.cta-inline .tel-display {
	color: var(--paper);
	font-size: var(--fs-3xl);
}

.cta-inline .btn--ghost {
	color: var(--paper);
	box-shadow: inset 0 0 0 1px var(--stone-light);
}

.cta-inline .btn--ghost:hover {
	background-color: var(--paper);
	color: var(--ink);
}

/* --- Author box ------------------------------------------------------- */

.author-box {
	display: grid;
	gap: var(--sp-40);
	align-items: start;
}

@media (min-width: 40rem) {
	.author-box {
		grid-template-columns: auto minmax(0, 1fr);
	}
}

.author-box__media img {
	inline-size: 7rem;
	block-size: 7rem;
	border-radius: var(--radius-pill);
	object-fit: cover;
}

.author-box__name {
	font-family: var(--font-display);
	font-size: var(--fs-2xl);
	font-weight: 500;
	line-height: 1.2;
}

.author-box__title {
	margin-block-end: var(--sp-30);
	color: var(--stone);
	font-size: var(--fs-sm);
}

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

.info__list .info {
	padding: 0 0 var(--sp-40);
	border: 0;
	border-block-end: 1px solid var(--line);
	background: none;
	border-radius: 0;
}

.info__list .info:last-child {
	padding-block-end: 0;
	border-block-end: 0;
}

.info__title {
	font-family: var(--font-display);
	font-size: var(--fs-lg);
	font-weight: 500;
	line-height: 1.25;
}

.info__body {
	color: var(--graphite);
	font-size: var(--fs-sm);
}

.pill {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	padding: 0.45em 0.9em;
	border: 1px solid var(--line);
	border-radius: var(--radius-pill);
	font-size: var(--fs-sm);
	font-weight: 500;
}

.pill__dot {
	inline-size: 7px;
	block-size: 7px;
	border-radius: 50%;
	background-color: var(--moss);
}

@media (prefers-reduced-motion: no-preference) {
	.pill--live .pill__dot {
		animation: cacl-pulse 2.4s var(--ease-in-out) infinite;
	}
}

/* --- Form ------------------------------------------------------------- */

.form-card {
	padding: var(--sp-40);
	border: 1px solid var(--line);
	border-radius: var(--radius-lg);
	background-color: var(--sand);
	scroll-margin-top: calc(var(--header-h) + var(--sp-40));
}

.form__intro {
	margin-block-end: var(--sp-40);
	color: var(--graphite);
	font-size: var(--fs-sm);
}

.form__row--split {
	display: grid;
	gap: var(--sp-30);
}

@media (min-width: 32rem) {
	.form__row--split {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

.field {
	display: block;
	margin-block-end: var(--sp-30);
}

.field label {
	display: block;
	margin-block-end: 0.4rem;
	font-size: var(--fs-sm);
	font-weight: 500;
}

.field__optional,
.field__hint {
	color: var(--stone);
	font-weight: 400;
}

.field__hint {
	display: block;
	margin-block-start: 0.2rem;
	font-size: var(--fs-xs);
}

.field input[type="text"],
.field input[type="tel"],
.field input[type="email"],
.field textarea {
	inline-size: 100%;
	min-block-size: 3rem;
	padding: 0.7rem 0.9rem;
	border: 1px solid var(--line);
	border-radius: var(--radius-md);
	background-color: var(--white);
	color: var(--ink);
	font-size: var(--fs-md);
}

.field textarea {
	min-block-size: 6rem;
	line-height: 1.5;
}

.field input:focus-visible,
.field textarea:focus-visible {
	border-color: var(--signal-deep);
}

/* An invalid field is marked by colour, an icon-free message, and aria. */
.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
	border-color: var(--signal-deep);
	background-color: var(--signal-tint);
}

.field__error {
	display: none;
	margin-block-start: 0.35rem;
	color: var(--signal-deep);
	font-size: var(--fs-xs);
	font-weight: 500;
}

.field__error:not(:empty) {
	display: block;
}

.field--consent {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	gap: 0.75rem;
	align-items: start;
}

.field--consent input {
	inline-size: 1.15rem;
	block-size: 1.15rem;
	margin-block-start: 0.15rem;
	accent-color: var(--moss);
}

.field--consent label {
	margin: 0;
	font-weight: 400;
}

.field--files input[type="file"] {
	inline-size: 100%;
	padding: 0.7rem;
	border: 1px dashed var(--stone-light);
	border-radius: var(--radius-md);
	background-color: var(--white);
	font-size: var(--fs-sm);
}

.field__previews {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem;
	margin: 0.6rem 0 0;
	padding: 0;
	list-style: none;
}

.field__previews li {
	position: relative;
	inline-size: 4.5rem;
	block-size: 4.5rem;
	border-radius: var(--radius-sm);
	overflow: hidden;
	background-color: var(--white);
}

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

.field__previews button {
	position: absolute;
	top: 2px;
	right: 2px;
	inline-size: 1.35rem;
	block-size: 1.35rem;
	display: grid;
	place-items: center;
	border: 0;
	border-radius: 50%;
	background-color: color-mix(in srgb, var(--ink) 80%, transparent);
	color: var(--paper);
	cursor: pointer;
	font-size: 0.7rem;
	line-height: 1;
}

/*
 * Off-screen rather than display:none. A field that is not rendered at all is
 * skipped by some automated form fillers, which defeats the point of it.
 */
.form__hp {
	position: absolute;
	left: -10000px;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
}

.form__status {
	margin-block-end: var(--sp-30);
	font-size: var(--fs-sm);
	font-weight: 500;
}

.form__status:empty {
	margin-block-end: 0;
}

.form__status.is-error {
	color: var(--signal-deep);
}

.form__submit[aria-busy="true"] {
	opacity: 0.7;
	cursor: progress;
}

.form__success {
	display: grid;
	gap: var(--sp-30);
	justify-items: start;
}

.form__tick {
	display: grid;
	place-items: center;
	inline-size: 3rem;
	block-size: 3rem;
	border-radius: var(--radius-pill);
	background-color: var(--moss-tint);
	color: var(--moss);
	font-size: 1.5rem;
}

.form__success-title {
	font-family: var(--font-display);
	font-size: var(--fs-2xl);
	font-weight: 500;
	line-height: 1.2;
}

/* --- Post cards (blog fallbacks) -------------------------------------- */

.posts {
	display: grid;
	gap: var(--sp-40);
}

.post-card .wp-block-post-title {
	font-size: var(--fs-2xl);
}

/* --- Services table --------------------------------------------------- */

.table--services td:first-child {
	font-size: var(--fs-lg);
}


/* --- Biography -------------------------------------------------------- */

.bio__updated {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	margin-block-start: var(--sp-40);
}




/* 06  Chrome — header, footer, call bar ===================================
   ------------------------------------------------------------------------ */

/* --- Header ------------------------------------------------------------
   The sticky rule sits on the template part wrapper itself. Putting it on an
   inner group instead would confine stickiness to that group's own height,
   which is to say it would not stick at all.

   Sticky rather than fixed, so the header occupies layout space and nothing
   has to be padded to compensate. That keeps cumulative layout shift at zero
   through the compaction.
   -------------------------------------------------------------------- */

.site-header {
	position: sticky;
	top: 0;
	z-index: 50;
	padding-block: var(--sp-20);
}

/*
 * The bar narrows into a frosted pill as the page scrolls.
 *
 * Driven by a `--shrink` value between 0 and 1 that site.js writes from the
 * scroll position, rather than by toggling a class at a threshold. A class
 * toggle can only ever snap between two states — a transition then plays out
 * on its own clock, unrelated to how fast the visitor is scrolling, which is
 * what made this feel abrupt. Interpolating every property against scroll
 * position means the pill forms exactly as fast as the page moves, and
 * reverses just as smoothly.
 */
.site-header__bar {
	position: relative;
	min-height: calc(var(--header-h) - (var(--header-h) - var(--header-h-compact)) * var(--shrink, 0));
	padding-inline: var(--sp-30);
}

/*
 * The pill surface, built to read as a piece of glass rather than a
 * translucent rectangle.
 *
 * Four things do the work, and all of them matter:
 *
 *   - A saturating, brightening blur. Glass does not merely obscure what is
 *     behind it, it enriches the colour and lifts the brightness, which is
 *     what separates this from a grey scrim.
 *   - A vertical gradient, lighter at the top, because that is where light
 *     lands on a curved surface.
 *   - An inset highlight along the top edge and a faint ring all round, in
 *     place of a drawn border. A flat 1px border reads as a box; a bright
 *     inner rim reads as a lit edge.
 *   - A specular bloom in the upper left, the reflection of the light source.
 *
 * The highlights are warm rather than pure white, so the glass belongs to the
 * paper palette instead of sitting on top of it.
 *
 * All of it fades in on one opacity, tied to scroll, so the glass forms as
 * the page moves.
 */
.site-header__bar::before,
.site-header__bar::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	border-radius: var(--radius-pill);
	opacity: var(--shrink, 0);
	pointer-events: none;
}

.site-header__bar::before {
	/*
	 * Opaque enough that the navigation stays readable wherever the header
	 * happens to be sitting. The page has an ink band and an orange band, and
	 * the header crosses both; at lower opacity the ink nav text lost its
	 * contrast against whatever showed through. At these values the glass
	 * still reads as glass — shapes and colour move behind it — but the text
	 * always has a light surface under it, about 7:1 even over solid ink.
	 */
	background-image: linear-gradient(
		to bottom,
		color-mix(in srgb, #fff 80%, transparent),
		color-mix(in srgb, var(--paper) 70%, transparent)
	);
	backdrop-filter: blur(20px) saturate(1.9) brightness(1.06);
	-webkit-backdrop-filter: blur(20px) saturate(1.9) brightness(1.06);
	box-shadow:
		/* Lift off the page. */
		0 12px 32px -12px rgba(28, 26, 23, 0.22),
		0 2px 6px -3px rgba(28, 26, 23, 0.12),
		/* The lit top edge. */
		inset 0 1px 0 rgba(255, 255, 255, 0.9),
		/* A ring, brighter than a border and softer than one. */
		inset 0 0 0 1px rgba(255, 255, 255, 0.45),
		/* The underside catches a little of the surface it sits on. */
		inset 0 -1px 3px rgba(28, 26, 23, 0.05);
}

/*
 * The reflection of the light source.
 *
 * Two gradients in one layer: a fixed bloom in the upper left, which is the
 * ambient highlight, and a second centred on the pointer so the glass catches
 * the light as the cursor crosses it. The pointer position defaults to the
 * same upper-left spot, so before the cursor arrives — and on a touch screen,
 * where it never does — the pill still looks lit.
 */
.site-header__bar::after {
	background-image:
		radial-gradient(
			14rem circle at var(--spot-x, 18%) var(--spot-y, 50%),
			rgba(255, 255, 255, 0.42),
			rgba(255, 255, 255, 0) 60%
		),
		radial-gradient(
			130% 200% at 18% -60%,
			rgba(255, 255, 255, 0.45),
			rgba(255, 255, 255, 0) 58%
		);
	mix-blend-mode: screen;
}

/*
 * Without backdrop-filter the glass would be a pale wash over unreadable
 * text, so fall back to an opaque surface.
 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
	.site-header__bar::before {
		background-image: none;
		background-color: var(--paper);
	}

	.site-header__bar::after {
		display: none;
	}
}

/*
 * Narrowing needs to beat WordPress's own alignwide rule, which is two
 * classes, hence the third here.
 */
.site-header .site-header__bar.alignwide {
	max-inline-size: calc(var(--wide) - (var(--wide) - 52rem) * var(--shrink, 0));
	margin-inline: auto;
}

/* --- Reading progress -------------------------------------------------
   A hairline across the foot of the header that fills as the page is read.

   Driven by a native CSS scroll timeline, which is the part worth noting:
   `animation-timeline: scroll()` ties the animation to the scroll container
   without a single line of JavaScript, and because only a transform is
   animated the whole thing runs on the compositor thread. It cannot stutter
   when the main thread is busy, which a scroll event handler always can.

   Support is around 84% (Chrome and Edge 115, Firefox 132, Safari 18), so
   it sits behind @supports and site.js drives the same custom property for
   the rest. Either way the bar is decorative: if neither path runs it simply
   stays empty.
   -------------------------------------------------------------------- */

.site-progress {
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	block-size: 2px;
	overflow: hidden;
	pointer-events: none;
}

.site-progress__bar {
	display: block;
	block-size: 100%;
	inline-size: 100%;
	border-radius: var(--radius-pill);
	background-image: linear-gradient(90deg, var(--signal-deep), var(--signal));
	transform: scaleX(var(--read, 0));
	transform-origin: left center;
}

@supports (animation-timeline: scroll()) {
	@media (prefers-reduced-motion: no-preference) {
		.site-progress__bar {
			animation: cacl-read linear;
			animation-timeline: scroll(root block);
		}
	}
}

/*
 * Both endpoints are declared. Relying on an implicit `from` would inherit
 * whatever the base rule computed, which left the bar stuck at zero in the
 * case where the timeline is supported but never attaches.
 */
@keyframes cacl-read {
	from {
		transform: scaleX(0);
	}

	to {
		transform: scaleX(1);
	}
}

/* --- Logo -------------------------------------------------------------- */

.site-logo {
	display: inline-flex;
	align-items: center;
	gap: 0.75rem;
	flex: none;
	color: var(--ink);
	text-decoration: none;
}

.site-logo__mark {
	block-size: calc(2.25rem - 0.5rem * var(--shrink, 0));
	inline-size: calc(2.25rem - 0.5rem * var(--shrink, 0));
	flex: none;
}

/*
 * The wordmark is live text in Fraunces, not outlined paths: the face is
 * preloaded and metric-matched, so it costs nothing and stays selectable.
 */
/*
 * The wordmark collapses as the bar narrows, leaving the mark, which stays
 * recognisable on its own.
 */
.site-logo__word {
	font-family: var(--font-display);
	font-size: 1rem;
	font-weight: 500;
	line-height: 1.05;
	letter-spacing: -0.01em;
	white-space: nowrap;
	overflow: hidden;
	max-inline-size: calc(9rem * (1 - var(--shrink, 0)));
	opacity: calc(1 - var(--shrink, 0) * 1.6);
}

@media (max-width: 30rem) {
	/* Below 480px the wordmark and the phone number cannot both fit. */
	.site-logo__word {
		display: none;
	}
}

/* --- Navigation --------------------------------------------------------
   core/navigation with overlayMenu "mobile" switches to a hamburger below
   600px, which is core's own breakpoint. Rather than fight that media query,
   the 600-782px band tightens the nav and reduces the phone button to its
   icon so all three elements still fit.
   -------------------------------------------------------------------- */

.site-nav {
	flex: 1 1 auto;
	justify-content: flex-end;
}

.site-nav .wp-block-navigation__container {
	gap: clamp(1rem, 2.5vw, 2rem);
}

.site-nav .wp-block-navigation-item__content {
	position: relative;
	padding-block: 0.35rem;
	text-decoration: none;
}

/* Underline grows from the left on hover, and stays put for the current page. */
.site-nav .wp-block-navigation-item__content::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	block-size: 1px;
	background-color: var(--signal);
	transform: scaleX(0);
	transform-origin: left;
	transition: transform var(--dur-base) var(--ease-out);
}

.site-nav .wp-block-navigation-item__content:hover::after,
.site-nav .wp-block-navigation-item__content:focus-visible::after,
.site-nav .current-menu-item .wp-block-navigation-item__content::after {
	transform: scaleX(1);
}

@media (min-width: 37.5rem) and (max-width: 48.9375rem) {
	.site-nav .wp-block-navigation__container {
		gap: 1rem;
	}

	.site-nav .wp-block-navigation-item__content {
		font-size: var(--fs-sm);
	}
}

/*
 * Icon-only phone button below 48.9375rem: the accessible name comes from the
 * sr-only span. This has to run the full range down to 0, not just the
 * tablet band above — a phone under 37.5rem needs the shrink most of all, and
 * the full "Call 02 5550 1234" label at that width pushed the header bar
 * wider than the viewport, forcing the whole page into horizontal scroll.
 */
@media (max-width: 48.9375rem) {
	.site-header__bar .btn--phone .btn__label {
		display: none;
	}

	.site-header__bar .btn--phone {
		padding-inline: 0.85em;
	}
}

/* Mobile overlay panel. */
.site-nav .wp-block-navigation__responsive-container.is-menu-open {
	padding: var(--sp-40) var(--gutter);
	background-color: var(--paper);
}

.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
	gap: var(--sp-30);
}

.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
	font-family: var(--font-display);
	font-size: var(--fs-3xl);
	font-weight: 400;
	line-height: 1.1;
}

/*
 * Staggered entrance for the overlay links. CSS keyframes rather than GSAP,
 * because the panel is opened by core's own script and this needs no
 * coordination with it.
 */
@media (prefers-reduced-motion: no-preference) {
	.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item {
		animation: cacl-nav-in var(--dur-base) var(--ease-out) backwards;
		animation-delay: calc(var(--i, 0) * 60ms);
	}

	.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:nth-child(1) { --i: 1; }
	.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:nth-child(2) { --i: 2; }
	.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:nth-child(3) { --i: 3; }
	.site-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item:nth-child(4) { --i: 4; }
}

@keyframes cacl-nav-in {
	from {
		opacity: 0;
		transform: translateY(0.75rem);
	}
}

/* --- Footer ------------------------------------------------------------
   The template part wrapper carries no layout of its own, so the part's
   contents are wrapped in a constrained group. Without it `alignwide` has no
   container to align within and the root gutter never applies, which left the
   footer's text flush against the viewport edge.
   -------------------------------------------------------------------- */

.site-footer {
	padding-block: var(--sp-70) var(--sp-40);
	background-color: var(--ink);
	color: var(--stone-light);
	overflow: hidden;
}

.site-footer a {
	color: var(--paper);
	text-decoration: none;
}

.footer__grid {
	display: grid;
	gap: var(--sp-50);
	grid-template-columns: 1fr;
}

@media (min-width: 40rem) {
	.footer__grid {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 64rem) {
	.footer__grid {
		grid-template-columns: 1.25fr 0.7fr 1.2fr 1.35fr;
	}
}

.footer__col--brand .site-logo {
	color: var(--paper);
	margin-block-end: var(--sp-30);
}

.footer__tagline {
	max-width: 30ch;
	margin-block-end: var(--sp-30);
	color: var(--stone-light);
	font-size: var(--fs-sm);
}

.footer__heading {
	margin-block-end: var(--sp-30);
	color: var(--paper);
	font-family: var(--font-body);
	font-size: var(--fs-xs);
	font-weight: 500;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

.footer__list {
	display: grid;
	gap: 0.6rem;
	padding: 0;
	list-style: none;
	font-size: var(--fs-sm);
}

.footer__list--creds li {
	display: flex;
	align-items: start;
	gap: 0.6rem;
}

.footer__list--creds .icon {
	margin-block-start: 0.2em;
	color: var(--moss-tint);
}

.footer__address,
.footer__hours {
	margin-block-end: var(--sp-30);
	font-size: var(--fs-sm);
	font-style: normal;
	line-height: 1.7;
}

/*
 * Each line of the hours summary is a unit; breaking "8:30 AM - 6:00 PM"
 * across two lines makes it hard to read at a glance.
 */
.footer__hours span {
	display: block;
	white-space: nowrap;
}

.tel-display--sm {
	display: inline-block;
	font-size: var(--fs-lg);
}

/*
 * Oversized wordmark across the foot of the page. Decorative only, and
 * hidden from assistive technology in the markup.
 *
 * The size is set in vw so the full name spans the page without ever being
 * cut off: at 6.4vw the 25 characters of Fraunces come to roughly the width
 * of the content area. A clipped wordmark reads as a bug rather than a
 * flourish, so it is sized to fit rather than allowed to bleed.
 */
.footer__wordmark {
	margin-block: var(--sp-50) 0;
	color: var(--ink-2);
	font-family: var(--font-display);
	font-size: min(6.4vw, 7.5rem);
	font-weight: 400;
	line-height: 1;
	letter-spacing: -0.035em;
	text-align: center;
	white-space: nowrap;
	user-select: none;
}

@media (max-width: 40rem) {
	/* Two short lines read better than one tiny one on a phone. */
	.footer__wordmark {
		font-size: 11vw;
		white-space: normal;
		text-wrap: balance;
	}
}

.footer__base {
	margin-block-start: var(--sp-40);
	padding-block-start: var(--sp-30);
	border-block-start: 1px solid var(--ink-2);
}

.footer__legal,
.footer__meta {
	color: var(--stone-light);
	font-size: var(--fs-xs);
	line-height: 1.6;
}

.footer__legal {
	max-width: 70ch;
	margin-block-end: 0.5rem;
}

.footer__meta {
	display: flex;
	flex-wrap: wrap;
	gap: 0.5rem var(--sp-40);
	margin-block-start: var(--sp-30);
}

/* --- Visit block ------------------------------------------------------- */

.visit__details {
	display: grid;
	gap: var(--sp-30);
	margin-block: var(--sp-40);
}

.visit__line {
	display: flex;
	align-items: start;
	gap: 0.75rem;
}

.visit__line .icon {
	margin-block-start: 0.25em;
	color: var(--signal-deep);
}

.visit__line address {
	font-style: normal;
	line-height: 1.6;
}

.tel-display {
	font-size: var(--fs-3xl);
}

/*
 * Fixed call bar for small screens, where tapping a phone number is the
 * fastest useful action. Revealed past the hero by site.js.
 */
.call-bar {
	position: fixed;
	inset-inline: 0;
	bottom: 0;
	z-index: 60;
	display: none;
	gap: var(--sp-20);
	padding: var(--sp-20);
	padding-bottom: calc(var(--sp-20) + env(safe-area-inset-bottom, 0px));
	background-color: color-mix(in srgb, var(--paper) 92%, transparent);
	backdrop-filter: blur(12px);
	border-block-start: 1px solid var(--line);
	transform: translateY(101%);
	transition: transform var(--dur-base) var(--ease-out);
}

@media (max-width: 48.9375rem) {
	.call-bar {
		display: flex;
	}

	.call-bar.is-visible {
		transform: translateY(0);
	}

	.call-bar.is-hidden {
		transform: translateY(101%);
	}

	/* Reserve room so the bar never covers the end of the page. */
	body:has(.call-bar) .site-footer {
		padding-block-end: calc(var(--sp-60) + env(safe-area-inset-bottom, 0px));
	}
}


/* 07  Page-specific ======================================================
   ------------------------------------------------------------------------ */

/* --- Hero ------------------------------------------------------------- */

.hero {
	padding-block-start: var(--sp-60);
}

.hero--home {
	position: relative;
	overflow: clip;
	padding-block: var(--sp-60) var(--sp-70);
}

@media (min-width: 64rem) {
	.hero--home {
		min-height: clamp(34rem, 82dvh, 56rem);
		display: grid;
		align-content: center;
	}
}

/*
 * A large route line drifting behind the hero content. It is twice its
 * visible size and translated by exactly half its own width in a seamless
 * loop, the same technique the credentials marquee uses — the graphic itself
 * is drawn as two identical 500-unit periods back to back, so the loop point
 * is invisible. Positioned absolute so it never takes part in the grid the
 * hero switches to at 64rem; the hero's own `overflow: clip` above stops it
 * ever creating horizontal scroll.
 */
.hero--home .illo--route-bg {
	position: absolute;
	inset-inline: 0;
	inset-block-start: 58%;
	transform: translateY(-50%);
	z-index: 0;
	color: var(--ink);
	opacity: 0.16;
	pointer-events: none;
}

.hero--home .illo--route-bg svg {
	inline-size: 160%;
	max-inline-size: none;
	block-size: auto;
	flex: none;
	animation: cacl-marquee 70s linear infinite;
}

.hero--home .hero__grid {
	position: relative;
	z-index: 1;
}

.hero__title {
	margin-block-end: var(--sp-40);
}

.hero__kicker {
	display: block;
	margin-block-end: var(--sp-30);
	color: var(--stone);
	font-family: var(--font-body);
	font-size: var(--fs-xs);
	font-weight: 500;
	line-height: 1.2;
	letter-spacing: 0.12em;
	text-transform: uppercase;
}

/*
 * The display half of the headline. This is the largest contentful paint on
 * the page, so it is never given opacity 0: the load animation moves it by
 * transform inside a clipped wrapper instead.
 */
.hero__display {
	display: block;
	max-width: 15ch;
	font-size: var(--fs-display);
	font-weight: 380;
	line-height: 0.96;
	letter-spacing: -0.025em;
	text-wrap: balance;
}

.hero--page .hero__display,
.hero--page .wp-block-post-title {
	max-width: 20ch;
	font-size: var(--fs-5xl);
	line-height: 1.02;
	letter-spacing: -0.02em;
}

.hero__lede {
	max-width: 46ch;
	margin-block-end: var(--sp-40);
}

.hero__actions {
	margin-block-end: var(--sp-40);
}

.hero__trust {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem var(--sp-40);
	color: var(--stone);
	font-size: var(--fs-sm);
}

.hero__trust-item {
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	white-space: nowrap;
}

.hero__trust-item strong {
	color: var(--ink);
	font-family: var(--font-display);
	font-size: var(--fs-lg);
	font-variant-numeric: tabular-nums;
	line-height: 1;
}

.illo--hero {
	color: var(--ink);
}

@media (max-width: 63.9375rem) {
	/* On a phone the illustration comes after the actions, not before them. */
	.hero__grid {
		flex-direction: column;
		/*
		 * `are-vertically-aligned-center` sets `align-items: center` for the
		 * row layout above this breakpoint, where it centers the two columns
		 * on the cross (vertical) axis as intended. Once flex-direction
		 * switches to column here, cross-axis and main-axis swap: that same
		 * `center` now shrinks each column to its own content width and
		 * centers it horizontally instead of stretching it full-width, so a
		 * long word or an unbreakable row (the phone number, the button
		 * pair) pushed the column — and with it the whole page — wider than
		 * the viewport. Stretch is what column direction needs here.
		 */
		align-items: stretch;
	}

	.illo--hero {
		max-width: 26rem;
		margin-block-start: var(--sp-40);
	}
}

/* --- Section headers -------------------------------------------------- */

.section__head--narrow {
	max-width: 34rem;
}

.credentials__head {
	max-width: 56rem;
}

/* The marquee's lower rule closes the band, so no padding is needed under it. */
.credentials {
	padding-block-end: 0;
}

/* --- Why us, on the dark band ----------------------------------------- */

.illo--signpost {
	max-width: 11rem;
	margin-block-start: var(--sp-50);
	color: var(--stone-light);
}

/* Same mark reused on a light section, so it keeps the default ink colour. */
.illo--differentiator {
	max-width: 9rem;
	margin-block-start: var(--sp-40);
}

/* --- About page, sticky table of contents ------------------------------ */

.illo--photos {
	max-width: 8rem;
	margin-block-start: var(--sp-40);
}

@media (max-width: 63.9375rem) {
	.illo--photos {
		display: none;
	}
}

/* --- FAQ page, section head --------------------------------------------- */

.illo--qa {
	max-width: 7rem;
	margin-block-start: var(--sp-30);
}

@media (max-width: 63.9375rem) {
	.illo--signpost {
		display: none;
	}
}

/* --- Biography -------------------------------------------------------- */

.bio__grid .media {
	max-width: 26rem;
	/* Definite, so the oversized parallax figure has something to exceed. */
	aspect-ratio: 4 / 5;
}

/* --- Contact and About page grids ------------------------------------- */

.about-grid,
.contact-grid {
	margin-block-start: var(--sp-50);
}


/* 08  Motion =============================================================
   Starting states are no longer needed here, and that is the point.

   This section used to hide every reveal target in CSS and rely on script to
   bring it back, which meant carrying a three-second failsafe animation, a
   class that script added to cancel it, and a sweep after load in case a
   trigger never fired. All of that machinery existed to protect against the
   script not arriving.

   Native scroll-driven animations removed the need for it: the animation is
   declared inside @supports, so a browser that cannot run it applies nothing
   and the element is simply visible. There is no state to recover from.

   What still belongs to GSAP, and why:

     - the hero load sequence, which is a timeline in real time, not scroll
     - the counters, which have to format numbers as they change
     - the progressive word reading, which needs text split into spans first
     - the process rail, which also moves a marker and flags the active step
     - the pointer effects, which are not scroll at all
   ------------------------------------------------------------------------ */

/*
 * Reveals, driven by the browser rather than by script.
 *
 * `animation-timeline: view()` gives an element a timeline made of its own
 * journey through the viewport; `animation-range` says which part of that
 * journey the animation occupies. Two consequences matter here.
 *
 * It runs on the compositor, so it cannot be starved by main-thread work.
 * And an element already on screen at load sits at its correct progress
 * immediately, with no frame where the copy is hidden waiting to be revealed.
 * That frame was the navigation flicker.
 *
 * Browsers without support get no reveal and the finished page instead. For
 * decorative motion that is the right trade.
 *
 * Chrome and Edge 115, Firefox 132, Safari 18.
 */

@keyframes cacl-rise {
	from {
		opacity: 0;
		transform: translateY(1.5rem);
	}
}

@keyframes cacl-deal {
	from {
		opacity: 0;
		transform: perspective(1200px) translateY(2.5rem) rotateX(12deg) scale(0.95);
	}
}

@keyframes cacl-draw {
	from {
		stroke-dasharray: 1;
		stroke-dashoffset: 1;
	}

	to {
		stroke-dasharray: 1;
		stroke-dashoffset: 0;
	}
}

@keyframes cacl-slide {
	from {
		opacity: 0;
		transform: translateX(2rem);
	}
}

@keyframes cacl-row {
	from {
		opacity: 0;
		transform: translateX(-0.75rem);
	}
}

@keyframes cacl-wipe {
	from {
		clip-path: inset(0 0 100% 0);
	}
}

/*
 * Through 1, not to it. The subject arrives held back, reaches its natural
 * size around the middle of the hold, and is magnified past it on the way
 * out. That single pass is what reads as the product being brought forward.
 */
@keyframes cacl-zoom {
	from {
		scale: 0.88;
		border-radius: var(--radius-xl);
	}

	to {
		scale: 1.18;
		border-radius: 0;
	}
}

@keyframes cacl-parallax {
	from {
		transform: translateY(-5%);
	}

	to {
		transform: translateY(5%);
	}
}

@keyframes cacl-glyph-drive {
	from {
		translate: -1.25rem 0;
	}

	to {
		translate: 1.25rem 0;
	}
}

@keyframes cacl-unmask {
	from {
		mask-position: 50% 0%;
		-webkit-mask-position: 50% 0%;
	}

	to {
		mask-position: 50% 100%;
		-webkit-mask-position: 50% 100%;
	}
}

@keyframes cacl-num-in {
	from {
		opacity: 0;
		/* Scaled and lifted together, so the year reads as approaching. */
		transform: translateY(2.5rem) scale(0.86);
	}
}

/*
 * The year recedes rather than disappears. It stays legible at 0.32 against
 * sand, which keeps the chronology readable while the eye follows the copy.
 */
@keyframes cacl-num-out {
	to {
		opacity: 0.32;
	}
}

/*
 * Only `scale`, never `transform`. The node already carries `translate: -50%`
 * to centre itself on the rail; a keyframe that restated that offset inside a
 * `transform` applied it a second time and pushed every node 4px off the
 * line. Individual transform properties compose, so keeping each animation to
 * its own property is the difference between composing and colliding.
 */
@keyframes cacl-node {
	from {
		scale: 0;
	}

	to {
		scale: 1;
	}
}

@keyframes cacl-drift {
	from {
		transform: translateY(-6%);
	}

	to {
		transform: translateY(6%);
	}
}

@media (prefers-reduced-motion: no-preference) {
	@supports (animation-timeline: view()) {

		/* Single elements rising as they come into view. */
		.a-fade-up {
			animation: cacl-rise linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 55%;
		}

		/*
		 * Groups whose children arrive one after another.
		 *
		 * Each child has its own timeline, so the offset is expressed as a
		 * later start in the range rather than as a delay. A delay would be
		 * measured in time, and there is no time here — only distance.
		 */
		.a-stagger > *:not(.areas__glider) {
			animation: cacl-rise linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 52%;
		}

		.a-stagger > *:nth-child(2) { animation-range: entry 4% entry 56%; }
		.a-stagger > *:nth-child(3) { animation-range: entry 8% entry 60%; }
		.a-stagger > *:nth-child(4) { animation-range: entry 12% entry 64%; }
		.a-stagger > *:nth-child(5) { animation-range: entry 16% entry 68%; }
		.a-stagger > *:nth-child(6) { animation-range: entry 20% entry 72%; }

		/*
		 * Card grids get a richer entrance than the plain rise: they tilt up
		 * out of the page as though being dealt.
		 *
		 * Only the card grids. The same hook also drives the stats row, the
		 * checklist, the FAQ and the footer columns, and a 3D tilt on a line
		 * of text reads as a wobble rather than a flourish — so those three
		 * selectors are named rather than inheriting it.
		 *
		 * The perspective is a transform function, not the `perspective`
		 * property on the grid. That gives every card its own vanishing point
		 * directly in front of it, so the one in the right-hand column does
		 * not appear to lean toward the middle of the row, which is what a
		 * shared perspective on the parent would do.
		 *
		 * These rules set only the animation name, so the stagger offsets
		 * above still apply.
		 */
		.a-stagger.services__grid > *,
		.a-stagger.features__grid > *,
		.a-stagger.info__list > * {
			animation-name: cacl-deal;
		}

		/* Table rows, which are already at staggered heights. */
		.a-rows tbody tr {
			animation: cacl-row linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 40%;
		}

		/*
		 * The contents list assembles itself as the section arrives. Each
		 * entry has its own view timeline, so a time delay would mean
		 * nothing; the offsets are ranges instead, 3% apart, which is what
		 * produces the stagger.
		 */
		.a-spy .toc__item {
			animation: cacl-slide linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 45%;
		}

		.a-spy .toc__item:nth-child(2) { animation-range: entry 3% entry 48%; }
		.a-spy .toc__item:nth-child(3) { animation-range: entry 6% entry 51%; }
		.a-spy .toc__item:nth-child(4) { animation-range: entry 9% entry 54%; }
		.a-spy .toc__item:nth-child(5) { animation-range: entry 12% entry 57%; }
		.a-spy .toc__item:nth-child(6) { animation-range: entry 15% entry 60%; }
		.a-spy .toc__item:nth-child(7) { animation-range: entry 18% entry 63%; }
		.a-spy .toc__item:nth-child(8) { animation-range: entry 21% entry 66%; }
		.a-spy .toc__item:nth-child(9) { animation-range: entry 24% entry 69%; }
		.a-spy .toc__item:nth-child(n+10) { animation-range: entry 27% entry 72%; }

		/*
		 * The line illustrations draw themselves as they arrive.
		 *
		 * Every drawable element in them was authored with `pathLength="1"`,
		 * which is what makes this a two-line effect: with the length
		 * normalised to 1, a dash array of 1 is exactly one full outline and
		 * the offset runs 1 to 0 regardless of how long the real path is. No
		 * measuring, and no script.
		 *
		 * Scoped to `[pathLength]` deliberately. A marker circle authored
		 * without it would read `stroke-dasharray: 1` as one user unit and
		 * dissolve into stipple.
		 */
		.illo--signpost [pathLength],
		.illo--australia [pathLength],
		.illo--cta [pathLength] {
			animation: cacl-draw linear both;
			animation-timeline: view();
			animation-range: entry 12% entry 72%;
		}

		/* Drawn in sequence rather than all at once. */
		.illo [pathLength]:nth-child(2) { animation-range: entry 16% entry 76%; }
		.illo [pathLength]:nth-child(3) { animation-range: entry 20% entry 80%; }
		.illo [pathLength]:nth-child(4) { animation-range: entry 24% entry 84%; }
		.illo [pathLength]:nth-child(5) { animation-range: entry 28% entry 88%; }
		.illo [pathLength]:nth-child(6) { animation-range: entry 32% entry 92%; }
		.illo [pathLength]:nth-child(n+7) { animation-range: entry 36% entry 96%; }

		/*
		 * Casework timeline.
		 *
		 * Each year's numeral rises as its group arrives and dims as the group
		 * leaves, so the year the reader is inside is always the boldest one on
		 * screen. Two animations on one element, each with its own range: the
		 * entry one finishes before the marker starts sticking, so the two never
		 * overlap.
		 */
		.a-timeline .timeline__num {
			animation: cacl-num-in linear both, cacl-num-out linear both;
			animation-timeline: view(), view();
			animation-range: entry 0% entry 45%, exit 10% exit 90%;
		}

		/* Matters are vertically separated, so each earns its own timing. */
		.a-timeline .timeline__item {
			animation: cacl-slide linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 55%;
		}

		.a-timeline .timeline__item::before {
			animation: cacl-node linear both;
			animation-timeline: view();
			animation-range: entry 10% entry 60%;
		}

		/*
		 * The vehicle glyph drifts across its row as the row crosses the
		 * viewport, so the car reads as travelling rather than sitting. It
		 * animates `translate` rather than `transform` because the row above
		 * it is already animating `transform`, and because hover wants
		 * `scale` on this same element — three separate properties, no
		 * collisions.
		 */
		.a-timeline .timeline__glyph {
			animation: cacl-glyph-drive linear both;
			animation-timeline: view();
			animation-range: cover 0% cover 100%;
		}

		/*
		 * Feature zoom.
		 *
		 * The stage declares a named view timeline, and the photograph reads
		 * it from inside. A named timeline on an ancestor is in scope for its
		 * descendants, which is the whole reason this works: the subject is
		 * sticky, so its own `view()` progress stalls the moment it pins —
		 * it stops moving through the viewport by definition. The stage keeps
		 * moving, so the stage is what has to be measured.
		 *
		 * `contain` is the right range, not `cover`. For an element taller
		 * than the scrollport, `contain` is the span during which it
		 * completely covers that scrollport, which is precisely the window in
		 * which the frame is held. The zoom therefore starts as the frame
		 * pins and finishes as it releases, with nothing happening either
		 * side.
		 *
		 * It ends at 1.18. On a 1440 viewport that is about 1510px of a
		 * 2000px source, so the photograph is still being displayed under its
		 * native resolution at the most magnified frame and never softens.
		 * The overflow that magnification creates is clipped by the stage.
		 */
		.showcase {
			min-block-size: 190vh;
			overflow: clip;
			view-timeline-name: --cacl-showcase;
			view-timeline-axis: block;
		}

		.showcase__frame {
			position: sticky;
			top: calc(var(--header-h-compact) + var(--sp-30));
			display: grid;
			place-items: center;
			min-block-size: calc(100svh - var(--header-h-compact) - var(--sp-60));
		}

		.showcase__media {
			animation: cacl-zoom linear both;
			animation-timeline: --cacl-showcase;
			animation-range: contain 0% contain 100%;
		}

		/*
		 * A phone pays for this in thumb travel, so it gets a shorter hold.
		 * 190vh of one photograph is a long scroll on a handset; 135vh still
		 * reads as a hold rather than a jump, and the zoom lands the same.
		 */
		@media (max-width: 47.9375rem) {
			.showcase {
				min-block-size: 135vh;
			}

			.showcase__frame {
				min-block-size: calc(100svh - var(--header-h-compact) - var(--sp-70));
			}
		}

		/*
		 * A photograph feathered in by a mask rather than cut in by a clip
		 * path. The mask is twice the height of the frame and slides across
		 * it: at the start the frame sees the mask's transparent half, at the
		 * end its solid half, and in between a soft edge travels upward.
		 *
		 * Nothing declares the hidden state on its own. The animation is
		 * `both`, so its `from` holds before the range opens, which means a
		 * browser without scroll timelines never applies a mask at all and
		 * shows the finished photograph.
		 */
		.a-mask {
			mask-image: linear-gradient(to top, #000 0%, #000 46%, transparent 74%);
			mask-size: 100% 200%;
			mask-repeat: no-repeat;
			-webkit-mask-image: linear-gradient(to top, #000 0%, #000 46%, transparent 74%);
			-webkit-mask-size: 100% 200%;
			-webkit-mask-repeat: no-repeat;
			animation: cacl-unmask linear both;
			animation-timeline: view();
			animation-range: entry 5% entry 80%;
		}

		/* Footer columns, as the foot of the page arrives. */
		.site-footer .footer__grid > * {
			animation: cacl-rise linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 60%;
		}

		.site-footer .footer__grid > *:nth-child(2) { animation-range: entry 5% entry 65%; }
		.site-footer .footer__grid > *:nth-child(3) { animation-range: entry 10% entry 70%; }
		.site-footer .footer__grid > *:nth-child(4) { animation-range: entry 15% entry 75%; }

		/*
		 * Images revealed by wiping a clip path. Opacity is untouched, so the
		 * image is painted throughout — it is never invisible, only clipped.
		 */
		.a-clip {
			animation: cacl-wipe linear both;
			animation-timeline: view();
			animation-range: entry 10% entry 75%;
		}

		/*
		 * Photographs drifting against the scroll. This is the one reveal
		 * that spans the element's whole crossing rather than just its
		 * entrance, so the range is `cover` rather than `entry`.
		 */
		.a-parallax {
			animation: cacl-parallax linear both;
			animation-timeline: view();
			animation-range: cover 0% cover 100%;
		}

		.footer__wordmark {
			animation: cacl-drift linear both;
			animation-timeline: view();
			animation-range: cover 0% cover 100%;
		}
	}
}

/* --- Progressive reading ----------------------------------------------
   Body copy that brightens word by word as it scrolls through the reading
   zone, so the eye is led down the paragraph at the pace of the scroll.

   Scrubbed rather than played, which is the difference that matters: the
   words are tied to scroll position, so scrolling back un-reads them. A
   played animation would fire once and finish regardless of where the
   reader actually is.

   Nothing is pre-hidden in CSS for this, deliberately. The starting opacity
   is applied by site.js, so if the script never runs the paragraph is simply
   fully legible from the start.
   -------------------------------------------------------------------- */

.a-read .w {
	/* Transitioned rather than set, so the tail of the stagger still looks
	   deliberate when the reader scrolls quickly. */
	transition: opacity 120ms linear;
}


/* --- Split headings ---------------------------------------------------
   The hero heading is the largest contentful paint, so it is never made
   transparent. Each line sits in a clipped wrapper and slides in from
   below, which animates it without ever hiding it from a paint timing
   measurement.
   -------------------------------------------------------------------- */

.split {
	display: block;
}

.line {
	display: block;
	overflow: hidden;
	/* Descenders would be clipped by an overflow box sized to the line box. */
	padding-block-end: 0.12em;
	margin-block-end: -0.12em;
}

.line__inner {
	display: block;
	will-change: transform;
}

/* --- Hero illustration ------------------------------------------------ */

/*
 * The road markings only move once the load sequence has finished drawing
 * the car, and only while motion is welcome.
 */
@media (prefers-reduced-motion: no-preference) {
	.illo--hero .is-live .illo__dash,
	.illo--hero.is-live .illo__dash {
		animation: cacl-road 1.1s linear infinite;
	}

	.illo--hero.is-live .illo__glow {
		animation: cacl-lamp 2.8s var(--ease-in-out) infinite alternate;
	}
}

@keyframes cacl-road {
	to {
		/* One full dash plus gap, so the loop is seamless. */
		stroke-dashoffset: -64;
	}
}

@keyframes cacl-lamp {
	from {
		opacity: 0.55;
	}

	to {
		opacity: 1;
	}
}

/* --- City markers ----------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
	.illo--australia .illo__pin {
		animation: cacl-pulse 3.2s var(--ease-in-out) infinite;
	}

	.illo--australia .illo__pin:nth-child(2) { animation-delay: 0.4s; }
	.illo--australia .illo__pin:nth-child(3) { animation-delay: 0.8s; }
	.illo--australia .illo__pin:nth-child(4) { animation-delay: 1.2s; }
	.illo--australia .illo__pin:nth-child(5) { animation-delay: 1.6s; }
}

@keyframes cacl-pulse {
	0%,
	100% {
		opacity: 0.55;
	}

	50% {
		opacity: 1;
	}
}

/* --- Page transitions -------------------------------------------------
   Navigating between pages animates instead of cutting.

   `@view-transition` opts both documents into a cross-document transition:
   the browser holds a snapshot of the page being left, paints the new one
   underneath, and animates between them. That is what removes the flash on
   navigation — the old page stays on screen until the new one is ready,
   rather than the viewport blanking while the next document parses and this
   stylesheet's reveal states are applied.

   Chrome and Edge 126, Safari 18.2. Firefox has not shipped it, and there
   nothing changes: the navigation is exactly what it was.

   Both pages have to opt in, which they do, because every page on this site
   loads this one stylesheet.
   -------------------------------------------------------------------- */

@view-transition {
	navigation: auto;
}

/*
 * The chrome is named, so it is snapshotted separately and carried across
 * rather than cross-faded with the page. Without this the header and footer
 * would fade out and back in on every navigation, which is the most visible
 * part of the flicker — they are identical on both pages and should simply
 * stay put.
 */
.site-header {
	view-transition-name: cacl-header;
}

.site-footer {
	view-transition-name: cacl-footer;
}

@media (prefers-reduced-motion: no-preference) {
	::view-transition-group(root) {
		animation-duration: 380ms;
	}

	::view-transition-old(root) {
		animation: cacl-page-out 220ms var(--ease-out) both;
	}

	::view-transition-new(root) {
		animation: cacl-page-in 380ms var(--ease-out) both;
	}

	/* The chrome eases into position rather than cutting. */
	::view-transition-group(cacl-header),
	::view-transition-group(cacl-footer) {
		animation-duration: 300ms;
		animation-timing-function: var(--ease-out);
	}
}

@keyframes cacl-page-out {
	to {
		opacity: 0;
		transform: translateY(-8px);
	}
}

@keyframes cacl-page-in {
	from {
		opacity: 0;
		transform: translateY(12px);
	}
}

/*
 * A page transition is motion across the whole viewport, which is exactly
 * the kind this setting is asking to avoid. The navigation still happens,
 * just without the animation.
 */
@media (prefers-reduced-motion: reduce) {
	::view-transition-group(*),
	::view-transition-old(*),
	::view-transition-new(*) {
		animation: none !important;
	}
}


/* --- Reduced motion ---------------------------------------------------
   Nothing above hides content in this mode, so this only has to stop the
   things that move on their own.
   -------------------------------------------------------------------- */

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

	/* A paused marquee has to wrap, or the second copy sits off-screen. */
	.marquee__track {
		animation: none;
		flex-wrap: wrap;
		width: auto;
		justify-content: center;
	}

	.marquee__viewport {
		mask: none;
		-webkit-mask: none;
	}

	/* The duplicate exists only to make the loop seamless. */
	.marquee__item[aria-hidden="true"] {
		display: none;
	}

	.marquee__item:last-child::after {
		display: none;
	}

	/* The rail has nothing to track, so show it complete. */
	.process__progress {
		transform: scaleY(1);
	}

	.process__marker {
		display: none;
	}

	.step {
		opacity: 1;
	}
}


/* 09  Editor corrections ==================================================
   This file is the editor stylesheet too. A few effects have to be calmed
   down so the canvas stays usable.
   ------------------------------------------------------------------------ */

.editor-styles-wrapper .site-header {
	position: static;
}

.editor-styles-wrapper .call-bar {
	position: static;
	display: flex;
	transform: none;
}

/* Animations would loop distractingly while editing. */
.editor-styles-wrapper .marquee__track {
	animation: none;
}


/* 10  Print ===============================================================
   ------------------------------------------------------------------------ */

@media print {
	.site-header,
	.call-bar,
	.illo,
	.marquee,
	.wp-block-navigation {
		display: none !important;
	}

	body {
		background: #fff;
		color: #000;
		font-size: 11pt;
	}

	.section {
		padding-block: 1rem;
	}

	/* An unopened answer is invisible on paper; site.js opens them all first. */
	.wp-block-details {
		break-inside: avoid;
	}

	a[href^="http"]::after {
		content: " (" attr(href) ")";
		font-size: 9pt;
		word-break: break-all;
	}
}
