/* =========================================================
   FINSOLVE SOLUTIONS
   Implements docs/finsolve-design-system.md v1.1, with the
   section rhythm, imagery and interaction language of the
   client-approved finsolveplanning.co.za.
   1. Reset
   2. Design tokens
   3. Base & typography
   4. Layout primitives
   5. Components
   6. Sections
   7. Motion
   8. Utilities
   9. Responsive
   ========================================================= */


/* =========================================================
   1. Reset
   ========================================================= */

*,
*::before,
*::after {
	box-sizing: border-box;
}

html {
	-webkit-text-size-adjust: 100%;
	scroll-behavior: smooth;
}

body,
h1, h2, h3, h4,
p, figure, blockquote, dl, dd, ol, ul {
	margin: 0;
}

ul, ol {
	padding: 0;
	list-style: none;
}

img, picture, svg, video {
	display: block;
	max-width: 100%;
}

img {
	height: auto;
}

button, input, select, textarea {
	font: inherit;
	color: inherit;
}

button {
	background: none;
	border: none;
	cursor: pointer;
}

summary::-webkit-details-marker {
	display: none;
}


/* =========================================================
   2. Design tokens
   ========================================================= */

:root {
	/* --- Colour: blue ramp (hue-preserved from logo #006FB9) --- */
	--blue-900: #002E4A;
	--blue-800: #003A5D;
	--blue-700: #00446F;
	--blue-600: #006FB9;
	/* Lifted for use on the near-black surface, where --blue-600 measures
	   only 3.73:1. This reaches 7.14:1. */
	--blue-500: #3FA3E8;
	--blue-50:  #EEF4FA;

	/* --- Colour: action. Blue is the single accent; the only red left on
	   the page is inside the logo artwork itself. --- */
	--action:       var(--blue-600);  /* 5.28:1 with white, 3.73:1 on ink */
	--action-hover: var(--blue-700);  /* 10.21:1 with white */

	/* --- Colour: ink. --ink-950 is the approved site's dark surface. --- */
	--ink-950: #0B0B0B;
	--ink-900: #14181C;
	--ink-600: #4A5560;
	--ink-on-dark: #FFFFFF;
	--ink-muted-on-dark: #B4CBDB;
	--ink-muted-on-ink: #B9BEC4;

	/* --- Colour: surface & line --- */
	--surface: #FFFFFF;
	--line: #DDE3E8;
	--line-strong: #7C8791;
	--line-on-dark: rgba(255, 255, 255, 0.18);

	/* --- Colour: state --- */
	--focus-ring: var(--blue-600);
	--focus-ring-on-dark: #FFFFFF;
	/*
	 * The one non-blue in the implemented palette, and a state rather than an
	 * accent. "One accent colour — blue" governs brand and decoration; an
	 * error is functional, and blue is already spoken for by focus and
	 * actions, so an error rendered in blue would not read as one.
	 *
	 * This is §2's own --red-700. 6.27:1 on white, so it passes 4.5:1 as text
	 * and 3:1 as a border (1.4.11). Validation only — never decorative.
	 */
	--error: #B33016;

	/* --- Scrims. Guarantee legibility over photography. --- */
	--scrim-hero: linear-gradient(
		100deg,
		rgba(11, 11, 11, 0.9) 0%,
		rgba(11, 11, 11, 0.78) 40%,
		rgba(11, 11, 11, 0.42) 72%,
		rgba(11, 11, 11, 0.28) 100%);
	--scrim-card: linear-gradient(
		to top,
		rgba(11, 11, 11, 0.96) 0%,
		rgba(11, 11, 11, 0.86) 42%,
		rgba(11, 11, 11, 0.52) 74%,
		rgba(11, 11, 11, 0.34) 100%);

	/* --- Typography: family --- */
	--font-display: "Source Serif 4", Georgia, "Times New Roman", serif;
	--font-body: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;

	/* --- Typography: size --- */
	--fs-xs:   0.8125rem;
	--fs-sm:   0.9375rem;
	--fs-base: 1.0625rem;
	--fs-md:   1.25rem;
	--fs-lg:   1.5rem;
	--fs-xl:   clamp(1.75rem, 1.4rem + 1.4vw, 2.375rem);
	--fs-2xl:  clamp(2.25rem, 1.6rem + 2.6vw, 3.25rem);
	--fs-3xl:  clamp(2.5rem, 1.4rem + 4.4vw, 5.5rem);    /* hero */
	--fs-display: clamp(2.75rem, 0.9rem + 8.4vw, 8rem);  /* section titles */

	/* --- Typography: metrics --- */
	--lh-tight: 1.1;
	--lh-snug:  1.22;
	--lh-base:  1.6;
	--lh-loose: 1.7;
	--tracking-tight: -0.02em;
	--tracking-display: -0.035em;
	--tracking-wide:  0.1em;
	--measure: 68ch;

	/* --- Spacing --- */
	--space-1: 0.25rem;
	--space-2: 0.5rem;
	--space-3: 0.75rem;
	--space-4: 1rem;
	--space-5: 1.5rem;
	--space-6: 2rem;
	--space-7: 3rem;
	--space-8: 4rem;
	--space-9: 6rem;
	--space-10: 8rem;

	/* --- Section rhythm ---
	 * --section-join is the third value in this category and is for adjacent
	 * sections that SHARE a surface. The two gaps above describe the distance
	 * between rooms, and they are padding on each section, so two neighbours
	 * both pay one and the reader sees the sum. Where the surface changes that
	 * sum is doing real work. Where it does not — three white sections in a
	 * row on the contact page — it is just a hole: measured 172px between the
	 * form and the map at 900px and 192px at 1440px.
	 *
	 * Roughly two thirds of --section-gap at every width (40/56 at 390px,
	 * 51/86 at 900px, 64/96 at 1440px), so the page still reads in the same
	 * rhythm rather than switching to a second one.
	 */
	--section-gap:    clamp(3.5rem, 2rem + 6vw, 6rem);
	--section-gap-lg: clamp(4.5rem, 2.5rem + 8vw, 8rem);
	--section-join:   clamp(2.5rem, 1.5rem + 3vw, 4rem);

	/* --- Container --- */
	--container-wide: 75rem;
	--container-text: 44rem;
	--container-pad: 1.5rem;

	/* --- Border & radius --- */
	--border-width: 1px;
	--border-width-strong: 2px;
	--radius-sm: 0.25rem;
	--radius-md: 0.5rem;

	/* --- Motion --- */
	/*
	 * Slowed on request 2026-09-02: the entrances were reading as too quick.
	 * --duration-fast is deliberately not slowed — it is hover, focus and
	 * border feedback, and a control that takes longer than about 150ms to
	 * acknowledge a pointer feels broken rather than considered. §19 sets these
	 * three; the two that describe movement have moved and the one that
	 * describes response has not.
	 */
	--duration-fast: 150ms;
	--duration-base: 320ms;
	--duration-slow: 560ms;
	--ease-out: cubic-bezier(0.2, 0, 0, 1);

	/* --- Z-index --- */
	/* Below the content of the same stacking context: a card's scrim sits
	   at --z-behind and its photograph at --z-behind-deep, under it. */
	--z-behind-deep: -2;
	--z-behind: -1;
	--z-base: 1;
	--z-low: 10;
	--z-mid: 100;
	--z-high: 1000;
	--z-overlay: 10000;
	--z-overlay-close: 100000;

	/* --- Shadow: the single functional shadow --- */
	--shadow-header: 0 1px 0 rgba(20, 24, 28, 0.08), 0 6px 24px -12px rgba(0, 46, 74, 0.28);
}


/* =========================================================
   3. Base & typography
   ========================================================= */

body {
	font-family: var(--font-body);
	font-size: var(--fs-base);
	line-height: var(--lh-base);
	color: var(--ink-900);
	background-color: var(--surface);
	-webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 {
	font-family: var(--font-body);
	font-weight: 600;
	line-height: var(--lh-snug);
	letter-spacing: var(--tracking-tight);
}

/* Secondary clause inside a heading. Its own line, but never its own
   colour — headings are a single colour throughout. */
.h-accent {
	display: block;
	color: inherit;
}

p {
	text-wrap: pretty;
}

a {
	color: var(--blue-600);
}

::selection {
	background: var(--blue-800);
	color: var(--ink-on-dark);
}


/* =========================================================
   4. Layout primitives
   ========================================================= */

.container {
	width: 100%;
	max-width: var(--container-wide);
	margin-inline: auto;
	padding-inline: var(--container-pad);
}

/* Spans the page rather than the content column. */
.container--full {
	max-width: none;
	padding-inline: clamp(1.5rem, 4vw, 4.5rem);
}

.section {
	padding-block: var(--section-gap);
}

/*
 * Keep in-page anchor targets clear of the sticky header.
 *
 * `li` joined the list on 2026-09-14, when the four services without pages
 * became menu items pointing at their card on /overview/. Without it the
 * header lands on top of the card the visitor was sent to, which reads as the
 * link having gone to the wrong place.
 */
:where(section, h2, li)[id] {
	scroll-margin-top: 6.5rem;
}

.surface--wash {
	background-color: var(--blue-50);
}

.surface--ink {
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
	padding-block: var(--section-gap-lg);
}

.surface--ink h2,
.surface--ink h3,
.surface--ink summary {
	color: var(--ink-on-dark);
}

/* Oversized lowercase section title, the approved site's signature device —
   sized to fit rather than bleeding off the viewport. */
.display-title {
	font-size: var(--fs-display);
	line-height: 0.95;
	letter-spacing: var(--tracking-display);
	font-weight: 600;
	color: var(--blue-600);
	margin-bottom: clamp(2rem, 1rem + 3vw, 3.5rem);
}

.display-title--ink {
	color: var(--ink-900);
}

.display-title--on-ink {
	color: var(--ink-on-dark);
}

.display-title--center {
	text-align: center;
}

/*
 * Scroll reveal for a display title: the words rise into place from behind a
 * mask.
 *
 * This animates `transform` only, which runs on the compositor. The previous
 * version transitioned `clip-path`, which paints on the main thread — so the
 * reveal stalled behind the decode of the six card images in the same section
 * and appeared to "wait for the images".
 */
.title-wipe {
	display: block;
	overflow: hidden;
	/* Room for descenders, which the mask would otherwise clip. */
	padding-bottom: 0.12em;
}

.title-wipe__inner {
	display: block;
}

/*
 * Driven by the IntersectionObserver rather than a scroll timeline.
 *
 * `animation-timeline: view()` was tried twice here and failed both times:
 * the mask above makes .title-wipe a scroll container, so view() resolved
 * against that box instead of the page. Naming the timeline on the outer
 * element fixed the pinning but the animation still would not advance.
 * The observer is verifiable and drives the same compositor-only transform,
 * which is what fixed the original stall.
 */
.js .title-wipe__inner {
	transform: translateY(105%);
	transition: transform var(--duration-slow) var(--ease-out);
}

.js .title-wipe.is-visible .title-wipe__inner {
	transform: none;
}


/* =========================================================
   5. Components
   ========================================================= */

/* --- Skip link --- */
.skip-link {
	position: absolute;
	top: -100%;
	left: var(--space-4);
	z-index: var(--z-overlay-close);
	padding: var(--space-3) var(--space-5);
	background-color: var(--blue-800);
	color: var(--ink-on-dark);
	border-radius: var(--radius-sm);
	text-decoration: none;
	font-weight: 600;
}

.skip-link:focus {
	top: var(--space-4);
}

/* --- Eyebrow --- */
.eyebrow {
	font-size: var(--fs-xs);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--tracking-wide);
	color: var(--ink-600);
	margin-bottom: var(--space-4);
}

.eyebrow--on-dark {
	color: var(--ink-muted-on-dark);
}

/* --- Buttons --- */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/*
	 * An inline-flex box sizes to its label, so a long one simply grew past
	 * the viewport rather than wrapping: "Download your Free Business Travel
	 * Log Book" measured 426px inside a 390px screen and put 60px of
	 * horizontal scroll on the page. Every label on the site was short enough
	 * to hide this until that one arrived.
	 */
	max-width: 100%;
	min-height: 2.75rem;          /* 44px Finsolve usability standard */
	padding: var(--space-3) var(--space-6);
	border: var(--border-width) solid transparent;
	border-radius: var(--radius-sm);
	font-size: var(--fs-base);
	font-weight: 600;
	line-height: 1.2;
	text-decoration: none;
	text-align: center;
	/*
	 * THE LABEL WRAPS. It used to be nowrap, and nowrap next to the
	 * max-width above is a contradiction: the cap stopped the BOX at the
	 * container edge while nowrap refused to let the TEXT reflow, so the
	 * words carried on past the box — and because this is a centring flex
	 * box, out of both sides at once. Three labels did it on every phone
	 * width measured (360/390/414):
	 *
	 *   "Subscribe to updates"                       footer, every page
	 *   "Download our Services Brochure Here"        /about/
	 *   "Download your Free Business Travel Log Book"  /taxation-services/
	 *
	 * The cap was added to stop that last one putting 60px of horizontal
	 * scroll on the page, which it did — it converted a page-scroll bug into
	 * a text-overflow bug rather than fixing it. Wrapping is what makes the
	 * cap mean something.
	 *
	 * Short labels are unaffected: an inline-flex box still sizes to
	 * max-content when there is room, so nothing that fits on one line
	 * starts wrapping. balance splits the ones that do not fit evenly
	 * instead of stranding a single word on line two, and is ignored by
	 * browsers that do not support it.
	 */
	white-space: normal;
	overflow-wrap: break-word;
	text-wrap: balance;
	transition:
		background-color var(--duration-fast) var(--ease-out),
		border-color var(--duration-fast) var(--ease-out),
		color var(--duration-fast) var(--ease-out);
}

.btn--primary {
	background-color: var(--action);
	color: var(--ink-on-dark);
}

.btn--primary:hover,
.btn--primary:focus-visible {
	background-color: var(--action-hover);
	color: var(--ink-on-dark);
}

/*
 * No extra edge needed on dark surfaces: the --action fill measures 3.73:1
 * against the near-black ground, clearing the 3:1 WCAG 1.4.11 minimum for
 * identifying a UI component.
 */

.btn--ghost {
	background-color: transparent;
	border-color: var(--line-strong);
	color: var(--ink-900);
}

.btn--ghost:hover,
.btn--ghost:focus-visible {
	border-color: var(--ink-900);
	background-color: rgba(20, 24, 28, 0.04);
}

.btn--ghost.btn--on-dark {
	border-color: rgba(255, 255, 255, 0.55);
	color: var(--ink-on-dark);
}

.btn--ghost.btn--on-dark:hover,
.btn--ghost.btn--on-dark:focus-visible {
	border-color: var(--ink-on-dark);
	background-color: rgba(255, 255, 255, 0.12);
}

.btn--sm {
	min-height: 2.5rem;
	padding: var(--space-2) var(--space-5);
	font-size: var(--fs-sm);
}


.btn--block {
	display: flex;
	width: 100%;
}

/* --- Form field ---
 * Built to §12 rather than styled inline in the newsletter, because this is
 * the site's first form and the contact form will reuse it as-is.
 */
.field {
	margin-bottom: var(--space-5);
}

/* Always visible and above the field. §12: placeholders are never labels —
   they vanish on input and fail contrast. */
.field__label {
	display: block;
	margin-bottom: var(--space-2);
	font-size: var(--fs-sm);
	font-weight: 600;
	color: var(--ink-900);
}

.field__input {
	display: block;
	width: 100%;
	padding: var(--space-3) var(--space-4);
	/* --line-strong, not --line: a control border is a UI component and owes
	   3:1 under 1.4.11. --line measures 1.29:1 and would fail. */
	border: var(--border-width) solid var(--line-strong);
	border-radius: var(--radius-sm);
	background-color: var(--surface);
	color: var(--ink-900);
	/* 17px. Anything under 16px makes iOS Safari zoom the page on focus. */
	font-size: var(--fs-base);
	transition: border-color var(--duration-fast) var(--ease-out);
}

/* Border unchanged on focus — the ring is the focus cue, and moving both at
   once reads as an error. */
.field__input:focus-visible {
	outline: var(--border-width-strong) solid var(--focus-ring);
	outline-offset: 2px;
}

.field__input[aria-invalid="true"] {
	border-color: var(--error);
}

/* Icon and text together. Colour alone would fail 1.4.1. */
.field__error {
	display: flex;
	align-items: flex-start;
	gap: var(--space-2);
	margin-top: var(--space-2);
	font-size: var(--fs-sm);
	color: var(--error);
}

.field__error[hidden] {
	display: none;
}

.field__error-icon {
	flex: none;
	width: 1.125rem;
	height: 1.125rem;
	margin-top: 0.15em;
	stroke: currentColor;
	stroke-width: 2;
	stroke-linecap: round;
	fill: none;
}

/* Same border, padding and type as the input — §12 treats text input, textarea
   and select as one control with three shapes. */
.field__textarea {
	resize: vertical;
	min-height: 9rem;
	line-height: var(--lh-base);
}

.field__select {
	/* Undo the platform control so the border and radius above actually show,
	   then draw the chevron rather than lose the affordance with it. */
	appearance: none;
	padding-right: var(--space-8);
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234A5560' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right var(--space-4) center;
	background-size: 1.125rem;
}

/* --- Consent checkbox ---
 * The box keeps its native rendering: it is the one control where the platform
 * already draws a state that assistive tech, high-contrast modes and every
 * browser agree on, and replacing it buys nothing.
 */
.field--check {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: var(--space-3);
	align-items: start;
}

.field__check {
	width: 1.25rem;
	height: 1.25rem;
	margin: 0;
	accent-color: var(--action);
	/* The label is the rest of the 44px target — see the label rule. */
	align-self: center;
}

.field__check-label {
	font-size: var(--fs-sm);
	line-height: var(--lh-base);
	color: var(--ink-600);
	/* Clicking the text toggles the box, so the target is the whole line
	   rather than a 20px square. */
	cursor: pointer;
	min-height: 2.75rem;
	display: flex;
	align-items: center;
}

.field--check .field__error {
	grid-column: 1 / -1;
	margin-top: 0;
}

/* --- Newsletter modal ---
 * A native <dialog>. The user-agent styles are heavy — border, padding, a
 * max-width and centring — so most of this is undoing them. Closed state is
 * left to the UA's own `dialog:not([open]) { display: none }`, which outranks
 * a single class, so nothing here needs to hide it.
 */
.newsletter {
	width: min(34rem, 92vw);
	max-width: none;
	max-height: 90vh;
	padding: 0;
	border: 0;
	border-radius: var(--radius-md);
	background-color: var(--surface);
	color: var(--ink-900);
	overflow: hidden;
}

.newsletter[open] {
	display: grid;
	grid-template-columns: 1fr;
}

.newsletter::backdrop {
	background-color: rgba(11, 11, 11, 0.6);
}

/* The photograph is desktop only. Stacked above the form on a narrow screen it
   is one more thing between the reader and the field — the same call already
   made for the What We Do and FAQ images. */
.newsletter__media {
	display: none;
}

.newsletter__body {
	position: relative;
	padding: var(--space-7) var(--space-6) var(--space-6);
	overflow-y: auto;
}

.newsletter__close {
	position: absolute;
	top: var(--space-3);
	right: var(--space-3);
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;      /* 44px, the Finsolve standard */
	height: 2.75rem;
	border-radius: 50%;
	transition: background-color var(--duration-fast) var(--ease-out);
}

.newsletter__close:hover {
	background-color: rgba(20, 24, 28, 0.06);
}

.newsletter__close svg {
	width: 1.25rem;
	height: 1.25rem;
	stroke: var(--ink-900);
	stroke-width: 2;
	stroke-linecap: round;
	fill: none;
}

.newsletter__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
	max-width: 18ch;
	margin-bottom: var(--space-4);
	text-wrap: balance;
	/* Clears the close control so a long heading cannot run under it. */
	padding-right: var(--space-7);
}

.newsletter__lead {
	margin-bottom: var(--space-6);
	color: var(--ink-600);
	max-width: 46ch;
}

.newsletter__consent {
	margin-top: var(--space-4);
	font-size: var(--fs-xs);
	line-height: var(--lh-base);
	color: var(--ink-600);
	max-width: 52ch;
}

/* Entrance: pattern 1 from §19, opacity plus a short rise. ::backdrop is not
   matched by the universal selector in the reduced-motion block, so both this
   and the backdrop are switched off explicitly there. */
@keyframes newsletter-in {
	from {
		opacity: 0;
		transform: translateY(12px);
	}
}

@keyframes newsletter-backdrop-in {
	from {
		opacity: 0;
	}
}

.newsletter[open] {
	animation: newsletter-in var(--duration-slow) var(--ease-out);
}

.newsletter[open]::backdrop {
	animation: newsletter-backdrop-in var(--duration-slow) var(--ease-out);
}

/* ---------- CONTACT PAGE ---------- */

/*
 * Tighter than a section gap top and bottom. The approved page gives its title
 * a full screen of empty grey before anything usable appears; principle 1 says
 * nothing delays access to what the page is for, and on a contact page that is
 * the phone number and the form.
 */
.contact-intro {
	padding-block: var(--section-gap) 0;
}

/*
 * 20ch forced this onto two short lines and left the band looking weighted to
 * the left with a column of white beside it. At 40ch the line runs its natural
 * length and only wraps when the viewport makes it, which is the difference
 * between a measure and an arbitrary break.
 *
 * text-wrap: balance is gone with it — balancing a line that no longer needs to
 * wrap just reintroduces the break it was there to tidy.
 */
.contact-intro__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
	max-width: 40ch;
}

.contact-intro__lead {
	margin-top: var(--space-4);
	max-width: 52ch;
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

/*
 * Both joins on this page are same-surface, so neither pays the doubled gap.
 *
 *  - Top: the intro and this band are one thought — the lead says "call, email
 *    or send the form" and this is where those three are. A full --section-gap
 *    between a sentence and the thing it points at reads as a missing section.
 *  - Bottom: zero, so the single gap before the map is --section-gap from
 *    .contact-map's own padding rather than that plus this one.
 *
 * .contact-map keeps .section's padding untouched. Its bottom gap is against
 * the footer, which IS a surface change, so there the full value is right.
 */
.contact {
	padding-block: var(--section-join) 0;
}

.contact__inner {
	display: grid;
	gap: clamp(2.5rem, 1.5rem + 4vw, 4rem);
}

.contact__subhead {
	font-size: var(--fs-lg);
	margin-bottom: var(--space-5);
}

/*
 * No labels above each item, so space does the separating instead. Each block
 * gets its own gap rather than one rhythm throughout: the email sits closer to
 * the address than the number does, because the number is the thing being
 * given room.
 */
.contact__email {
	margin-top: var(--space-5);
}

.contact__phone {
	margin-top: var(--space-3);
}

/* The fastest route to a person, and the only thing in this column set larger
   than the rest. */
.contact__strong {
	font-size: var(--fs-md);
	font-weight: 600;
}

.contact__social {
	margin-top: var(--space-6);
}

/* address defaults to italic, and there is no italic anywhere on this site. */
.contact__address,
.map__address {
	font-style: normal;
	white-space: pre-line;
	line-height: var(--lh-base);
}

.contact__row {
	display: grid;
	gap: 0 var(--space-5);
}

/* Sits under the submit button and carries whatever the form has to say back —
   role="status", so it is announced without stealing focus. */
.contact__notice {
	margin-top: var(--space-5);
	padding: var(--space-4);
	border-left: var(--border-width-strong) solid var(--error);
	background-color: var(--blue-50);
	font-size: var(--fs-sm);
	color: var(--ink-900);
}

.contact__notice[hidden] {
	display: none;
}

/* --- Map ---
 * A facade until asked. The embed is built by main.js; everything below is
 * what stands in for it, and what remains if the script never runs.
 */
.map {
	border: var(--border-width) solid var(--line);
	border-radius: var(--radius-md);
	background-color: var(--blue-50);
	overflow: hidden;
}

/* Only ever seen if the script does not run — the iframe replaces it otherwise.
   Kept at a real height so the fallback is a panel with an address in it, not
   a stray line of text. */
.map__facade {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	justify-content: center;
	min-height: 16rem;
	padding: var(--space-6);
}

/* Outside .map, so the iframe swap cannot take it with it. */
.map__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3);
	margin-top: var(--space-5);
}

/* Roughly a quarter of the approved page's map, which ran past a full screen. */
.map__frame {
	display: block;
	width: 100%;
	height: clamp(18rem, 40vh, 24rem);
	border: 0;
}


/* ---------- SERVICE PAGES ---------- */

/*
 * Every page rendered by page-service.php: a short white masthead, one
 * unbroken near-black field carrying the display title, the cards and the
 * closing statement, then a centred call to action back on white.
 *
 * The cards themselves have no rules here. They are the homepage's `.svc`
 * classes, reused rather than reimplemented, so the two pages cannot drift.
 * The only thing this page changes about them is how many go across.
 */

.service-intro__inner {
	display: grid;
	gap: var(--space-6);
}

.service-intro__title {
	font-size: var(--fs-2xl);
	line-height: var(--lh-tight);
	letter-spacing: var(--tracking-tight);
}

/*
 * Both opening paragraphs, set identically.
 *
 * The second one was a step down the scale for a pass - --fs-base and
 * --ink-900 against the lead's --fs-md and --ink-600 - on the argument that
 * it was supporting detail rather than a second proposition. Seen on the
 * page they just read as two paragraphs that did not match. They are one
 * opening in two parts, so they are set as one thing.
 *
 * --measure, not the 46ch they were pinned at. Below 64rem the photograph
 * beside them is hidden and a 46ch column left most of the band empty; 68ch
 * is the widest the type scale sanctions for prose, so this is as far as
 * they stretch without running past a comfortable line. Above 64rem the 5fr
 * column is narrower than either value and does the constraining itself.
 */
.service-intro__para {
	max-width: var(--measure);
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--ink-600);
	margin-top: var(--space-5);
}

/*
 * The photograph beside the heading. 3:2 is the editorial ratio in section
 * 15; at 7fr of the container that is about 660x440, which is 49vh on a
 * 900px viewport and so inside the 60vh ceiling on decorative imagery.
 */
.service-intro__media {
	margin: 0;
}

.service-intro__media img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 3 / 2;
	object-fit: cover;
	border-radius: var(--radius-md);
}


/*
 * .services carries no bottom padding, because the index's cards are meant
 * to run to the edge of the band. A service page ends in copy and a button,
 * so it needs the room back or the last thing on it sits flush against the
 * boundary.
 *
 * Two classes deep, and that is the whole point. .services is a single class
 * setting the padding-block shorthand, it is 600 lines further down this
 * file, and at equal specificity the later rule wins — so the single-class
 * version of this rule computed to 0 and did nothing at all. Same fix as
 * .svc-grid.svc-grid--index, for the same reason.
 *
 * --section-gap, not --section-gap-lg. The band opens on 60px of padding
 * under a display title that supplies its own mass; 128px underneath against
 * 60px above reads as a band that has come loose at the bottom.
 */
.services.services--detail {
	padding-bottom: var(--section-gap);
}

/* --- What a service covers ---
 *
 * The single-service counterpart to the card grid, and it sits in the same
 * near-black field under the same display title, so a visitor moving from the
 * index to a service stays in the same room.
 *
 * No cards, no panels, no boxes: section 5 rules out decorative elevation and
 * these are two headings with copy under them. Space and a rule do the
 * separating, which is what section 8 gives rules to do.
 */
.covers {
	display: grid;
	gap: var(--space-7);
	padding-block: var(--space-7) 0;
	border-top: var(--border-width) solid var(--line-on-dark);
}

.cover__title {
	font-size: var(--fs-lg);
	color: var(--ink-on-dark);
}

.cover__intro {
	margin-top: var(--space-4);
	color: var(--ink-muted-on-ink);
}

.cover__body {
	margin-top: var(--space-4);
	max-width: var(--measure);
	line-height: var(--lh-loose);
	color: rgba(255, 255, 255, 0.92);
}

/*
 * One block or two.
 *
 * Taxation names two areas, so .covers is a pair of columns. Accounting and
 * Financial Health were supplied as a single unheaded list, so there is one
 * block and it takes the whole width rather than sitting in the left column
 * with an empty one beside it.
 */
.cover:only-child {
	grid-column: 1 / -1;
}

.cover__items {
	margin-top: var(--space-5);
	display: grid;
	gap: var(--space-3);
}

/*
 * Long lists split, short ones do not, and the count decides rather than the
 * width — the same test the Overview's schedule used before the cards
 * replaced it. Accounting has ten deliverables and a single column of ten
 * would run past the photograph above it; Financial Health has two and
 * splitting those would strand one.
 *
 * Multi-column rather than a grid, because it fills down and then across:
 * a grid would run them left to right and reading ten in order would mean
 * zig-zagging. Where :has() is unsupported the list stays one column, which
 * is the correct fallback rather than a broken one.
 */
.cover:only-child .cover__items:has(.cover__item:nth-child(5)) {
	display: block;
	columns: 20rem 2;
	column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
}

/*
 * margin-bottom, not the grid gap, once this is multi-column: the first item
 * of the second column is still a sibling, so a top margin would push that
 * column down and the two would start on different lines.
 */
.cover:only-child .cover__items:has(.cover__item:nth-child(5)) .cover__item {
	margin-bottom: var(--space-3);
	break-inside: avoid;
}

.cover__item {
	position: relative;
	padding-left: var(--space-5);
	line-height: var(--lh-base);
	color: rgba(255, 255, 255, 0.92);
}

/*
 * The lifted blue, not --action. --blue-600 measures 3.73:1 on the near-black
 * ground, which is enough to identify a control under 1.4.11 but thin for a
 * 6px dot; --blue-500 is the value the palette keeps for exactly this surface.
 */
.cover__item::before {
	content: "";
	position: absolute;
	top: 0.6em;
	left: 0;
	width: 0.375rem;
	height: 0.375rem;
	border-radius: 50%;
	background-color: var(--blue-500);
}

/*
 * Centred under the pair, not left under the first of them. The two covered
 * areas are columns of equal standing and this action belongs to both, so
 * hanging it off the left one implied it was part of Tax Compliance.
 */
.covers__action {
	margin-top: var(--space-8);
	text-align: center;
}


/* --- The service grid ---
 *
 * The cards are the homepage's, class for class, so the only rules here are
 * the ones that make six of them a page rather than four of them a band.
 *
 * The section keeps its top padding but loses the display title's usual gap
 * to the grid: the title is the field's masthead and the photographs start
 * directly under it.
 */
.services--index .display-title {
	margin-bottom: clamp(1.5rem, 0.75rem + 2vw, 2.5rem);
}

/* Sits in the footer's Get in Touch column and is the only way in on a phone,
   where the modal never opens by itself. */
.site-footer__subscribe {
	margin-top: var(--space-5);
}

.site-footer__subscribe[hidden] {
	display: none;
}

/* --- Focus (modality-aware; keyboard focus always visible) --- */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
	outline: var(--border-width-strong) solid var(--focus-ring);
	outline-offset: 2px;
	border-radius: var(--radius-sm);
}

.surface--ink :where(a, button, summary):focus-visible,
.hero :where(a, button):focus-visible,
.services :where(a, button):focus-visible,
.site-footer :where(a, button):focus-visible,
.site-nav :where(a, button):focus-visible {
	outline-color: var(--focus-ring-on-dark);
}

/* --- Professional body marks --- */
.credentials__label {
	text-align: center;
}

.credentials__marks {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
	gap: var(--space-5) var(--space-7);
}

/*
 * Height comes from --mark-h, computed per mark in the template so every logo
 * carries the same optical weight rather than the same pixel height. §16 names
 * fixed height as the usual failure in a row like this; max-width is only a
 * safety net for a mark with an extreme ratio.
 */
/*
 * THE TARGET, NOT THE MARK. Each mark is optically sized and several come out
 * around 20px tall, which would be a 20px hit area. §21 puts the floor at 44
 * and calls it non-negotiable, so the link carries the target and the mark sits
 * centred inside it.
 *
 * The row's height does not move: it is set by the tallest mark, which is about
 * 51px, so growing the short ones to 44 changes nothing. Measured before and
 * after — the homepage geometry diff is identical.
 */
.credentials__link {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 2.75rem;
	min-width: 2.75rem;
	border-radius: var(--radius-sm);
	transition: opacity var(--duration-fast) var(--ease-out);
}

/*
 * §20: every interactive element defines all four states and every hover
 * changes something other than colour. Here the mark comes out of greyscale and
 * up to full opacity, which is a change of rendering rather than of hue.
 */
.credentials__link:hover img,
.credentials__link:focus-visible img {
	filter: grayscale(0);
	opacity: 1;
}

.credentials__marks img {
	height: var(--mark-h, 2.5rem);
	width: auto;
	max-width: 11rem;
	object-fit: contain;
	filter: grayscale(1);
	opacity: 0.85;
	transition: filter var(--duration-fast) var(--ease-out),
	            opacity var(--duration-fast) var(--ease-out);
	/* The supplied marks carry baked-in white backgrounds; multiply drops
	   them into the tinted surface instead of showing white boxes. */
	mix-blend-mode: multiply;
}

/* --- Site header --- */
.site-header {
	position: sticky;
	top: 0;
	z-index: var(--z-high);
	background-color: var(--surface);
	border-bottom: var(--border-width) solid var(--line);
	transition: box-shadow var(--duration-base) var(--ease-out);
}

.site-header.is-stuck {
	box-shadow: var(--shadow-header);
}

/* Full width across the viewport, as on the live site — the bar is not
   held to the content container. */
.site-header__inner {
	display: flex;
	align-items: stretch;
	gap: var(--space-4);
	min-height: 4.5rem;
	padding-left: var(--container-pad);
}

.site-header__brand {
	flex: none;
	display: flex;
	align-items: center;
	line-height: 0;
}

.site-header__logo {
	width: auto;
	height: 2.25rem;
}

.site-header__actions {
	display: flex;
	align-items: stretch;
	gap: var(--space-5);
	margin-left: auto;
}

/* Flush-right action block, echoing the approved site's header treatment.
   The negative margin covers the header's bottom rule so no white line
   crosses the block. */
.site-header__cta {
	display: none;
	align-items: center;
	margin-bottom: calc(var(--border-width) * -1);
	padding-inline: var(--space-7);
	background-color: var(--action);
	color: var(--ink-on-dark);
	font-size: var(--fs-sm);
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	text-decoration: none;
	white-space: nowrap;
	transition: background-color var(--duration-fast) var(--ease-out);
}

.site-header__cta:hover,
.site-header__cta:focus-visible {
	background-color: var(--action-hover);
	color: var(--ink-on-dark);
}

/* --- Nav toggle: three bars closed ---
 *
 * SIZED FOR TOUCH, NOT FOR THE POINTER. This rule only ever applies below
 * 64rem — the 64rem block sets display: none — so everything here is a phone
 * or tablet measurement and desktop cannot be affected by it.
 *
 * It was 44x44 with a 24x16 glyph, which is a desktop-density icon, and it
 * measured identically at 390 and at 768: same target, same bars, in a header
 * bar nearly twice as wide. That is why it read as tiny on a tablet and not on
 * a phone — nothing about it shrank, the field around it doubled, and an
 * icon's presence is a function of the space it sits in.
 *
 * 48x48 is the touch-target size Material and the Apple HIG both land on, and
 * clears the 44x44 Finsolve standard rather than sitting exactly on it. The
 * glyph goes to 28x21 on 3px strokes: the stroke matters more than the width
 * here, since three 2px hairlines are what made it read as faint against a
 * 96x36 logo. Whole pixels, not 2.5, so it stays crisp at 1x.
 *
 * This is the only navigation affordance on screen below 64rem, so it is the
 * one control in the header that should not be quiet.
 */
.nav-toggle {
	margin-left: calc(var(--space-2) * -1);
	display: flex;
	align-items: center;
	justify-content: center;
	width: 3rem;
	height: 3rem;
	flex: none;
	align-self: center;
}

.nav-toggle__bars {
	display: block;
	width: 1.75rem;
}

.nav-toggle__bars span {
	display: block;
	height: 3px;
	border-radius: 1px;
	background-color: var(--ink-900);
}

.nav-toggle__bars span + span {
	margin-top: 6px;
}

/* --- Nav close: an explicit X inside the drawer --- */
.site-nav__head {
	display: flex;
	justify-content: flex-end;
	margin-bottom: var(--space-4);
}

.nav-close {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	margin-right: calc(var(--space-2) * -1);
}

.nav-close svg {
	width: 1.5rem;
	height: 1.5rem;
	stroke: var(--ink-on-dark);
	stroke-width: 2;
	stroke-linecap: round;
	fill: none;
}

/* --- Site nav (drawer on small screens) --- */
.site-nav {
	position: fixed;
	inset: 0 0 0 auto;
	width: min(22rem, 88vw);
	z-index: var(--z-overlay);
	background-color: var(--ink-950);
	padding: var(--space-4) var(--space-6) var(--space-7);
	display: flex;
	flex-direction: column;
	transform: translateX(100%);
	visibility: hidden;
	/* Visibility flips instantly on open so focus can move inside, and is
	   deferred to the end of the slide on close. */
	transition: transform var(--duration-base) var(--ease-out),
	            visibility 0s linear var(--duration-base);
	overflow-y: auto;
}

.site-nav.is-open {
	transform: translateX(0);
	visibility: visible;
	transition: transform var(--duration-base) var(--ease-out),
	            visibility 0s linear 0s;
}

.site-nav__list {
	display: flex;
	flex-direction: column;
	border-top: var(--border-width) solid var(--line-on-dark);
}

.site-nav__link {
	display: block;
	padding: var(--space-4) 0;
	font-size: var(--fs-md);
	font-weight: 500;
	color: var(--ink-on-dark);
	text-decoration: none;
	border-bottom: var(--border-width) solid var(--line-on-dark);
}

.site-nav__link:hover,
.site-nav__link:focus-visible {
	color: var(--ink-muted-on-ink);
}

/* --- Submenu ---
 *
 * In the drawer the item is a row: the link takes the space it needs and the
 * toggle sits at the end of it, so the two do not fight over one tap. The
 * panel then opens inline beneath, pushing the rest of the list down, which is
 * what a drawer should do rather than float something over itself.
 */
.site-nav__item--has-sub {
	display: grid;
	grid-template-columns: 1fr auto;
	align-items: center;
}

.site-nav__item--has-sub .site-nav__link {
	grid-column: 1;
}

/*
 * 44x44 including the padding, which is the standard in section 21 and is not
 * negotiable — this control sits next to a link it must not be mistaken for.
 */
.site-nav__sub-toggle {
	grid-column: 2;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border: 0;
	background: none;
	color: var(--ink-on-dark);
	cursor: pointer;
}

.site-nav__sub-toggle svg {
	width: 1.25rem;
	height: 1.25rem;
	fill: none;
	stroke: currentColor;
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
	transition: transform var(--duration-base) var(--ease-out);
}

/* The chevron points at what it does: down to open, up once open. */
.site-nav__item--has-sub.is-open .site-nav__sub-toggle svg {
	transform: rotate(180deg);
}

.site-nav__sub-toggle[hidden] {
	display: none;
}

.site-nav__sub {
	grid-column: 1 / -1;
	display: flex;
	flex-direction: column;
}

.site-nav__sub[hidden] {
	display: none;
}

.site-nav__sub-link {
	display: block;
	padding: var(--space-3) 0 var(--space-3) var(--space-5);
	font-size: var(--fs-base);
	color: var(--ink-muted-on-ink);
	text-decoration: none;
	border-bottom: var(--border-width) solid var(--line-on-dark);
}

.site-nav__sub-link:hover,
.site-nav__sub-link:focus-visible {
	color: var(--ink-on-dark);
}


.site-nav__drawer-actions {
	margin-top: var(--space-6);
}

/* Backdrop */
.nav-backdrop {
	position: fixed;
	inset: 0;
	z-index: var(--z-mid);
	background-color: rgba(11, 11, 11, 0.55);
	opacity: 0;
	visibility: hidden;
	transition: opacity var(--duration-base) var(--ease-out),
	            visibility 0s linear var(--duration-base);
}

.nav-backdrop.is-open {
	opacity: 1;
	visibility: visible;
	transition: opacity var(--duration-base) var(--ease-out),
	            visibility 0s linear 0s;
}


/* ---------- ABOUT PAGE ---------- */

/*
 * page-about.php: a near-black opening, a white prose band, then /overview/'s
 * services field and the shared closing CTA. The last two contribute no CSS
 * here at all — `.services--index` and `template-parts/service-card.php` are
 * that page's, reused rather than reimplemented, so the geometry diff on
 * /overview/ covers them.
 *
 * Everything below is the two bands above them, and all of it exists to keep
 * text off a narrow column. The measure arithmetic runs through the whole
 * block: Plex Sans averages about half its size per character, the container
 * holds 1136px at 1440, and --measure is 68ch. So a full-width line is 114
 * characters at --fs-base, 95 at --fs-lg and 60 at --fs-xl — which is why the
 * opening paragraph is set large and the prose band is set in columns.
 */

/* --- The opening --- */

/*
 * --section-gap-lg, the room §14 gives a dark surface so the colour reads as a
 * room rather than a stripe.
 */
.about-hero {
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
	padding-block: var(--section-gap-lg);
}

/*
 * Headings carry no colour of their own in this stylesheet, so the band's
 * colour is enough — but this is stated rather than inherited, because the
 * heading is the one thing on the page that must not be muted.
 */
.about-hero__title {
	font-size: var(--fs-2xl);
	line-height: var(--lh-tight);
	letter-spacing: var(--tracking-tight);
	color: var(--ink-on-dark);
}

/*
 * `.service-intro__para`'s formatting, which is what this paragraph had before
 * the band went dark and is what it keeps: --fs-md, --lh-loose and --measure.
 * It was set at --fs-xl for one pass on the argument that the larger size made
 * the full container width a sane measure; that changed the type on a band
 * that was only meant to change colour, and it was reverted.
 *
 * Only the colour differs from the light-surface version, because it has to:
 * --ink-muted-on-ink is the palette's secondary on --ink-950 and measures
 * 10.4:1, where --ink-600 would be unreadable.
 */
.about-hero__lead {
	margin-top: var(--space-5);
	max-width: var(--measure);
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--ink-muted-on-ink);
}

/*
 * Centred, on request. §7 reserves centred text for closing-CTA copy — a
 * single control centred under a left-aligned band is a position rather than
 * centred copy, so the two do not conflict.
 */
.about-hero__action {
	margin-top: var(--space-8);
	text-align: center;
}

/*
 * The eyebrow on the near-black surface. --ink-muted-on-dark is tuned for
 * --blue-800; this is the near-black counterpart the rest of the file already
 * uses for secondary text on --ink-950.
 *
 * A class of this name existed before and was deleted with the band that used
 * it. It is back because there is a band using it again.
 */
.eyebrow--on-ink {
	color: var(--ink-muted-on-ink);
}

/* --- The prose band --- */

/*
 * 26rem is the column width at which the pair survives at 1440 (1136px holds
 * two 416px columns and a 72px gap) and collapses at 900, where 803px does
 * not. One column of 803px would run 94 characters, so the paragraphs carry
 * --measure as their own guard below — in two columns each is 526px and the
 * cap never bites.
 *
 * `columns` rather than a grid, because it fills down and then across: a grid
 * would run three paragraphs left, right, then left again on a second row, and
 * reading them in order would mean zig-zagging.
 */
.about-prose {
	columns: 26rem 2;
	column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
}

/*
 * break-inside so a paragraph is never split down the column break, and
 * margin-bottom rather than a gap because the first paragraph of the second
 * column is still a sibling — a top margin would push that column down and
 * the two would start on different lines.
 */
.about-prose p {
	break-inside: avoid;
	margin-bottom: var(--space-5);
	max-width: var(--measure);
	font-size: var(--fs-base);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

.about-prose p:last-child {
	margin-bottom: 0;
}

/* ---------- THE TEAM PAGE ---------- */

/*
 * page-team.php: two person entries on white, then the organogram on the
 * near-black field, then the shared closing CTA.
 */

.team__title {
	font-size: var(--fs-2xl);
	line-height: var(--lh-tight);
	letter-spacing: var(--tracking-tight);
}

/*
 * §18's person entry. One component, used for every individual, so a third
 * person is a third entry in inc/team.php and nothing here changes.
 *
 * Stacked at every width below 64rem; the two-column split is in the 64rem
 * block. It is written there and not in the 48rem block on purpose — a 4fr
 * portrait column at 768px is 208px wide, which is a thumbnail rather than a
 * portrait, and putting a rule in the wrong breakpoint block is the fault
 * docs/HANDOVER.md §4 records against `.service-intro__inner`.
 */
.person {
	margin-top: var(--space-8);
	display: grid;
	gap: var(--space-6);
}

.person + .person {
	margin-top: var(--space-9);
	padding-top: var(--space-8);
	border-top: var(--border-width) solid var(--line);
}

.person__media {
	margin: 0;
}

/*
 * 4:5, the §15 person ratio, with object-fit doing the crop — so the 300x300
 * files the client has now and a proper re-shoot later both drop into the same
 * slot with no design change. §18 forbids a circular crop: it loses the
 * shoulders and flattens the difference between a professional portrait and a
 * snapshot.
 *
 * 18rem caps the upscale. The source is 300px square, so a 4:5 crop uses 240px
 * of it and 288px is a 1.2x enlargement — visible if you look for it, past
 * that it is visible without looking.
 */
.person__media img {
	display: block;
	width: 100%;
	max-width: 18rem;
	height: auto;
	aspect-ratio: 4 / 5;
	object-fit: cover;
	border-radius: var(--radius-md);
}

.person__name {
	font-size: var(--fs-lg);
	line-height: var(--lh-snug);
}

/*
 * Qualifications and role, set as one two-line label rather than as two
 * different things: they are both the answer to "who is this", and the client
 * writes them as a pair.
 */
.person__quals {
	margin-top: var(--space-2);
	font-size: var(--fs-sm);
	color: var(--ink-600);
}

.person__role {
	font-weight: 600;
	font-size: var(--fs-sm);
	color: var(--ink-600);
}

.person__bio {
	margin-top: var(--space-5);
	max-width: var(--measure);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}


/* --- The organogram --- */

/*
 * THE REDESIGN OF THE CLIENT'S CHART.
 *
 * What it keeps: two leaders over eleven functions, and who reports to whom.
 * What it drops: the rounded boxes, the blue gradients and the bevelled edges.
 * §5 rules out all three by name — "no cards, gradients, floating panels or
 * decorative elevation. Sections are defined by surface and space." So the
 * hierarchy is carried by rules, which is the structural job §8 gives them,
 * and by indentation.
 *
 * What it gains: it is text. The original is a PNG, so its twenty-seven names
 * cannot be read by a screen reader, indexed, searched, selected or enlarged,
 * and at the width the live page renders it they cannot be read by anyone. As
 * a nested list the reporting lines are in the markup, and the layout can wrap
 * instead of shrinking.
 */

.org {
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
	padding-block: var(--section-gap-lg);
}

.org__role {
	font-weight: 600;
	color: var(--ink-on-dark);
}

.org__name {
	color: var(--ink-muted-on-ink);
}

/* The two at the top of the chart, a step up in size from the eleven. */
.org__leaders {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-5) var(--space-9);
}

.org__node--lead .org__role {
	font-size: var(--fs-lg);
	line-height: var(--lh-snug);
}

.org__node--lead .org__name {
	margin-top: var(--space-1);
	font-size: var(--fs-base);
}

/*
 * The spine: the rule between the leadership and the eleven functions. A
 * `div` rather than a border because it draws itself in from the left, and a
 * border cannot be transformed.
 */
.org__spine {
	margin-block: var(--space-7);
	height: var(--border-width);
	background-color: var(--line-on-dark);
}

/*
 * `columns`, not a grid, and the reason is the shape of this particular
 * organisation: Bookkeeping has eight people under it and IT Technical Support
 * has none. In a grid every row is as tall as its tallest column and the short
 * ones leave holes; in columns the eleven pack. Same choice, for the same
 * reason, as `.cover__items` and `.about-prose`.
 *
 * 16rem gives four columns at 1440, two at 900 and one at 390, and the count
 * follows the width with no breakpoint of its own.
 */
.org__list--d0 {
	columns: 16rem 4;
	column-gap: var(--space-7);
}

/*
 * margin-bottom rather than a gap: the first item of the second column is
 * still a sibling, so a top margin would push that column down and the two
 * would start on different lines.
 */
.org__list--d0 > .org__node {
	break-inside: avoid;
	margin-bottom: var(--space-7);
	position: relative;
	padding-left: var(--space-5);
}

/*
 * The blue tick that marks a function head. It is the only blue in the
 * diagram, and it is doing work rather than decoration — eleven ticks are
 * countable at a glance, which is the one thing the reader should take from
 * the chart before reading a word of it. §3: colour is architecture.
 *
 * --blue-500, not --action. --blue-600 measures 3.73:1 on this ground, which
 * is enough to identify a control but thin for a 2px rule; --blue-500 is the
 * value the palette keeps for this surface, at 7.14:1.
 */
.org__list--d0 > .org__node::before {
	content: "";
	position: absolute;
	left: 0;
	top: 0.2em;
	width: 2px;
	height: 2.5em;
	background-color: var(--blue-500);
}

.org__list--d0 > .org__node > .org__role {
	font-size: var(--fs-base);
	line-height: var(--lh-snug);
}

.org__list--d0 > .org__node > .org__name {
	font-size: var(--fs-sm);
}

/*
 * Everything below a function head. The reporting line is a hairline down the
 * left with a tick out to each person — the same vocabulary as `.covers`, one
 * level of indentation deeper.
 */
.org__list--d0 .org__list {
	position: relative;
	margin-top: var(--space-4);
	padding-left: var(--space-5);
}

.org__list--d0 .org__list::before {
	content: "";
	position: absolute;
	left: 0;
	top: 0;
	bottom: 0.9em;
	width: var(--border-width);
	background-color: var(--line-on-dark);
}

.org__list--d0 .org__node {
	position: relative;
	margin-top: var(--space-4);
	font-size: var(--fs-sm);
}

.org__list--d0 .org__node:first-child {
	margin-top: 0;
}

/* The tick out to each person, meeting the hairline at the cap height. */
.org__list--d0 .org__list > .org__node::before {
	content: "";
	position: absolute;
	left: calc(var(--space-5) * -1);
	top: 0.62em;
	width: var(--space-3);
	height: var(--border-width);
	background-color: var(--line-on-dark);
}

/* --- The animation ---
 *
 * §19 allows three motion patterns and this uses two of them: pattern 2, the
 * scroll reveal, on the text, and pattern 1's entrance applied to a rule —
 * a line that has no text to fade arrives by drawing itself instead. Both are
 * compositor-only (transform and opacity), both fire once, and both come from
 * the observer that is already in main.js.
 *
 * NO NEW JAVASCRIPT. The eleven function heads carry `data-reveal` and
 * everything inside a column keys off the `.is-visible` that observer adds, so
 * the whole diagram animates from eleven observed elements and nothing was
 * added to the shared IIFE — where, per docs/HANDOVER.md §3, two real bugs
 * have come from `var` collisions.
 */

.js .org__spine {
	transform: scaleX(0);
	transform-origin: left;
	transition: transform var(--duration-slow) var(--ease-out);
}

.js .org__spine.is-visible {
	transform: none;
}

/*
 * The stagger. `--org-i` is set on each column by the renderer and turned into
 * a delay here; `data-reveal-delay` stops at 4 and there are eleven of these,
 * so the number rides in as a value rather than as seven more attribute rules.
 * 55ms a step puts 605ms between the first column and the last, which is
 * close to --duration-slow of the previous one and so still reads as one
 * movement rather than eleven.
 */
.js .org__list--d0 > .org__node.reveal {
	transition-delay: calc(var(--org-i, 0) * 55ms);
}

/*
 * The tick draws down after its column's text has begun to arrive, and the
 * reporting line under it after that. Three things in sequence per column,
 * eleven columns — but each one is 400ms, so the band reads as filling in
 * rather than as a sequence of events.
 */
.js .org__list--d0 > .org__node::before,
.js .org__list--d0 .org__list::before {
	transform: scaleY(0);
	transform-origin: top;
	transition: transform var(--duration-slow) var(--ease-out);
	transition-delay: calc(var(--org-i, 0) * 55ms + 170ms);
}

.js .org__list--d0 > .org__node.is-visible::before,
.js .org__list--d0 > .org__node.is-visible .org__list::before {
	transform: none;
}

/* The ticks out to each person are horizontal, so they draw from the line. */
.js .org__list--d0 .org__list > .org__node::before {
	transform: scaleX(0);
	transform-origin: left;
	transition: transform var(--duration-base) var(--ease-out);
	transition-delay: calc(var(--org-i, 0) * 55ms + 360ms);
}

.js .org__list--d0 > .org__node.is-visible .org__list > .org__node::before {
	transform: none;
}


/* ---------- FINSOLVE GROUP PROFILE ---------- */

/*
 * page-group-profile.php: the proposition and the practice on white, the
 * client's hub diagram rebuilt on the one dark field, the competencies and the
 * service detail back on white, then the shared CTA.
 */

.profile-intro__title {
	font-size: var(--fs-2xl);
	line-height: var(--lh-tight);
	letter-spacing: var(--tracking-tight);
}

/*
 * The seven promises. Columns follow the width by auto-fit rather than a
 * breakpoint — seven short lines in a single 1136px column is a ribbon down the
 * left of an empty band.
 */
/*
 * The promises and the photograph. Stacked below 64rem, side by side above it —
 * the split is in the 64rem block, not the 48rem one, because a 5fr image
 * column at 768px is a thumbnail and because putting a rule in the wrong
 * breakpoint block is the fault docs/HANDOVER.md §4 records twice.
 */
.profile-opening {
	display: grid;
	gap: var(--space-7);
}

.profile-opening__media {
	margin: 0;
}

.profile-opening__media img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 3 / 2;
	object-fit: cover;
	border-radius: var(--radius-md);
}

.profile-promises {
	margin-top: var(--space-7);
	display: grid;
	gap: var(--space-4) var(--space-7);
	grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}

/*
 * The marker is the 6px dot `.cover__item` uses on the dark field, in --action
 * because this one is on white. Nothing else on the page is blue except the
 * diagram's arrowheads.
 */
.profile-promises__item,
.profile-specialisms__item,
.profile-block__items li {
	position: relative;
	padding-left: var(--space-5);
	line-height: var(--lh-base);
}

.profile-promises__item::before,
.profile-specialisms__item::before,
.profile-block__items li::before {
	content: "";
	position: absolute;
	top: 0.62em;
	left: 0;
	width: 0.375rem;
	height: 0.375rem;
	border-radius: 50%;
	background-color: var(--action);
}

/*
 * The six practice statements. `columns` rather than a grid because they are
 * unequal — one runs three lines and one runs half a line — and a grid row is
 * as tall as its tallest cell.
 */
.profile-practice {
	margin-top: var(--space-8);
	padding-top: var(--space-7);
	border-top: var(--border-width) solid var(--line);
	columns: 24rem 2;
	column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
}

.profile-practice p {
	break-inside: avoid;
	margin-bottom: var(--space-5);
	max-width: var(--measure);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

.profile-practice p:last-child {
	margin-bottom: 0;
}


/* --- The hub --- */

/*
 * THE CLIENT'S DIAGRAM, REBUILT.
 *
 * The original is hub-and-spoke on a yellow field: seven rounded boxes in six
 * different fills, grey arrows, a drop shadow on each, converging on a circle
 * marked "Business". §5 rules out the boxes, the gradients and the elevation by
 * name and §2 leaves one accent, so what carries over is the meaning — seven
 * disciplines all serving one thing.
 *
 * THE ARRANGEMENT IS THE CLIENT'S: seven disciplines on a half-circle above a
 * ringed word — Accounting due left at 180deg, Business Plans at the top,
 * Independent Reviews due right at 360deg, 30deg apart.
 *
 * ONE DRAWING AT EVERY WIDTH, SCALED. It was a spine on phones and a fan above
 * 64rem for a pass; the fan now runs all the way down and every length is on a
 * clamp, so the same illustration gets smaller rather than a second one
 * appearing. That is only possible because this is a drawing and its labels are
 * a drawing's labels — §22's floor of --fs-base is about body text, and at
 * 390px these are 10px. On request, and worth knowing rather than discovering.
 *
 * THE ARITHMETIC THAT SIZES IT. Adjacent labels clear each other only when the
 * radius to a label's centre is more than twice the label's width: the 240deg
 * and 270deg labels are separated horizontally by R x |cos240 - cos270|, which
 * is R/2. And the fan has to fit, so R + width/2 must stay inside half the
 * container. At 390px that leaves about 10px of type and a 60px label — the
 * narrowest the longest unbreakable word, "Independent", will go.
 */

.hub-band {
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
	padding-block: var(--section-gap-lg);
}

.hub {
	margin: 0;
}

/*
 * The client's own sentence, and the figure's accessible description — so what
 * a screen reader gets for the drawing is the client's description of it rather
 * than something written here to fill the gap. Above the diagram, which is
 * where a caption that introduces rather than annotates belongs.
 */
.hub__caption {
	max-width: var(--measure);
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--ink-muted-on-ink);
}

/*
 * The five lengths the whole drawing is built from. Change one and the fan
 * stays in register, because everything else is derived from them.
 */
.hub__diagram {
	--hub-ring: clamp(5rem, 12vw, 13rem);
	--hub-spoke: clamp(2.75rem, 12vw, 13rem);
	--hub-reach: clamp(0.5rem, 1.2vw, 1rem);
	--hub-label: clamp(3.75rem, 9vw, 10rem);

	position: relative;
	margin-top: var(--space-8);
	height: clamp(15rem, 34vw, 35rem);
}

/*
 * The word every spoke points at, in a ring.
 *
 * §8 caps radius at 8px and rules out pill shapes — that governs controls and
 * sections, and this is a node in a drawing: the circle is the one thing the
 * client's diagram is recognisable by, and it was asked for. An outline rather
 * than a fill, because a filled disc on a near-black field is the decorative
 * panel §5 rules out.
 *
 * The centre sits at 78% because the fan only occupies the half above it. At
 * 80% the ring's bottom edge ran 3-8px past the diagram's own box — nothing
 * clipped, since the band has its padding below, but a drawing should sit
 * inside the box it is given.
 */
.hub__core {
	position: absolute;
	left: 50%;
	top: 78%;
	width: var(--hub-ring);
	height: var(--hub-ring);
	margin: calc(var(--hub-ring) / -2) 0 0 calc(var(--hub-ring) / -2);
	display: flex;
	align-items: center;
	justify-content: center;
	border: var(--border-width) solid var(--line-on-dark);
	border-radius: 50%;
	text-align: center;
	/*
	 * Sized against the ring rather than against the type scale. At
	 * clamp(1.125rem, 2.6vw, 2.375rem) the word had 3px of clearance inside the
	 * circle at 390 and 8px at 1440, so it read as a word crammed into a ring
	 * rather than a word sitting in one. The drawing's circle is comfortably
	 * larger than its label and this is what makes that true at every size.
	 */
	font-size: clamp(0.9375rem, 2.1vw, 2rem);
	line-height: var(--lh-snug);
	font-weight: 600;
	color: var(--ink-on-dark);
}

.hub__list {
	position: relative;
	display: block;
	height: 100%;
}

/*
 * Rule first, then label. row-reverse rather than putting the rule first in the
 * markup: the label is what a screen reader should reach first, and the rule is
 * aria-hidden decoration that happens to sit to its left.
 */
/*
 * Each discipline is a zero-height row pinned to the centre of the ring and
 * rotated out to its own angle. Zero height plus `align-items: center` is what
 * makes the origin exact whatever the label wraps to: the content overflows
 * equally above and below the line it is rotated about, so the rotation origin
 * is the centre point and not "the centre point plus half a label".
 *
 * `transform` here is placement, which is why the item carries `data-reveal`
 * but not the `.reveal` class — that class animates transform, and the two
 * would overwrite each other.
 *
 * row-reverse, so the rule sits at the inner end and the label at the outer
 * one — the order a spoke needs, while the markup still puts the label first
 * for anything reading it. Setting `row` here for a pass put every label inside
 * the ring and every arrowhead out past it.
 */
.hub__item {
	position: absolute;
	left: 50%;
	top: 78%;
	height: 0;
	width: max-content;
	display: flex;
	flex-direction: row-reverse;
	justify-content: flex-end;
	align-items: center;
	gap: var(--hub-reach);
	padding-left: calc(var(--hub-ring) / 2);
	transform: rotate(var(--hub-a, 0deg));
	transform-origin: 0 0;
}

/*
 * Counter-rotated so it reads horizontally wherever it sits on the circle.
 * Rotating about its own centre keeps that centre on the point the fan put it,
 * so the seven stay evenly spaced.
 *
 * Capped narrow and allowed to wrap, which is both what the drawing does — its
 * boxes are narrow with the text broken over two and three lines — and what
 * stops the three at the top of the arc running into each other.
 */
.hub__label {
	max-width: var(--hub-label);
	font-size: clamp(0.625rem, 0.45rem + 0.62vw, 0.9375rem);
	line-height: var(--lh-base);
	transform: rotate(calc(-1 * var(--hub-a, 0deg)));
}

/*
 * The entrance lives on the label because the item's own `transform` is what
 * places it on the circle. Same movement `.reveal` describes — opacity and a
 * short rise — written out here rather than borrowed, since borrowing it would
 * mean two rules fighting over one property.
 */
.js .hub__label {
	opacity: 0;
	transform: rotate(calc(-1 * var(--hub-a, 0deg))) translateY(10px);
	transition:
		opacity var(--duration-slow) var(--ease-out),
		transform var(--duration-slow) var(--ease-out);
	transition-delay: calc(var(--hub-i, 0) * 80ms);
}

.js .hub__item.is-visible .hub__label {
	opacity: 1;
	transform: rotate(calc(-1 * var(--hub-a, 0deg)));
}

.hub__rule {
	position: relative;
	flex: none;
	width: var(--hub-spoke);
	height: var(--border-width);
	background-color: var(--line-on-dark);
	transform-origin: right;
}

/*
 * The arrowhead. A CSS triangle rather than an SVG or a glyph: it scales with
 * the type, costs no request, and it is the one thing a rule on its own cannot
 * say — which way the discipline feeds. It points left below 64rem and inward
 * at the word above it, which is the same direction either way.
 */
.hub__rule::after {
	content: "";
	position: absolute;
	left: 0;
	top: 50%;
	margin-top: -0.24em;
	border-top: 0.24em solid transparent;
	border-bottom: 0.24em solid transparent;
	border-right: 0.32em solid var(--blue-500);
}


/* --- Core competencies and the service detail --- */

.profile-detail__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
}

.profile-detail__lead {
	margin-top: var(--space-5);
	font-size: var(--fs-md);
	color: var(--ink-600);
}

.profile-specialisms {
	margin-top: var(--space-5);
	display: grid;
	gap: var(--space-4) var(--space-6);
	grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

.profile-detail__note {
	margin-top: var(--space-6);
	max-width: var(--measure);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

/*
 * The seven detail blocks. `columns` and not a grid, for the reason the
 * organogram uses columns: Training has three lines under it and Leadership
 * meetings has one, so a grid row would be as tall as the tallest and the short
 * blocks would leave holes.
 *
 * Each block reveals on its own, staggered down the page. It was one reveal on
 * the container for a pass, and seven blocks of dense copy arriving together
 * was too much at once. Document order runs down the first column and then down
 * the second, which is what `columns` does, so the stagger reads top to bottom
 * rather than jumping about — and `break-inside: avoid` still holds each block
 * together, which was the worry that put the reveal on the container.
 */
.profile-blocks {
	margin-top: var(--space-9);
	padding-top: var(--space-7);
	border-top: var(--border-width) solid var(--line);
	columns: 22rem 3;
	column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
}

.profile-block {
	break-inside: avoid;
	margin-bottom: var(--space-8);
}

/*
 * Staggered down the page rather than seven blocks of dense copy arriving at
 * once — asked for on 2026-09-02 after the container carried a single reveal.
 * 110ms a step spreads the seven over 660ms, which on top of --duration-slow
 * puts about 1.2s between the first block settling and the last, so the eye
 * has somewhere to start.
 */
.js .profile-block.reveal {
	transition-delay: calc(var(--blk-i, 0) * 110ms);
}

.profile-block__title {
	font-size: var(--fs-base);
	line-height: var(--lh-snug);
}

.profile-block__items {
	margin-top: var(--space-4);
	display: grid;
	gap: var(--space-3);
}

.profile-block__items li {
	font-size: var(--fs-sm);
	line-height: var(--lh-base);
	color: var(--ink-600);
}


/* --- The closing band --- */

/*
 * Zero at the bottom, because the CTA below shares this white surface: the two
 * neighbours would otherwise each pay a full gap and the reader would see the
 * sum, which is a hole where the surface does not change. `/about/` settles it
 * the same way, and the join goes on the sibling selector because `.cta` is
 * shared with pages where it follows the near-black field and the full gap is
 * right.
 */
.section.profile-detail {
	padding-block: var(--section-gap) 0;
}

.profile-detail + .cta,
.profile-assoc + .cta {
	padding-top: var(--section-join);
}

/*
 * The group's numbers and sites, as a description list under the client's own
 * three labels.
 *
 * The first pass ran all eight as one centred row of underlined links, which
 * wrapped five-then-three and put a phone number, an email address and a domain
 * beside each other with nothing to tell them apart. Three labelled groups fix
 * that: the label says what the values are and each group aligns internally.
 *
 * Left-aligned inside a centred band. §7 reserves centred text for closing-CTA
 * copy and this is that band — but a list of eight addresses centred has eight
 * ragged left edges, and the tidiness is the whole point of this change. The
 * block is centred; its contents are not.
 */
/*
 * One column on a phone; two from 40rem, with Email and the office number
 * stacked in the first and the five websites filling the second.
 *
 * Column flow with two rows does that placement: the first two groups fill
 * column one and the third starts column two. Row flow would have put the
 * office number beside Email instead of under it, which is the thing this was
 * changed to fix.
 */
.profile-contact {
	margin-top: var(--space-7);
	max-width: 44rem;
	margin-inline: auto;
	text-align: left;
	display: grid;
	gap: var(--space-6);
	align-items: start;
}

@media (min-width: 40rem) {
	.profile-contact {
		grid-template-columns: 1fr 1fr;
		column-gap: var(--space-7);
	}

	/*
	 * Placed explicitly rather than flowed. Column flow put the office number
	 * in row two, and row two began below the five websites — so it sat 200px
	 * under the email addresses it is meant to follow. Spanning the websites
	 * across both rows lets the first column close up.
	 */
	.profile-contact__group:nth-child(1) { grid-area: 1 / 1; }
	.profile-contact__group:nth-child(2) { grid-area: 2 / 1; }
	.profile-contact__group:nth-child(3) { grid-area: 1 / 2 / span 2 / 3; }
}

/* The label. §12's treatment for a label, and not an eyebrow — §13 confines
   uppercase to eyebrows and there is already one on this page. */
.profile-contact dt {
	font-size: var(--fs-sm);
	font-weight: 600;
	color: var(--ink-600);
	margin-bottom: var(--space-2);
}

.profile-contact dd {
	margin: 0;
	font-size: var(--fs-sm);
}

/*
 * 44px high, which is more than the phone and email links elsewhere on this
 * site manage — docs/HANDOVER.md §9 records those as a standing gap against
 * §21, and there was no reason to add an eleventh case to it.
 *
 * Underlined at rest: §10 allows a bare link only where position identifies it,
 * and here the label sits above rather than around the values.
 */
.profile-contact a {
	display: inline-flex;
	align-items: center;
	min-height: 2.75rem;
	color: var(--action);
	text-decoration: underline;
	text-underline-offset: 0.08em;
}

.profile-contact a:hover {
	color: var(--action-hover);
	text-decoration-thickness: 2px;
}

/*
 * The marks row and the CTA share the white surface below the detail band, so
 * the joins between all three are --section-join rather than three full gaps.
 */
.section.profile-assoc {
	padding-block: var(--section-join);
}

.profile-assoc .credentials {
	margin-top: 0;
}


/* --- The hub's animation ---
 *
 * The same two patterns the organogram uses: §19's pattern 2 on the labels, and
 * pattern 1's entrance applied to a rule — a line with no text to fade arrives
 * by drawing itself. Each rule draws toward the centre, its arrowhead appears
 * once it has arrived, and the word comes last.
 *
 * NO NEW JAVASCRIPT. Each of the seven carries `data-reveal` and everything
 * about it keys off the `.is-visible` the observer in main.js already adds, so
 * nothing went into the shared IIFE — where two real bugs have come from `var`
 * collisions.
 *
 * `--hub-i` is written on each item by the template and turned into a delay
 * here. 80ms a step is 480ms across the seven, inside --duration-slow of the
 * first, so the band reads as one convergence rather than seven events. The
 * arrowhead waits 440ms, which is just after its own rule has finished
 * drawing.
 */

.js .hub__rule {
	transform: scaleX(0);
	transition: transform var(--duration-slow) var(--ease-out);
	transition-delay: calc(var(--hub-i, 0) * 80ms);
}

.js .hub__item.is-visible .hub__rule {
	transform: none;
}

/* The arrowhead only appears once its rule has arrived, so nothing is left
   pointing at a word from a line that is not there yet. */
.js .hub__rule::after {
	opacity: 0;
	transition: opacity var(--duration-base) var(--ease-out);
	transition-delay: calc(var(--hub-i, 0) * 80ms + 440ms);
}

.js .hub__item.is-visible .hub__rule::after {
	opacity: 1;
}

/* ---------- LONG-FORM PROSE ---------- */

/*
 * page-terms.php and single.php — anything that is a document rather than a
 * page. Two things needed the same treatment, so it stopped being the terms
 * page's block and became the shared one: a legal document and a newsletter
 * issue are both long-form copy with headings, lists and links, read start to
 * finish, and there is no reason for them to drift apart.
 *
 * The whole block is spacing and list markers. There is no colour, no rule, no
 * card and no motion in it, which is what "a very basic plain page" means and
 * also what a contract wants: it is read top to bottom, and every device that
 * would help someone scan a marketing page is noise on this one.
 */

/*
 * `--container-text`, and this is the one page where a narrow column is right
 * rather than a huddle. §6 sets this value for prose specifically — about 68
 * characters — and line length is the whole usability of a document nobody
 * reads for pleasure.
 *
 * It sits inside `.container` rather than on it, so it gets the full 44rem.
 * A max-width on an element that also carries `.container` is 64px narrower
 * than it looks, because that class adds `padding-inline: 2rem` to a
 * border-box — the arithmetic that caught `.cta__inner` at 44rem.
 */
.prose-col {
	max-width: var(--container-text);
}

.prose-col__title {
	font-size: var(--fs-2xl);
	line-height: var(--lh-tight);
	letter-spacing: var(--tracking-tight);
}

.prose {
	margin-top: var(--space-7);
}

/*
 * Space does the sectioning, not rules. Nine headings down a long page with a
 * hairline over each reads as nine boxes; the gap above a heading being three
 * times the gap below it is enough to group without drawing anything.
 */
.prose h2 {
	margin-top: var(--space-8);
	font-size: var(--fs-lg);
	line-height: var(--lh-snug);
}

.prose h2:first-child {
	margin-top: 0;
}

/*
 * .prose CARRIES AN h3 EVEN THOUGH NOTHING EMITS ONE TODAY. /terms/ is the only
 * document using this block since Gavin's desk was removed, and a contract has
 * one heading level. The rule stays because it belongs to .prose rather than to
 * a page: without it a sub-heading falls through to the browser default — bold
 * at 1.17em of the parent — which is both larger and heavier than the h2 above
 * it at --fs-lg.
 *
 * Set at the body size rather than a step down from --fs-lg, so the level is
 * carried by weight and by the space above it. A third step between 24px and
 * 17px would be a type size the scale does not have.
 */
.prose h3 {
	margin-top: var(--space-6);
	font-size: var(--fs-base);
	font-weight: 600;
	line-height: var(--lh-snug);
	color: var(--ink-900);
}

.prose h3 + p {
	margin-top: var(--space-2);
}

.prose p {
	margin-top: var(--space-4);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

/*
 * The markers come back. The reset strips them site-wide, and everywhere else
 * that is right — a list of services is a set and its numbers would be
 * decoration. Here the numbers are content: legal clauses are cited by number,
 * so "Disclaimer 2" has to be findable.
 */
.prose ol,
.prose ul {
	margin-top: var(--space-4);
	padding-left: var(--space-6);
	display: grid;
	gap: var(--space-3);
	line-height: var(--lh-loose);
	color: var(--ink-600);
}

.prose ol {
	list-style: decimal;
}

.prose ul {
	list-style: disc;
}

/* A sub-list is part of the clause above it, so it sits closer to that clause
   than the next clause sits to it. */
.prose li > ul {
	margin-top: var(--space-3);
	margin-bottom: var(--space-2);
}

.prose li::marker {
	color: var(--ink-600);
}

/*
 * A newsletter carries section breaks the terms page never does, and they were
 * rendering as the browser default — `1px inset rgb(128,128,128)`, a 2px box
 * with 8.5px of margin, which is the ridged grey line of an unstyled document.
 * The client uses them inconsistently, so this makes the ones they did write
 * look deliberate rather than removing them and losing their sectioning.
 *
 * A hairline in --line, and the space around it is what does the separating —
 * the same reasoning /terms/ uses for having no rules at all.
 */
.prose hr {
	height: 0;
	margin-block: var(--space-8);
	border: 0;
	border-top: var(--border-width) solid var(--line);
}

/*
 * A heading straight after a rule already has the rule's space above it, so it
 * does not add its own on top.
 */
.prose hr + h2,
.prose hr + h3 {
	margin-top: 0;
}

/*
 * §10: underlined at rest in body copy, because colour alone never marks a
 * link. There are three on this page and two of them are the same address.
 */
.prose a {
	color: var(--action);
	text-decoration: underline;
	text-underline-offset: 0.08em;
}

.prose a:hover {
	color: var(--action-hover);
	text-decoration-thickness: 2px;
}

/*
 * The POPI link is the one thing on the page a reader is meant to act on, so it
 * gets a line to itself and a 44px target — §21's minimum, which the phone and
 * email links elsewhere on this site still do not meet.
 */
.legal__popi {
	margin-top: var(--space-7);
}

.legal__popi a {
	display: inline-flex;
	align-items: center;
	min-height: 2.75rem;
}

/* ---------- HERO CAROUSEL ---------- */
.hero {
	position: relative;
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
}

.hero-carousel {
	position: relative;
}

/*
 * A scroll-snap track, not a stack of absolutely positioned panels. Without
 * JavaScript this stays a real horizontal scroller: all four slides remain
 * reachable by touch, wheel, scrollbar and keyboard, so nothing on the page
 * waits for a script (§19).
 */
.hero__viewport {
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	overscroll-behavior-x: contain;
}

/* The scrollbar is the only affordance when the script has not run, so it is
   left in place there and hidden only once dots and arrows replace it. */
.js .hero__viewport {
	scrollbar-width: none;
}

.js .hero__viewport::-webkit-scrollbar {
	display: none;
}

.hero__track {
	display: flex;
}

/* min-height and padding-block are the previous .hero values unchanged, so the
   hero occupies the same frame it did before. Every slide shares the tallest
   slide's height, so the frame does not resize between slides. */
.hero__slide {
	flex: 0 0 100%;
	scroll-snap-align: start;
	scroll-snap-stop: always;
	position: relative;
	isolation: isolate;
	display: flex;
	align-items: center;
	min-height: min(42rem, 88vh);
	padding-block: var(--section-gap);
	overflow: hidden;
}

.hero__bg,
.hero__scrim {
	position: absolute;
	inset: 0;
	z-index: var(--z-behind);
}

.hero__bg {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center;
}

/* A committed scrim: the approved hero puts text over a busy photograph with
   too little separation. This keeps white text above 11:1. */
.hero__scrim {
	background: var(--scrim-hero);
}

.hero__inner {
	position: relative;
	text-align: center;
}

.hero__title {
	font-size: var(--fs-3xl);
	line-height: var(--lh-tight);
	max-width: 26ch;
	margin-inline: auto;
	text-wrap: balance;
	color: var(--ink-on-dark);
}

.hero__lead {
	margin-top: var(--space-5);
	max-width: 62ch;
	margin-inline: auto;
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: rgba(255, 255, 255, 0.9);
}

.hero__actions {
	margin-top: var(--space-5);
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: var(--space-3);
}

/* Reserve the control cluster's row so it never sits on top of a CTA. Only
   on the .js branch, since that is the only time controls exist. The slide
   min-height still governs at every viewport, so the hero frame is unchanged. */
.js .hero__slide {
	padding-bottom: calc(var(--section-gap) + 3.5rem);
}

/* --- Carousel controls: one centred cluster, ‹ • • • • › ⏸ --- */
.hero__controls {
	position: absolute;
	inset-inline: 0;
	bottom: var(--space-5);
	z-index: var(--z-low);
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--space-2);
}

/* Restated because the rule above would otherwise beat the attribute. */
.hero__controls[hidden] {
	display: none;
}

.hero__dots {
	display: flex;
	align-items: center;
}

.hero__dot {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;      /* 44px target around a deliberately small mark */
}

.hero__dot::before {
	content: "";
	width: 0.625rem;
	height: 0.625rem;
	border-radius: 50%;
	border: var(--border-width-strong) solid rgba(255, 255, 255, 0.75);
	transition: background-color var(--duration-fast) var(--ease-out),
	            border-color var(--duration-fast) var(--ease-out);
}

/* The current slide is a filled mark among hollow ones — a shape difference,
   so the state does not rest on colour alone. */
.hero__dot[aria-current="true"]::before {
	background-color: var(--ink-on-dark);
	border-color: var(--ink-on-dark);
}

.hero__dot:hover::before {
	background-color: rgba(255, 255, 255, 0.45);
	border-color: var(--ink-on-dark);
}

/* The disc keeps these legible over the two light-toned photographs. */
.hero__nav {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border-radius: 50%;
	background-color: rgba(11, 11, 11, 0.5);
	transition: background-color var(--duration-fast) var(--ease-out);
}

.hero__nav:hover {
	background-color: rgba(11, 11, 11, 0.85);
}

.hero__nav svg {
	width: 1.25rem;
	height: 1.25rem;
	stroke: var(--ink-on-dark);
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
	fill: none;
}

/* Round controls take a round ring rather than the default rounded rect. */
.hero__dot:focus-visible,
.hero__nav:focus-visible {
	border-radius: 50%;
}

/* The scroll region is focusable so it stays operable without the script.
   Inset so the ring is not clipped by the full-bleed edge, and white rather
   than the default blue because it sits on photography. */
.hero__viewport:focus-visible {
	outline-color: var(--focus-ring-on-dark);
	outline-offset: -4px;
}

/* ---------- WHAT WE DO ---------- */
.whatwedo__inner {
	display: grid;
	gap: clamp(2rem, 1rem + 4vw, 3.5rem);
}

.whatwedo__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
	text-wrap: balance;
}

.whatwedo__title em {
	display: block;
	color: var(--blue-700);
}

.whatwedo__fig {
	overflow: hidden;
	border-radius: var(--radius-md);
}

.whatwedo__fig img {
	width: 100%;
	object-fit: cover;
	aspect-ratio: 3 / 4;
	transition: transform var(--duration-slow) var(--ease-out);
}

.whatwedo__fig:hover img {
	transform: scale(1.03);
}

.whatwedo__col {
	display: grid;
	gap: var(--space-6);
}

.whatwedo__copy + .whatwedo__copy {
	margin-top: var(--space-4);
}

.whatwedo__copy {
	color: var(--ink-600);
	font-size: var(--fs-base);
	max-width: 62ch;
}

.whatwedo__actions {
	margin-top: var(--space-6);
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-4);
}

/* Both actions carry the same footprint. */
.whatwedo__actions .btn {
	min-width: 13rem;
}

/* ---------- OUR SERVICES ---------- */
.services {
	background-color: var(--ink-950);
	color: var(--ink-on-dark);
	padding-block: clamp(2.5rem, 1.5rem + 2.5vw, 4rem) 0;
}

/* Edge to edge, as on the approved site — no gutters between cards. */
.svc-grid {
	display: grid;
	grid-template-columns: 1fr;
}

.svc {
	position: relative;
	isolation: isolate;
	display: flex;
	align-items: flex-end;
	min-height: clamp(19rem, 34vw, 24rem);
	padding: var(--space-6);
	overflow: hidden;
	background-color: var(--ink-950);
}

.svc__img {
	position: absolute;
	inset: 0;
	z-index: var(--z-behind-deep);
	width: 100%;
	height: 100%;
	object-fit: cover;
	transition: transform var(--duration-slow) var(--ease-out);
}

/* Scrim first, so the copy is legible before any hover state exists. */
.svc::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: var(--z-behind);
	background: var(--scrim-card);
}

/* Brand-blue wash on hover and keyboard focus. */
.svc::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: var(--z-behind);
	background-color: var(--blue-600);
	opacity: 0;
	transition: opacity var(--duration-base) var(--ease-out);
}

.svc:hover::after,
.svc:focus-within::after {
	opacity: 0.86;
}

.svc:hover .svc__img,
.svc:focus-within .svc__img {
	transform: scale(1.04);
}

/* The body lifts as the wash arrives — the approved site's card motion,
   kept to a single short movement. */
.svc__body {
	position: relative;
	transition: transform var(--duration-base) var(--ease-out);
}

.svc:hover .svc__body,
.svc:focus-within .svc__body {
	transform: translateY(-0.375rem);
}

.svc__index {
	display: block;
	font-size: var(--fs-xs);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	letter-spacing: var(--tracking-wide);
	color: rgba(255, 255, 255, 0.72);
	margin-bottom: var(--space-3);
	transition: color var(--duration-base) var(--ease-out);
}

.svc:hover .svc__index,
.svc:focus-within .svc__index {
	color: var(--ink-on-dark);
}

.svc__title {
	font-size: var(--fs-lg);
	color: var(--ink-on-dark);
	margin-bottom: var(--space-2);
}

/*
 * THE WHOLE CARD IS THE TARGET. The link sits on the title and this stretches
 * it over the card, so the accessible name stays the service rather than the
 * service plus its description read as one string.
 *
 * `::after` on the link, not on `.svc` — `.svc::before` is the scrim and
 * `.svc::after` is the brand-blue hover wash, so both of the card's own
 * pseudo-elements are already spoken for.
 */
.svc__link {
	color: inherit;
	text-decoration: none;
}

.svc__link::after {
	content: "";
	position: absolute;
	inset: 0;
}

/*
 * The ring goes round the card, not round the words: the hit area is the whole
 * card, so a ring drawn on the title would sit inside the thing it describes.
 * White, because the card is a photograph under a near-black scrim — §20 gives
 * that surface the white ring.
 */
.svc:has(.svc__link:focus-visible) {
	outline: var(--border-width-strong) solid var(--focus-ring-on-dark);
	outline-offset: calc(var(--border-width-strong) * -2);
}

.svc__desc {
	font-size: var(--fs-sm);
	line-height: var(--lh-base);
	color: rgba(255, 255, 255, 0.92);
	max-width: 34ch;
}

/* ---------- PROOF ---------- */
.proof__title {
	font-size: var(--fs-xl);
	text-align: center;
	max-width: 30ch;
	margin-inline: auto;
	text-wrap: balance;
	margin-bottom: clamp(2.5rem, 1.5rem + 3vw, 4rem);
}

.quotes {
	display: grid;
	gap: var(--space-7);
}

.quote {
	border-top: var(--border-width-strong) solid var(--blue-600);
	padding-top: var(--space-5);
}

.quote blockquote p {
	font-family: var(--font-display);
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--blue-800);
	text-wrap: pretty;
}

/*
 * The rotator's transition. Driven by a data attribute rather than a class, so
 * it cannot collide with the .reveal system's own hold on opacity. The
 * reduced-motion block already flattens every transition on the page, and the
 * rotator does not start at all in that case.
 *
 * Opacity plus a 10px rise — the entrance already defined in §19 and used by
 * .reveal, rather than a second motion idea invented for this one component.
 * The outgoing quote leaves upward and the incoming one arrives from below, so
 * the swap reads as a movement rather than a dissolve and is easier to notice
 * out of the corner of the eye.
 */
.js .quote {
	transition:
		opacity 500ms var(--ease-out),
		transform 500ms var(--ease-out);
}

.js .quote[data-state="out"] {
	opacity: 0;
	transform: translateY(-10px);
}

/* Repositioned below with no transition, so only the move back up animates. */
.js .quote[data-state="enter"] {
	opacity: 0;
	transform: translateY(10px);
	transition: none;
}

.quote figcaption {
	margin-top: var(--space-5);
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: var(--space-3);
	font-size: var(--fs-sm);
}

.quote__name {
	font-weight: 600;
	color: var(--ink-900);
}

.quote__org {
	color: var(--ink-600);
}

.quote__org::before {
	content: "— ";
}

.credentials {
	margin-top: clamp(2.5rem, 1.5rem + 3vw, 4rem);
	padding-top: var(--space-6);
	border-top: var(--border-width) solid var(--line);
	display: grid;
	gap: var(--space-5);
	justify-items: center;
	text-align: center;
}


/* ---------- FAQ ---------- */
.faq__inner {
	display: grid;
	gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
}

.faq__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
	text-wrap: balance;
	margin-bottom: var(--space-7);
}

.faq__title em {
	display: block;
	color: var(--ink-muted-on-ink);
}

.accordion {
	margin-top: var(--space-7);
}

.qa {
	border-bottom: var(--border-width) solid var(--line-on-dark);
}

.qa summary {
	display: flex;
	align-items: center;
	gap: var(--space-5);
	padding: var(--space-5) 0;
	font-size: var(--fs-md);
	font-weight: 600;
	line-height: var(--lh-snug);
	cursor: pointer;
	list-style: none;
	transition: color var(--duration-fast) var(--ease-out);
}

/* Thin white cross that becomes a minus when open — no icon font, no JS.
   currentColor ties the marker to the question, so both turn blue together. */
.qa summary::before {
	content: "";
	flex: none;
	width: 1rem;
	height: 1rem;
	background:
		linear-gradient(currentColor, currentColor) center/100% 2px no-repeat,
		linear-gradient(currentColor, currentColor) center/2px 100% no-repeat;
	transition: transform var(--duration-base) var(--ease-out);
}

.qa[open] summary::before {
	/* Drops the vertical stroke, leaving a minus. */
	background:
		linear-gradient(currentColor, currentColor) center/100% 2px no-repeat;
	transform: rotate(180deg);
}

/* The open question and its marker take the brand blue. */
.qa[open] summary {
	color: var(--blue-500);
}

.qa summary:hover {
	color: var(--blue-500);
}

.qa__body {
	padding: 0 0 var(--space-5) calc(1rem + var(--space-5));
}

.qa[open] .qa__body {
	animation: qa-open var(--duration-base) var(--ease-out);
}

@keyframes qa-open {
	from { opacity: 0; transform: translateY(-0.25rem); }
	to   { opacity: 1; transform: none; }
}

.qa__body p {
	color: var(--ink-muted-on-ink);
	font-size: var(--fs-base);
	max-width: 52ch;
}

.faq__media {
	position: relative;
}

.faq__media img {
	width: 100%;
	aspect-ratio: 3 / 4;
	object-fit: cover;
}

/* Solid panel overlapping the photograph — the approved site's device. */
.faq__panel {
	background-color: var(--blue-600);
	color: var(--ink-on-dark);
	font-size: var(--fs-md);
	font-weight: 600;
	line-height: var(--lh-snug);
	letter-spacing: var(--tracking-tight);
	padding: var(--space-5);
	max-width: 18rem;
	margin-top: calc(var(--space-8) * -1);
	margin-left: var(--space-5);
	position: relative;
}

/* ---------- CLOSING CTA ---------- */
/*
 * No top rule. This block was written to follow white content, where a
 * hairline earned its place; it now follows the near-black field, where the
 * surface change already separates and a grey line on top of it is noise.
 */
.cta {
	background-color: var(--surface);
}

/*
 * Centred, which section 7 reserves for closing-CTA copy and nothing else.
 *
 * 52rem, not the 44rem this was built with, and the arithmetic is the whole
 * point. This class shares its element with .container, so it inherits
 * padding-inline: 2rem, and box-sizing is border-box - so 44rem left 640px
 * of actual content for a heading that measures 717px on one line. It broke
 * to two lines and read as a decision rather than an accident. 52rem gives
 * 768px of content, which clears it with room to spare.
 *
 * The lead takes its own narrower measure below, so a single-line heading
 * over a tidy two-line sentence is a composition rather than one box
 * holding both.
 *
 * margin-inline was missing outright: this block was written for the
 * homepage closing section, orphaned when that section was removed on
 * request, and had been dead CSS ever since - so nothing ever rendered it
 * off-centre to notice.
 */
.cta__inner {
	max-width: 52rem;
	margin-inline: auto;
	text-align: center;
}

.cta__title {
	font-size: var(--fs-xl);
	line-height: var(--lh-snug);
	text-wrap: balance;
}

.cta__lead {
	margin-top: var(--space-5);
	font-size: var(--fs-md);
	line-height: var(--lh-loose);
	color: var(--ink-600);
	max-width: 34rem;
	margin-inline: auto;
}

/*
 * The same pair as the homepage's What we do band, deliberately. That one
 * sets gap: --space-4 and gives both buttons min-width: 13rem so they carry
 * the same footprint; this had --space-6 and let each size to its label, so
 * two buttons doing the same job at the same level looked like two
 * different kinds of control.
 */
.cta__actions {
	margin-top: var(--space-7);
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: var(--space-4);
}

/*
 * Both actions carry the same footprint, as on the homepage - but 15rem
 * rather than its 13rem. The homepage's two labels both fit inside 13rem
 * so they land on the floor and come out equal; these two measure 221px
 * and 235px, so at 13rem they would clear the floor and come out unequal.
 * 15rem is the next step that swallows the longer of the two, which is
 * 'Call +27 21 300 1230' and is not a label that should be shortened to
 * suit a width. If a longer label is ever added here, this needs raising
 * with it or the pair goes ragged again.
 */
.cta__actions .btn {
	min-width: 15rem;
}

/* ---------- FOOTER ---------- */
.site-footer {
	background-color: var(--ink-950);
	color: var(--ink-muted-on-ink);
	padding-block: var(--section-gap) var(--space-7);
	font-size: var(--fs-sm);
}

/*
 * Two columns, two rows on phones. Both breakpoints above override this — the
 * tablet block takes it to three and desktop to four — so these columns only
 * ever apply below 48rem.
 *
 * minmax(0, 1fr) rather than 1fr: a bare 1fr floors at the track's min-content
 * width, and the email address is a single unbreakable token wider than half a
 * 390px screen. That floor is what makes a grid child push its neighbour out of
 * the viewport, so the tracks are allowed to go under it and the two rules
 * below let the content give way instead.
 */
.site-footer__top {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: var(--space-7) var(--space-5);
	padding-bottom: var(--space-8);
}

.site-footer__heading {
	font-size: var(--fs-xs);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--tracking-wide);
	color: var(--ink-on-dark);
	margin-bottom: var(--space-4);
}

.site-footer__col ul {
	display: grid;
	gap: var(--space-3);
}

.site-footer__spaced {
	margin-top: var(--space-4);
}

/* break-word rather than anywhere: it only breaks a token that genuinely does
   not fit, and unlike anywhere it leaves min-content sizing alone, so the wider
   breakpoints still lay out from the real word widths. */
.site-footer a {
	color: var(--ink-muted-on-ink);
	overflow-wrap: break-word;
	text-decoration: none;
	border-bottom: var(--border-width) solid transparent;
	transition: color var(--duration-fast) var(--ease-out),
	            border-color var(--duration-fast) var(--ease-out);
}

.site-footer a:hover,
.site-footer a:focus-visible {
	color: var(--ink-on-dark);
	border-bottom-color: var(--ink-on-dark);
}

/*
 * The gap is the mobile value; --space-3 is restored from 48rem up.
 *
 * Three 44px targets plus two --space-3 gaps come to 156px, against a 151px
 * column on a 375px screen — four pixels short, on one of the commonest phone
 * widths. --space-2 brings it to 148px and they sit on one row. The targets
 * themselves stay at 44px; that is a Finsolve standard, not a variable.
 *
 * flex-wrap is the honest fallback below about 360px, where three targets do
 * not fit either way and wrapping beats overflowing.
 */
.site-footer__col .social {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
}

/* The list is now used on the contact page too, so the layout lives here
   rather than under .site-footer__col. That rule is more specific and still
   wins where the footer needs a tighter gap on a two-column phone layout. */
.social {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3);
}

.social a {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	border: var(--border-width) solid var(--line-on-dark);
	border-radius: 50%;
	transition: border-color var(--duration-fast) var(--ease-out),
	            background-color var(--duration-fast) var(--ease-out);
}

.social a:hover,
.social a:focus-visible {
	border-color: var(--ink-on-dark);
	background-color: rgba(255, 255, 255, 0.08);
}

.social svg {
	width: 1.125rem;
	height: 1.125rem;
	fill: var(--ink-muted-on-ink);
	transition: fill var(--duration-fast) var(--ease-out);
}

.social a:hover svg,
.social a:focus-visible svg {
	fill: var(--ink-on-dark);
}

/* --- On a light surface ---
 * The values above are tuned for the near-black footer: --line-on-dark is
 * white at 18% and --ink-muted-on-ink is near-white. Both vanish on the
 * contact page. --line-strong is the control-border token, 3.66:1 on white and
 * so meeting 1.4.11; the glyph takes --ink-600 at 7.61:1.
 */
.social--on-light a {
	border-color: var(--line-strong);
}

.social--on-light a:hover,
.social--on-light a:focus-visible {
	border-color: var(--ink-900);
	background-color: rgba(20, 24, 28, 0.05);
}

.social--on-light svg {
	fill: var(--ink-600);
}

.social--on-light a:hover svg,
.social--on-light a:focus-visible svg {
	fill: var(--ink-900);
}

.site-footer__bottom {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-4) var(--space-6);
	justify-content: space-between;
	align-items: center;
	padding-top: var(--space-6);
	border-top: var(--border-width) solid var(--line-on-dark);
	font-size: var(--fs-xs);
}


.site-footer__legal {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-5);
}


/* =========================================================
   7. Motion
   Content is visible by default. The .js class is added by
   main.js, so a script failure renders the complete page.
   ========================================================= */

.js .reveal {
	opacity: 0;
	transform: translateY(14px);
	transition:
		opacity var(--duration-slow) var(--ease-out),
		transform var(--duration-slow) var(--ease-out);
}

.js .reveal.is-visible {
	opacity: 1;
	transform: none;
}

.js .reveal[data-reveal-delay="1"] { transition-delay: 70ms; }
.js .reveal[data-reveal-delay="2"] { transition-delay: 140ms; }
.js .reveal[data-reveal-delay="3"] { transition-delay: 210ms; }
.js .reveal[data-reveal-delay="4"] { transition-delay: 280ms; }

@media (prefers-reduced-motion: reduce) {
	html {
		scroll-behavior: auto;
	}

	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		transition-delay: 0ms !important;
		scroll-behavior: auto !important;
	}

	.js .reveal {
		opacity: 1;
		transform: none;
	}

	.js .title-wipe__inner {
		transform: none;
	}

	/*
	 * The organogram's rules draw themselves in with a transform, and the
	 * universal rule above only kills the duration — so they are named here,
	 * the same way .title-wipe__inner is and for the same reason.
	 */
	.js .org__spine,
	.js .org__list--d0 > .org__node::before,
	.js .org__list--d0 .org__list::before,
	.js .org__list--d0 .org__list > .org__node::before {
		transform: none;
	}

	.js .hub__rule {
		transform: none;
	}

	.js .hub__rule::after {
		opacity: 1;
	}

	/* ::backdrop is not matched by the universal selector above, so the modal's
	   entrance has to be switched off by name. */
	.newsletter[open],
	.newsletter[open]::backdrop {
		animation: none;
	}

	/*
	 * EVERY HOVER SCALE ON THE SITE, NAMED. The universal rule above only kills
	 * the duration, so an unnamed scale still jumps — instantly, but it still
	 * moves, which is the thing being switched off.
	 *
	 * `.whatwedo__fig img` was never named at all before 2026-09-03, and a
	 * sibling entry named a class that did not exist — a selector that matches
	 * nothing fails silently, which is how both went unnoticed. Check the class
	 * exists before adding to this list.
	 *
	 * §6 of docs/HANDOVER.md has claimed since the homepage was signed off that
	 * reduced motion disables hover scales. It does now.
	 */
	.svc:hover .svc__img,
	.svc:focus-within .svc__img,
	.whatwedo__fig:hover img {
		transform: none;
	}
}


/* =========================================================
   8. Utilities
   ========================================================= */

.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.is-nav-open,
.is-dialog-open {
	overflow: hidden;
}


/* =========================================================
   9. Responsive
   Literal rem breakpoints — custom properties do not resolve
   inside media query conditions.
   ========================================================= */

@media (min-width: 40rem) {
	.hero__actions {
		flex-direction: row;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
	}


	.svc-grid {
		grid-template-columns: repeat(2, 1fr);
	}

	.quotes {
		grid-template-columns: repeat(2, 1fr);
	}
}

@media (min-width: 48rem) {
	/*
	 * The covered areas go two across. They are short blocks - a heading and
	 * either three lines or a sentence - so side by side they read as two
	 * halves of one service rather than a list of two things.
	 */
	.covers {
		grid-template-columns: repeat(2, 1fr);
		column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
		align-items: start;
	}

	:root {
		--container-pad: 2rem;
	}

	/* Room again, so the social row returns to the standard gap. The tighter
	   value below this width exists only to fit three targets on a phone. */
	.site-footer__col .social {
		gap: var(--space-3);
	}

	/* Name beside email, phone beside subject. Below this they stack, because
	   two fields sharing a phone's width is worse than a longer form. */
	.contact__row {
		grid-template-columns: 1fr 1fr;
	}

	/* Headline across the top; the left image steps down while the right
	   column carries its image and the copy beneath it. Offset, not
	   overlapping. */

	/* Near-square, so the stepped-down image reads as a considered crop
	   rather than a tall slab. */
	.whatwedo__fig--a img {
		aspect-ratio: 1 / 1;
	}

	.whatwedo__fig--b img {
		aspect-ratio: 3 / 2;
	}

	.faq__media img {
		aspect-ratio: 4 / 5;
	}

	/* The panel juts out past the image's left edge, as on the live site. */
	.faq__panel {
		position: absolute;
		left: 0;
		bottom: var(--space-7);
		transform: translateX(-22%);
		margin: 0;
	}
}

@media (min-width: 64rem) {

	/*
	 * Heading column left, photograph right - the approved page arrangement.
	 * Centred against each other rather than top-aligned: the image is taller
	 * than the heading and two paragraphs, and aligning their tops leaves the
	 * text hanging at the top of a tall picture.
	 *
	 * 64rem, and that matters. This lived in the 48rem block for a pass, so
	 * the masthead split into two columns from 768px up while the photograph
	 * that is meant to fill the second one is hidden below 1024px. Nothing
	 * showed it until the image came out at tablet and the text collapsed
	 * into a 323px column.
	 */
	.service-intro__inner {
		grid-template-columns: 5fr 7fr;
		column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
		align-items: center;
	}

	/*
	 * §7 pattern 2: explicit asymmetry, portrait in the narrow column. 4fr of
	 * the container is 337px at 1440, which the 18rem cap on the image holds
	 * to 288 — the rest is the gutter between the portrait and the words.
	 *
	 * align-items: start, not centre. The two biographies are four paragraphs
	 * and two; centred against a 360px portrait the shorter one floats and the
	 * longer one runs past it at both ends.
	 */
	.person {
		grid-template-columns: 4fr 8fr;
		column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
		align-items: start;
	}

	/*
	 * Promises left, photograph right. 7fr/5fr rather than the 5fr/7fr the
	 * service mastheads use, because here the text is the substance and the
	 * picture is support — seven lines need the room more than a 3:2 crop does.
	 */
	.profile-opening {
		grid-template-columns: 7fr 5fr;
		column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
		align-items: center;
	}

	/* Header switches from drawer to inline navigation */
	.nav-toggle {
		display: none;
	}

	.site-header__cta {
		display: flex;
	}

	/*
	 * A hero CTA carries a short label, so padding alone leaves it looking
	 * undersized against the headline. A minimum width gives it presence and
	 * a larger acquisition target (Fitts's law), while the 3.5rem height sits
	 * in the 48–60px band recommended for primary actions and clears WCAG
	 * 2.5.5's 44×44 comfortably. Desktop only for now.
	 */
	.hero__actions .btn {
		min-width: 16rem;
		min-height: 3.5rem;
		padding-inline: var(--space-8);
		font-size: var(--fs-md);
	}

	.site-header__inner {
		min-height: 5.25rem;
		gap: var(--space-7);
		padding-left: clamp(var(--container-pad), 3vw, 3rem);
	}

	.site-header__logo {
		height: 2.5rem;
	}

	/* Menu sits immediately right of the logo; the action block is what
	   pushes to the far edge. */
	.site-nav {
		position: static;
		width: auto;
		padding: 0;
		background: none;
		transform: none;
		visibility: visible;
		overflow: visible;
		flex-direction: row;
		align-items: center;
		transition: none;
	}

	.site-nav__head,
	.site-nav__drawer-actions {
		display: none;
	}

	/*
	 * The gap closes up between 64rem and 72rem, and only there.
	 *
	 * A second dropdown put a second 44px toggle in this row, and between the
	 * desktop breakpoint and about 1150px the row no longer fitted: measured at
	 * a 1017px content width the five items then present wanted 542px and had
	 * 498px, so flex shrank them and three labels broke onto a second line.
	 * Gaps at 32px were 128px of the row, so taking them to 20px bought back
	 * the 44px the toggle costs.
	 *
	 * The menu is four items since 2026-09-11 and no longer needs this, but it
	 * is kept: the row has to hold whatever the client adds next, and this is
	 * the rule that makes it able to.
	 *
	 * Solved rather than guessed: 20px at 1024, 32px from 1152 up, straight
	 * line between. Above 72rem this clamps to --space-6 and the signed-off
	 * header is the value it always was.
	 */
	.site-nav__list {
		flex-direction: row;
		gap: clamp(1.25rem, -4.75rem + 9.375vw, 2rem);
		border-top: 0;
	}

	.site-nav__link {
		padding: var(--space-2) 0;
		font-size: var(--fs-sm);
		font-weight: 500;
		color: var(--ink-900);
		border-bottom: var(--border-width-strong) solid transparent;
		transition: border-color var(--duration-fast) var(--ease-out),
		            color var(--duration-fast) var(--ease-out);

		/*
		 * A label that wraps turns a one-line header into a two-line one, which
		 * is what happened between 64rem and about 1200px once the second
		 * dropdown toggle was added.
		 */
		white-space: nowrap;
	}

	.site-nav__link:hover,
	.site-nav__link:focus-visible {
		color: var(--blue-600);
		border-bottom-color: var(--blue-600);
	}

	/* --- Submenu, on the desktop bar ---
	 *
	 * A panel that floats over the page rather than pushing the bar apart. It
	 * hangs from the item, so it needs the item positioned; the list is a row
	 * here, so the item stops being a grid and becomes an inline pair.
	 */
	.site-nav__item--has-sub {
		position: relative;
		display: flex;
		align-items: center;
		gap: 0;
	}

	/*
	 * 44x44 here too. It was 1.75rem, which looked right beside a 15px label
	 * and measured 28x28 — under the standard section 21 calls non-negotiable,
	 * on a control sitting next to a link it must not be mistaken for. The
	 * chevron inside stays small; the button around it is the target.
	 *
	 * The gap to the label goes with it, so the item grows by 12px rather than
	 * 16px and the two hit areas still do not touch.
	 */
	.site-nav__sub-toggle {
		width: 2.75rem;
		height: 2.75rem;
		color: var(--ink-900);

		/*
		 * And it may not shrink: flex measured it at 39x44 in that same range,
		 * against the 44x44 §21 calls non-negotiable.
		 */
		flex: none;
	}

	.site-nav__sub-toggle svg {
		width: 1rem;
		height: 1rem;
	}

	.site-nav__sub-toggle:hover,
	.site-nav__sub-toggle:focus-visible {
		color: var(--blue-600);
	}

	/*
	 * --z-high is the sticky header's own layer, and this panel lives inside
	 * it. --shadow-header is the one functional shadow in the system (section
	 * 5) and it does the same job here that it does for the header: separate a
	 * surface from the content it is sitting over.
	 */
	.site-nav__sub {
		position: absolute;
		top: calc(100% + var(--space-3));
		left: 0;
		min-width: 14rem;
		z-index: var(--z-high);
		padding: var(--space-2) 0;
		background-color: var(--surface);
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-sm);
		box-shadow: var(--shadow-header);
	}

	/*
	 * Bridges the gap between the label and the panel.
	 *
	 * The panel hangs --space-3 below the item, and that strip belongs to
	 * neither — so a pointer travelling from one to the other crosses ground
	 * that is outside both and fires mouseleave. A transparent extension of
	 * the panel over exactly that strip means the pointer stays inside the
	 * item the whole way, and the 200ms grace period in main.js is then a
	 * second line of defence rather than the only one.
	 */
	.site-nav__sub::before {
		content: "";
		position: absolute;
		inset: auto 0 100% 0;
		height: var(--space-3);
	}

	.site-nav__sub-link {
		padding: var(--space-3) var(--space-5);
		font-size: var(--fs-sm);
		color: var(--ink-900);
		border-bottom: 0;
		/* 44px including the padding, as everywhere else. */
		min-height: 2.75rem;
		display: flex;
		align-items: center;
	}

	.site-nav__sub-link:hover,
	.site-nav__sub-link:focus-visible {
		color: var(--blue-600);
		background-color: var(--blue-50);
	}


	.nav-backdrop {
		display: none;
	}

	/* The narrower left column keeps the stepped-down image from towering,
	   and gives the copy column room to breathe. */
	.whatwedo__inner {
		grid-template-columns: 5fr 7fr;
		column-gap: clamp(2rem, 1rem + 4vw, 4.5rem);
		/*
		 * No row gap. A gap pushes both columns down from the same baseline,
		 * which shrinks the step between the two figures — the stagger is the
		 * point of this composition. The clearance is applied to the right
		 * column alone, and the left figure adds the step on top of it.
		 *
		 * The heading's glyphs stop 15px short of the image column, so they
		 * never truly overlap; the pair simply read as cramped when their
		 * edges sit at the same height.
		 */
		row-gap: 0;
		align-items: start;

		--wwd-clearance: var(--space-7);           /* heading to right figure */
		/*
		 * Deeper than the 6vw this was while the copy column held two short
		 * paragraphs. The column is much longer now, and the left column holds
		 * only the photograph, so the leftover space has to go somewhere.
		 * Above the figure it is the stagger — the point of the composition.
		 * Below it, it is just a hole at the end of the section.
		 */
		--wwd-stagger: clamp(2.5rem, 12vw, 11rem); /* right figure to left    */
	}

	/* Clears the heading. */
	.whatwedo__col {
		margin-top: var(--wwd-clearance);
	}

	/* Clears the heading, then steps down again past the right figure. */
	.whatwedo__fig--a {
		margin-top: calc(var(--wwd-clearance) + var(--wwd-stagger));
	}

	/*
	 * 4:5 rather than square. The source is 900×1155, so this is very close to
	 * the photograph as shot instead of a heavy centre crop, it is the
	 * portrait ratio the design system already specifies (§15), and the taller
	 * frame absorbs part of the second column's much longer copy.
	 */
	.whatwedo__fig--a img { aspect-ratio: 4 / 5; }

	.whatwedo__intro { grid-column: 1 / -1; grid-row: 1; }
	.whatwedo__title { max-width: 32ch; }

	/*
	 * Four across, one row. Six cards divided evenly by three; four would
	 * leave a single orphan on a second row. Putting all four in one line also
	 * lets 01–04 be read straight across, which is the only thing that earns
	 * the numbering its place.
	 */
	/*
	 * Details left, form right — the approved page's arrangement, at the same
	 * 5fr/7fr the What We Do band uses, so the two pages share a proportion
	 * rather than each inventing one.
	 */
	.contact__inner {
		grid-template-columns: 5fr 7fr;
		column-gap: clamp(2.5rem, 1.5rem + 4vw, 4.5rem);
		align-items: start;
	}

	/* Two panels, photograph left, in the shape of the approved modal. */
	.newsletter {
		width: min(56rem, 92vw);
	}

	.newsletter[open] {
		grid-template-columns: 5fr 7fr;
	}

	.newsletter__media {
		display: block;
		position: relative;
	}

	.newsletter__media img {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.newsletter__body {
		padding: var(--space-8) var(--space-7) var(--space-7);
	}

	/*
	 * THREE ACROSS, NOT FOUR. The homepage band went from four services to six
	 * on 2026-09-14, and four columns leave six cards as a full row and an
	 * orphaned pair — the grid stops reading as a set. Three gives two even
	 * rows.
	 *
	 * The column count is the whole change: same card, same photograph
	 * treatment, same hover and focus states, same reveal. Below this
	 * breakpoint nothing moves at all — tablet was already two across and the
	 * phone one, and six divides into both.
	 */
	.svc-grid {
		grid-template-columns: repeat(3, 1fr);
	}

	/*
	 * Two across, not four.
	 *
	 * Four works on the homepage because that band is a summary inside a
	 * longer page. This page IS the services, so the cards get the room the
	 * approved site gives them - half the viewport each, big enough that the
	 * photograph does work rather than sits behind the text. Six of them then
	 * read as three rows rather than a strip.
	 *
	 * Two classes deep so it wins over .svc-grid whatever the source order.
	 */
	.svc-grid.svc-grid--index {
		grid-template-columns: repeat(2, 1fr);
	}

	/* Wider cards need the height back, or they read as letterboxes. */
	.svc-grid--index .svc {
		min-height: clamp(22rem, 28vw, 30rem);
	}

	/* 34ch was tuned for a quarter-width card; these are half-width. */
	.svc-grid--index .svc__desc {
		max-width: 46ch;
	}

	/*
	 * Height held rather than tuned. This was raised when the cards went from
	 * a third of the viewport to a quarter and the copy ran a couple of lines
	 * longer; they are back to a third as of 2026-09-14, so the height is now
	 * generous rather than needed. It stays: it is what keeps the photograph a
	 * photograph instead of a sliver behind the text, which is the whole idea
	 * of the card.
	 */
	.svc {
		min-height: clamp(24rem, 26vw, 30rem);
	}

	.faq__inner {
		grid-template-columns: 7fr 5fr;
		align-items: center;
	}

	/*
	 * The footer runs the full width, like the header, hero, What We Do and
	 * services above it — the page had three different left edges and this
	 * removes one of them. The gutter is --container-full's rather than a new
	 * value, so the footer shares its edge with the What We Do band instead of
	 * inventing a fourth.
	 */
	.site-footer > .container {
		max-width: none;
		padding-inline: clamp(1.5rem, 4vw, 4.5rem);
	}

	/*
	 * Content-width columns spread to the edges, not four equal 1fr tracks.
	 * Across the full viewport an equal split hands each column ~400px to hold
	 * three short links, and the copy ends up marooned in the middle of its
	 * track. Sizing to content and letting space-between do the distribution
	 * anchors the first column to the left edge and the last to the right, so
	 * the width reads as deliberate rather than stretched.
	 */
	.site-footer__top {
		grid-template-columns: repeat(4, auto);
		justify-content: space-between;
		gap: var(--space-7);
	}
}

/* ---------------------------------------------------------
   Below the desktop breakpoint both sections collapse to one
   column, and a portrait ratio in a wide single column renders
   taller than the screen — at 1023px the FAQ photograph reached
   1199px, about 1.3 screens.

   These are deliberately max-width, against the mobile-first
   grain of the rest of the sheet, so the signed-off desktop
   layout above 64rem is untouched by construction rather than
   by a long list of resets.
   --------------------------------------------------------- */

@media (max-width: 63.9375rem) {

	/* --- What we do: the copy sits on the second image --- */

	/* The band is sized by its text, so it can never become a
	   screen of photograph on its own. */
	.whatwedo__col {
		position: relative;
		isolation: isolate;
		overflow: hidden;
		border-radius: var(--radius-md);
	}

	.whatwedo__fig--b {
		position: absolute;
		inset: 0;
		z-index: var(--z-behind-deep);
		border-radius: 0;
	}

	.whatwedo__fig--b img {
		height: 100%;
		max-height: none;
		aspect-ratio: auto;
	}

	/* No hover zoom while it is acting as a backdrop. */
	.whatwedo__fig--b:hover img {
		transform: none;
	}

	/*
	 * Not --scrim-card here. That gradient reaches only 0.34 at its top edge,
	 * which was fine while this band held two short paragraphs and the copy sat
	 * in its dark lower half. The band is sized by its text, so the longer copy
	 * stretched the ramp and left the first paragraph on 3.11:1 at 882px and
	 * 2.85:1 at 504px — both below 4.5:1.
	 *
	 * A scrim whose coverage depends on how much copy it happens to be carrying
	 * is the actual bug. This one stays dark across the whole band and only
	 * lifts at the very top, so contrast holds at any length.
	 */
	.whatwedo__col::after {
		content: "";
		position: absolute;
		inset: 0;
		z-index: var(--z-behind);
		background: linear-gradient(
			to top,
			rgba(11, 11, 11, 0.96) 0%,
			rgba(11, 11, 11, 0.88) 55%,
			rgba(11, 11, 11, 0.74) 100%);
	}

	.whatwedo__body {
		padding: var(--space-8) var(--space-6);
	}

	.whatwedo__col .whatwedo__copy {
		color: rgba(255, 255, 255, 0.92);
	}

	.whatwedo__col .btn--ghost {
		border-color: rgba(255, 255, 255, 0.55);
		color: var(--ink-on-dark);
	}

	.whatwedo__col .btn--ghost:hover,
	.whatwedo__col .btn--ghost:focus-visible {
		border-color: var(--ink-on-dark);
		background-color: rgba(255, 255, 255, 0.12);
	}

	/*
	 * Both of these earn their place on desktop, where they sit beside the copy
	 * as one composition. Stacked into a single column they stop being part of
	 * that composition and become full-width interruptions between blocks of
	 * text, so they come out below the desktop breakpoint.
	 *
	 * Note they are still downloaded. loading="lazy" does not save the request
	 * for a display:none image — with no box there is nothing to intersect, so
	 * the browser fetches it immediately. Measured: both whatwedo-1.jpg and
	 * faq.jpg appear in the resource timing at 882px and 504px, about 450KB
	 * that is never drawn. Fixing that needs <picture> with a
	 * media="(min-width: 64rem)" source, which is markup and would touch
	 * desktop; logged in docs/HANDOVER.md instead of done here.
	 *
	 * This takes the FAQ panel with it: the "Protecting Your Legacy" line is
	 * positioned inside .faq__media. Flagged in docs/HANDOVER.md — that copy is
	 * Planning-brand and due to be replaced anyway.
	 *
	 * The sizing rules these two used to need are gone with them rather than
	 * left behind as dead weight; the figure that remains, .whatwedo__fig--b,
	 * is the copy band's backdrop and is unaffected.
	 */
	.whatwedo__fig--a,
	.faq__media {
		display: none;
	}

	/*
	 * And the service masthead photograph, on request. Unlike the two above
	 * it is not still being downloaded: its <picture> resolves to a 1x1 data
	 * URI below this width, so hiding the box here costs nothing on a phone.
	 */
	.service-intro__media,
	.profile-opening__media {
		display: none;
	}


	/* --- Contact page --- */

	/*
	 * Form first once the two stack. Side by side the details column is a
	 * glance to the left of the form; stacked it is 415px of address and
	 * social links that everyone scrolls past to reach the thing the page is
	 * named for.
	 *
	 * Source order is unchanged, so above 64rem details still leads and the
	 * signed-off arrangement is untouched. The cost is here: below 64rem the
	 * tab order runs details-then-form while the eye sees form-then-details.
	 * Two whole labelled regions rather than interleaved controls, and both
	 * carry an <h2> so heading navigation is unaffected — but it is a real
	 * 2.4.3 mismatch and it is recorded in docs/HANDOVER.md, not hidden.
	 */
	.contact__form-panel {
		order: -1;
	}

	/*
	 * And the stagger with it. The 70ms belongs to whichever of the two is
	 * second, so leaving the attributes alone would reveal the top block after
	 * the one beneath it. Same specificity as the delay rules in §7 Motion and
	 * later in the file, so it wins without !important.
	 */
	.js .contact__form-panel[data-reveal-delay="1"] {
		transition-delay: 0s;
	}

	.js .contact__details {
		transition-delay: 70ms;
	}

	/*
	 * Where the office is, beside the three ways of reaching it.
	 *
	 * Sized from measurement, not halves. The address needs 176px for its
	 * longest line and the reach column 158px for the phone number at
	 * --fs-md; at 390px there are 342px to divide, so something has to give.
	 * minmax(max-content, 1fr) on the second column means the give never
	 * comes out of the phone number — it holds its 158px and the address
	 * column absorbs the squeeze, where a wrapped line costs nothing. Below
	 * about 375px "Frazzitta Business Park" takes a second line; that was
	 * accepted in review as the price of two columns on a phone.
	 *
	 * Both columns are 1fr once there is room, so by tablet this is an even
	 * split rather than a content-hugging one — which is the complaint it
	 * exists to answer.
	 */
	.contact__details-grid {
		display: grid;
		grid-template-columns: minmax(0, 1fr) minmax(max-content, 1fr);
		column-gap: clamp(1rem, 0.4rem + 2.5vw, 2rem);
		align-items: start;
	}

	/*
	 * The 24px that separated the email from the address in one column would
	 * push the whole reach column down beside it in two. Grid items do not
	 * collapse margins outward, so this has to be zeroed rather than left to
	 * cancel itself.
	 */
	.contact__reach .contact__email {
		margin-top: 0;
	}
}

@media (min-width: 48rem) and (max-width: 63.9375rem) {
	/*
	 * Hours, office and links are reference material and read as a row of
	 * three. Get in touch is the only block that is a next step rather than a
	 * fact, so it takes its own line beneath them, centred — principle 2,
	 * favour the treatment that makes contact feel closer.
	 *
	 * Content-width columns spread edge to edge, matching the desktop footer
	 * and the reference layout: first block flush left, last flush right,
	 * text left-aligned inside each. Equal thirds would leave the narrow Links
	 * column stranded mid-track.
	 *
	 * No rule above the centred block. The row gap already separates it, and a
	 * second hairline this close to the one above the legal line made the
	 * footer read as three stacked bands rather than one.
	 */
	.site-footer__top {
		grid-template-columns: repeat(3, auto);
		justify-content: space-between;
		gap: var(--space-8) var(--space-6);
	}

	.site-footer__col:last-child {
		grid-column: 1 / -1;
		text-align: center;
	}

	.site-footer__col:last-child .social {
		justify-content: center;
	}

	/*
	 * What we do: centre the pair.
	 *
	 * Asked for on 2026-09-08, and the reason it looks wrong here and not at
	 * the other two sizes is the width of the band rather than the buttons.
	 * On desktop the copy runs the full column and the actions sit under the
	 * text they belong to; on a phone the two buttons stack and fill most of
	 * a 326px row. At tablet the band is 723px wide, the pair measures about
	 * 440px, and every one of the remaining 280px falls on the right — so the
	 * cluster reads as pushed into a corner of the photograph rather than
	 * placed.
	 *
	 * Tablet only. Desktop is signed off and mobile was not raised, so this
	 * is scoped to the band where the problem is rather than applied to the
	 * component and then undone twice.
	 */
	.whatwedo__actions {
		justify-content: center;
	}
}
