/*
 * Shared foundations: design tokens and the components blocks are built from.
 *
 * This file is loaded BOTH on the front end and into the block editor canvas
 * (see inc/setup.php). That is the whole point of it existing separately from
 * style.css, which carries site chrome the editor must not get.
 *
 * Anything a block's own style.css references must live here. A block that
 * reaches for a token defined only in style.css renders correctly on the front
 * end and collapses in the editor, because an undefined var() makes the whole
 * declaration invalid and the property falls back to its initial value. That is
 * how the grid silently became "no grid" in the editor: widths and heights went
 * to their defaults and every column stacked full width.
 *
 * Tokens are declared on :root AND body. The editor rewrites the selectors in
 * editor stylesheets, and body is the selector that survives that rewrite in
 * every version we have tested, so declaring both is belt and braces.
 */

/* ============================================================
   Design conventions theme.json cannot express
   ============================================================ */

:root,
body {
	/*
	 * Full-bleed "fill the screen" sections (hero, statement, and any future
	 * section meant to fill the viewport) use this height. It fills ~100vh but
	 * stays in a sensible band so it never gets silly on very short or very tall
	 * screens. Tune the band here, once, and every section follows.
	 */
	--gm-section-vh: clamp(35rem, 100svh, 62.5rem);

	/*
	 * The layout grid, taken from the Figma layout grids on the desktop and
	 * mobile variants. Desktop is 12 columns with a 24 gutter and 32 margins;
	 * mobile is 4 columns with a 24 gutter and 24 margins.
	 *
	 * Sections build their own grid from these three values rather than sharing
	 * a layout class, so no block depends on another block's CSS. Horizontal
	 * placement on desktop is by column span, not by pixel width, so sections
	 * keep their proportions as the viewport grows past 1440.
	 */
	--gm-grid-columns: 12;
	--gm-grid-gutter: var(--wp--preset--spacing--24);
	--gm-grid-margin: var(--wp--preset--spacing--32);
}

@media (max-width: 900px) {

	:root,
	body {
		--gm-grid-columns: 4;
		--gm-grid-margin: var(--wp--preset--spacing--24);
	}
}

/* ============================================================
   Text rendering
   ============================================================ */

/*
 * Figma draws type with greyscale antialiasing. Browsers on macOS default to
 * subpixel, which thickens strokes and makes the same weight look heavier on
 * screen than it does in the design. Matching Figma's approach is what closes
 * that gap, and it is a rendering hint only: no metric, weight or size changes.
 */
body {
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

/* ============================================================
   Arrow link — the "Button Secondary" component
   ============================================================ */

/*
 * The Figma component set "Button Secondary" has two colours (Light on dark
 * grounds, Dark on light ones) and a hover state per colour. It is a real part
 * of the design system rather than one section's decoration, so it lives here
 * and any block can use it.
 *
 * Markup: an <a class="gm-link-arrow"> holding a hidden arrow span and a label
 * span. The arrow is a text glyph, as it is in Figma, so it inherits the font
 * and the colour transition without an icon file.
 *
 *   <a class="gm-link-arrow">
 *     <span class="gm-link-arrow__glyph" aria-hidden="true">&rarr;</span>
 *     <span class="gm-link-arrow__label">Find out more</span>
 *   </a>
 */

.gm-link-arrow {
	display: inline-flex;
	/* Baselines differ between the glyph and the label, so centre them. */
	align-items: center;
	gap: var(--wp--preset--spacing--8);
	color: var(--wp--preset--color--white);
	font-family: var(--wp--preset--font-family--sans);
	font-size: var(--wp--preset--font-size--sm);
	font-weight: 500;
	line-height: 1;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	text-decoration: none;
	transition: color 150ms ease;
}

/*
 * Hover and keyboard focus follow Figma Button Secondary: the light link steps to
 * Blue 4 (170:434) and the dark link to Blue 2 (170:440). The arrow also steps
 * forward, so the link reads as active for anyone who cannot pick up the tonal
 * change. Two classes, so a block that sets its own link colour does not win.
 */
.gm-link-arrow:hover,
.gm-link-arrow:focus-visible {
	color: var(--wp--preset--color--blue-4);
}

.gm-link-arrow:hover .gm-link-arrow__glyph,
.gm-link-arrow:focus-visible .gm-link-arrow__glyph {
	transform: translateX(3px);
}

/* Dark colourway: for use on the light (Blue 5) grounds. */
.gm-link-arrow.gm-link-arrow--dark {
	color: var(--wp--preset--color--blue-1);
}

.gm-link-arrow.gm-link-arrow--dark:hover,
.gm-link-arrow.gm-link-arrow--dark:focus-visible {
	color: var(--wp--preset--color--blue-2);
}

/*
 * The glyph is decorative — the label already says where the link goes — so it
 * is hidden from assistive technology in the markup, not here.
 */
.gm-link-arrow__glyph {
	/* Stops the arrow being squeezed if the label wraps. */
	flex: none;
	transition: transform 200ms ease;
}

@media (prefers-reduced-motion: reduce) {

	.gm-link-arrow,
	.gm-link-arrow__glyph {
		transition: none;
	}

	.gm-link-arrow:hover .gm-link-arrow__glyph,
	.gm-link-arrow:focus-visible .gm-link-arrow__glyph {
		transform: none;
	}
}

/* ============================================================
   Primary button — a wash slides across on hover
   ============================================================ */

/*
 * The ground colour never changes. A wash slides in from the left instead, so
 * the button reads as active without the text ever sitting on a new colour it
 * was not tested against.
 *
 * The wash is currentColor at a low strength rather than a named colour, so one
 * rule serves both colourways: on the light button it is a little Blue 1 over
 * Blue 5, and on the dark button (the header once it is scrolled) it is a
 * little Blue 5 over Blue 1. Measured, the label keeps 7.9:1 on the light
 * button and 6.6:1 on the dark one, both well clear of AA.
 *
 * Tune the strength here and every button follows.
 */

.wp-element-button,
.wp-block-button__link {
	--gm-btn-wash: 0.22;

	position: relative;
	/*
	 * Its own stacking context, so the wash can sit at -1: above the button's
	 * background, below its label. That saves wrapping the label in a span,
	 * which core's button block does not give us anywhere to do.
	 */
	isolation: isolate;
	/* Clips the wash to the corner radius. */
	overflow: hidden;
	transition: transform 120ms ease;
}

/*
 * A soft-edged wash that sweeps across, rather than a hard block or a plain fade.
 *
 * The geometry matters and the numbers depend on each other. The element is one
 * and a half button widths, and only its last sixth is the fade. That puts the
 * soft edge at about a quarter of a button wide: narrow enough to read as an
 * edge travelling across, which is the whole point.
 *
 * Getting this wrong is easy in a specific way. Make the fade as wide as the
 * button and there is never a moment when part of the button is covered and part
 * is not, so the whole face just darkens together and it reads as a fade with no
 * sideways movement at all. The fade must stay a fraction of the button.
 *
 * At rest the element sits entirely off to the left. It travels one of its own
 * widths, and the opaque part has covered the button before the end, so the
 * button finishes evenly covered rather than caught mid-gradient.
 */
.wp-element-button::before,
.wp-block-button__link::before {
	content: "";
	position: absolute;
	z-index: -1;
	top: 0;
	bottom: 0;
	left: 0;
	width: 150%;
	/*
	 * Translate rather than animating `left`: it runs on the compositor, so the
	 * slide cannot stutter on a long page.
	 */
	transform: translateX(-100%);
	/*
	 * currentcolor and opacity rather than color-mix: same result, and if the
	 * gradient ever fails to parse the button would be left with no hover
	 * affordance at all, which is a poor thing to depend on a newer function for.
	 */
	background-image: linear-gradient(
		to right,
		currentcolor 0%,
		currentcolor 83%,
		transparent 100%
	);
	opacity: var(--gm-btn-wash);
	transition: transform 350ms ease;
}

.wp-element-button:hover::before,
.wp-element-button:focus-visible::before,
.wp-block-button__link:hover::before,
.wp-block-button__link:focus-visible::before {
	transform: translateX(0);
}

.wp-element-button:active,
.wp-block-button__link:active {
	transform: scale(0.94);
}

/*
 * Keyboard focus gets the wash and a ring. The wash alone is a colour change,
 * and a colour change on its own is not a focus indicator.
 */
.wp-element-button:focus-visible,
.wp-block-button__link:focus-visible {
	outline: 2px solid currentcolor;
	outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {

	.wp-element-button,
	.wp-block-button__link,
	.wp-element-button::before,
	.wp-block-button__link::before {
		transition: none;
	}

	/*
	 * No slide and no squash, but the button must still answer the pointer, so
	 * the wash appears in place instead.
	 */
	.wp-element-button:hover::before,
	.wp-element-button:focus-visible::before,
	.wp-block-button__link:hover::before,
	.wp-block-button__link:focus-visible::before {
		transform: none;
	}

	.wp-element-button:active,
	.wp-block-button__link:active {
		transform: none;
	}
}

/* ============================================================
   Parallax media — the frame holds still, the photo drifts
   ============================================================ */

/*
 * Put .gm-parallax on the <img> and give its parent position and
 * overflow: hidden. The frame never moves, so nothing around it shifts and no
 * layout is affected; only the picture inside travels.
 *
 * The maths, so it can be changed safely. Pick an overhang: the photo is taller
 * than its frame by that much at the top AND at the bottom. Then:
 *
 *   height = 100% + (2 x overhang)
 *   top    = -overhang
 *   drift  = a little under overhang / height, because the transform is a
 *            percentage of the PHOTO's height, not the frame's
 *
 * At 8%: height 116%, top -8%, and the drift is 6.8% of the photo, which is
 * 7.89% of the frame. That sits inside the 8% overhang with room for sub-pixel
 * rounding. Taking the exact fit (6.897%) leaves a tenth of a pixel and can show
 * a hairline edge at the extremes of the scroll.
 *
 * Change one of these three numbers and you must change the others. Raise the
 * overhang for a stronger effect, lower it for a subtler one.
 *
 * Driven by a scroll-driven animation rather than JavaScript, so it costs no
 * main-thread work and cannot jank. Where the browser has no support (Firefox
 * at the time of writing) the photo simply sits still, correctly cropped, which
 * is the design without the enhancement rather than a broken version of it.
 *
 * IMPORTANT, and the reason this needs two classes rather than one: the frame
 * must carry .gm-parallax-frame, and the photo .gm-parallax.
 *
 * A bare `animation-timeline: view()` on the photo does NOT work here. view()
 * measures its subject against the nearest scroll container, and the frame sets
 * overflow: hidden, which makes the frame itself that container. The photo never
 * moves relative to a frame that never scrolls, so progress sticks at 50% and the
 * picture sits frozen at the midpoint. It looks plausible and does nothing.
 *
 * So the timeline is declared on the frame, whose own nearest scroll container is
 * the page, and the photo refers to it by name.
 */

.gm-parallax-frame {
	view-timeline-name: --gm-parallax;
	/*
	 * The frame must be the containing block for its own photo.
	 *
	 * .gm-parallax is absolutely positioned, so it is measured against the
	 * nearest positioned ancestor. Most blocks happened to set position:relative
	 * on their frame, so this looked fine for months. It was luck. The section
	 * reveal added a transform to every section, a transform makes an element a
	 * containing block, and every photo whose frame was not positioned started
	 * being sized against the whole section instead: top: -8% and height: 116%
	 * of something several times too big, and the drift stopped being visible.
	 * Declared here so the pattern cannot be broken by whatever an ancestor does.
	 */
	position: relative;
}

.gm-parallax {
	position: absolute;
	inset-inline: 0;
	top: -8%;
	width: 100%;
	height: 116%;
	object-fit: cover;
}

@keyframes gm-parallax-drift {

	from {
		transform: translate3d(0, -6.8%, 0);
	}

	to {
		transform: translate3d(0, 6.8%, 0);
	}
}

@supports (animation-timeline: --gm-parallax) {

	@media (prefers-reduced-motion: no-preference) {

		.gm-parallax {
			animation: gm-parallax-drift linear both;
			/* Named timeline from the frame, not view() on the photo. See above. */
			animation-timeline: --gm-parallax;
			/*
			 * Runs across the whole time the frame is crossing the viewport, so
			 * the drift is even and never finishes early on a tall section.
			 */
			animation-range: cover 0% cover 100%;
		}
	}
}

/*
 * Sections butt up against each other.
 *
 * Every gm/ section sets `margin-block-start: 0` in its own stylesheet, and on
 * its own that loses. WordPress emits `:root :where(.is-layout-flow) > *` for
 * the block gap, which is specificity (0,1,0), exactly the same as a single
 * class, and it is inlined in the head AFTER the block stylesheets. A tie is
 * decided by source order, so the 24 won and every page ran with a 24px band of
 * background showing between one full-bleed section and the next.
 *
 * Two classes here, so this wins on specificity rather than on order. Scoped to
 * ADJACENT sections on purpose: the component gallery puts a label between each
 * one, and there the gap is wanted.
 *
 * The per-block declarations are left in place. They say what each block
 * intends, and they are correct; this is the one rule that makes them stick.
 */
[class*="wp-block-gm-"] + [class*="wp-block-gm-"] {
	margin-block-start: 0;
}

/*
 * Metric-matched fallbacks, so the webfont swap does not move the page.
 *
 * The fonts load with `font-display: swap`, which is right: the brand type
 * always arrives, even on a slow connection. The cost is that the fallback
 * paints first and the real font replaces it, and if the two set text to
 * different widths, everything laid out against them jumps. Aidan saw it on the
 * header nav, which sits in a right-aligned group, so a few pixels of width
 * change slid the whole menu sideways on every page load.
 *
 * These faces are the system fallback with `size-adjust` set to the measured
 * ratio, so the fallback occupies the same width as the real font and the swap
 * changes the shapes without moving anything. Measured in the browser at 100px
 * over real DOM text: Spectral is 108.84% of Times, and Hanken Grotesk is
 * 101.7% of Arial in mixed case.
 *
 * One number cannot be right for everything. Hanken's CAPITALS are only 93% of
 * Arial's width while its lowercase is 102%, so a fallback tuned for the
 * all-caps navigation would throw every paragraph out instead. This is tuned to
 * body copy, which is the bulk of the text, and `gm_preload_fonts()` in
 * inc/setup.php handles the rest by getting the real face in before first paint.
 *
 * They sit second in the stack, straight after the real family, so they are
 * what gets used while the webfont is in flight.
 */
@font-face {
	font-family: "Hanken Grotesk Fallback";
	src: local("Arial"), local("Helvetica"), local("Liberation Sans");
	size-adjust: 101.7%;
	ascent-override: 95%;
	descent-override: 27%;
	line-gap-override: 0%;
}

@font-face {
	font-family: "Spectral Fallback";
	src: local("Times New Roman"), local("Times"), local("Liberation Serif");
	size-adjust: 108.84%;
	ascent-override: 95%;
	descent-override: 27%;
	line-gap-override: 0%;
}

/* ============================================================
   Page
   ============================================================ */

/*
 * No sideways scroll on phones. Aidan could drag the page sideways on his phone
 * and see blank white beyond it (12 September 2026). Chromium and WebKit
 * emulation both measured the page at exactly the screen width, so the likely
 * cause is iOS Safari counting the off-screen mobile menu, which is fixed and
 * translated out to the right, into the scrollable width.
 *
 * `clip` rather than `hidden`: clip cuts the overflow without turning the
 * element into a scroll container, so the parallax scroll timelines and the
 * fixed header keep working exactly as before.
 */
html,
body {
	overflow-x: clip;
}

/* ============================================================
   Section grounds
   ============================================================ */

/*
 * The Dark blue and Light blue styles offered on every section (registered in
 * inc/block-styles.php). Two parts to each selector, the block's own wrapper
 * class and the style class, so this beats the single-class background each
 * block sets in its own stylesheet without relying on load order.
 */
[class*="wp-block-gm-"].is-style-gm-ground-dark {
	background-color: var(--wp--preset--color--blue-1);
}

[class*="wp-block-gm-"].is-style-gm-ground-light {
	background-color: var(--wp--preset--color--blue-2);
}

/*
 * Pale, the third ground, offered on the two accreditation blocks only.
 *
 * The accreditation marks now carry their schemes' own colours, which cannot
 * read on Blue 1 or Blue 2, so both blocks sit on Blue 5 with Blue 1 text
 * (Figma 929:1578, 929:1592 and 929:2378). Each block colours its own text; this
 * sets the ground.
 */
[class*="wp-block-gm-"].is-style-gm-ground-pale {
	background-color: var(--wp--preset--color--blue-5);
}

/*
 * Pages built before 18 September carry the old Light blue class on the
 * accreditation bar (the services page does). Pale is what that means for these
 * two blocks now, so the class is mapped rather than every page edited.
 */
.wp-block-gm-accreditation-bar.is-style-gm-ground-light,
.wp-block-gm-accreditations-table.is-style-gm-ground-light {
	background-color: var(--wp--preset--color--blue-5);
}

/* ============================================================
   Form controls
   ============================================================ */

/*
 * The input box shared by gm/contact-form and gm/application. The two designs
 * label their fields differently (serif on the enquiry form, small capitals on
 * the application), so the labels live in each block; the box is one thing.
 * Figma: Blue 2 at 40% on Blue 1, 4 radius, 16 inset, Body type.
 */
.gm-input {
	display: block;
	box-sizing: border-box;
	inline-size: 100%;
	margin: 0;
	padding: var(--wp--preset--spacing--16);
	border: 0;
	border-radius: 0.25rem;
	background-color: var(--wp--custom--color--blue-2-40);
	color: var(--wp--preset--color--white);
	font-family: var(--wp--preset--font-family--sans);
	font-size: var(--wp--preset--font-size--base);
	line-height: 1.4;
}

/* On the Light blue ground a Blue 2 box would vanish; it takes Blue 1 at 40%. */
.is-style-gm-ground-light .gm-input {
	background-color: var(--wp--custom--color--blue-1-40);
}

.gm-input::placeholder {
	color: var(--wp--preset--color--blue-4-light);
}

.gm-input:focus-visible {
	outline: 2px solid var(--wp--preset--color--blue-4);
	outline-offset: 2px;
}

.gm-input--area {
	resize: vertical;
}

/* A select wearing the input box, with the vector chevron from Figma. */
.gm-select {
	position: relative;
	inline-size: 100%;
}

.gm-select .gm-input {
	appearance: none;
	block-size: 48px;
	padding-block: 0;
	padding-inline-end: var(--wp--preset--spacing--48);
	font-size: var(--wp--preset--font-size--body-m);
	line-height: 1.5;
	cursor: pointer;
}

/* Muted while the prompt is still showing, the way a placeholder is. */
.gm-select .gm-input:has(option[value=""]:checked) {
	color: var(--wp--preset--color--blue-4-light);
}

.gm-select__chevron {
	position: absolute;
	inset-block-start: 50%;
	inset-inline-end: var(--wp--preset--spacing--16);
	inline-size: 16px;
	block-size: 16px;
	transform: translateY(-50%);
	pointer-events: none;
}

/*
 * Figma: a 16 square, Blue 3 hairline, 2 radius. Ticked is a solid Blue 4 box with
 * a white tick (icons/check.svg), not the ring it used to show.
 */
.gm-checkbox {
	flex: none;
	box-sizing: border-box;
	inline-size: 16px;
	block-size: 16px;
	margin: 0;
	border: 1px solid var(--wp--preset--color--blue-3);
	border-radius: 2px;
	background-color: transparent;
	appearance: none;
	cursor: pointer;
}

.gm-checkbox:checked {
	border-color: var(--wp--preset--color--blue-4);
	background-color: var(--wp--preset--color--blue-4);
	background-image: url(images/icons/check.svg);
	background-position: center;
	background-repeat: no-repeat;
	background-size: 12px 12px;
}

.gm-checkbox:focus-visible {
	outline: 2px solid var(--wp--preset--color--blue-4);
	outline-offset: 2px;
}

/* The honeypot: there for bots, invisible and unreachable for people. */
.gm-form-trap {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* ============================================================
   Motion
   ============================================================ */

/*
 * The page fade (a cross-document view transition) and the rise-and-fade section
 * reveal were built here on 9 September and removed on 11 September: Aidan
 * rejected both. The image parallax is the approved motion. See CLAUDE.md §5.
 */

/*
 * Fade-in for text and inset photos (Aidan, 12 September 2026).
 *
 * The brief was precise: backgrounds appear straight away; text and inset photos
 * fade in over about 300ms, the first screen once its photos have loaded, and
 * the rest as they come into view. Nothing moves. Full-bleed photos (the hero,
 * the photo bands, the CTA band, photo page headers and the map) are shown
 * straight away rather than faded.
 *
 * html.gm-fade is set by a script in the head (inc/setup.php) and the reveal is
 * in assets/js/site.js, which adds .gm-shown. Without that class nothing here
 * applies, so the page is fully visible when scripts are off, when motion is
 * reduced, and in the editor. The selector must match the one in site.js.
 */
html.gm-fade main :is(h1, h2, h3, h4, h5, h6, p, li, address, blockquote, figcaption, label, img, .wp-element-button),
html.gm-fade .gm-site-footer :is(h2, p, li) {
	opacity: 0;
	transition: opacity 0.3s ease-out;
}

html.gm-fade main :is(h1, h2, h3, h4, h5, h6, p, li, address, blockquote, figcaption, label, img, .wp-element-button).gm-shown,
html.gm-fade .gm-site-footer :is(h2, p, li).gm-shown,
html.gm-fade main :is(.gm-hero__media, .gm-photo-band__img, .gm-cta-band__img, .gm-page-header__img, .gm-map__img),
html.gm-fade main .gm-accreditation__track * {
	opacity: 1;
}

/*
 * In-page anchors ease rather than jump. Set on the scroll container rather than
 * globally so it cannot interfere with a programmatic scroll.
 */
@media (prefers-reduced-motion: no-preference) {

	html {
		scroll-behavior: smooth;
	}
}

/*
 * Anything anchored to lands clear of the fixed bar rather than under it.
 */
:target {
	scroll-margin-block-start: calc(var(--gm-header-h, 72px) + var(--wp--preset--spacing--32));
}
