/* ==========================================================================
   Quantum PT & Chiro — global stylesheet
   ==========================================================================

   Loaded on every screen. Screen-specific styles live in assets/css/pages/.

   Contents
   1. Design tokens (and the adjusted fallback faces the font tokens name)
   2. Reset & base
   3. Typography
   4. Layout (container, sections, grids)
   5. Utilities
   6. Buttons & links
   7. Small components (icons, badges, chips, pill forms, stars, script note)
   8. Header (top bar, masthead, primary navigation, language switcher)
   9. Page hero (inner screens)
   10. Section heading & intro
   11. Info banner & CTA band
   12. Footer
   13. Entry content & generic templates
   14. Modal
   15. Photograph viewer (lightbox)
   16. Per-screen token overrides

   Breakpoints (mobile-first, same values as Bootstrap 5):
   sm 576px · md 768px · lg 992px · xl 1200px · xxl 1400px
   ========================================================================== */


/* 1. Design tokens (and the adjusted fallback faces the font tokens name)
   ========================================================================== */

/*
 * The fallback faces, with their metrics corrected.
 *
 * While the real font is in flight the browser paints the text with a system one
 * (`font-display: swap`). They do not measure the same, so when ours arrives the
 * lines rewrap and the whole page jumps. These faces are not fonts: they are
 * Arial — or Roboto on Android — scaled to take up the same room as ours.
 *
 * The values were chosen by measurement, not by ratio, on 29 Sep 2026: eight
 * screens (Terms, Privacy, Services, About, FAQ, Contact, Home, one article) on
 * Lighthouse's own phone (412x823 at 1.75) and desktop (1350x940), cold cache,
 * with the shift read from the Chrome trace exactly as Lighthouse reads it, and
 * every candidate served in flight instead of edited here. The tool is
 * `fuentes-cls-barrido.mjs`, kept outside this repository. Sum of CLS over the
 * 16 loads:
 *
 *   before (Inter 107, Poppins bold 104 from Arial regular)   0.516
 *   after  (Inter 108, Poppins bold 106 from Arial Bold)      0.047
 *   Terms on the phone 0.266 -> 0.001 · Privacy 0.139 -> 0.001
 *
 * Inter is a knife edge and the reason it is 108: at 103–107 one line near the
 * top of Terms breaks differently in the fallback and the document drops when
 * Inter lands (0.26 on the phone); 108–110 all hold. 108 was the best of those.
 *
 * Poppins bold now starts from Arial *Bold*. From Arial regular the browser
 * paints thin letters in a face declared as bold (no synthesis happens, the face
 * already claims the weight), and no percentage makes thin letters take up the
 * room of bold ones across every heading. 106 was the top of the range tried and
 * the best; the 400–500 face was left at 112 because nothing in 108–116 moved.
 *
 * Only Arial could be measured (a Mac has no Roboto). On Android the same
 * percentages apply to Roboto and the fit will be looser.
 *
 * Caveat is deliberately left without an adjusted fallback: the handwritten note
 * is two lines, and swapping its fallback for Arial would fix a shift that
 * barely exists at the cost of losing the script letterform, which is the only
 * thing that matters there.
 *
 * Safari did not understand `size-adjust` until 17 (2023): for that 6% of visits
 * the fallback is unscaled Arial.
 */
@font-face {
	font-family: "Inter fallback";
	src: local("Arial"), local("Roboto"), local("Helvetica Neue");
	size-adjust: 108%;
}

@font-face {
	font-family: "Poppins fallback";
	src: local("Arial"), local("Roboto"), local("Helvetica Neue");
	font-weight: 400 500;
	size-adjust: 112%;
}

@font-face {
	font-family: "Poppins fallback";
	src: local("Arial Bold"), local("Arial-BoldMT"), local("Roboto Bold"), local("Roboto-Bold"), local("Helvetica Neue Bold");
	font-weight: 600 900;
	size-adjust: 106%;
}

:root {
	/*
	 * Brand blues
	 *
	 * The brand blue, and there is ONE: #004096, the value the guide gives
	 * (Brand-Guidelines.pdf, page 20 — Pantone 288C). There used to be three
	 * near-identical blues doing three different jobs: #0c2b6f on headings,
	 * #013d9d on the top bar and #0b3a8c on the rule beside a heading. Their
	 * colour distances from the guide's were 8.0, 1.8 and 2.3 — the last two
	 * invisible unless you put them side by side. None of them was the brand's.
	 *
	 * The three names still exist because each one says WHERE the colour is used,
	 * which is information the hex does not carry. But the value comes from one
	 * place: changing --color-brand-blue changes all three at once, and there is
	 * no way for them to drift apart again without someone deciding to.
	 *
	 * What was NOT merged, because it really is distinct — with each one's ΔE2000
	 * distance from the brand blue, which is what says why it earns its own value:
	 *
	 *   --color-primary      4.6   buttons and the focus ring
	 *   --color-navy-soft    6.0
	 *   --color-slate-blue  14.4
	 *   --color-ink         16.4   headline text
	 *   --color-nav         16.7   the menu, far darker
	 *   --color-link        17.2   links
	 *   --color-sky         51.0   exists precisely to be seen ON the dark blue
	 *
	 * Keep this table if any of them is ever revisited: the README records only the
	 * range, and a range cannot tell you which token you are about to collapse.
	 */
	--color-brand-blue: #004096;   /* the brand guide, Pantone 288C */

	--color-ink: #0f1f33;          /* primary headline text */
	--color-navy: var(--color-brand-blue);        /* inner-page card titles, names and badges */
	--color-accent-deep: var(--color-brand-blue); /* the supporting line by a title — eyebrows, subtitles, title accents — and .link-phone */
	/*
	 * Eyebrows share one color on every screen (Home uses .eyebrow--on-dark).
	 * Resolved here, on :root, so per-route re-pointing of --color-navy
	 * (Services) does not change it.
	 *
	 * Moved from --color-navy to --color-accent-deep. The two differ by an amount
	 * almost nobody can see, and they never appear in the same block — a section
	 * carries an eyebrow or a subtitle, never both — so the difference could
	 * never register as deliberate. It only showed up as the same kind of line
	 * looking slightly unlike itself between Careers and About.
	 *
	 * Navy keeps the surfaces it was measured for: card titles, names, badges.
	 * Accent-deep now covers the one role it always described, the supporting
	 * line next to a title, whichever side of it that line sits.
	 */
	--color-eyebrow: var(--color-accent-deep);
	--color-primary: #0a47b5;      /* primary actions, accent headline text */
	--color-primary-hover: #083a95;
	/*
	 * Anything clickable: arrow links, soft outline buttons, line icons.
	 *
	 * It was #2b6aee, and it was verified against white ONLY (4.77:1). On the
	 * blue-tinted surfaces — the banners (#eef5fd), the active pills, the icon
	 * tiles (#e5eff9) — it fell to 4.35 and 4.10, below the minimum. This one
	 * gives 5.44 on white, 4.95 on the banner tint and 4.67 on the darkest: it
	 * passes on all three. It is 3.6 of colour distance from its predecessor,
	 * which means you notice by comparing them, not by browsing.
	 *
	 * The lesson, the same one the orange taught: a colour is not verified until
	 * it is measured against EVERY background it gets painted on.
	 */
	--color-link: #2862db;
	--color-link-hover: #1f55c4;
	--color-link-border: #6ca5ee;  /* decorative border only */
	--color-nav: #0b1349;          /* header menu, payment strip labels, home card titles */
	--color-nav-current: #1e45b0;
	--color-slate-blue: #516a97;   /* secondary copy: taglines, captions, value labels */
	--color-navy-soft: #283c73;    /* patient quote, closing script tagline */
	--color-topbar: var(--color-brand-blue);
	--color-hero: var(--color-brand-blue); /* home hero panel */
	--color-sky: #7fd0f8;          /* accents on dark blue */
	--color-sky-strong: #62c4f5;

	/*
	 * Accents
	 *
	 * The site's orange, and there is ONE. Four used to be in circulation: the
	 * brand guide's (#F58526), the logo file's (#F28226), the one the
	 * documentation wrote (#f28126) and the one the button painted (#ec7118). No
	 * two were the same and nobody had decided between them.
	 *
	 * #ec7118 became #e46e0b on 22 September 2026, on this basis: it gives 6% more
	 * contrast (3.21:1 against white, up from 3.03:1) and it is the CLOSEST to the
	 * orange everyone knows without reading as dark. The colour distance between
	 * the two is 2.0: side by side they are hard to tell apart.
	 *
	 * Two darker steps were tried first — #d6670a (3.62:1) and #dc6b0a (3.41:1) —
	 * applied to the real screens, and both read as dull over the hero blue and in
	 * the closing band. The decision was the practice's, with the whole measured
	 * scale in front of them.
	 *
	 * It does not reach the 4.5 the standard asks for regular text, and that is
	 * accepted knowingly: no orange that still reads as orange does. The first in
	 * the family that passes is #bd5b09, and by then it is burnt earth. What this
	 * tone DOES pass is anything set large — from 24px, or 18.7px bold — where the
	 * threshold is 3:1.
	 *
	 * The hover (#cd6109, 3.96:1) keeps the same visual step the previous pair
	 * had; it does not pass either, for the same reason.
	 */
	--color-orange: #e46e0b;       /* 3.21:1 against white — see the note above */
	--color-orange-hover: #cd6109; /* 3.96:1 */
	--color-star: #f6a60d;

	/* Neutrals */
	--color-text: #4b5563;
	--color-muted: #6b7280;
	/*
	 * Only for a message that says something did not work. Weighted to match
	 * --color-muted against the page (6.24:1 vs 6.49:1 on the palest surface),
	 * so it reads as the same size of remark and differs only in hue — a red
	 * that shouts would outrank the content it interrupts. Nowhere near the
	 * brand orange in hue (357° against 27°), which is a call to action.
	 */
	/*
	 * The two semantic states, as a matched pair. Neither is a brand colour — a
	 * status has to read as a status — but they are balanced against each other so
	 * one does not shout louder: 7.26:1 and 7.13:1 on white, 0.13 apart, and their
	 * tints sit at 1.074 and 1.072 against a white card. Anything added here later
	 * (a warning amber, say) should be measured into the same range.
	 */
	--color-error: #a4262c;
	--color-error-tint: #fdf5f5;   /* pale wash behind the form's error summary. Fields use the border alone */
	--color-success: #166534;
	--color-success-tint: #f1f9f4;
	--color-white: #fff;
	--color-surface-soft: #f4f9fd;  /* tinted sections */
	--color-surface-tint: #eef5fd;  /* banners, active pills */
	--color-surface-chip: #ecf3fa;
	--color-icon-bg: #e6eff9;
	--color-border: #e3ebf4;
	/*
	 * The outline of something you can type in or choose from. Separate from
	 * --color-border because the two answer to different rules: a card's border is
	 * decoration, while a control's border is what tells a visitor the control is
	 * there, and WCAG 1.4.11 asks 3:1 of that against both neighbours. This clears
	 * it on both sides — 3.37:1 against a white card, 3.18:1 against the field's own
	 * --color-surface-soft fill. Not for decorative borders; they stay on the line above.
	 */
	--color-border-control: #7b8da9;
	--color-footer: #142434;
	--color-footer-text: #c3ccd8;

	/* Typography */
	--font-heading: "Poppins", "Poppins fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
	--font-body: "Inter", "Inter fallback", system-ui, -apple-system, "Segoe UI", sans-serif;
	--font-script: "Caveat", "Segoe Script", cursive;

	/*
	 * Glow for a handwritten note sitting over a hero photo. drop-shadow (not text-shadow)
	 * so it also reaches the swoosh svg and the credit line. It follows the strokes, unlike
	 * the radial backdrop it replaced, which read as a white disc over a dark photo and
	 * changed size with the number of lines. Hero photos are still picked so the note lands
	 * on a bright, low-detail corner, as the mockups do.
	 */
	--script-glow: drop-shadow(0 0 3px rgb(255 255 255 / 0.95)) drop-shadow(0 0 9px rgb(255 255 255 / 0.7));

	--fs-xs: 0.8125rem;   /* 13px */
	--fs-sm: 0.875rem;    /* 14px */
	--fs-base: 1rem;      /* 16px */
	--fs-md: 1.125rem;    /* 18px */
	--fs-lg: 1.25rem;     /* 20px */
	--fs-h2: clamp(1.75rem, 1.3rem + 1.4vw, 2.375rem);
	--fs-display: clamp(2.25rem, 1.35rem + 2.9vw, 3.6rem);

	/* Spacing scale */
	--space-1: 0.25rem;
	--space-2: 0.5rem;
	--space-3: 0.75rem;
	--space-4: 1rem;
	--space-5: 1.25rem;
	--space-6: 1.5rem;
	--space-8: 2rem;
	--space-10: 2.5rem;
	--space-12: 3rem;

	/*
	 * A hero's vertical breathing room, top and bottom, in one place. Two rules
	 * read it: .page-hero__inner and .home-hero__inner, which are the same piece
	 * of the page seen twice. Until 22 September 2026 each carried its own number
	 * — 2.25rem/2rem for the inner screens and space-12 for the home page — which
	 * is how the home page ended up with more room than the rest without anyone
	 * deciding it should.
	 */
	--hero-padding-block: 2.25rem 2rem;

	/*
	 * And below 992px, where the hero photograph stops sitting beside the text and
	 * goes above it, that photograph's height: it is what decides how far down the
	 * text starts. Same story as the padding — the home page carried 260 and the
	 * rest 240, so its first line landed 20px lower than on every other screen.
	 */
	--hero-media-height: 240px;

	/*
	 * And which gradient fades that photograph into the background. Two of them,
	 * because the photograph moves: below 992px it sits above the text and fades
	 * downwards; above, it sits beside it and fades to the left.
	 *
	 * The horizontal ramp has five stops, not three. Measured on 22 September 2026
	 * by swapping one for the other on both screens: over the home page's blue the
	 * difference touches 14.6% of the pixels at an average intensity of 44/765,
	 * and over the near-white background of the others 10.8% at 19/765 — less than
	 * half. Which is to say: the soft ramp matters where the background is dark
	 * and costs nothing where it is light, so it serves both.
	 *
	 * The exception is the Locations hero, which carries the illustrated map
	 * rather than a photograph and cancels the mask on purpose — its own comment
	 * explains why: a long fade ate the westernmost pins.
	 */
	--hero-mask-vertical: linear-gradient(180deg, #000 60%, transparent 100%);
	--hero-mask-horizontal: linear-gradient(90deg, transparent 0%, rgb(0 0 0 / 0.12) 12%, rgb(0 0 0 / 0.5) 24%, rgb(0 0 0 / 0.85) 36%, #000 46%);

	/*
	 * And the photograph's width once the screen is wide: the share it had at
	 * 1344px, the last width at which the page container still grows. From 1400
	 * the photograph stops growing and stops at the content (see .page-hero__media
	 * there), so this is its one size on every wide screen: 806px on an inner
	 * hero (60%), 860 on the home page (64%).
	 */
	--hero-frame: calc(var(--container-max) + var(--gutter) * 2);

	/*
	 * And the width of the hero left over on each side of the page container,
	 * once the container stops growing. A percentage, so it is only meaningful
	 * where the percentage is of the hero: an absolutely positioned child's
	 * right, or the hero's own background-size.
	 */
	--hero-outside: calc((100% - var(--container-max)) / 2);

	/*
	 * And how far inside the photograph's right edge the handwritten note stops
	 * once that edge is the content's: nothing while the photograph runs to the
	 * window, 1.5rem from 1400, so the note does not end on the photograph's cut.
	 */
	--hero-note-clearance: 0px;

	--section-space: clamp(1.75rem, 1.4rem + 1vw, 2rem); /* mockups: ~24–32px above/below section content */

	/* Shape & depth */
	--radius-sm: 8px;
	/*
	 * Photo tiles in a gallery or strip. Slightly tighter than --radius, and
	 * already the shared value before it had a name: Testimonials' photo
	 * gallery, About's locations strip and Home's city tiles each carried a raw
	 * 10px of their own. Careers made a fourth, so it earned a token.
	 */
	--radius-tile: 10px;
	--radius: 12px;
	--radius-lg: 16px;
	--radius-pill: 999px;

	/*
	 * Distance from a .pill-form's left edge to where its placeholder text starts:
	 * border + padding + the icon's margin and width + the gap and the input's own
	 * padding. Anything that sits under the field and has to line up with the text
	 * rather than the pill's edge uses this (see .location-geo).
	 */
	--pill-text-inset: calc(6px + var(--space-4) + 22px + var(--space-2) + var(--space-1));
	--shadow-sm: 0 2px 10px rgb(16 42 92 / 0.06);
	--shadow: 0 10px 30px rgb(16 42 92 / 0.09);
	--shadow-lg: 0 18px 40px rgb(16 42 92 / 0.14);

	/* Layout */
	--container-max: 1280px;
	--gutter: 1rem;
	/* Distance from a full-width section's edge to the page container. Resolved where it is used, against the
	   nearest size container (hero sections declare container-type: inline-size). */
	--container-edge: max(var(--gutter), (100cqw - var(--container-max)) / 2);

	--transition: 180ms ease;
}

@media (min-width: 768px) {
	:root { --gutter: 1.5rem; }
}

@media (min-width: 1200px) {
	:root { --gutter: 2rem; }
}

@media (min-width: 1400px) {
	:root { --hero-note-clearance: 1.5rem; }
}


/* 2. Reset & base
   ========================================================================== */

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

html {
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
	scroll-behavior: smooth;
	scroll-padding-top: 1.5rem;

	/*
	 * Mobile browsers paint rgba(51, 181, 229, 0.4) over whatever is tapped: a
	 * cyan rectangle belonging to no palette here, ignoring the element's own
	 * border radius, on every link, button, menu toggle and language switch.
	 *
	 * Turning it off is a promise that each touch target answers a tap on its
	 * own, so every interactive element in this file carries a :hover or
	 * :active of its own — check that before adding a new one.
	 */
	-webkit-tap-highlight-color: transparent;
}

@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;
	}
}

body {
	margin: 0;
	font-family: var(--font-body);
	font-size: var(--fs-base);
	line-height: 1.6;
	color: var(--color-text);
	background: var(--color-white);
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
}

.has-open-nav body {
	overflow: hidden;
}

img,
svg,
video {
	display: block;
	max-width: 100%;
}

img {
	height: auto;
}

a {
	color: var(--color-primary);
	text-decoration: none;
	transition: color var(--transition);
}

a:hover {
	color: var(--color-primary-hover);
}

/*
 * A phone number on a light card. Colour alone does not mark it as a control:
 * against the grey values it sits beside in those lists it measures 1.58:1,
 * short of the 3:1 that lets a colour difference carry that job on its own
 * (WCAG 1.4.11), and the icon next to it reads "phone", not "link". So it
 * underlines on hover and on keyboard focus, the same 3px offset the top bar
 * and the footer use for the same reason.
 *
 * Shared because the location card, the contact sidebar and the map popup on
 * Locations (built in assets/js/locations-map.js) are the same situation; they
 * had drifted into separate rules with different colours.
 */
.link-phone {
	font-weight: 500;
}

/*
 * Qualified with the tag so it outranks the text colour a page's own list rule
 * sets on the same element — the sidebar list paints every value grey, and a
 * bare .link-phone loses to it on source order alone. It is only ever an anchor.
 */
a.link-phone {
	color: var(--color-accent-deep);
}

a.link-phone:hover,
a.link-phone:focus-visible {
	text-decoration: underline;
	text-underline-offset: 3px;
}

/*
 * A card title that is also the link to its page: the location cards and the
 * article cards. The title is the link because Google reads a link's visible
 * text, and its own link guide gives "Read more" as anchor text too generic to
 * say anything; the card's button stays as the visible call to action.
 *
 * It keeps the title's colour rather than the link blue, so the card looks as
 * it did, and shows itself as a link the way .link-phone does: underlined on
 * hover and on keyboard focus. Qualified with the tag for the same reason too,
 * to outrank the global a:hover.
 */
a.link-title {
	color: inherit;
}

a.link-title:hover,
a.link-title:focus-visible {
	color: inherit;
	text-decoration: underline;
	text-underline-offset: 3px;
}

/*
 * A two-tone focus ring: a white band against the element, then the dark blue.
 *
 * It used to be a single --color-sky-strong ring, which measures **1.96:1 against
 * white** and 1.75:1 against the chip surface — WCAG 1.4.11 asks 3:1 of a focus
 * indicator, so on most of this site a keyboard user could barely see where they
 * were. That colour was presumably picked for the navy top bar and footer, where it
 * does work; it just fails everywhere else.
 *
 * One colour cannot do both. Two can: --color-primary is 8.13:1 on the light
 * surfaces that make up most of the site, and where the surface is dark enough to
 * swallow it, the white band inside carries the indicator instead. The box-shadow
 * fills the outline-offset gap exactly, so the two read as one ring rather than as
 * a ring and a halo.
 *
 * All three surfaces were measured and all three clear the 3:1 that 1.4.11 asks:
 * 8.13:1 on light, 9.74:1 on the navy top bar, 3.03:1 on the orange band. The last
 * two are the white band's figures, not the blue's — which is the point of the band,
 * and the reason neither number can be dropped from here.
 *
 * No border-radius here. The rule used to set one, which does not shape the ring — it
 * reshapes the ELEMENT while it has focus, so tabbing to a pill chip squared it off to
 * 4px. Every browser this theme supports already draws the outline around whatever
 * radius the element has, so a pill gets a pill and a square gets a square.
 */
:focus-visible {
	outline: 3px solid var(--color-primary);
	outline-offset: 2px;
	box-shadow: 0 0 0 2px var(--color-white);
}

/*
 * ...but it does not reach into the MxChat chat input. The white band is painted on the
 * focused element's own box, and that textarea sits inside a container whose chrome we do
 * not control, so our ring was drawing over geometry we cannot see. A text input matches
 * :focus-visible on a plain mouse click too, so it fired every time a visitor went to type.
 * MxChat's own widget shows only the caret there; this matches it.
 *
 * Deliberately narrow. The first version of this rule cleared outline AND box-shadow for
 * everything inside the widget, which also wiped the plugin's own focus ring on the
 * question chips (.mxchat-popular-question:focus uses a box-shadow, not an outline) and
 * left them with no indicator at all. Neutralising a third party's styling wholesale
 * removes theirs along with ours; scope it to the one element that needs it.
 */
.mxchat-chatbot .chat-input:focus-visible {
	outline: none;
	box-shadow: none;
}


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

button {
	cursor: pointer;
}

/* :where() keeps these resets at zero specificity, so any component class
   can set its own margins without fighting the reset. */
:where(ul[role="list"], ol[role="list"]) {
	list-style: none;
	margin: 0;
	padding: 0;
}

:where(blockquote, figure) {
	margin: 0;
}

address {
	font-style: normal;
}

[hidden] {
	display: none !important;
}

.site-main:focus {
	outline: none;
}


/* 3. Typography
   ========================================================================== */

h1,
h2,
h3,
h4 {
	margin: 0;
	font-family: var(--font-heading);
	font-weight: 700;
	line-height: 1.2;
	color: var(--color-ink);
	letter-spacing: -0.01em;
	text-wrap: balance;
}

/* Card titles read better with natural wrapping than balanced lines. */
h3 {
	text-wrap: pretty;
}

p {
	margin: 0;
}

/*
 * The accent line of a hero title, one colour on every inner screen. Blog used
 * to override this to --color-primary and the rest used --color-accent-deep,
 * which is the per-route colouring this theme is getting rid of. Primary is the
 * right one of the two: the token's own role is "accent headline text", and at
 * display size it has to separate from the near-black lines above it, which
 * accent-deep barely does. accent-deep keeps the quieter job it is better at,
 * subtitles. Home is not covered here — its hero sits on a dark panel and has
 * its own rule in home.css.
 */
.text-accent {
	color: var(--color-primary);
}

.title-line {
	display: block;
}

.eyebrow {
	margin-bottom: var(--space-3);
	font-family: var(--font-heading);
	font-size: var(--fs-xs);
	font-weight: 600;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--color-eyebrow);
}

.eyebrow--sm {
	margin-bottom: var(--space-2);
	font-size: 0.75rem;
}

.eyebrow--on-dark {
	color: var(--color-sky);
}


/* 4. Layout
   ========================================================================== */

.container {
	width: min(100% - (var(--gutter) * 2), var(--container-max));
	margin-inline: auto;
}

.container--narrow {
	max-width: 780px;
}

/*
 * The two-column frame: a body of text on the left, the facts stacked in a
 * narrower column on the right.
 *
 * Contact Us set the 1.9 : 1 proportion, the location screen copied the
 * declaration because it answers the same kind of question, and the article
 * screen made it three. Three identical copies in three per-screen stylesheets
 * is how they drift: one gets tuned, the others do not, and screens that should
 * read as one layout slowly stop matching. It lives here now, and a screen adds
 * only what is genuinely its own on top.
 */
.split-layout {
	display: grid;
	gap: var(--space-6);
}

@media (min-width: 992px) {
	.split-layout {
		grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
		align-items: start;
	}
}

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

.section--compact {
	padding-block: clamp(1.5rem, 1.25rem + 0.6vw, 1.625rem);
}

.section--tight {
	padding-block: 0.75rem 1.5rem;
}

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

.section--tinted {
	background: var(--color-surface-soft);
}

.button-group {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3) var(--space-4);
	align-items: center;
}

/* Phones: buttons share the row when they fit, and become full width once they wrap. */
@media (max-width: 575.98px) {
	.button-group > .btn {
		flex: 1 1 auto;
	}

	/* A lone button in a stacked column is easier to hit at full width. */
	.site-footer__contact > .btn,
	.info-banner__action > .btn {
		width: 100%;
	}
}


/* 5. Utilities
   ========================================================================== */

/*
 * .screen-reader-text is WordPress's own name for this, used by core markup the theme does not
 * write — pagination's heading, comment labels, block editor output. Without it those strings
 * print on the page in full size; "Results navigation" sat above the pagination until this was
 * added. Cheaper to answer to both names than to hunt them down one screen at a time.
 */
.visually-hidden,
.screen-reader-text,
.skip-link:not(:focus),
.btn[aria-disabled="true"] [data-submit-label] {
	position: absolute !important;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

.skip-link:focus {
	position: fixed;
	z-index: 1000;
	top: var(--space-3);
	left: var(--space-3);
	padding: var(--space-3) var(--space-5);
	font-weight: 600;
	color: var(--color-white);
	background: var(--color-primary);
	border-radius: var(--radius-sm);
}

.u-br-lg {
	display: none;
}

@media (min-width: 992px) {
	.u-br-lg { display: inline; }
}

/*
 * Hover zoom, shared.
 *
 * Testimonials wrote this first for its video cards and it was the only copy;
 * the gallery wanted the same thing, so it moved here rather than becoming the
 * second. Put the class on whatever clips the image — it needs the overflow —
 * and the image inside is the thing that grows.
 *
 * Lifting it also fixed something the copy in pages/testimonials.css never had:
 * a reduced-motion guard. That file is the only per-screen stylesheet with no
 * such block, so the video cards zoomed regardless of the setting.
 *
 * Hover is a desktop affordance and there is no such thing on a touch screen,
 * so this can never be the only sign that something is pressable. Whatever uses
 * it has to look pressable without it.
 */
.hover-zoom {
	overflow: hidden;
}

/*
 * will-change keeps the image on its own layer from the start. Without it
 * Chrome promotes the image only while the transition runs, stretches the
 * bitmap it already has, and re-rasters it at the end — so every hover went
 * soft for 400ms and then snapped sharp. With it nothing is re-rastered under
 * the transform (developer.chrome.com/blog/re-rastering-composite): no snap in
 * or out, at the cost of the hovered state being that same bitmap 3% larger,
 * which is not visible at this size.
 */
.hover-zoom img {
	transition: transform 400ms ease;
	will-change: transform;
}

.hover-zoom:hover img {
	transform: scale(1.03);
}

@media (prefers-reduced-motion: reduce) {
	.hover-zoom img { transition: none; will-change: auto; }
	.hover-zoom:hover img { transform: none; }
}

.icon-sprite {
	position: absolute;
	width: 0;
	height: 0;
	overflow: hidden;
}


/* 6. Buttons & links
   ========================================================================== */

.btn {
	--btn-bg: var(--color-primary);
	--btn-color: var(--color-white);
	--btn-border: var(--btn-bg);
	--btn-bg-hover: var(--color-primary-hover);
	--btn-color-hover: var(--btn-color);

	display: inline-flex;
	gap: 0.6rem;
	align-items: center;
	justify-content: center;
	min-height: 46px;
	padding: 0.7rem 1.5rem;
	font-family: var(--font-heading);
	font-size: 0.9375rem;
	font-weight: 600;
	line-height: 1.1;
	color: var(--btn-color);
	text-align: center;
	white-space: nowrap;
	background: var(--btn-bg);
	border: 1.5px solid var(--btn-border);
	border-radius: var(--radius-pill);
	transition: background-color var(--transition), color var(--transition), border-color var(--transition), box-shadow var(--transition), transform var(--transition);
}

/*
 * The label is centred on its letters, not on the font's box. Flex centring
 * centres the box, and with Poppins that put the capitals 0.4-0.7px above the
 * middle of every button (measured in Chrome on 7 October 2026, 294 buttons),
 * which the screen rounds to centred or to a whole pixel high depending on
 * where the button lands. Trimmed to cap height and baseline, the letters are
 * what gets centred. A two-line label loses the same space, so those buttons
 * are a few pixels shorter and their padding is the gap that shows. Browsers
 * without text-box keep the old position.
 *
 * Text placed straight in a button cannot be trimmed, so every label is a
 * <span>, in the templates and in assets/js/locations-map.js.
 */
.btn > span:not(.visually-hidden) {
	text-box: trim-both cap alphabetic;
}

/*
 * Labels wrap on phones. The `white-space: nowrap` above is a promise that every
 * label is short enough for the narrowest screen, and "Read Our Google Reviews"
 * broke it: its 299px min-content widened the info banner's grid track past a
 * 320px viewport and put 18px of horizontal scroll on the whole page. Spanish
 * labels run longer than English ones, so the promise would have broken again on
 * translation. Buttons are full width or stacked at this size, so a second line
 * costs nothing, and nowrap still holds wherever buttons sit side by side.
 *
 * This block has to come after the rule it overrides, not in the 575.98px block
 * further up: same specificity, so source order decides, and up there the base
 * .btn simply won.
 */
@media (max-width: 575.98px) {
	.btn {
		white-space: normal;
	}
}


.btn:hover {
	color: var(--btn-color-hover);
	background: var(--btn-bg-hover);
	border-color: var(--btn-bg-hover);
}

.btn:active {
	transform: translateY(1px);
}

/*
 * Square-ish, for an action pair that belongs to a form rather than to the
 * site's navigation. The pill is still the default everywhere: this is a
 * variant offered by the system, not a radius re-pointed for one screen — the
 * Careers mockup draws both shapes on the same page, and the CTA band below
 * those buttons keeps its pills.
 */
.btn--rect {
	border-radius: var(--radius-sm);
}

/*
 * A button that names its destination on a second line. Block, so it breaks
 * under the label rather than trailing after it, and lighter so the label still
 * leads — it is the address, not the instruction.
 */
.btn__sublabel {
	display: block;
	margin-top: 0.15rem;
	font-family: var(--font-body);
	font-size: 0.8125rem;
	font-weight: 400;
}

/*
 * Busy, while the form is posting and waiting on the mail API — several seconds
 * on a slow connection, long enough that a visitor reaches for a second click.
 *
 * Marked with aria-disabled and never the `disabled` attribute. A disabled
 * control is left out of the submission entirely, and the handler gates on this
 * button's own name, so disabling it would quietly stop the form from sending
 * anything at all. The repeat click is refused in JavaScript instead, and the
 * pointer-events here only make the button feel as inert as it looks.
 *
 * The spinner is frozen by the global reduced-motion block above; there the
 * swapped accessible name carries the meaning on its own.
 */
.btn[aria-disabled="true"] {
	cursor: progress;
	pointer-events: none;
}

/*
 * Label and arrow give way to the spinner, so the button reads as one state
 * rather than a label that no longer matches what it is doing. The label is
 * hidden from the eye only (see the visually-hidden group above): it still
 * carries the button's accessible name, swapped to "Sending…", which is what a
 * screen reader announces when the state changes. Nothing is dimmed — the
 * spinner says "working", which is truer here than "unavailable".
 */
.btn[aria-disabled="true"] .icon {
	display: none;
}

.btn[aria-disabled="true"]::after {
	content: "";
	width: 1.05em;
	height: 1.05em;
	border: 2px solid currentcolor;
	border-right-color: transparent;
	border-radius: 50%;
	animation: quantum-spin 0.6s linear infinite;
}

@keyframes quantum-spin {
	to { transform: rotate(360deg); }
}

.btn .icon {
	flex-shrink: 0;
	transition: transform var(--transition);
}

.btn:hover .icon--arrow-right {
	transform: translateX(3px);
}

.btn--lg {
	min-height: 54px;
	padding: 0.85rem 1.9rem;
	font-size: 1rem;
}

.btn--sm {
	min-height: 40px;
	padding: 0.5rem 1rem;
	font-size: var(--fs-xs);
}

.btn--block {
	width: 100%;
}

.btn--orange {
	--btn-bg: var(--color-orange);
	--btn-bg-hover: var(--color-orange-hover);
	box-shadow: 0 6px 16px rgb(236 113 24 / 0.25);
}

.btn--outline {
	--btn-bg: var(--color-white);
	--btn-color: var(--color-primary);
	--btn-border: var(--color-primary);
	--btn-bg-hover: var(--color-primary);
	--btn-color-hover: var(--color-white);
}

.btn--outline:hover {
	border-color: var(--color-primary);
}

.btn--outline-soft {
	--btn-bg: var(--color-white);
	--btn-color: var(--color-link);
	--btn-border: var(--color-link-border);
	--btn-bg-hover: var(--color-link);
	--btn-color-hover: var(--color-white);
}

.btn--outline-light {
	--btn-bg: transparent;
	--btn-color: var(--color-white);
	--btn-border: rgb(255 255 255 / 0.85);
	--btn-bg-hover: rgb(255 255 255 / 0.12);
}

.btn--outline-light:hover {
	border-color: var(--color-white);
}

.btn--white {
	--btn-bg: var(--color-white);
	--btn-color: var(--color-primary);
	--btn-bg-hover: var(--color-surface-tint);
	--btn-color-hover: var(--color-primary-hover);
	box-shadow: var(--shadow-sm);
}

.link-arrow {
	display: inline-flex;
	gap: 0.4rem;
	align-items: center;
	font-family: var(--font-heading);
	font-size: 0.9375rem;
	font-weight: 600;
	color: var(--color-link);
}

.link-arrow:hover {
	color: var(--color-link-hover);
}

.link-arrow .icon {
	transition: transform var(--transition);
}

.link-arrow:hover .icon {
	transform: translateX(3px);
}


/* 7. Small components
   ========================================================================== */

/*
 * Card interactions, shared by every card grid.
 *
 * .card-lift lifts on hover, and when a link inside has keyboard focus. It is
 * the only one left. .card-stretched — one link stretched over the whole card —
 * went with the service cards when they stopped being a link, and its four rules
 * stayed here painting nothing until 28 September 2026. Checked before deleting
 * them: not a template, not a script, not another plugin named the class, and it
 * did not appear in any of the site's 217 URLs.
 */
.card-lift {
	transition: box-shadow var(--transition), transform var(--transition), border-color var(--transition);
}

.card-lift:hover,
.card-lift:has(:focus-visible) {
	box-shadow: var(--shadow);
	transform: translateY(-3px);
}

.icon {
	flex-shrink: 0;
}

/* Soft circular background behind an icon. */
.icon-badge {
	display: inline-grid;
	flex-shrink: 0;
	place-items: center;
	width: 48px;
	height: 48px;
	color: var(--color-primary);
	background: var(--color-icon-bg);
	border-radius: 50%;
}

.icon-badge--lg {
	width: 60px;
	height: 60px;
}

/*
 * The badge sizes the icon inside it, rather than each call site passing a
 * number. Three of them did, and one picked 46 for a 52px circle — the glyph
 * came within 3px of the edge while the same component elsewhere left 11px.
 * A component that owns both halves of a proportion cannot drift.
 */
.icon-badge .icon {
	width: 26px;
	height: 26px;
}

.icon-badge--lg .icon {
	width: 30px;
	height: 30px;
}

.badge {
	display: inline-flex;
	align-items: center;
	padding: 0.3rem 0.75rem;
	font-family: var(--font-heading);
	font-size: 0.6875rem;
	font-weight: 700;
	line-height: 1.2;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--color-navy);
	background: var(--color-white);
	border-radius: var(--radius-pill);
	box-shadow: var(--shadow-sm);
}

/*
 * Only the fill changes. The blog's badges used to square their corners to 6px
 * and the featured one also ran a size bigger, so the same component was a pill
 * on Locations and a rectangle on Blog, in two sizes. A pill is also what the
 * filter chips above these cards are, and a card's badge is the echo of the chip
 * you just pressed — they should not be different shapes.
 */
.badge--orange {
	color: var(--color-white);
	background: var(--color-orange);
}

/*
 * Initials standing in for a photograph that does not exist, drawn by
 * quantum_initials(): the team on About, the reviews on Testimonials. Only
 * the look lives here; the caller's own class sizes and shapes the frame.
 *
 * The brand's palest blue rather than a grey silhouette: a name without a
 * face still reads as a person, and an empty frame reads as a broken image.
 * aria-hidden in the markup, because the name is always printed beside it,
 * and a screen reader announcing "SB" before it would only be noise.
 */
.initials {
	display: flex;
	align-items: center;
	justify-content: center;
	font-family: var(--font-heading);
	font-weight: 700;
	color: var(--color-primary);
	letter-spacing: 0.02em;
	background: #eaf1fb;
}

/* Filter chips (links styled as pills). */
.chip-list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3);
}

/*
 * On phones a chip list is one scrolling row, not a stack of wrapped ones. Wrapping
 * makes the block's height a function of how many filters there are, with no ceiling:
 * measured at 360px, the Blog's seven topics took **five rows and 232px** — a third of
 * the viewport spent before the first article — and every category added later would
 * have cost another row. Scrolling holds it at one row, 52px, whatever the count.
 *
 * The negative margin pulls the row out to the screen edges so the last chip is visibly
 * cut off rather than ending neatly inside the gutter: that slice is the affordance that
 * says there is more. The scrollbar is hidden because this only applies where scrolling
 * is a touch gesture; a chip reached by Tab still scrolls itself into view.
 *
 * This is the default rather than a modifier so a list added later gets it without
 * anyone having to remember. Lists short enough to fit simply never overflow.
 */
@media (max-width: 767.98px) {
	.chip-list {
		flex-wrap: nowrap;
		width: calc(100% + var(--gutter) * 2);
		margin-inline: calc(var(--gutter) * -1);
		padding: 0 var(--gutter) var(--space-2);
		overflow-x: auto;
		scrollbar-width: none;
	}
}

.chip,
.pagination .page-numbers {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0.55rem 1.5rem;
	font-size: 0.9375rem;
	font-weight: 500;
	color: #334155;
	white-space: nowrap;
	background: var(--color-surface-chip);
	border-radius: var(--radius-pill);
	transition: background-color var(--transition), color var(--transition);
}

.chip:hover,
.pagination a.page-numbers:hover {
	color: var(--color-primary);
	background: #e0ebf7;
}

.chip.is-active,
.pagination .page-numbers.current {
	font-weight: 600;
	color: var(--color-white);
	background: var(--color-primary);
}

.chip-list--compact {
	gap: var(--space-2) var(--space-3);
}

.chip-list--compact .chip {
	min-height: 40px;
	padding: 0.45rem 1.1rem;
	font-size: var(--fs-sm);
}

/* Pill-shaped input + button (location search, blog search, newsletter). */
.pill-form {
	position: relative;
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	align-items: center;
	padding: 5px;
	background: var(--color-white);
	border: 1px solid var(--color-border);
	border-radius: var(--radius-pill);
	box-shadow: var(--shadow-sm);
}

.pill-form__icon {
	margin-left: var(--space-4);
	color: var(--color-muted);
}

.pill-form__input {
	flex: 1 1 10rem;
	min-width: 0;
	min-height: 44px;
	padding: 0 var(--space-4);
	background: transparent;
	border: 0;
}

.pill-form__icon + .pill-form__input {
	padding-left: var(--space-1);
}

.pill-form__input::placeholder {
	color: #8a94a3;
}

/*
 * The field draws no focus indicator of its own: the whole pill lights up instead,
 * through .pill-form:focus-within below, because the field and its button read as
 * one control and that is what should be ringed.
 *
 * box-shadow has to be cancelled too, not just outline. The global :focus-visible
 * ring carries a white band for dark backgrounds, and a band on this field is a
 * square-cornered rectangle sitting inside a 999px pill: at the left end the field
 * starts only 6px in, which is still inside the pill's curve, so the band's corners
 * came out through the edge as two white notches across the blue ring.
 */
.pill-form__input:focus {
	outline: none;
	box-shadow: none;
}

/*
 * The browser's own clear button, replaced by .pill-form__clear. It is not a
 * standard control: Chrome and Safari draw ~15px with no pointer and no hover
 * (nobody could tell it was a button), Firefox draws nothing, and there is no
 * standard selector for it — so both what it looked like and whether it existed
 * depended on the visitor's browser.
 */
.pill-form__input::-webkit-search-cancel-button {
	-webkit-appearance: none;
	appearance: none;
}

/* 32px so the target clears the 24px WCAG 2.5.8 asks of one, as the map popup's does. */
.pill-form__clear {
	display: flex;
	flex-shrink: 0;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	padding: 0;
	color: var(--color-muted);
	cursor: pointer;
	background: none;
	border: 0;
	border-radius: 50%;
	transition: color var(--transition), background-color var(--transition);
}

.pill-form__clear:hover {
	color: var(--color-navy);
	background: var(--color-surface-chip);
}

.pill-form__clear:focus-visible {
	outline: 2px solid var(--color-primary);
	outline-offset: 2px;
}

.pill-form:focus-within {
	border-color: var(--color-primary);
	box-shadow: 0 0 0 3px rgb(10 71 181 / 0.15);
}

/* Phones: stack the field above a full-width button so placeholders stay readable. */
@media (max-width: 575.98px) {
	.pill-form {
		padding: var(--space-2);
		border-radius: 24px;
	}

	.pill-form__input {
		flex-basis: calc(100% - 3rem);
	}

	/*
	 * Stacked, the field fills its row exactly, so the clear button would wrap to
	 * a row of its own. It gives up the button's width only while the button is
	 * there — hidden, the field keeps the whole row rather than leaving a gap.
	 */
	.pill-form:has(.pill-form__clear:not([hidden])) .pill-form__input {
		flex-basis: calc(100% - 3rem - 2.5rem);
	}

	.pill-form__button {
		flex-basis: 100%;
	}
}

/*
 * …and the same stacking whenever the pill's own container is too narrow for one
 * row, whatever the viewport is doing.
 *
 * A viewport rule alone was not enough: between 992 and about 1030px the hero is
 * still two columns, so its text column drops to 434-449px while the viewport
 * still says "desktop". One row needs 440px (button 214 + field 160 + the icon,
 * gaps and padding), so the button wrapped under a stretched field — measured at
 * 995px, exactly where it looked broken.
 *
 * 460px is just above the 440px one row needs, so the field stacks only when it
 * genuinely cannot fit — at 1100px the column is 484px and one row still works.
 *
 * The container is the .location-search wrapper, not .page-hero__content: at
 * 992px and up that column is a flex item sized by its own content, and
 * inline-size containment makes an element stop contributing its content's
 * width — it collapsed to 18px. A wrapper that is a plain block takes its width
 * from its parent, so containment has something definite to work from.
 */
@container location-search (max-width: 460px) {
	.pill-form {
		padding: var(--space-2);
		border-radius: 24px;
	}

	.pill-form__input {
		flex-basis: calc(100% - 3rem);
	}

	/* Mirrors the viewport rule above; see the note there. */
	.pill-form:has(.pill-form__clear:not([hidden])) .pill-form__input {
		flex-basis: calc(100% - 3rem - 2.5rem);
	}

	.pill-form__button {
		flex-basis: 100%;
	}
}

.pill-form__status {
	flex-basis: 100%;
	padding-inline: var(--space-4);
	font-size: var(--fs-sm);
	color: var(--color-navy);
}

.pill-form__status:empty {
	display: none;
}

.stars {
	display: inline-flex;
	gap: 3px;
	color: var(--color-star);
}

/* Filter live status: announced to screen readers, visually subtle. */
.filter-status:empty {
	display: none;
}

.filter-status {
	margin-top: var(--space-4);
	font-size: var(--fs-sm);
	color: var(--color-muted);
}

/*
 * With nothing found, the status line and the notice sit one above the other
 * saying the same thing ("Showing 0 questions" / "No questions match that
 * search."). Only the notice stays on screen: it is the clearer of the two and
 * it is built to be seen, while a count of zero is not information anyone needs
 * twice in the same glance.
 *
 * Hidden visually, NOT emptied or removed: this is the live region, and someone
 * who just typed a query is owed the result in their screen reader.
 *
 * Only on a real zero. On Locations an out-of-area ZIP keeps all ten listed and
 * its explanation lives in this very line, so that case must stay visible.
 *
 * Locations had this to itself until the screens that filter in the browser
 * started announcing a filter that arrived in the URL — which is what put "0"
 * on the FAQ for the first time. data-matches is written by filterable.js on
 * every render, so this covers any [data-filterable] root, present or future.
 */
[data-filterable][data-matches="0"] .filter-status {
	position: absolute !important;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/*
 * A quiet notice, not a panel: one line of copy does not need a full-width box. It sits at
 * the top of its grid row (the card grids give their rows equal heights, which was stretching
 * this to the height of a card) and its own width follows the text.
 *
 * The vertical margin is the notice's own, and it has to be, because the thing
 * that normally spaces a list from its toolbar is the status line ("Showing N
 * articles") — and that line is never rendered in the state where this one is.
 * Without it the notice sat flush against the filter chips: measured at 0px on
 * /blog/?q=<no match>, on every screen width, in both languages. Robert caught
 * it in Spanish, where the chips wrap to three rows and the two blocks touch
 * across the full width.
 *
 * Equal on both sides, and 32 rather than 20 for the side below: an offer of
 * help follows on every screen that uses this — the newsletter band on the
 * blog, the help card on Locations — and at 20 the two boxes read as one stack
 * instead of an answer and then an offer.
 *
 * Locations had this rule to itself, having hit the same problem first. It is
 * shared now: the Blog, the topic archive, the FAQ and Services all print the
 * same notice in the same position, and all four were relying on a margin only
 * one of them had.
 */
.filter-empty {
	grid-column: 1 / -1;
	align-self: start;
	justify-self: center;
	max-width: 36rem;
	margin-block: var(--space-8);
	margin-inline: auto;
	padding: var(--space-5) var(--space-6);
	color: var(--color-text);
	text-align: center;
	text-wrap: balance;
	background: var(--color-surface-soft);
	border-radius: var(--radius);
}

/*
 * With nothing above it — the state where nothing is published at all, so there
 * is no toolbar and no status line either — the margin above would stack on the
 * section's own padding and leave more air over the box than under it, which
 * reads as the page having lost something rather than as a deliberate notice.
 *
 * Three selectors rather than a plain :first-child, because :first-child turned
 * out to be testing the wrong thing on both sides:
 *
 * - Blog, the topic archive and Services open their container with a
 *   visually-hidden <h2>. It takes no space, but it is an element, so the
 *   notice is never the container's first child and the guard never fired at
 *   all. Hence the adjacent-sibling form.
 * - Locations prints its notice BEFORE the card grid on purpose (in map view a
 *   notice after the grid lands below a 500px map), so there it is ALWAYS the
 *   first child of .locations-results — and the guard always fired, cancelling
 *   the margin that screen had added for itself. That is the gap under the area
 *   chips. Its real "nothing above me" test is whether the filter bar and the
 *   status line were rendered at all: if they were not, the results block
 *   follows the hidden heading directly.
 * - The bare :first-child stays for the FAQ, whose container opens with the
 *   notice itself.
 */
.container > .visually-hidden:first-child + .filter-empty,
.container > .visually-hidden:first-child + .locations-results > .filter-empty,
.container > .filter-empty:first-child {
	margin-block-start: 0;
}

/* Handwritten brand note with orange brush underline. */
.script-note {
	display: inline-flex;
	flex-direction: column;
	font-family: var(--font-script);
	font-size: clamp(1.6rem, 1.2rem + 1vw, 2.25rem);
	font-weight: 600;
	line-height: 1.05;
	color: var(--color-ink);
	transform: rotate(-8deg);

	/*
	 * Decoration, not copy: these three or four words are a flourish over a photograph,
	 * and dragging across the hero used to paint them in the browser's selection blue.
	 * Left readable to a screen reader, which does not go through selection.
	 */
	user-select: none;
}

/*
 * The swoosh is sized in em of the note (never %: an svg has no intrinsic width, so a percentage
 * inflated every note to ~300px). Each placement tunes it through these properties, measured on its mockup:
 * --swoosh-width, --swoosh-offset (from the text's left edge), --swoosh-gap (below the last line).
 */
.script-note__swoosh {
	display: block;
	flex-shrink: 0;
	width: var(--swoosh-width, 2.6em);
	height: auto;
	margin: var(--swoosh-gap, 0.2em) 0 0 var(--swoosh-offset, 1.3em);
	color: var(--color-orange);
}

.script-note__cite {
	margin-top: 0.3rem;
	margin-left: var(--cite-offset, 30%);
	font-family: var(--font-body);
	font-size: var(--fs-sm);
	font-weight: 500;
	color: var(--color-muted);
	white-space: nowrap;
}


/*
 * Shared surface for the screens added after the first phase. The cards already
 * on the site (article, review, location, service) each style their own surface
 * with the same intent, and they are not being retrofitted here — they are
 * verified screens and a sweep through them belongs in its own change. New work
 * uses this one. Note for whoever does that sweep: those cards hard-code
 * #e8eef5 for the border where --color-border is #e3ebf4, the same intent at two
 * values; the token is the one to keep.
 */
.card {
	padding: var(--space-6);
	background: var(--color-white);
	border: 1px solid var(--color-border);
	border-radius: var(--radius);
	box-shadow: var(--shadow-sm);
}

/*
 * The title of a white card whose content carries its own icons — Contact
 * Information on Contact, Related Services beside a service's page — so the
 * title has none, unlike the tinted .aside-card's.
 */
.card__title {
	margin-bottom: var(--space-5);
	font-size: var(--fs-lg);
	color: var(--color-navy);
}


/* Sidebar card: icon title, a line of text, one action. See the template part. */
.aside-card {
	background: var(--color-surface-tint);
	border-color: transparent;
	box-shadow: none;
}

.aside-card__title {
	display: flex;
	gap: var(--space-3);
	align-items: center;
	margin-bottom: var(--space-2);
	font-size: var(--fs-lg);
	color: var(--color-navy);
}

.aside-card__text {
	margin-bottom: var(--space-4);
	font-size: var(--fs-sm);
	color: var(--color-text);
}


/* ---- Forms ---------------------------------------------------------------
 * The theme's first real form. .pill-form above is a different animal: one
 * control in a rounded bar, where the filled button and the placeholder carry
 * the affordance. Here there are five stacked fields and the visitor has to see
 * where each one begins. The reference draws them white, so the outline does
 * that job instead of a fill, which is why --color-border-control exists and is
 * held to the 3:1 of WCAG 1.4.11 rather than to whatever reads as "subtle".
 */
.form__row {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-4);
}

.field,
.form__row {
	margin-bottom: var(--space-4);
}

/* Two fields share a row from 480px up; below that they stack like the rest. */
.field--half {
	flex: 1 1 13rem;
	min-width: 0;
	margin-bottom: 0;
}

.field__label {
	display: block;
	margin-bottom: var(--space-2);
	font-size: var(--fs-sm);
	font-weight: 600;
	color: var(--color-ink);
}

/*
 * The asterisk is decoration: it is aria-hidden in the markup and the word
 * "(required)" sits beside it for screen readers, because a lone * is read as
 * "star" or skipped entirely depending on the reader and its verbosity.
 */
.field__required {
	color: var(--color-error);
}

.field__control {
	display: block;
	width: 100%;
	padding: 0.75rem 1rem;
	font-family: inherit;
	font-size: 1rem;
	color: var(--color-ink);
	background: var(--color-white);
	border: 1px solid var(--color-border-control);
	border-radius: var(--radius-sm);
	transition: border-color var(--transition), box-shadow var(--transition);
}

.field__control::placeholder {
	color: var(--color-muted);
	opacity: 1;
}

.field__control:hover {
	border-color: var(--color-link);
}

/*
 * A field lights up on its own instead of taking the global two-tone ring. That
 * ring is built to sit outside any control on any background; on something that
 * already carries a 3:1 border it lands as a second frame around the first one.
 * And a browser matches :focus-visible on a text field for a plain mouse click
 * too — correctly, since the visitor is about to type — so this is what they see
 * every time they click into the form, not only when tabbing through it.
 *
 * This replaces the indicator, it does not drop it: the border moves to
 * --color-primary at 8.13:1 against the white field, far past the 3:1 that 1.4.11
 * asks, and the halo widens it to 4px so it is visible at a glance rather than
 * being a one-pixel colour change.
 */
.field__control:focus-visible {
	outline: none;
	border-color: var(--color-primary);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}

/*
 * Forced colours throw away box-shadows and honour outlines, so the halo above
 * would leave nothing behind. Put a real outline back for that mode only.
 */
@media (forced-colors: active) {
	.field__control:focus-visible {
		outline: 2px solid;
		outline-offset: 1px;
	}
}

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

/* The native select arrow cannot be styled, so it is replaced with the theme's. */
.field__select {
	position: relative;
}

/*
 * The native arrow cannot be styled, so it is switched off and the theme's own
 * chevron is placed over it. Prefixed as well as plain, the same as
 * .pill-form__input above: Safari only took the unprefixed property in 15.4, and
 * without it those devices draw the system arrow next to ours instead of in
 * place of it.
 */
.field__select .field__control {
	padding-right: 2.75rem;
	-webkit-appearance: none;
	appearance: none;
	cursor: pointer;
}

.field__caret {
	position: absolute;
	top: 50%;
	right: 1rem;
	color: var(--color-link);
	pointer-events: none;
	transform: translateY(-50%);
}

/*
 * The border carries it, not a fill. --color-error is 7.26:1 against the white
 * field, so a 1px outline is far past the 3:1 that makes a state visible, and
 * the tinted background that used to sit behind it added loudness rather than
 * information — five washed fields read as alarm where five outlined ones read
 * as a list of things to fix.
 *
 * The focus halo turns red with it. Before, an invalid field that had focus drew
 * a red border inside a blue halo, because this rule comes after the focus one
 * and won only the border. Two colours disagreeing about the same control.
 */
.field--invalid .field__control {
	border-color: var(--color-error);
}

.field--invalid .field__control:focus-visible {
	border-color: var(--color-error);
	box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-error) 18%, transparent);
}

/*
 * The caret goes with it. It is an accent, and an accent in the interactive blue
 * sitting inside a red frame is the same disagreement the halo had — the eye
 * stops on it and asks why that one piece did not change. 7.26:1 against the
 * white field, so the glyph stays well clear of the 3:1 that 1.4.11 asks.
 */
.field--invalid .field__caret {
	color: var(--color-error);
}

/*
 * Google's checkbox is a fixed 304x78 iframe: it cannot be restyled, reflowed or
 * asked to be narrower, and the compact variant is chosen when the widget is
 * rendered, which is server side, so it cannot answer to the viewport either.
 * Measured inside the form card, 304px fits from 390px up and overflows below
 * that, pushing the whole page sideways. Shrinking it is the one honest option
 * left; a form that scrolls horizontally on a phone is worse.
 *
 * zoom, not transform. A transform only changes what is painted — the element
 * still reserves its original 304px, so the page kept overflowing at 320 and 360
 * while the widget merely looked smaller. zoom changes the used values, so the
 * layout box shrinks with it and no negative margins are needed to claw back the
 * difference. Standardised in the CSS Viewport module and supported everywhere
 * this theme targets.
 */
.g-recaptcha {
	--recaptcha-scale: 1;

	zoom: var(--recaptcha-scale);
}

@media (max-width: 389.98px) {
	.g-recaptcha {
		--recaptcha-scale: 0.9;
	}
}

@media (max-width: 359.98px) {
	.g-recaptcha {
		--recaptcha-scale: 0.78;
	}
}

/* Colour is never the only signal: the text below says what is wrong. */
.field__error {
	margin-top: var(--space-2);
	font-size: var(--fs-sm);
	color: var(--color-error);
}

.form-status:empty,
.form-status:focus {
	outline: none;
}

.form-status__message {
	display: flex;
	gap: var(--space-3);
	align-items: flex-start;
	margin-bottom: var(--space-5);
	padding: var(--space-4);
	font-size: var(--fs-sm);
	border-radius: var(--radius-sm);
}

.form-status__message .icon {
	flex-shrink: 0;
}

/*
 * Green, not the theme's pale blue. An earlier version argued that green appears
 * nowhere else in the palette so the confirmation should stay on brand — but the
 * error state already introduced a red that is not a brand colour either, for the
 * same reason: a status has to read as a status. Blue reads as "information",
 * which is not what "your message is on its way" means.
 *
 * Colour is never the only signal here: the check icon and the sentence carry it
 * too, which is what WCAG 1.4.1 asks for.
 */
.form-status__message--ok {
	color: var(--color-success);
	background: var(--color-success-tint);
	border: 1px solid currentcolor;
}

.form-status__message--error {
	color: var(--color-error);
	background: var(--color-error-tint);
	border: 1px solid currentcolor;
}

/*
 * The honeypot. Not display:none — some bots skip hidden inputs, and the point
 * is that they fill this one. Pushed off-screen instead, with aria-hidden and
 * tabindex=-1 in the markup so no real visitor, sighted or not, ever reaches it.
 */
.honeypot {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	overflow: hidden;
}

/*
 * The notice MapLibre lays over both maps when a scroll or a one-finger drag is
 * left to the page (cooperativeGestures, in assets/js/locations-map.js and
 * location-map.js). MapLibre centres the box but not its lines, so a notice that
 * wraps — the Spanish one on a location's map, or any of them in a narrow
 * window — reads ragged inside a centred block. Here and not in a page's
 * stylesheet because both maps show it; MapLibre's own sets no text-align for
 * this to fight.
 */
.maplibregl-cooperative-gesture-screen {
	text-align: center;
}


/* 8. Header
   ========================================================================== */

.site-header {
	position: relative;
	z-index: 50;
}

/* Top bar */
.topbar {
	font-size: var(--fs-sm);
	font-weight: 500;
	color: var(--color-white);
	background: var(--color-topbar);
}

.topbar__inner {
	display: flex;
	gap: var(--space-4);
	align-items: center;
	justify-content: space-between;
	min-height: 44px;
}

.topbar__info {
	display: flex;
	gap: var(--space-8);
	align-items: center;
}

.topbar__item {
	display: inline-flex;
	gap: var(--space-2);
	align-items: center;
	color: inherit;
}

/* The phone number never breaks across lines. */
.topbar__item span {
	white-space: nowrap;
}

/*
 * Every control in the bar is the same height, declared once so the two cannot drift apart. The
 * phone link used to fill the bar instead (padding-block: 0.65rem, 43px), which looked identical
 * until it took focus: the ring is 2px of offset plus a 3px outline on each side, and a control
 * flush with the bar has nowhere to put them — the top edge fell off the viewport and the bottom
 * landed on the white header, where the white band that carries the ring on navy disappears and
 * only a blue line is left hanging under the bar. At 32px there is 6px of navy on each side to
 * hold it, and 125x32 is still well past the 24x24 WCAG 2.5.8 asks of a target.
 */
a.topbar__item,
.lang-switch__toggle {
	min-height: 32px;
}

/*
 * Underline instead of a color change: white text on the navy bar is 9.7:1, and any tint lowers that.
 * The colour is restated because the global link hover (#083a95) would be invisible here, and the
 * underline is set on the label because text decorations do not propagate into flex items.
 */
a.topbar__item:hover {
	color: var(--color-white);
}

a.topbar__item:hover span {
	text-decoration: underline;
	text-underline-offset: 3px;
}

/* Flex list items: an inline-flex child in a plain <li> sits on the text baseline (strut) and rides ~2px high. */
.topbar__info-item {
	display: flex;
}

.topbar__info-item--hours,
.topbar__item--note {
	display: none;
}

.topbar__actions {
	display: flex;
	gap: var(--space-6);
	align-items: center;
}

@media (min-width: 768px) {
	.topbar__info-item--hours,
	.topbar__item--note {
		display: flex;
	}
}

/*
 * Very narrow screens (a folded Galaxy Fold cover screen is ~280px): the language button keeps only
 * its flag so the top bar stays on one line.
 */
@media (max-width: 359.98px) {
	.topbar__inner {
		gap: var(--space-2);
	}

	.lang-switch__label {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		overflow: hidden;
		clip-path: inset(50%);
	}
}

/* Language switcher */
/*
 * Both flags sit on one vertical line, and these four values are how that is kept true rather
 * than tuned. --lang-inset is the padding between the control's edge and its flag: the button
 * takes it as padding, and the menu reaches the same line from the other direction, by hanging
 * past the button by however much its own two paddings (the card, then the row) exceed it. The
 * menu is anchored by its right edge so it cannot run off the side of the screen, which leaves
 * width as the only thing that moves its left edge — hence the calc rather than a left offset.
 * Reads as: the button's width, plus what the menu puts before its flag, minus what the button
 * puts before its own (its border counts — the card has none). Change any one value and the
 * alignment follows; hard-code the difference and it does not.
 */
.lang-switch {
	position: relative;

	--lang-inset: 0.6rem;
	--lang-card-pad: var(--space-2);
	--lang-row-pad: 0.75rem;
	--lang-border: 1px;
}

.lang-switch__toggle {
	display: inline-flex;
	gap: var(--space-2);
	align-items: center;
	padding: 0.2rem 0.4rem 0.2rem var(--lang-inset);
	font-weight: 500;
	color: var(--color-white);
	background: rgb(255 255 255 / 0.1);
	border: var(--lang-border) solid rgb(255 255 255 / 0.22);
	border-radius: var(--radius-sm);
}

.lang-switch__toggle:hover {
	background: rgb(255 255 255 / 0.18);
}

.lang-switch__flag {
	width: 20px;
	height: 14px;
	border-radius: 2px;
}

.lang-switch__caret {
	padding: 2px;
	background: rgb(255 255 255 / 0.15);
	border-radius: 4px;
	transition: transform var(--transition);
}

.lang-switch__toggle[aria-expanded="true"] .lang-switch__caret {
	transform: rotate(180deg);
}

/*
 * The menu is as wide as its longest option and never narrower than the button it drops from:
 * max-content stops it padding itself out to a number someone picked, and min-width resolves
 * against .lang-switch, which is the button. It used to be a flat 170px against 125px of content,
 * so it hung 51px past the button's left edge with the air sitting to the right of the labels.
 * Both halves matter — a third language with a longer name widens it, and the narrow layout that
 * strips the button down to its flag does not drag the menu down with it.
 */
.lang-switch__menu {
	position: absolute;
	top: calc(100% + 6px);
	right: 0;
	z-index: 60;
	width: max-content;
	/*
	 * One line on purpose. Production minifies this file with LiteSpeed Cache, which only protects a
	 * calc() written on a single line: split over three, it came out as "100%+var(", invalid without
	 * the spaces round "+", and the menu lost its minimum width.
	 */
	min-width: calc(100% + var(--lang-card-pad) + var(--lang-row-pad) - var(--lang-inset) - var(--lang-border));
	padding: var(--lang-card-pad);
	background: var(--color-white);
	border-radius: var(--radius);
	box-shadow: var(--shadow-lg);
}

.lang-switch__option {
	display: flex;
	gap: var(--space-3);
	align-items: center;
	padding: 0.55rem var(--lang-row-pad);
	color: var(--color-ink);
	border-radius: var(--radius-sm);
}

.lang-switch__option:hover,
.lang-switch__option[aria-current] {
	color: var(--color-primary);
	background: var(--color-surface-tint);
}

/* Masthead */
.masthead {
	background: var(--color-white);
	box-shadow: 0 1px 0 var(--color-border);
}

.masthead__inner {
	display: flex;
	gap: var(--space-4);
	align-items: center;
	min-height: 72px;
}

.brand {
	display: block;
	margin-right: auto;
}

.brand__logo {
	width: auto;
	height: 44px;
}

.masthead__cta {
	display: none;
}

.nav-toggle {
	display: inline-grid;
	place-items: center;
	width: 46px;
	height: 46px;
	color: var(--color-ink);
	background: var(--color-surface-tint);
	border: 0;
	border-radius: var(--radius-sm);
}

/* The one touch target with no state of its own; see the note on html above. */
.nav-toggle:hover {
	background: var(--color-icon-bg);
}

.nav-toggle:active {
	transform: translateY(1px);
}

.nav-toggle__close,
.is-nav-open .nav-toggle__open {
	display: none;
}

.is-nav-open .nav-toggle__close {
	display: block;
}

/* Primary navigation — mobile panel first */
.primary-nav {
	position: absolute;
	top: 100%;
	right: 0;
	left: 0;
	display: none;
	max-height: calc(100vh - 120px);
	padding: var(--space-4) var(--gutter) var(--space-6);
	overflow-y: auto;
	background: var(--color-white);
	border-top: 1px solid var(--color-border);
	box-shadow: var(--shadow-lg);
}

.is-nav-open .primary-nav {
	display: block;
}

.primary-nav__list {
	display: grid;
	gap: var(--space-1);
}

.primary-nav__link {
	display: block;
	padding: 0.8rem 1rem;
	font-family: var(--font-heading);
	font-size: var(--fs-base);
	font-weight: 600;
	color: var(--color-nav);
	border-radius: 10px;
	transition: background-color var(--transition), color var(--transition);
}

.primary-nav__link:hover {
	color: var(--color-nav-current);
	background: var(--color-surface-soft);
}

.primary-nav__link.is-current {
	color: var(--color-nav-current);
	background: var(--color-surface-tint);
}

.primary-nav__mobile-actions {
	display: grid;
	gap: var(--space-3);
	margin-top: var(--space-4);
}

@media (min-width: 576px) {
	.brand__logo { height: 50px; }
	.masthead__inner { min-height: 80px; }
}

@media (min-width: 1200px) {
	.masthead__inner {
		gap: var(--space-6);
		min-height: 88px;
	}

	.brand__logo {
		height: 56px;
	}

	.brand {
		margin-right: var(--space-4);
	}

	.primary-nav {
		position: static;
		display: block;
		flex: 1;
		max-height: none;
		padding: 0;
		overflow: visible;
		background: none;
		border: 0;
		box-shadow: none;
	}

	.primary-nav__list {
		display: flex;
		gap: var(--space-2);
	}

	.primary-nav__link {
		/*
		 * Tighter than the 1.05rem the menu gets from 1400 up, because at 1200 it
		 * does not fit: measured, the seven English labels need 727px of a 724px
		 * bar. Three pixels short is enough for flex to break "About Us" across
		 * two lines, which is what made the header grow a row and look wrong just
		 * past the breakpoint. Trimming 0.2rem a side gives back ~45px.
		 */
		padding: 0.7rem 0.85rem;
		/*
		 * A label is a phrase, not a paragraph: it may be dropped from the bar,
		 * never broken in half. Without this, flex treats "About Us" as wrappable
		 * text and squeezes it rather than admitting it does not fit.
		 */
		white-space: nowrap;
	}

	.primary-nav__mobile-actions,
	.nav-toggle {
		display: none;
	}

	.masthead__cta {
		display: inline-flex;
	}
}

/* Room to breathe again once the viewport can pay for it. */
@media (min-width: 1400px) {
	.primary-nav__link {
		padding: 0.7rem 1.05rem;
	}
}


/* 9. Page hero (inner screens)
   ========================================================================== */

/*
 * The surface is written twice, the second time mirrored, for the right-hand
 * side on a wide screen: see the 1400px rule further down.
 */
.page-hero {
	--hero-surface: linear-gradient(100deg, #f8fbff 0%, #eef5fd 50%, #e5eff9 100%);
	--hero-surface-mirrored: linear-gradient(260deg, #f8fbff 0, #eef5fd 50vw, #e5eff9 100vw);

	position: relative;
	display: flex;
	flex-direction: column;
	container-type: inline-size; /* for --container-edge */
	overflow: hidden;
	background: var(--hero-surface);
}

/* Faint brand-mark arcs on the left edge. */
.page-hero::before {
	position: absolute;
	top: 30%;
	left: -230px;
	width: 380px;
	height: 380px;
	pointer-events: none;
	content: "";
	border: 46px solid rgb(10 71 181 / 0.045);
	border-radius: 50%;
	box-shadow: 0 0 0 60px rgb(10 71 181 / 0.02);
}

.page-hero__media {
	position: relative;
	height: var(--hero-media-height);
	-webkit-mask-image: var(--hero-mask-vertical);
	mask-image: var(--hero-mask-vertical);
}

/*
 * The crop is written as two custom properties so a screen can move one axis
 * without restating the other: --hero-photo-y frames a particular photograph
 * (Services, Blog), --hero-photo-x is set further down, for the heroes whose
 * handwritten note needs one side of the picture kept in view.
 */
.page-hero__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: var(--hero-photo-x, center) var(--hero-photo-y, 30%);
}

.page-hero__inner {
	position: relative;
	z-index: 1;
	padding-bottom: var(--space-10);
}

/*
 * Hero entrance
 * -------------
 * Two separate gestures, because they belong on different screens.
 *
 *   .page-hero--intro       the photograph settles from 5% oversize
 *   .page-hero--note-intro  the handwritten note fades in once it has stopped
 *
 * **The left column is never animated.** Robert's rule for this theme: the
 * eyebrow, the heading, the lead and the buttons are there from the first
 * paint. What arrives is the image side — photo, then note — and the content
 * side simply is. An earlier build staggered the text in and it was wrong here:
 * this hero is a text panel beside a photograph, not a slide.
 *
 * **Why a one-off and not a loop.** The previous theme's zoom lived inside the home
 * carousel: each slide entered at scale(1.04), landed, drifted to 1.06 during
 * its turn and left at 1.09 (previous theme, main.css:285). The zoom WAS the slide
 * change — it began and ended. A perpetual zoom on a still photograph is motion
 * with no event behind it, beside text somebody is reading; it runs into WCAG
 * 2.2.2, which wants a real pause control that prefers-reduced-motion does not
 * provide; and here it has a specific cost, because the handwritten note sits
 * OVER the photograph and would be left floating above a scene sliding under it.
 *
 * **Scale only on the image, never opacity.** Chrome leaves out of the LCP
 * calculation any element with "an opacity of 0, that are invisible to the
 * user", and this photograph is the LCP element of every screen that carries it
 * — painted with priority/eager for exactly that reason. Fading it in from zero
 * would push that measurement later on every page; scaling does not.
 *
 * **And the `scale` property, not `transform`.** A transform in a keyframe
 * replaces the element's own transform, and with `both` it keeps replacing it
 * after the animation ends. A screen that mirrors its photograph with
 * scaleX(-1) would show it unmirrored to everyone except visitors who ask for
 * reduced motion, who get no animation and so the mirror — two different
 * pictures, one with the note on the subject. Testimonials was that screen
 * until 8 October 2026. `scale` composes with whatever transform is there.
 *
 * **And opacity only on the note, never transform.** .script-note carries a
 * rotate(-8deg) of its own, and .page-hero__script is that very element, not a
 * wrapper around it (see quantum_script_note). Animating transform there
 * flattened the handwriting to horizontal — it did, for one build.
 */
@keyframes quantum-hero-settle {
	from { scale: 1.05; }
	to   { scale: none; }
}

@keyframes quantum-note-in {
	from { opacity: 0; }
	to   { opacity: 1; }
}

/*
 * 1.4s and 5%, not the 1s and 4% this started at: that was running — measured,
 * 1.0309 on the way to 1 — and nobody could see it. The curve front-loaded so
 * hard that the travel was over before the page had finished settling. This is
 * the slowest of the three ease-outs that still reads as deceleration, and on a
 * 844px-wide hero the image now travels about 42px.
 */
.page-hero--intro .page-hero__image {
	animation: quantum-hero-settle 1.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}

/*
 * The note comes last, after the photograph has stopped. That turns the one
 * thing this effect could have spoiled into its closing beat: the phrase lands
 * on a scene that is already still. On a screen with no photo settle it is the
 * whole gesture, and 0.55s is still the right wait — long enough to read as a
 * second beat rather than as part of the page loading.
 */
.page-hero--note-intro .page-hero__script,
.page-hero--note-intro .page-hero__note {
	animation: quantum-note-in 0.6s ease-out 0.55s both;
}

@media (prefers-reduced-motion: reduce) {
	.page-hero--intro .page-hero__image,
	.page-hero--note-intro .page-hero__script,
	.page-hero--note-intro .page-hero__note {
		animation: none;
	}
}

.page-hero__content {
	max-width: 640px;
}

/*
 * A simple hero can share the body's narrow column (page.php). The 640/780px
 * caps on the content then belong to the outer width, and keeping them would
 * wrap the title earlier than the text underneath it between 768 and 991px.
 *
 * `flex: 1` is not decoration. Above 992px the inner is a flex row, so a content
 * block with no width of its own shrinks to fit — and what it fits is the widest
 * line in it, which is the title. Measured: the column was 780px and the block
 * sat at 398, wrapping the sentence at 41 characters inside a screen whose body
 * text runs at 91. The lead's own 33rem measure comes off for the same reason:
 * here the title, the sentence and the first paragraph are one column, and a
 * sentence that stops 250px short of the paragraph under it reads as a mistake
 * rather than as a measure.
 */
.page-hero__inner.container--narrow .page-hero__content {
	flex: 1;
	max-width: none;
}

.page-hero__inner.container--narrow .page-hero__lead {
	max-width: none;
}

.page-hero__title {
	font-size: var(--fs-display);
	font-weight: 700;
	line-height: 1.08;
	color: var(--color-ink);
	letter-spacing: -0.02em;
}

.page-hero__subtitle {
	margin-top: var(--space-2);
	font-family: var(--font-heading);
	font-size: clamp(1.4rem, 1rem + 1.3vw, 2.1rem);
	font-weight: 700;
	line-height: 1.2;
	color: var(--color-accent-deep);
}

.page-hero__lead {
	/*
	 * A measure, not a line-break trick. This used to carry the comment "mockup
	 * line breaks", because the width had been chosen hoping the text would wrap
	 * where the design does. It did not: at 560px the About lead broke four words
	 * later than the mockup on every line and left a 242px last line under a
	 * 555px first one.
	 *
	 * `text-wrap: balance` — the same thing headings already use above — does it
	 * properly, and reproduces the mockup's four lines word for word. It evens the
	 * rag instead of guessing a width, so it keeps working when the copy is edited
	 * or translated. Unsupported browsers simply wrap as before.
	 *
	 * 33rem. It went 35 -> 30.5 -> 33, and the middle step is the instructive one:
	 * 30.5rem tightened the measure around About's balanced lines, but it also cut
	 * the Blog lead's second half (512px of text) below the width it needs, so
	 * that hero ran to three lines where the design has two. 33rem is the width at
	 * which every lead gets the break its mockup shows — measured on all five
	 * screens, only Blog changes, from 3 lines to 2 — and at 18px it is about 65
	 * characters, inside the comfortable range either way.
	 */
	max-width: 33rem;
	margin-top: var(--space-4);
	font-size: var(--fs-md);
	line-height: 1.55;
	color: var(--color-text);
	text-wrap: balance;
	container: hero-lead / inline-size;
}

/*
 * A lead may declare its own line shape with "|" (see quantum_format_groups).
 * Those groups are honoured only where the measure is wide enough to hold them,
 * because a break that does not fit stops being a line and becomes two.
 *
 * The test has to be the LEAD's width, not the viewport's, because the two do
 * not move together: this column is 488px at a 900px viewport but only 330px at
 * 992px, where the hero is still two columns and the copy is squeezed beside the
 * image. A min-width media query would switch the groups on exactly where there
 * is least room for them.
 *
 * The container is the <p> itself. .page-hero__content cannot be one — it is a
 * flex item sized by its own content at 992px and up, and inline-size containment
 * collapses it (the same trap is documented on .location-search above). A block
 * that takes its width from its parent has something definite to contain.
 */
.page-hero__lead .line-group {
	display: inline;
}

@container hero-lead (min-width: 470px) {
	.page-hero__lead .line-group {
		display: block;
	}
}

.page-hero__flourish {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-8);
	align-items: center;
	margin-top: var(--space-6);
}

/*
 * Buttons under the hero copy. Only the gap above them: the row itself is
 * .button-group, which page-hero.php now also sets, the way the home hero
 * already did. That is where the flex, the gaps and — the reason this matters —
 * the phone rule live, so these buttons stretch to the full width on a narrow
 * screen like every other button row on the site. Hand-rolling the flex here
 * quietly opted the location and 404 heroes out of that.
 *
 * Same margin as .feature-list and .page-hero__flourish above: whichever of the
 * three follows the lead, the gap under it is the same.
 */
.page-hero__actions {
	margin-top: var(--space-6);
}

.accent-rule {
	display: block;
	width: 64px;
	height: 3px;
	background: var(--color-orange);
	border-radius: 3px;
}

.feature-list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-4) var(--space-8);
	margin-top: var(--space-6);
}

.feature-list__item {
	display: flex;
	gap: var(--space-3);
	align-items: center;
}

.feature-list__label {
	font-size: var(--fs-sm);
	font-weight: 600;
	line-height: 1.35;
	color: var(--color-ink);
}

/*
 * A service, linked to its page: a .feature-list item whose whole content is
 * the link (template-parts/components/service-link.php), on a location's
 * "Services available at…" list and beside a service's page. It takes the
 * item's layout; the label then reads as .link-arrow does everywhere else — the
 * link blue and an arrow — which is what marks it as a link. Colour alone
 * would not (WCAG 1.4.1). Round, because the focus ring follows the element's
 * radius: it wraps the badge instead of boxing it.
 */
.service-link {
	display: flex;
	gap: var(--space-3);
	align-items: center;
	border-radius: var(--radius-pill);
}

.service-link .feature-list__label {
	color: var(--color-link);
}

.service-link:hover .feature-list__label {
	color: var(--color-link-hover);
}

/*
 * The name's last word and the arrow, printed together by the template part:
 * one piece that cannot break, so the arrow follows that word onto a second
 * line and never stands alone or at the far edge. Laid out as .link-arrow is —
 * centred on the line, .4rem after the word — so it lines up the way "Get
 * Directions" does.
 */
.service-link__tail {
	display: inline-flex;
	gap: 0.4rem;
	align-items: center;
}

.service-link__tail .icon {
	transition: transform var(--transition);
}

.service-link:hover .service-link__tail .icon {
	transform: translateX(3px);
}

.page-hero .pill-form {
	max-width: 600px;
	margin-top: var(--space-6);
}

.page-hero__script {
	display: none;
}

/*
 * The other handwriting: the same note on a translucent card, for a hero whose media is
 * an illustration rather than a photograph.
 *
 * It belongs to the hero and not to the illustration it sits on, and that is not a
 * naming preference. The map declares container-type for its own pins, which both makes
 * it the containing block for anything absolute inside it and makes 100cqw — and so
 * --container-edge — measure the map instead of the page. Nested there, the card drifted
 * to the window's edge on wide screens while every other note stopped where the content
 * stops. As a child of the media it lines up with the page container, like the corner one.
 */
.page-hero__note {
	position: absolute;
	right: calc(var(--container-edge) + 0.375rem); /* card ends at the page container's right edge (+ rotation overhang) */
	bottom: 6%;
	display: none;
	padding: 1.4rem 1.2rem;
	background: rgb(255 255 255 / 0.8);
	border-radius: 14px;
	box-shadow: var(--shadow);
	transform: rotate(-6deg);
	backdrop-filter: blur(4px);
}

.page-hero__note .script-note {
	--swoosh-width: 4em;
	--swoosh-offset: 0.85em;
	--swoosh-gap: 0.25em;
	font-size: clamp(1.4rem, 0.9rem + 1.25vw, 2rem);
	transform: none;
}

.page-hero--simple {
	padding-top: var(--space-10);
}

.page-hero--simple .page-hero__inner {
	/*
	 * padding-top: 0 because two of them were stacking. The section above already
	 * sets space-10, and the inner adds its own 2.25rem from 992px up — that one
	 * exists for the heroes with a photo, where the block is centred beside the
	 * image. On this hero there is no photo to centre against, so the two simply
	 * added: 76px above the first line against 32 below it. Nobody had noticed
	 * while the hero held a title and nothing else.
	 */
	padding-top: 0;
	padding-bottom: var(--space-8);
}

@media (min-width: 992px) {
	/* Content-driven height (mockups: 372–430px). */
	.page-hero {
		display: block;
	}

	.page-hero__media {
		position: absolute;
		inset: 0 0 0 auto;
		width: 60%;
		height: auto;
		-webkit-mask-image: var(--hero-mask-horizontal);
		mask-image: var(--hero-mask-horizontal);
	}

	/*
	 * A hero with the handwritten note keeps the right edge of its photograph in
	 * view and crops from the left, which the mask fades out anyway.
	 *
	 * The photographs behind these notes were composed for them: the subject
	 * between 46% and 72% of the width and a blank wall from about 74% to the
	 * edge, where the note goes (the brief is in the project's image specifications).
	 * The note stops at the page container, one gutter from the photograph's
	 * right edge (1.5rem from 1400). But from 992 to about 1280 the hero is tall
	 * beside a narrow text column, so cover draws the photograph much wider than
	 * its box and, centred, cut the wall off the right: the note landed on the
	 * subject — 60-81% of the picture on Testimonials at 992, over her hair, and
	 * on the logo sign on the Spanish About page. Anchored right, the wall is
	 * the part that always shows. Where the box is already about as wide as the
	 * picture (from 1400, where it stops growing) there is little sideways crop
	 * and this changes next to nothing.
	 *
	 * Only with a note: an article's or a location's photograph is whatever was
	 * uploaded, and the middle is the safest guess for that.
	 */
	.page-hero:has(> .page-hero__script) {
		--hero-photo-x: right;
	}

	.page-hero__inner {
		display: flex;
		align-items: center;
		min-height: inherit;
		padding-block: var(--hero-padding-block);
	}

	.page-hero__content {
		max-width: 46%;
	}

	/* Soft white glow keeps the handwriting legible over any photo.
	   The note ends at the page container's right edge (+0.25em offsets the rotation overhang),
	   and from 1400, where that is also the photograph's edge, 1.5rem inside it (--hero-note-clearance). */
	.page-hero__note {
		display: block;
	}

	.page-hero__script {
		--swoosh-width: 3.6em;
		--swoosh-offset: 0.25em;
		--swoosh-gap: 0.05em;
		position: absolute;
		top: 4.5rem;
		right: calc(var(--container-edge) + 0.25em + var(--hero-note-clearance));
		display: inline-flex;
		font-size: clamp(1.6rem, 1rem + 1.1vw, 2rem);
		filter: var(--script-glow);
	}

	.page-hero--simple {
		min-height: 0;
	}

	.page-hero--simple .page-hero__content {
		max-width: 780px;
	}
}

@media (min-width: 1400px) {
	.page-hero__content {
		max-width: 620px;
	}

	/*
	 * From here the photograph stops where the content stops, square-cut, at
	 * one size on every screen.
	 *
	 * Below 1400 it runs to the window's right edge, as the designs draw it. Past
	 * that the hero keeps the height its text gives it, about 350 to 460px,
	 * while 60% of the window keeps growing — 1,097px at 1829, 1,536 at 2560 —
	 * and cover shows less and less of the picture's height: 57% at 1829, 41% at
	 * 2560. That read as a stretched photo, and the note, which stops at the
	 * content, landed on the people. So the photograph stops growing at the
	 * size it had when the container stopped (--hero-frame), and its right edge
	 * moves to the content's: the same line as the header's button above it and
	 * the cards below it. The hero's own background carries on beyond, as it
	 * does on the left. The hero does not get taller.
	 *
	 * A plain edge, not a fade. A fade into the background was tried first
	 * (8 October 2026): it reads well over a pale wall on a pale hero, but over
	 * the home page's blue it drew a smudged band, and what it looks like
	 * depends on whatever photograph is chosen in wp-admin. A square edge on
	 * the content line looks the same whatever the picture is.
	 *
	 * 1400 because it is this theme's breakpoint for the widest layout
	 * (.page-hero__content above), and right in percent because, on an
	 * absolutely positioned box, percent is of the hero: no container units.
	 * The Locations hero keeps its map to the window's edge; locations.css says
	 * why.
	 */
	.page-hero__media {
		right: var(--hero-outside);
		width: calc(var(--hero-frame) * 0.6);
	}

	/*
	 * And the background right of the photograph mirrors the background left of
	 * the content, so the two sides match.
	 *
	 * The surface runs light to blue-grey across the whole hero, which was
	 * meant to sit under the photograph: light behind the text, tinted where the
	 * picture is. Once the photograph stops at the content, the end of that ramp
	 * shows on the right — measured at 2560, rgb(232 239 249) there against
	 * rgb(249 250 255) on the left — and two different margins either side of a
	 * square edge read as a mistake. So the strip right of the content gets the
	 * left strip's colours, mirrored: same stops, measured from the right edge.
	 * The two meet exactly at the photograph's edge, which hides the join; that
	 * is why this only applies when there is a photograph (a hero without one
	 * keeps the ramp, which has no edge to compare across). Measured afterwards
	 * at 2560: the two margins are the same colour pixel for pixel, except where
	 * the brand arcs sit on the left.
	 */
	.page-hero:has(.page-hero__image) {
		background:
			var(--hero-surface-mirrored) right top / var(--hero-outside) 100% no-repeat,
			var(--hero-surface);
	}
}


/* 10. Section heading & intro
   ========================================================================== */

.section-heading {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3) var(--space-6);
	align-items: flex-end;
	justify-content: space-between;
	margin-bottom: var(--space-6);
}

.section-heading__title {
	font-size: var(--fs-h2);
}

.section-heading__subtitle {
	margin-top: var(--space-1);
	font-size: var(--fs-base);
	color: var(--color-text);
}

.section-heading__subtitle strong {
	font-weight: 600;
	color: var(--color-navy-soft);
}

.section-heading__link {
	margin-bottom: 0.35rem;
}

/*
 * Two-column section: an intro beside its content. Shared by About and Careers,
 * so the skeleton lives here — but never the proportion. How wide the two sides
 * are is a per-screen design decision, set at 992px in that screen's stylesheet;
 * fixing one here would make every future user fight it.
 */
.split-section {
	padding-block: var(--section-space);
}

.split-section__inner {
	display: grid;
	gap: var(--space-8);
	align-items: center;
}

/*
 * No colour: headings are --color-ink from the base rule. This used to override
 * it to exactly that, back when the base was --color-heading (#0f2447) and the
 * two sat on the same tinted background eleven ΔE apart — a bigger gap than the
 * one between the brand orange and its hover, which exists to be noticed.
 */
.section-intro__title {
	font-size: clamp(1.75rem, 1.3rem + 1.1vw, 2.15rem);
}

/*
 * A group emitted by quantum_format_groups(): its own line, but still ordinary
 * text. Where the column is too narrow for the group it wraps rather than
 * overflowing, which is the difference between this and a <br>.
 */
.line-group {
	display: block;
}

.section-intro__subtitle {
	margin-top: var(--space-2);
	font-size: var(--fs-md);
	font-weight: 500;
	/*
	 * --color-accent-deep, not --color-navy: the tokens above assign navy to
	 * names, card titles and badges, and accent-deep to subtitles, title accents
	 * and arrow links. This one had the names colour, which is why it matched
	 * "Dr. Michael Russo, DC" below it instead of the hero subtitle above it.
	 */
	color: var(--color-accent-deep);
}

.section-intro__text {
	margin-top: var(--space-5);
	line-height: 1.7;
}


/* 11. Info banner & CTA band
   ========================================================================== */

.info-banner {
	display: grid;
	gap: var(--space-4) var(--space-6);
	align-items: center;
	margin-top: var(--space-6);
	padding: var(--space-6);
	background: var(--color-surface-tint);
	border-radius: var(--radius-lg);
}

.info-banner:first-child {
	margin-top: 0;
}

.info-banner__icon {
	color: var(--color-primary);
}

/*
 * The confirmed state — .info-banner.is-subscribed
 * ------------------------------------------------
 * The banner becomes the answer instead of growing a message under the form.
 * Three of the four things on it change and the fourth disappears: the icon
 * turns into a check, the heading and the text are rewritten, and the form goes.
 *
 * **Green on the icon, and nowhere else.** --color-success is the site's own
 * confirmation colour — the contact form uses it to say the same kind of thing
 * — and it measures 6.49:1 against this banner's background, where WCAG 1.4.11
 * asks 3:1 for a non-text element. But the heading stays navy and the band stays
 * pale blue on purpose: the full green block belongs on the contact form, where
 * somebody is asking about their care and needs to know the message left. The
 * weight of a status should match what is at stake, and a newsletter signup is
 * not that. Colour is never the only signal either — the check and the sentence
 * carry it too, which is what WCAG 1.4.1 asks for.
 *
 * Two alternatives were built and rejected: the check in brand blue, which left
 * the banner looking like it had merely changed its words, and the check on a
 * white disc, which made the icon the loudest object in a message whose point is
 * the sentence.
 */
.info-banner__done-icon,
.info-banner.is-subscribed .info-banner__icon > .icon:not(.info-banner__done-icon) {
	display: none;
}

.info-banner.is-subscribed .info-banner__done-icon {
	display: block;
	color: var(--color-success);
}

.info-banner.is-subscribed .info-banner__action {
	display: none;
}

/*
 * The failure block sits under the pill, not inside it, and carries no bottom
 * margin: .form-status__message has one for the contact form, where fields
 * follow it, and here there is nothing underneath.
 */
.info-banner__error {
	margin-block: var(--space-3) 0;
}

/*
 * The action is a column, not a row. With a single button this reads exactly
 * the same as the justify-content: center it replaces — measured identical on
 * all four button banners at 360, 575, 768, 900 and 1280 — and with the
 * newsletter it is what keeps the error message under the pill instead of
 * beside it. It sits outside any media query because both layouts need it.
 */
.info-banner__action {
	display: flex;
	flex-direction: column;
	align-items: center;
	width: 100%;
}

.info-banner__icon--circle {
	display: grid;
	place-items: center;
	width: 80px;
	height: 80px;
	background: var(--color-white);
	border-radius: 50%;
	box-shadow: var(--shadow-sm);
}

.info-banner__title {
	font-size: var(--fs-lg);
	color: var(--color-navy);
}

.info-banner__lead {
	margin-top: var(--space-1);
	font-size: 0.9375rem;
}

/*
 * The pill and the message under it are declared together because they are one
 * thing: a control and the answer to it, in one column. Left to itself the
 * message filled the column — 486px against the pill's 440 — and 46px of
 * overhang made the field look shrunken and the error look like it belonged to
 * something else. Two stacked elements in the same column share their edges.
 */
.info-banner .pill-form,
.info-banner__error {
	width: 100%;
	max-width: 440px;
}

/*
 * Stacked: centre the icon and the copy instead of leaving them hugging the
 * left edge with empty space beside them. Same treatment as the CTA band; the
 * button itself only goes full width on phones (see the 575.98px block above),
 * so at tablet widths it stays its natural size.
 */
@media (max-width: 767.98px) {
	.info-banner {
		justify-items: center;
		text-align: center;
	}
}

@media (min-width: 768px) {
	.info-banner {
		grid-template-columns: auto minmax(0, 1fr) auto;
		padding: var(--space-6) var(--space-10);
	}

	.info-banner__icon--plain {
		margin-inline: var(--space-4);
	}
}

/*
 * The newsletter is the exception to that row, and Robert found it at 777px.
 * Its action is a 440px pill, not a button, and between 768 and 991 the row
 * cannot hold both that and a readable column of copy. Measured at 772px: the
 * copy column collapsed to 153px — the heading broke over two lines and the
 * sentence over five — and the pill never reached its 440px either, because
 * `width: 100%` inside an `auto` grid track is circular, so the track fell
 * back to the field's intrinsic 361px and stayed there all the way to 991px.
 *
 * Both faults end at the same number: 992px is where the rule below gives the
 * pill a definite width, so that is where the row becomes honest. Below it the
 * banner stays stacked, which is the layout Robert pointed to at 689px.
 */
@media (min-width: 768px) and (max-width: 991.98px) {
	.info-banner--newsletter {
		grid-template-columns: none;
		justify-items: center;
		text-align: center;
	}
}

@media (min-width: 992px) {
	.info-banner .pill-form,
	.info-banner__error {
		width: 440px;
	}
}

.cta-band {
	position: relative;
	overflow: hidden;
	color: var(--color-white);
	background: var(--color-orange);
}

/* Faint brand-mark arcs, matching the page hero. */
.cta-band::before {
	position: absolute;
	top: -150px;
	left: 36%;
	width: 340px;
	height: 340px;
	pointer-events: none;
	content: "";
	border: 34px solid rgb(255 255 255 / 0.05);
	border-radius: 50%;
}

/* Stacked and centered below xxl; text left / buttons right from xxl. */
.cta-band__inner {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: var(--space-5);
	align-items: center;
	padding-block: clamp(1.5rem, 1.2rem + 0.8vw, 1.75rem); /* ~132px band at 1440px, as in the mockups */
	text-align: center;
}

.cta-band__title {
	font-size: clamp(1.5rem, 1.1rem + 1.2vw, 2rem);
	font-weight: 700;
	color: var(--color-white);
}

.cta-band__lead {
	margin-top: var(--space-1);
	font-size: 1.0625rem;
	color: rgb(255 255 255 / 0.85);
}

.cta-band__actions {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-4);
	justify-content: center;
}

@media (max-width: 575.98px) {
	.cta-band__actions,
	.cta-band__actions .btn {
		width: 100%;
	}
}

/* Row layout only from xxl, where the longest CTA title still fits on one line next to the buttons. */
@media (min-width: 1400px) {
	.cta-band__inner {
		flex-direction: row;
		gap: var(--space-6);
		justify-content: space-between;
		text-align: left;
	}

	.cta-band__text {
		flex: 1 1 0;
		min-width: 0;
	}

	.cta-band__actions {
		flex-shrink: 0;
		flex-wrap: nowrap;
	}
}


/* 12. Footer
   ========================================================================== */

.site-footer {
	font-size: 0.9375rem;
	color: var(--color-footer-text);
	background: var(--color-footer);
}

.site-footer a {
	color: inherit;
}

.site-footer a:hover {
	color: var(--color-white);
}

.site-footer__grid {
	display: grid;
	gap: var(--space-8);
	padding-block: var(--space-6) var(--space-8);
}

/*
 * The ratio is stated here as well as in the width and height attributes. The
 * attributes alone already reserve the box, but `height: auto` (set on every
 * image) leaves no author height or ratio, and Chrome's DevTools then flags the
 * one lazy-loaded image on every page as "Lazy-loaded images should have
 * explicit dimensions". 2266 / 930 is the logo file's own shape — the same
 * ratio template-parts/footer/site-footer.php passes for the attributes.
 */
.site-footer__logo img {
	width: 120px;
	height: auto;
	aspect-ratio: 2266 / 930;
}

.site-footer__about {
	max-width: 38ch;
	margin-top: 0.6rem;
	line-height: 1.6;
}

.social-links {
	display: flex;
	gap: var(--space-3);
	margin-top: var(--space-6);
}

.social-links__link {
	position: relative;
	display: grid;
	place-items: center;
	width: 44px;
	height: 44px;
	background: rgb(255 255 255 / 0.08);
	border-radius: 50%;
	transition: background-color var(--transition), color var(--transition);
}

/*
 * Sky is the accent this theme uses on dark surfaces, so the hovered button is unmistakable — 9.22:1
 * against the footer — and the icon that lands on it reads at 10.20:1. Brand navy or a brighter blue
 * would have been barely distinguishable from the footer itself.
 *
 * The second figure said 7.7:1 until 25 September and matched nothing on the page: not --color-nav
 * (10.20), not --color-navy (5.65), not --color-ink (9.71). It was measured before the brand-blue
 * unification of 22 September moved the navies, and nobody re-measured it. Remeasured now; the real
 * number is better than the one claimed, so nothing was ever wrong on screen except this line.
 *
 * --color-sky is NOT the brand's secondary cyan, AND THAT IS DELIBERATE since 25 September. The guide
 * gives #03D1FF (Pantone 2985C); this is #7fd0f8, paler and less saturated — dE 14.6 apart, visible
 * side by side. The three were compared on this very footer, scaled up, and Robert chose to keep this
 * one: the brand cyan is an ACCENT, and at 44px filled it stops being an accent — in a closing footer
 * it competes for attention that is not its to take. A brand colour used at the wrong scale can be
 * wrong even when the hex is right.
 *
 * Contrast was never the argument either way: the brand cyan measures 8.69:1 here against this one's
 * 9.22, and both are far past the 3:1 a non-text control needs.
 *
 * So do not "fix" this to #03D1FF. It was looked at and decided.
 */
.site-footer .social-links__link {
	color: var(--color-white);
}

.site-footer .social-links__link:hover {
	color: var(--color-nav);
	background: var(--color-sky);
}

/*
 * Tooltip with the network name. It is the link's own label (not a `title` attribute, which never
 * appears on keyboard focus or touch), so it is announced once and shown on hover and on focus.
 */
.social-links__name {
	position: absolute;
	bottom: calc(100% + 0.5rem);
	left: 50%;
	padding: 0.2rem 0.5rem;
	font-size: 0.75rem;
	font-weight: 600;
	color: var(--color-footer);
	white-space: nowrap;
	pointer-events: none;
	background: var(--color-white);
	border-radius: var(--radius-sm);
	opacity: 0;
	transition: opacity var(--transition);
	transform: translateX(-50%);
}

.social-links__link:hover .social-links__name,
.social-links__link:focus-visible .social-links__name {
	opacity: 1;
}

.site-footer__title {
	margin-bottom: 0.4rem;
	font-size: var(--fs-base);
	font-weight: 600;
	color: var(--color-white);
}

.site-footer__links {
	display: grid;
	gap: 0.25rem;
	line-height: 1.45;
}

/*
 * Two columns at every width that has room for them. One column of nine ran
 * well past the brand block and the contact block on either side of it, and a
 * footer whose middle drops a long thin tail reads as three unrelated pieces;
 * split 5/4 the three finish at roughly the same line. It also means the list
 * behaves the same on a phone as on a desktop.
 *
 * Filled down the first column and then the second, not across: the eye reads
 * a column of links downwards, and across would put Home beside Services with
 * Locations under Home. Five rows is the nine links halved and rounded up — a
 * tenth balances 5/5 by itself, an eleventh would want six rows here.
 */
@media (min-width: 360px) {
	.site-footer__links {
		grid-auto-flow: column;
		grid-template-rows: repeat(5, auto);
		grid-auto-columns: minmax(0, 1fr);
		gap: 0.25rem var(--space-5);
	}
}


.site-footer__address {
	display: grid;
	gap: var(--space-3);
	margin-bottom: var(--space-6);
}

.site-footer__contact-item {
	display: flex;
	gap: var(--space-3);
	align-items: flex-start;
}

.site-footer__contact-item .icon {
	margin-top: 2px;
	color: var(--color-white);
}

.site-footer__phone {
	font-weight: 600;
}

/*
 * Already white, so the footer's "link turns white" hover is invisible on it:
 * it underlines its label instead (decoration is set on the span because it
 * does not propagate into flex items).
 */
.site-footer .site-footer__phone {
	color: var(--color-white);
}

.site-footer__phone:hover span {
	text-decoration: underline;
	text-underline-offset: 3px;
}

.site-footer__bottom {
	font-size: var(--fs-sm);
	border-top: 1px solid rgb(255 255 255 / 0.08);
}

.site-footer__bottom-inner {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-3) var(--space-6);
	align-items: center;
	justify-content: space-between;
	padding-block: var(--space-5) var(--space-6);
}

.site-footer__legal {
	display: flex;
	gap: var(--space-3);
}

.site-footer__legal li + li::before {
	margin-right: var(--space-3);
	/*
	 * Escaped, not the character itself. Local's nginx serves this file as
	 * `text/css` with no charset, so the browser decoded the two UTF-8 bytes of
	 * "·" as Latin-1 and printed **Â·** in the footer of every page — on the
	 * live site too, since the header depends on the server, not on the file.
	 * An escape has no encoding to get wrong.
	 */
	content: "\00B7";
}

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

	/* Quick Links and Contact pair up underneath; the brand block takes the row. */
	.site-footer__brand {
		grid-column: 1 / -1;
	}
}

@media (min-width: 992px) {
	.site-footer__grid {
		grid-template-columns: 1.7fr 1.3fr 1.1fr;
		gap: var(--space-10);
	}

	.site-footer__brand {
		grid-column: auto;
	}
}


/* 13. Entry content & generic templates
   ========================================================================== */

/*
 * The Privacy Policy's table has six columns and does not fit at 320px — the
 * width the reflow criterion is measured at, and what a phone at 400% zoom comes
 * to. It used to drag the whole page sideways. Now the table keeps its width and
 * what scrolls is this container, not the page. The focusability and the
 * accessible name come from quantum_scrollable_tables(), in the theme.
 */
.table-scroll {
	overflow-x: auto;
	max-width: 100%;
	-webkit-overflow-scrolling: touch;
}

.entry-content table {
	min-width: 32rem;
	border-collapse: collapse;
}

.entry-content > * + * {
	margin-top: var(--space-5);
}

/*
 * The owl above already leaves the first child alone — that is the whole point
 * of `* + *`. What breaks it are the rules that set a top margin on an element
 * TYPE rather than on a position: `.entry-content h2` and `h3` below,
 * `.article-body h2` and `h3` in the article stylesheet, `.entry-content--checks ul`
 * further down. A descendant selector cannot tell whether it is looking
 * at the first thing in the box, so an article opening with a heading carried
 * 32px of empty space above it while one opening with a paragraph carried none.
 * Several posts open with an `<h2>`, so it was not a one-off.
 *
 * Restated here once rather than qualifying each of those five rules, because
 * this is the invariant and not an exception to it: nothing inside this box
 * adds space above where the box begins. The gap from whatever comes before
 * belongs to the section, which already sets it.
 *
 * One rule reaches all four containers because `.article-body` and
 * `.location-about` sit on the same element as `.entry-content`, and it wins
 * over every one of them on specificity — (0,2,0) against (0,1,1) — so a new
 * rule on some other element type cannot bring the gap back either.
 */
.entry-content > :first-child {
	margin-top: 0;
}

.entry-content h2 {
	margin-top: var(--space-10);
	font-size: 1.75rem;
}

.entry-content h3 {
	margin-top: var(--space-8);
	font-size: 1.35rem;
}

/*
 * The legal documents quote whole URLs (Google Analytics' opt-out one measures
 * 316px). Without this, at 320px that URL alone dragged the page sideways: 16px
 * of horizontal scroll on every legal page. Only here, in editorial content — in
 * the rest of the site the copy is ours and there are no unbroken runs.
 */
.entry-content {
	overflow-wrap: anywhere;
}

.entry-content a {
	text-decoration: underline;
	text-underline-offset: 3px;
}

.entry-content ul,
.entry-content ol {
	padding-left: 1.25rem;
}

/*
 * The check list of the text an editor writes on a location's page and on a
 * service's page: the same kind of page, so the same list. Any <ul> in the box
 * gets it without its author knowing a class name. The tick is the theme's own
 * circle-check icon as a mask, so it follows --color-link like an inline SVG.
 * Here since 17 October 2026; until then it was the location's alone
 * (.location-about in pages/location-detail.css), and a service's list fell
 * back to the article's bullets.
 */
.entry-content--checks ul {
	display: grid;
	gap: var(--space-3);
	margin: var(--space-5) 0 0;
	padding: 0;
	list-style: none;
}

/*
 * The tick hangs in the item's left padding, and the item's text is one run of
 * lines beside it. Until 14 October 2026 the item was a flex row, which made
 * every inline element an editor adds (a bold label, a link) a column of its
 * own: in Baldwin's list, "Walk-ins:" broke onto two lines beside its sentence.
 * The padding is the 20px tick plus the gap the flex row had, and top: 3px
 * centres the tick on the first line, as margin-top did.
 */
.entry-content--checks li {
	position: relative;
	padding-left: calc(20px + var(--space-3));
	color: var(--color-text);
}

.entry-content--checks ul > li::before {
	content: "";
	position: absolute;
	top: 3px;
	left: 0;
	width: 20px;
	height: 20px;
	background-color: var(--color-link);
	-webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>') center / contain no-repeat;
	mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>') center / contain no-repeat;
}

/*
 * An ordered list keeps its numbers, which a tick would contradict, in the
 * tick's colour and its column: the text of both lists starts at the same
 * place, with the same space between items.
 */
.entry-content--checks ol {
	padding-left: calc(20px + var(--space-3));
}

.entry-content--checks ol > li {
	padding-left: 0;
}

.entry-content--checks ol > li::marker {
	font-weight: 600;
	color: var(--color-link);
}

.entry-content--checks ol > li + li {
	margin-top: var(--space-3);
}

/*
 * The classic editor's blockquote.
 *
 * The button exists in the article toolbar, so the theme has to style it:
 * without this a <blockquote> looks exactly like a paragraph — the reset above
 * only strips its margin — and the editor would be offering something with no
 * visible effect. The rule, from 22 September 2026: the toolbar only offers what
 * the theme styles.
 */
/*
 * A safety net for an image inside an article.
 *
 * The add-media button is removed from that screen (an article's photograph is
 * its featured image), but an image can still arrive by a paste or through the
 * Code tab. This keeps it inside the column instead of overflowing it; it is not
 * an invitation to use them.
 */
.entry-content img {
	max-width: 100%;
	height: auto;
	border-radius: var(--radius-lg);
}

.entry-content figcaption {
	margin-top: var(--space-2);
	font-size: var(--fs-sm);
	color: var(--color-body);
}

.entry-content blockquote {
	padding-left: var(--space-5);
	border-left: 3px solid var(--color-brand-blue);
	font-size: 1.125rem;
	line-height: 1.6;
	color: var(--color-navy);
}

.entry-content blockquote > * + * {
	margin-top: var(--space-3);
}

.entry-content blockquote cite {
	display: block;
	font-size: var(--fs-sm);
	font-style: normal;
	color: var(--color-body);
}

.entry-content__figure img {
	border-radius: var(--radius-lg);
}

.post-list {
	display: grid;
	gap: var(--space-6);
}

.post-list__item {
	padding-bottom: var(--space-6);
	border-bottom: 1px solid var(--color-border);
}

.post-list__title {
	margin-bottom: var(--space-2);
	font-size: var(--fs-lg);
}

.meta-list {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2) var(--space-6);
	align-items: center;
	font-size: var(--fs-xs);
	color: var(--color-muted);
}

.meta-list__item {
	display: inline-flex;
	gap: var(--space-2);
	align-items: center;
}

.meta-list .icon {
	color: var(--color-primary);
}

/*
 * Pagination. The markup is WordPress's own (the_posts_pagination): it already writes the <nav>
 * landmark, a screen-reader heading and aria-current on the current page, so the theme styles
 * those classes rather than rebuilding markup that is correct. The look is the chip's, shared
 * through the selector lists in section 6 rather than copied, because a list of pages and a list
 * of filters are the same idea and should not read as two different systems. Only the sizing and
 * the two states a chip does not have are declared here.
 */
.pagination {
	margin-top: var(--space-8);
}

.pagination .nav-links {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-2);
	align-items: center;
	justify-content: center;
}

.pagination .page-numbers {
	min-width: 40px;
	min-height: 40px;
	padding: 0.45rem 0.9rem;
	font-size: var(--fs-sm);
	justify-content: center;
}

/*
 * Two arrows out of one icon: chevron-down is the only chevron in the sprite, and the language
 * switcher already rotates it. Cheaper than shipping two more SVGs of a shape we have.
 */
.pagination .prev .icon {
	transform: rotate(90deg);
}

.pagination .next .icon {
	transform: rotate(-90deg);
}

/* The ellipsis marks a gap in the sequence. It is not a target, so it gets no surface. */
.pagination .page-numbers.dots {
	min-width: 0;
	padding-inline: var(--space-2);
	color: var(--color-muted);
	background: none;
}


/* 14. Modal
   ========================================================================== */

.modal {
	width: min(100% - 2rem, 460px);
	padding: 0;
	border: 0;
	border-radius: var(--radius-lg);
	box-shadow: var(--shadow-lg);
}

.modal::backdrop {
	background: rgb(15 31 51 / 0.6);
}

.modal__body {
	display: grid;
	gap: var(--space-4);
	justify-items: start;
	padding: var(--space-8);
}

.modal__title {
	font-size: var(--fs-lg);
}

/*
 * Same 32px circle as .pill-form__clear, for the same reason: WCAG 2.5.8 asks
 * 24px of a target and the glyph inside is 16. A dialog whose only way out is a
 * button at the bottom makes the reader travel the whole thing to leave it,
 * which a gallery makes obvious; Escape and a backdrop click already work, but
 * neither is visible.
 */
.modal__close {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 32px;
	height: 32px;
	padding: 0;
	color: var(--color-muted);
	cursor: pointer;
	background: none;
	border: 0;
	border-radius: 50%;
	transition: color var(--transition), background-color var(--transition);
}

.modal__close:hover {
	color: var(--color-navy);
	background: var(--color-surface-chip);
}

.modal__close:focus-visible {
	outline: 2px solid var(--color-primary);
	outline-offset: 2px;
}


/* 15. Photograph viewer (lightbox)
   ========================================================================== */

/*
 * A full-bleed <dialog> living in the top layer. Nothing here positions it
 * against the page, because showModal() takes it out of the flow entirely and
 * the browser paints ::backdrop behind it. Opened from inside the gallery
 * dialog it simply stacks — the top layer is a stack, not a z-index race, so no
 * value in this block has to beat anything in another.
 */
.lightbox {
	width: 100%;
	max-width: 100%;
	height: 100%;
	max-height: 100%;
	padding: 0;
	color: var(--color-white);
	background: transparent;
	border: 0;
}

.lightbox::backdrop {
	background: rgb(6 14 25 / 0.94);
}

/*
 * Two rows: the photograph takes what is left, the controls take what they
 * need. `min-height: 0` on the stage is the part that is easy to miss — a grid
 * item's min-height defaults to auto, so without it the 1fr row refuses to
 * shrink below the image's intrinsic height and a tall photograph pushes the
 * controls off the bottom of the screen.
 */
.lightbox__frame {
	position: relative;
	display: grid;
	grid-template-rows: 1fr auto;
	gap: var(--space-4);
	height: 100%;
	padding: var(--space-4);
}

/*
 * The photograph is sized by this box, not by itself.
 *
 * The obvious construction — centre the image and cap it with
 * `max-height: 100%` — does not work, and it was measured rather than guessed.
 * A percentage max-height on a grid item resolves against the grid area, and a
 * stretched area is not a definite height during intrinsic sizing, so the
 * declaration is thrown away: a portrait photograph painted 1100x1650 inside a
 * 704px window. Five variants were tried in the browser (a definite 100dvh on
 * the frame, minmax(0, 1fr) rows, width/height 100% in flow, and this); only
 * this one fits both a landscape and a portrait photograph.
 *
 * So the <img> fills the stage absolutely — percentages against a positioned
 * ancestor always resolve — and object-fit does the fitting. `min-height: 0`
 * stays because a grid item's min-height defaults to auto, which would
 * otherwise stop the 1fr row shrinking and push the controls off screen.
 *
 * The cost is that the element now covers its own letterboxing, so a click
 * beside the photograph lands on the image rather than on this box. That is
 * handled in initLightbox() by measuring where the photograph actually is,
 * rather than by making the image ignore the pointer — which would have closed
 * the viewer when someone taps the photograph itself.
 */
.lightbox__stage {
	position: relative;
	min-height: 0;

	/*
	 * The stand-in while the full-size file is in flight, painted from the
	 * thumbnail the page already downloaded. `contain` puts it in exactly the
	 * rectangle object-fit gives the real photograph, so nothing moves when the
	 * big one arrives on top of it. The script sets and clears the image.
	 */
	background-repeat: no-repeat;
	background-position: center;
	background-size: contain;
}

/*
 * contain, not cover. This is the one place in the theme that must not crop:
 * everywhere else a photograph fills a box the design drew, and here the
 * photograph is the point. It is also what lets a portrait photograph stay
 * portrait — and the practice does upload those by mistake, which is how the
 * gallery tiles came to enforce a shape in the first place.
 *
 * No border-radius: the element is the whole stage now, so a radius would round
 * a box nobody can see and leave the photograph's own corners square.
 */
.lightbox__image {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/*
 * A note on the state this element has when the file never arrives, so nobody
 * spends the afternoon on it again.
 *
 * The browser replaces a broken image with its alt text, next to the broken
 * image marker, at the start of the box — and this box is the whole stage, so
 * that lands in the top-left corner. It looks stray, and the obvious fix is
 * `text-align: center`. It was tried and it is worse: the text centres and the
 * marker does not follow, so the two end up at opposite ends of the screen with
 * nothing between them. `display: grid` with `place-content: center` changes
 * nothing at all — a replaced element's fallback is not laid out as grid
 * content.
 *
 * So the fallback is left exactly as the browser draws it. A broken image that
 * looks like a broken image is not a defect; it is the one presentation every
 * visitor already knows, and the alt beside it says which photograph it was.
 */

/*
 * The trigger the script wraps around each thumbnail. Built in JavaScript, not
 * in PHP, for the reason this theme keeps arriving at: a control that cannot
 * work yet must not look pressable. The gallery's own "View More Photos" button
 * ships hidden and is revealed by the same script; this one does not exist
 * until there is something to open.
 *
 * It is a reset plus a shape. The radius matches the tiles it wraps so the
 * clipped zoom does not square off the corners.
 */
.lightbox-open {
	display: block;
	width: 100%;
	padding: 0;
	cursor: pointer;
	background: none;
	border: 0;
	border-radius: var(--radius-tile);
}

.lightbox-open:focus-visible {
	outline: 2px solid var(--color-primary);
	outline-offset: 3px;
}

.lightbox__bar {
	display: flex;
	gap: var(--space-5);
	align-items: center;
	justify-content: center;
}

/*
 * The same control the home hero's pause button wears — 32px, a dark disc and a
 * white ring — so it stays legible over any photograph without needing a bar
 * behind it. 32 rather than 24 is this theme's house size for an icon-only
 * control; SC 2.5.8 asks for 24x24 and every other icon button here is 32.
 */
.lightbox__control,
.lightbox__close {
	display: grid;
	width: 32px;
	height: 32px;
	padding: 0;
	color: var(--color-white);
	cursor: pointer;
	background: rgb(15 31 51 / 0.55);
	border: 0;
	border-radius: 50%;
	box-shadow: 0 0 0 1px rgb(255 255 255 / 0.92);
	backdrop-filter: blur(3px);
	place-items: center;
}

/*
 * Hover inverts the disc instead of darkening it, and that is a correction.
 *
 * The first version deepened the same translucent navy from 0.55 to 0.85,
 * copying the home hero's pause control. It works there because that button
 * sits on a photograph. Here it sits on a backdrop that is already almost the
 * same colour, so the two states composite to rgb(18 30 45) and rgb(16 31 49) —
 * measured: 1.01:1 between them, which is no feedback at all.
 *
 * It also explains why the close button seemed to have a hover and the arrows
 * did not: from 768 the arrows are always over the backdrop, while the close
 * button can fall over a bright part of the photograph, where the same pair
 * reaches 2.24:1 and becomes visible. Same rule, different surface.
 *
 * White with the icon in ink is 16.86:1 against the resting state and reads on
 * any surface the viewer can put behind it. It is also the theme's existing
 * language for an icon button that has to carry on a photograph — the play
 * control on the video cards is a white disc with a dark glyph.
 */
.lightbox__control:hover,
.lightbox__close:hover {
	color: var(--color-ink);
	background: var(--color-white);
}

/*
 * The ring is a box-shadow, so a focus ring drawn the same way would replace it
 * rather than sit outside it. outline does not share that problem.
 */
.lightbox__control:focus-visible,
.lightbox__close:focus-visible {
	outline: 2px solid var(--color-white);
	outline-offset: 2px;
}

.lightbox__close {
	position: absolute;
	z-index: 1;
	top: var(--space-4);
	right: var(--space-4);
}

.lightbox__count {
	margin: 0;
	font-family: var(--font-heading);
	font-size: var(--fs-sm);
	font-variant-numeric: tabular-nums;
	font-weight: 600;
}

/*
 * From 768 the screen is big enough to put the two controls beside the
 * photograph instead of under it, which is where a mouse expects them. The
 * counter stays at the bottom on its own.
 */
@media (min-width: 768px) {
	.lightbox__frame {
		padding: var(--space-5) calc(var(--space-5) * 3);
	}

	.lightbox__control {
		position: absolute;
		top: 50%;
		transform: translateY(-50%);
	}

	.lightbox__control[data-lightbox-prev] { left: var(--space-4); }
	.lightbox__control[data-lightbox-next] { right: var(--space-4); }
}


/* 16. Per-screen token overrides
   There are none, and that is the point. Two lived here, both inherited from mockups drawn in two
   styles: --color-link re-pointed on four routes so anything clickable went navy on inner screens,
   and --color-navy / --color-accent-deep re-pointed on Services so its card titles ran lighter than
   the identical component everywhere else — the same info-banner title was #0c2b6f on Testimonials
   and #1b3994 here. Both are gone. A per-screen palette is a set of screens, not a system: a
   component should not change colour because of the page it happens to be on, and a reader should
   only have to learn the palette once.

   Route classes are still used for things that genuinely belong to one screen — the framing of a
   hero photograph, for instance — but never to re-point a token.
   ========================================================================== */

