/*
Theme Name: GRIT Foundation
Theme URI: https://thegritfoundation.com
Author: Kalos Services
Description: Theme for the GRIT Foundation website, built on the GRIT design system: Archivo and Inter, orange reserved for CTAs and accents, navy headings and dark surfaces, and a three-tier button set. Tokens are registered with the block editor through theme.json so new content inherits them. See docs/design-system.md.
Version: 1.30.0
Requires at least: 6.0
Requires PHP: 8.0
License: Proprietary
Text Domain: gritfoundation
*/

/* ---------------------------------------------------------------------------
   Tokens — the GRIT design system. See docs/design-system.md.

   Colours alias the theme.json presets so each hex is written in exactly one
   place. The literal fallback is not decoration: an unresolvable var() computes
   to `unset`, not "the previous value", so a malformed theme.json would turn
   every background transparent site-wide with nothing in any log. Core also
   swallows a bad theme.json silently.

   Only colours are aliased. Spacing and type stay as CSS values because the
   theme's rhythm is fluid (clamp) and theme.json presets are static.
   --------------------------------------------------------------------------- */
:root {
	--grit-primary: var(--wp--preset--color--brand-primary, #ff2f00);
	--grit-primary-hover: var(--wp--preset--color--brand-primary-hover, #d62800);
	--grit-navy: var(--wp--preset--color--brand-navy, #00162a);
	--grit-text: var(--wp--preset--color--neutral-900, #1a2733);
	--grit-text-muted: var(--wp--preset--color--neutral-600, #5a6b7a);
	--grit-border: var(--wp--preset--color--neutral-200, #dbdee6);
	--grit-bg-alt: var(--wp--preset--color--neutral-100, #eef0f4);
	--grit-bg: var(--wp--preset--color--neutral-0, #fff);
	--grit-success: var(--wp--preset--color--success, #1b7a43);
	--grit-error: var(--wp--preset--color--error, #c42b10);

	/* The old --grit-ink did two incompatible jobs: dark surfaces and body text.
	   The design system separates them (navy vs neutral/900), so this does too. */
	--grit-surface-dark: var(--grit-navy);

	--grit-max: 1200px;
	--grit-measure: 68ch;
	--grit-gutter: clamp(1.25rem, 4vw, 2.5rem);
	/* 32px on a phone, 64px from 1280px up. Down from 48/96, which put more air
	   between two sections than inside either of them once real copy was in
	   them. Halved by .grit-section--tight, so the two stay tied together. */
	--grit-section: clamp(2rem, 5vw, 4rem);

	/* How far the homepage's opening card row rides up over the hero. See the
	   "Homepage opening" block below. Fluid rather than flat: the hero's own
	   padding is fluid, so a static overlap would fight it, and 72px is a much
	   larger share of a small screen than of a wide one. The cap matches the
	   ~70px the design shows on a 1344px-wide card block. */
	--grit-hero-card-overlap: clamp(2.5rem, 5vw, 4.5rem);

	/* Three steps, replacing the 8px token plus hardcoded 4px and 999px. */
	--grit-radius-sm: 4px;
	--grit-radius-md: 8px;
	--grit-radius-full: 999px;

	/* Archivo carries headings, Inter carries body. Conthrax is logo-only and is
	   never loaded as a webfont — it lives in the uploaded logo image. */
	--grit-font: Inter, "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
	--grit-font-heading: Archivo, "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

	/* Type scale. The system specifies one size per role with no mobile values,
	   so each is the *desktop cap* and scales down fluidly below — a flat 40px
	   h1 overflows the hero at 320px. Comments give the capped px value. */
	--grit-text-display: clamp(2.25rem, 1.66rem + 2.62vw, 3.625rem); /* 58 */
	--grit-text-h1: clamp(2rem, 1.79rem + 0.95vw, 2.5rem); /* 40 */
	--grit-text-h2: clamp(1.625rem, 1.46rem + 0.71vw, 2rem); /* 32 */
	--grit-text-h3: clamp(1.25rem, 1.14rem + 0.48vw, 1.5rem); /* 24 */
	--grit-text-h4: clamp(1.125rem, 1.07rem + 0.24vw, 1.25rem); /* 20 */
	--grit-text-stat: clamp(2.5rem, 1.86rem + 2.86vw, 4rem); /* 64 */
	--grit-text-body-lg: 1.125rem; /* 18/28 */
	--grit-text-body: 1rem; /* 16/26 */
	--grit-text-small: 0.875rem; /* 14/20 */
	--grit-text-label: 0.8125rem; /* 13/16 */

	--grit-lh-display: 1.1;
	--grit-lh-heading: 1.25;
	--grit-lh-body: 1.625;

	/* One label treatment. Seven had drifted apart across five letter-spacings. */
	--grit-track-label: 0.08em;

	--grit-motion: 0.15s ease;
	/* Slower than --grit-motion: the secondary button's overlay sweeps across
	   the whole button, and at 150ms that reads as a flicker rather than a wipe. */
	--grit-motion-sweep: 0.35s ease;

	--grit-shadow: 0 2px 12px rgb(0 22 42 / 8%);

	/* Text-on-dark. Not in theme.json's palette because it is not a colour an
	   editor should be able to put on arbitrary block content — it is only
	   legible on navy. See inc/palette.php. */
	--grit-text-on-dark: #b8c4ce;

	/* The soft badge fill — orange at a tenth strength, for a status pill that
	   reads as a label rather than as a CTA. Derived from --grit-primary so it
	   follows a change to the brand colour instead of drifting from it.

	   The literal first is the same load-bearing fallback pattern as the token
	   aliases above: a custom property holding an unsupported value computes to
	   guaranteed-invalid, which would leave the pill with no background at all
	   rather than with the previous declaration's. */
	--grit-primary-soft: #ffe9e4;
	--grit-primary-soft: color-mix(in srgb, var(--grit-primary) 12%, #fff);

	/* Contextual defaults, re-pointed by :is(.grit-bg--navy, .grit-tone--dark)
	   below. Every section's heading/eyebrow/description reads these rather
	   than a fixed colour, which is what lets one background change re-colour
	   a whole section without the admin having to set three more dropdowns. */
	--grit-section-heading: var(--grit-navy);
	--grit-section-text: var(--grit-text-muted);
	--grit-section-border: var(--grit-border);

	/* A prose link's colour, read by the base `a` rule and re-pointed by the same
	   dark-surface block. It is a token for one reason: the section builder now
	   tells editors, in every section, that they may type a link into any prose
	   field — and navy is the one colour a link must not be on a navy band, where
	   it had been drawing itself invisible. */
	--grit-link: var(--grit-navy);
}

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

body {
	margin: 0;
	font-family: var(--grit-font);
	font-size: var(--grit-text-body);
	line-height: var(--grit-lh-body);
	color: var(--grit-text);
	background: var(--grit-bg);
	/* No -webkit-font-smoothing: antialiased. It thinned Inter 400 noticeably —
	   Poppins tolerated it, Inter does not. */
}

h1,
h2,
h3,
h4,
h5,
h6 {
	font-family: var(--grit-font-heading);
	margin: 0 0 0.6em;
	font-weight: 700;
	line-height: var(--grit-lh-heading);
	color: var(--grit-navy);
	/* Archivo runs tighter than Poppins did, so this is lighter than the -0.02em
	   that was tuned for the old face. */
	letter-spacing: -0.01em;
	text-wrap: balance;
}

h1 {
	font-size: var(--grit-text-h1);
	line-height: var(--grit-lh-display);
}

h2 {
	font-size: var(--grit-text-h2);
}

h3 {
	font-size: var(--grit-text-h3);
	font-weight: 600;
}

h4 {
	font-size: var(--grit-text-h4);
	font-weight: 600;
}

/* h5/h6 had no rule at all and fell through to the browser's defaults, which
   render them *smaller* than body copy. */
h5 {
	font-size: var(--grit-text-body-lg);
	font-weight: 600;
}

h6 {
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
}

/* Oversized lead heading, for a hero that wants the display size. */
.grit-display {
	font-size: var(--grit-text-display);
	font-weight: 800;
	line-height: var(--grit-lh-display);
}

/* Heading sizes as a utility family, for the sections whose heading size is an
   editor's choice (gritfoundation_heading_sizes() in inc/sections.php).

   SIZE ONLY, NEVER THE LEVEL. The element stays whatever the renderer opened —
   a section heading is an h2 — so choosing "Heading 3" changes what a heading
   looks like and not where it sits in the document outline. That separation is
   the whole reason these exist rather than the renderer switching tags.

   Each one repeats the weight and line-height its element rule sets, because
   those are part of the step and not decoration: an h2-weight 24px heading is
   not what h3 looks like. --display mirrors .grit-display rather than reusing
   it, so the hero's class keeps meaning "the hero's lead heading" and this one
   means "the display step of the scale"; the two happen to agree today.

   A class beats the element rules above (0,1,0 against 0,0,1), and lands after
   them, so nothing here needs to escalate. */
.grit-heading--display {
	font-size: var(--grit-text-display);
	font-weight: 800;
	line-height: var(--grit-lh-display);
}

.grit-heading--h1 {
	font-size: var(--grit-text-h1);
	font-weight: 700;
	line-height: var(--grit-lh-display);
}

.grit-heading--h2 {
	font-size: var(--grit-text-h2);
	font-weight: 700;
	line-height: var(--grit-lh-heading);
}

.grit-heading--h3 {
	font-size: var(--grit-text-h3);
	font-weight: 600;
	line-height: var(--grit-lh-heading);
}

.grit-heading--h4 {
	font-size: var(--grit-text-h4);
	font-weight: 600;
	line-height: var(--grit-lh-heading);
}

p {
	margin: 0 0 1rem;
}

/* Prose links read as body-adjacent navy and carry the affordance in the
   underline; orange is reserved for CTAs and accents, so it appears on hover
   only. */
a {
	color: var(--grit-link);
	text-underline-offset: 0.15em;
}

a:hover,
a:focus-visible {
	color: var(--grit-primary);
}

img {
	max-width: 100%;
	height: auto;
	display: block;
}

:focus-visible {
	outline: 2px solid var(--grit-navy);
	outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Layout
   --------------------------------------------------------------------------- */
/* No max-width: content runs the full viewport width and only the gutter holds
   it off the edge. --grit-max is kept as a token because .entry-content and the
   prose measure still need a value to clamp against. */
.grit-wrap {
	width: 100%;
	margin-inline: auto;
	padding-inline: var(--grit-gutter);
}

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

/* Half the rhythm, per section, for the case the page wants two bands to read
   as one run rather than as two separate things.

   calc() off the token rather than a second clamp, so retuning the rhythm moves
   both and they cannot drift into being unrelated numbers. Top and bottom only:
   the gutter lives on .grit-wrap and is not the editor's to change.

   Equal specificity with the rule above, so it wins by coming after — and loses
   to the homepage overlap's :has() rule further down, which pins that one
   section's top padding to zero. That is correct: the zero is what lets the
   opening card row straddle the hero. */
.grit-section--tight {
	padding-block: calc(var(--grit-section) / 2);
}

.grit-section--band {
	background: var(--grit-bg-alt);
}

/* ---------------------------------------------------------------------------
   Palette utilities — the approved lists in inc/palette.php, as CSS.

   Class names are built from the same slugs the PHP stores, so a slug added
   there needs exactly one rule here. Backgrounds and text colours are separate
   families (--bg / --tc) so a section can carry one of each.

   Nothing here uses !important. These sit later in the file than the element
   defaults they override and carry a class, so specificity already favours
   them; !important would only make the per-element overrides below impossible.
   --------------------------------------------------------------------------- */
/* --grit-fill is the rule's own colour, published as a token.

   A component that has to ECHO the fill it was handed — the list tables paint
   their bullets in their panel's colour — cannot read a `background` back out
   of CSS, and the renderer never knows the hex: it stores a slug and resolves it
   to one of these classes. Declaring the value twice, once as the paint and once
   as an inheritable token, is the whole mechanism, and it costs one line per
   rule here and one in gritfoundation_palette_compile_css() for admin-added
   colours.

   It inherits, so it reaches the descendants of whichever element carries the
   class and nothing else. Every consumer states a fallback, because a custom
   colour compiled before that generator changed carries no token until the
   palette screen is saved again. */
.grit-bg--navy {
	background: var(--grit-navy);
	--grit-fill: var(--grit-navy);
}

.grit-bg--light {
	background: var(--grit-bg-alt);
	--grit-fill: var(--grit-bg-alt);
}

.grit-bg--white {
	background: var(--grit-bg);
	--grit-fill: var(--grit-bg);
}

/* PANEL FILLS ONLY — orange is on the `panel` list in inc/palette.php and
   deliberately not on `section`. The design system reserves it for CTAs and
   accents, so it may paint a header bar on a component and never a band across
   the page. Nothing enforces that in CSS: the list is what an editor can pick
   from, and this rule only exists because that list allows it. */
.grit-bg--orange {
	background: var(--grit-primary);
	--grit-fill: var(--grit-primary);
}

/* A dark section re-points the contextual defaults rather than restyling each
   descendant. Anything reading var(--grit-section-heading) flips with it, so a
   new element added later inherits correct colours without touching this rule.

   .grit-tone--dark is emitted alongside every dark background (built-in navy
   and admin-added darks alike), so the flip is tone-based rather than keyed to
   the navy slug alone. :is() keeps specificity identical to a single class. */
:is(.grit-bg--navy, .grit-tone--dark) {
	--grit-section-heading: #fff;
	--grit-section-text: var(--grit-text-on-dark);
	--grit-section-border: rgb(255 255 255 / 22%);

	/* White against the pale body text, which is the same distinction navy makes
	   against the muted grey on a light section — and hover is orange either way.
	   Without this a prose link inherited the base navy and was invisible. */
	--grit-link: #fff;

	color: var(--grit-text-on-dark);
}

:is(.grit-bg--navy, .grit-tone--dark) :is(h1, h2, h3, h4, h5, h6) {
	color: var(--grit-section-heading);
}

/* A light card inside a dark section. The rule above is right for text sitting
   directly on navy, but a card that keeps its own light fill would then draw a
   white heading on a light surface — invisible. Re-pointing the same three
   tokens on the card is enough to flip everything that reads them, including
   that heading rule, with no specificity fight and nothing to repeat per
   component. */
/* A stat card joins the list only while it has no fill of its own. Its colour is
   an editor's choice from the section list, so unlike every other card here it
   can legitimately BE dark — and a dark stat card inside a navy section must
   keep the white-on-navy treatment its own .grit-tone--dark class gives it. The
   :not() draws that line, and takes this selector to (0,3,0) so it still wins
   over the card's own (0,1,0) in the case it does match. */
:is(.grit-bg--navy, .grit-tone--dark) :is(.grit-card, .grit-value-card, .grit-media-card, .grit-partners__card, .grit-resource-card, .host-rows, .host-tile, .grit-hero__details, .grit-list-table__body),
:is(.grit-bg--navy, .grit-tone--dark) .grit-stat--card:not([class*="grit-bg--"]) {
	--grit-section-heading: var(--grit-navy);
	--grit-section-text: var(--grit-text-muted);
	--grit-section-border: var(--grit-border);

	/* Back to navy with them: a white link on a light card is the same invisibility
	   the rule above exists to fix, one surface in. */
	--grit-link: var(--grit-navy);

	color: var(--grit-text);
}

/* Explicit text colours. These win over the contextual default above because
   they come later and match the element directly. */
.grit-tc--navy {
	color: var(--grit-navy);
}

.grit-tc--white {
	color: #fff;
}

.grit-tc--orange {
	color: var(--grit-primary);
}

.grit-tc--dark {
	color: var(--grit-text);
}

.grit-tc--muted {
	color: var(--grit-text-muted);
}

.grit-tc--pale {
	color: var(--grit-text-on-dark);
}

/* A colour set on a container is meant for the prose inside it — .entry-content,
   .grit-person and .wp-block-media-text__content all wrap text they don't own
   the tags for. Headings keep their own colour unless separately overridden. */
.grit-tc--muted p,
.grit-tc--pale p,
.grit-tc--dark p,
.grit-tc--white p,
.grit-tc--navy p {
	color: inherit;
}

/* Full-bleed dark band. Used by the CTA band pattern. */
.grit-section--dark {
	background: var(--grit-surface-dark);
	color: #fff;
}

.grit-section--dark h1,
.grit-section--dark h2,
.grit-section--dark h3 {
	color: #fff;
}

.grit-section__head {
	max-width: 46rem;
	margin-inline: auto;
	margin-bottom: clamp(2rem, 5vw, 3rem);
	text-align: center;
}

.grit-section__head > :last-child {
	margin-bottom: 0;
}

/* Heading with a link on the same row — "Past GRIT Camp Hosts" / "CAMP HOSTS →".
   Not a modifier of .grit-section__head: that one is a centred 46rem column,
   and this has to span the full width of the grid it sits above.

   Wraps rather than shrinks. Squeezing a two-word link out of the row would
   cost the heading more width than dropping the link to its own line does. */
.grit-section__bar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 0.25rem clamp(1rem, 3vw, 2rem);
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

/* The heading's own 0.6em bottom margin would sit on top of the row's gap. */
.grit-section__bar > :is(h1, h2, h3) {
	margin-bottom: 0;
}

/* ---------------------------------------------------------------------------
   Buttons — three tiers, each with hover and disabled.

   One declaration now covers every context this site renders a button in: the
   theme's own .grit-btn, core's block button (which also carries
   .wp-element-button), core's file block, and the CF7/Mailchimp submit inputs.
   These had drifted into three near-copies — the CF7 one used `border: 0`, so
   it rendered 4px shorter and narrower than the others side by side.

   `font: inherit` stays first on purpose. It is a shorthand, so it resets
   font-weight/font-size/line-height; declared after them it would wipe them.
   --------------------------------------------------------------------------- */
.grit-btn,
.wp-block-button__link,
.wp-block-file__button,
.wpcf7 input[type="submit"],
.mc4wp-form input[type="submit"],
.grit-newsletter__form input[type="submit"],
.grit-newsletter__form button,
.woocommerce :is(a, button, input).button,
.woocommerce :is(a, button, input).button.alt,
.woocommerce #respond input#submit,
.woocommerce #respond input#submit.alt {
	font: inherit;
	display: inline-block;
	font-weight: 700;
	font-size: var(--grit-text-small);
	line-height: 1.4;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	text-decoration: none;
	text-align: center;
	padding: 0.85rem 1.75rem;
	border: 2px solid transparent;
	border-radius: var(--grit-radius-sm);
	cursor: pointer;
	transition:
		background-color var(--grit-motion),
		color var(--grit-motion),
		border-color var(--grit-motion),
		opacity var(--grit-motion);
}

/* Primary. The contexts that have nowhere to hang a tier modifier — a core
   block button, a file download, a form submit — are primary by default. */
.grit-btn--primary,
.wp-block-button__link,
.wp-block-file__button,
.wpcf7 input[type="submit"],
.mc4wp-form input[type="submit"],
.grit-newsletter__form input[type="submit"],
.grit-newsletter__form button,
.woocommerce :is(a, button, input).button.alt,
.woocommerce #respond input#submit,
.woocommerce #respond input#submit.alt {
	background: var(--grit-primary);
	color: #fff;
}

.grit-btn--primary:hover,
.grit-btn--primary:focus-visible,
.wp-block-button__link:hover,
.wp-block-button__link:focus-visible,
.wp-block-file__button:hover,
.wp-block-file__button:focus-visible,
.wpcf7 input[type="submit"]:hover,
.wpcf7 input[type="submit"]:focus-visible,
.mc4wp-form input[type="submit"]:hover,
.mc4wp-form input[type="submit"]:focus-visible,
.grit-newsletter__form input[type="submit"]:hover,
.grit-newsletter__form input[type="submit"]:focus-visible,
.grit-newsletter__form button:hover,
.grit-newsletter__form button:focus-visible,
.woocommerce :is(a, button, input).button.alt:hover,
.woocommerce :is(a, button, input).button.alt:focus-visible,
.woocommerce #respond input#submit:hover,
.woocommerce #respond input#submit:focus-visible {
	background: var(--grit-primary-hover);
	color: #fff;
}

/* Secondary — transparent fill with a border, and an overlay that wipes across
   on hover rather than a flat colour change.

   The border and label colour come from a variable, not a literal. The design
   calls for a white border, which is right on the dark sections this tier was
   drawn for — but sections are light by default, and a white border on white is
   invisible. So navy is the base and dark contexts re-point it to white, below.
   Defaulting the other way would mean a section that forgot to set a background
   silently renders an invisible button. */
.grit-btn--secondary {
	--grit-btn-outline: var(--grit-navy);

	position: relative;
	/* Creates a stacking context so the overlay's z-index: -1 stays inside the
	   button, behind the label, instead of sliding under the section itself. */
	isolation: isolate;
	overflow: hidden;
	background: transparent;
	color: var(--grit-btn-outline);
	border-color: var(--grit-btn-outline);
}

/* The overlay. scaleX from a left origin is the left-to-right wipe; animating
   transform and opacity keeps it on the compositor, so it doesn't trigger
   layout on a button that may sit in a long row of cards. */
.grit-btn--secondary::before {
	content: "";
	position: absolute;
	inset: 0;
	z-index: -1;
	background: var(--grit-btn-outline);
	opacity: 0;
	transform: scaleX(0);
	transform-origin: left center;
	transition:
		transform var(--grit-motion-sweep),
		opacity var(--grit-motion-sweep);
}

.grit-btn--secondary:hover::before,
.grit-btn--secondary:focus-visible::before {
	opacity: 0.16;
	transform: scaleX(1);
}

/* Pressed sits deeper than hover so the button still responds once the overlay
   has finished sweeping. */
.grit-btn--secondary:active::before {
	opacity: 0.28;
	transform: scaleX(1);
}

.grit-btn--secondary:hover,
.grit-btn--secondary:focus-visible {
	background: transparent;
	color: var(--grit-btn-outline);
}

/* Dark contexts get the white outline the design system specifies.
   .grit-cta-band is listed separately because it paints its own navy panel
   rather than carrying .grit-bg--navy / .grit-tone--dark on the section. */
:is(.grit-bg--navy, .grit-tone--dark) .grit-btn--secondary,
.grit-cta-band .grit-btn--secondary,
.site-header .grit-btn--secondary,
.site-footer .grit-btn--secondary {
	--grit-btn-outline: #fff;
}

/* Reduced motion: keep the state change, drop the travel. */
@media (prefers-reduced-motion: reduce) {
	.grit-btn--secondary::before {
		transition: opacity var(--grit-motion);
		transform: scaleX(1);
	}
}

/* Tertiary — a text link with a trailing arrow. The arrow is scoped to anchors
   because <input> has no ::after to hang it on.

   THE CATALOG CARD'S BUTTON IS THIS TIER, and it is listed here rather than
   restated in the WooCommerce block for the reason the whole button family is one
   declaration: a second set of values is how they drift. It has to be named at
   full length because Woo's own `.woocommerce ul.products li.product a.button` is
   (0,4,3) and `.grit-btn--tertiary` is (0,1,0), so the short selector loses.

   It keeps the tier's colour, its transparent fill and its arrow, and drops the
   base rule's uppercase and vertical padding: on a card this reads as a link at
   the foot of the copy, not as a label. The control is still WooCommerce's
   add-to-cart — AJAX and all — so the word on it is Woo's too. */
.grit-btn--tertiary,
.woocommerce ul.products li.product :is(a, button).button {
	background: transparent;
	border-color: transparent;
	color: var(--grit-primary);
	padding-inline: 0;
}

.grit-btn--tertiary:hover,
.grit-btn--tertiary:focus-visible,
.woocommerce ul.products li.product :is(a, button).button:hover,
.woocommerce ul.products li.product :is(a, button).button:focus-visible {
	background: transparent;
	color: var(--grit-primary-hover);
	text-decoration: underline;
}

a.grit-btn--tertiary::after,
.woocommerce ul.products li.product a.button::after {
	content: "\2009\2192";
}

.woocommerce ul.products li.product :is(a, button).button {
	padding-block: 0;
	font-size: var(--grit-text-body);
	font-weight: 700;
	text-transform: none;
	letter-spacing: normal;
}

/* Shape and size modifiers. */
.grit-btn--pill {
	border-radius: var(--grit-radius-full);
}

.grit-btn--sm {
	font-size: var(--grit-text-label);
	padding: 0.55rem 1.25rem;
}

/* Disabled. Anchors and spans can never match :disabled, so the attribute and
   the modifier carry equal weight — .host-card__cta--soon is a <span>. Opacity
   rather than a mixed colour so every tier fades consistently and there is no
   support cliff. */
.grit-btn:disabled,
.grit-btn[aria-disabled="true"],
.grit-btn--disabled,
.wp-block-button__link:disabled,
.wp-block-file__button:disabled,
.wpcf7 input[type="submit"]:disabled,
.mc4wp-form input[type="submit"]:disabled,
.grit-newsletter__form input[type="submit"]:disabled,
.grit-newsletter__form button:disabled,
.woocommerce :is(a, button, input).button:disabled,
.woocommerce :is(a, button, input).button.disabled,
.woocommerce :is(a, button, input).button[aria-disabled="true"] {
	opacity: 0.45;
	cursor: default;
	pointer-events: none;
}

/* ---------------------------------------------------------------------------
   Header — a flat dark bar.

   Square corners. It used to round its lower two, following the Wix design, but
   the bar spans the viewport and a rounded corner on a full-bleed band reads as
   a card that has not finished loading rather than as a shape.

   position: relative and the z-index are load-bearing for the collapsed nav
   below: the panel is absolutely positioned against this element, and has to
   paint over the page rather than under the first section.

   The z-index clears .grit-donate-float's 50, because the open panel now fills
   the screen and the float would otherwise sit on top of it. The two are the
   only positive z-indexes in this file, and while the menu is closed they never
   overlap, so the order between them costs nothing.
   --------------------------------------------------------------------------- */
.site-header {
	background: var(--grit-surface-dark);
	color: #fff;
	position: relative;
	z-index: 60;
}

/* NOWRAP WHILE THE NAV IS COLLAPSED, wrap once it is back in the bar.

   Collapsed, the row is brand + button + hamburger and the nav is out of flow.
   `flex-wrap: wrap` is the wrong answer for those three: a flex line wraps
   BEFORE it shrinks, so on a 375px screen the brand kept its natural width and
   the hamburger dropped to a line of its own under the logo rather than the
   brand giving up the 27px that would have kept all three on one row.

   Above the breakpoint the wrap comes back (see the two expansion blocks below),
   because there it is doing the opposite job: it is what lets ten nav items take
   a second row instead of overflowing the bar. */
.site-header__inner {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 1rem clamp(1rem, 3vw, 2.5rem);
	padding-block: 1.5rem;
}

/* Shrinks rather than pushing the row over.

   It used to be `flex: 0 0 auto`, which was harmless while the nav absorbed the
   bar's slack — but once the nav collapses, the row is brand + button +
   hamburger, and on a 375px screen those do not fit at their natural widths. A
   rigid brand made the hamburger wrap to a line of its own under the logo.

   It only ever shrinks when there is nothing left to give: `flex-grow: 0` means
   it never claims space it is not using, so nothing changes above the
   breakpoint, where the nav is still the flexible item. */
.site-brand {
	display: inline-flex;
	align-items: center;
	text-decoration: none;
	color: #fff;
	font-weight: 700;
	font-size: 1.15rem;
	letter-spacing: -0.01em;
	flex: 0 1 auto;
	/* A flex item's default minimum is its content, so without this the brand
	   refuses to shrink and the rule above does nothing. */
	min-width: 0;
}

/* max-width so the logo shrinks with the link that holds it. `width: auto` keeps
   it from distorting: the height cap and the max-width are two limits on the
   same box, and the browser honours whichever bites first. */
.site-brand img {
	max-height: 40px;
	max-width: 100%;
	width: auto;
}

.site-nav {
	flex: 1 1 auto;
	min-width: 0;
}

.site-nav ul {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.4rem clamp(0.6rem, 1.1vw, 1.1rem);
	margin: 0;
	padding: 0;
	list-style: none;
}

.site-nav a {
	display: inline-block;
	color: #fff;
	text-decoration: none;
	font-weight: 500;
	font-size: var(--grit-text-label);
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	white-space: nowrap;
	padding-block: 0.25rem;
	border-bottom: 2px solid transparent;
}

/* One line only where the logo, ten nav items and the CTA genuinely fit.
   Below this the nav wraps to a second row — with nowrap, a squeezed flex-end
   list overflows out its left edge and slides under the logo. */
@media (min-width: 1400px) {
	.site-header__inner {
		flex-wrap: nowrap;
	}

	.site-nav ul {
		flex-wrap: nowrap;
		justify-content: flex-end;
	}
}

.site-nav a:hover,
.site-nav a:focus-visible,
.site-nav .current-menu-item > a,
.site-nav .current_page_parent > a {
	color: var(--grit-primary);
	border-bottom-color: var(--grit-primary);
}

.site-header__cta {
	flex: 0 0 auto;
	/* Pushes the CTA and the hamburger to the right edge once the nav is
	   collapsed and no longer absorbing the row's slack. Above the breakpoint the
	   nav's `flex: 1 1 auto` has already eaten it, so this changes nothing there
	   rather than needing a second rule to undo it. */
	margin-inline-start: auto;
}

/* ---------------------------------------------------------------------------
   Header navigation, collapsed to a hamburger

   MOBILE-FIRST, AND THE BREAKPOINT IS AN ADMIN SETTING. Appearance then Header
   offers 950px or 1150px (gritfoundation_header_nav_breakpoints()), and the
   header carries the choice as .site-header--nav-950 or .site-header--nav-1150.
   So COLLAPSED IS THE BASE here and the two blocks at the end of this section
   expand it again — two rules each. Writing it the other way round would mean
   the whole panel twice, once under each media query, and two copies of a panel
   is two panels to keep in step.

   THE BUTTON'S aria-expanded IS THE ONLY STATE. assets/site-nav.js flips that
   attribute and nothing else; the panel opens from the adjacent-sibling rules
   below. One source of truth means the accessible state and the visible state
   cannot disagree, which a separate .is-open class would eventually let them do.
   It is also why the toggle sits immediately before the nav in header.php.

   The open panel's list and link rules are scoped to that same attribute rather
   than to the collapsed base, so above the breakpoint there is nothing to undo
   and the expansion blocks stay two rules long. That holds because the script
   watches the same breakpoint and closes the menu when it stops matching — if it
   ever stopped doing that, a menu left open across a resize would carry the
   stacked list onto the desktop bar.

   WITHOUT JAVASCRIPT the button cannot do anything, so the collapse must not
   apply at all — a nav that is display:none with no way to reveal it is a site
   with no navigation. gritfoundation_header_nav_noscript_css() prints the undo
   inside a <noscript> in the head; see inc/header-settings.php.
   --------------------------------------------------------------------------- */
.site-nav-toggle {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	order: 1;
	flex: 0 0 auto;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
	border: 1px solid rgb(255 255 255 / 35%);
	border-radius: var(--grit-radius-sm);
	background: none;
	color: #fff;
	cursor: pointer;
}

.site-nav-toggle:hover,
.site-nav-toggle:focus-visible {
	border-color: var(--grit-primary);
	color: var(--grit-primary);
}

/* Three bars from one element: the middle bar is the element itself, the outer
   two are its pseudo-elements. currentColor, so the hover above moves all
   three. */
.site-nav-toggle__bars,
.site-nav-toggle__bars::before,
.site-nav-toggle__bars::after {
	display: block;
	width: 1.25rem;
	height: 2px;
	background: currentColor;
	border-radius: 1px;
}

.site-nav-toggle__bars {
	position: relative;
}

.site-nav-toggle__bars::before,
.site-nav-toggle__bars::after {
	content: "";
	position: absolute;
	inset-inline-start: 0;
}

.site-nav-toggle__bars::before {
	top: -6px;
}

.site-nav-toggle__bars::after {
	top: 6px;
}

/* Open: the outer bars rotate onto the middle one and it disappears, so the
   control reads as a close button rather than as a hamburger still offering to
   open something that is already open. */
.site-nav-toggle[aria-expanded="true"] .site-nav-toggle__bars {
	background: transparent;
}

.site-nav-toggle[aria-expanded="true"] .site-nav-toggle__bars::before {
	top: 0;
	transform: rotate(45deg);
}

.site-nav-toggle[aria-expanded="true"] .site-nav-toggle__bars::after {
	top: 0;
	transform: rotate(-45deg);
}

/* The panel. Absolute against .site-header rather than a row in the flex bar, so
   opening it does not push the page down — and it paints over the first section
   because the header carries a z-index.

   The header's own fill does not reach below the bar, so the panel repeats it;
   without that the links would sit on whatever the page happens to open with. */
.site-header .site-nav {
	display: none;
	position: absolute;
	inset-inline: 0;
	top: 100%;
	padding: 0.5rem var(--grit-gutter) 1.25rem;
	background: var(--grit-surface-dark);
	box-shadow: 0 12px 24px rgb(0 22 42 / 25%);
}

/* FULL HEIGHT, NOT THE HEIGHT OF THE LIST. A four-item menu that stopped a third
   of the way down read as a dropdown that had failed to open, and a long one on a
   short screen ran off the bottom with nothing to scroll: the panel is out of
   flow, so it is the page behind that scrolls, and the items below the fold could
   not be reached at all.

   NOTHING MEASURES THE BAR. A percentage in `bottom` resolves against the
   containing block's height, and the containing block is .site-header — so
   `100% - 100dvh` is negative by exactly the screen height below the bar, which
   lands the panel's bottom edge on the bottom of the screen whatever the header
   turns out to be. No custom property, no JavaScript reading offsetHeight, and
   nothing to keep in step with the header's padding.

   dvh rather than vh because it follows a mobile browser's toolbars sliding in
   and out; the vh line before it is the fallback for browsers without it, as with
   the hero's svh further down. */
.site-nav-toggle[aria-expanded="true"] + .site-nav {
	display: block;
	bottom: calc(100% - 100vh);
	bottom: calc(100% - 100dvh);
	overflow-y: auto;
	/* A drag that reaches the end of the list stops there, rather than carrying on
	   into the page underneath. */
	overscroll-behavior: contain;
	/* Clear of an iPhone's home bar. Dropped whole where env() is unsupported,
	   which leaves the base rule's 1.25rem. */
	padding-bottom: max(1.25rem, env(safe-area-inset-bottom));
}

/* THE PAGE MUST NOT SCROLL BEHIND IT. The panel is absolute, so it travels with
   the header: scrolling while it is open drags it up the screen and leaves a
   strip of the page showing along the bottom. overscroll-behavior above stops a
   drag inside the list from chaining out into the page; this stops one that
   starts on the bar.

   :has() on the root is the whole lock, so no class is added anywhere and
   aria-expanded stays the one piece of state this menu has. A browser without
   :has() keeps today's behaviour. */
@supports selector(:has(*)) {
	html:has(.site-nav-toggle[aria-expanded="true"]),
	html:has(.site-nav-toggle[aria-expanded="true"]) body {
		overflow: hidden;
	}
}

/* A column of full-width targets rather than the desktop row: a stacked list is
   what there is room for, and a link that spans the panel is a target a thumb
   does not have to aim at. */
.site-nav-toggle[aria-expanded="true"] + .site-nav ul {
	flex-direction: column;
	align-items: stretch;
	gap: 0;
}

.site-nav-toggle[aria-expanded="true"] + .site-nav a {
	display: block;
	padding-block: 0.75rem;
	white-space: normal;
	border-bottom: 1px solid rgb(255 255 255 / 12%);
}

/* The orange underline the bar uses would be indistinguishable from the row
   dividers above, so in the panel the current page and hover are marked by
   colour alone and the divider stays a divider. */
.site-nav-toggle[aria-expanded="true"] + .site-nav a:hover,
.site-nav-toggle[aria-expanded="true"] + .site-nav a:focus-visible,
.site-nav-toggle[aria-expanded="true"] + .site-nav .current-menu-item > a,
.site-nav-toggle[aria-expanded="true"] + .site-nav .current_page_parent > a {
	color: var(--grit-primary);
	border-bottom-color: rgb(255 255 255 / 12%);
}

/* Expanded, above whichever breakpoint the admin chose. Everything above is the
   collapsed state, so these three rules are the whole undo: the hamburger goes
   away, the nav goes back into the flex row, and the row is free to wrap again.
   The declarations restate the base values rather than using `revert`, which
   resolves to the UA stylesheet and not to an earlier author rule. */
@media (min-width: 950px) {
	.site-header--nav-950 .site-nav-toggle {
		display: none;
	}

	.site-header--nav-950 .site-header__inner {
		flex-wrap: wrap;
	}

	.site-header--nav-950 .site-nav {
		display: block;
		position: static;
		padding: 0;
		background: none;
		box-shadow: none;
	}
}

@media (min-width: 1150px) {
	.site-header--nav-1150 .site-nav-toggle {
		display: none;
	}

	.site-header--nav-1150 .site-header__inner {
		flex-wrap: wrap;
	}

	.site-header--nav-1150 .site-nav {
		display: block;
		position: static;
		padding: 0;
		background: none;
		box-shadow: none;
	}
}

/* ---------------------------------------------------------------------------
   Hero — text content on the left, an optional image on the right

   Single column, left-aligned, until there's an image to sit beside the copy
   (.grit-hero--has-media) — a hero row without one shouldn't reserve a blank
   half-width void.
   --------------------------------------------------------------------------- */
/* No background of its own: the hero always carries a .grit-bg--* class (navy
   by default, set in template-parts/hero.php). Painting one here would win
   against the utility class and make the section dropdown do nothing.

   HEIGHT — a fixed base plus a viewport unit, so the hero scales with the
   screen instead of needing a breakpoint per size. The viewport unit is svh,
   not vh: on mobile, vh resolves against the viewport with the browser chrome
   *hidden*, so a 100vh-ish hero is taller than what you can actually see until
   you scroll. svh is the small (chrome-visible) height, which is the honest
   number. The vh line before it is the fallback for browsers without svh.

   The two formulas below land at roughly:
     375x667  -> 423px (63% of viewport)   414x896 -> 480px (54%)
     1440x800 -> 648px (81%)              1920x1080 -> 774px (72%)
   which is inside the 50-70% mobile / 60-100% desktop targets across the
   common sizes. Tall narrow phones sit at the low end and very tall desktop
   monitors at the low-mid; that is the expected trade for not carrying a
   breakpoint per device. */
.grit-hero {
	min-height: calc(16rem + 25vh);
	min-height: calc(16rem + 25svh);
	display: grid;
	align-content: center;
	padding-block: var(--grit-hero-pad);

	/* Tokens rather than the clamp inline, because two other rules need this
	   number: the homepage overlap below adds to it, and --tight halves it. Both
	   used to repeat the clamp, which is two more copies to retune by hand and
	   two more chances for them to drift apart.

	   Two names, because a custom property cannot be defined in terms of itself:
	   --base is the unreduced rhythm and never changes, --pad is what this hero
	   actually uses and is what every other rule reads. */
	--grit-hero-pad-base: clamp(2.5rem, 7vw, 5rem);
	--grit-hero-pad: var(--grit-hero-pad-base);

	/* Desktop crop. Overridden below the split — a mobile hero image is a
	   different crop, not a scaled-down desktop one. */
	--grit-hero-media-ratio: 4 / 3;
}

/* The hero's answer to .grit-section--tight, which it cannot use: it never
   carries .grit-section, because its rhythm is a min-height and a padding
   rather than padding alone.

   BOTH NUMBERS MOVE, and that is the whole reason this rule is more than one
   line. The hero centres its content inside a reserved height, so on anything
   but a very tall hero the min-height is what decides how tall the band is and
   halving the padding alone would visibly do nothing — exactly the "a field
   that quietly does nothing" case that kept the hero off this control until now.
   Halved, like the section rhythm, so the two read as the same decision.

   The vh line before each svh one is the fallback the base rule carries, for
   the reason given above it. It cannot be folded into a custom property:
   `svh` in a custom property is stored unparsed, so a browser without it would
   fail at var() substitution time and drop the min-height entirely rather than
   fall back to the vh declaration. */
.grit-hero--tight {
	--grit-hero-pad: calc(var(--grit-hero-pad-base) / 2);

	min-height: calc(8rem + 12.5vh);
	min-height: calc(8rem + 12.5svh);
}

.grit-hero__grid {
	display: grid;
	grid-template-columns: 1fr;
	align-items: center;
	gap: clamp(1.5rem, 4vw, 3rem);
}

/* Two columns as soon as there is anything to put in the second one. The details
   panel earns the split on its own: a contact hero with an address card and no
   photograph is still the two-column design, and a hero with neither is still
   one column rather than a half-width void. */
.grit-hero--has-media .grit-hero__grid,
.grit-hero--has-details .grit-hero__grid {
	grid-template-columns: 1fr 1fr;
}

.grit-hero__content {
	text-align: left;
}

/* The second column, which stacks the media and the panel under it. A wrapper
   rather than two grid items — a third child of a two-column grid lands under
   the copy, not under the image. */
.grit-hero__aside {
	display: grid;
	gap: clamp(1rem, 2vw, 1.5rem);
}

.grit-hero__media {
	margin: 0;
}

/* aspect-ratio rather than a height: it reserves the right box before the image
   arrives, so the hero does not jump when it loads. height stays auto because a
   resolved height would make the browser ignore aspect-ratio entirely.

   The video takes the identical treatment — same box, same corners, same crop —
   because it stands in for the image rather than being a different object. Its
   poster is the same file the <img> would have been, so a hero must look the
   same at first paint whichever the editor chose. `display: block` is stated
   because <video> is inline by default and would otherwise sit on a text
   baseline, leaving a few pixels of section colour under the media box. */
.grit-hero__media img,
.grit-hero__media video {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: var(--grit-hero-media-ratio);
	object-fit: cover;
	object-position: center;
	border-radius: var(--grit-radius-md);
}

/* Shared with any future section eyebrow — orange is the one accent colour
   in the palette, and an eyebrow is exactly what it's reserved for. */
.grit-hero__eyebrow {
	display: block;
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	margin-bottom: 0.5rem;
}

/* THE COLOUR, AND ONLY WHEN NOTHING IS PINNED. Every layout carrying an eyebrow
   also carries gf_eyebrow_color, which puts a .grit-tc--* class on this element —
   and that class is declared at the top of this file, hundreds of lines above
   here, so an unconditional colour on a rule of equal weight silently beat the
   editor's own choice. The field was a control that did nothing. Same guard the
   eight __lede and __outro rules already carry, and for the same reason. */
.grit-hero__eyebrow:not([class*="grit-tc--"]) {
	color: var(--grit-primary);
}

.grit-hero__lede {
	font-size: clamp(1rem, 0.95rem + 0.35vw, 1.1875rem);
	max-width: 38rem;
	margin-bottom: 2rem;
}

/* THE COLOUR, AND ONLY WHEN NOTHING IS PINNED — the same guard
   .grit-value-cards__lede, .grit-stats__lede, .grit-cta__lede and
   .grit-map__lede carry, and it was missing here.

   --grit-section-text is muted on light and pale (#B8C4CE) on navy, which is the
   hero's default, so the design system's "pale on hero" falls out of the
   background choice rather than needing its own rule. But declaring it
   unconditionally on this class made the two description colour fields on a hero
   do nothing at all: .grit-tc--* is one class declared with the tokens seven
   hundred lines above this, so an editor pinning White or Body text wrote a
   (0,1,0) selector against another (0,1,0) selector that comes later in the
   file, and later wins every tie. The section background appeared to be
   overriding the picker because --grit-section-text is exactly what the dark-tone
   block re-points.

   The :not() keeps this rule out of that fight rather than escalating it, which
   is why it is the guard and not an !important: a pinned colour now simply has
   nothing to beat. Both ledes, because they share this class — the second
   paragraph's own gf_lede2_color went through the same (0,1,0) tie. */
.grit-hero__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* A hero may carry a SECOND description under the first, each with its own
   colour. Both are .grit-hero__lede, so they share the type scale and the 38rem
   measure and there is nothing to keep in step.

   The 2rem below a lede is the gap to the buttons, which is far too much between
   two paragraphs of the same thought — so a lede followed by another closes up
   and the last one keeps the gap. :has() rather than a negative margin on the
   second: this states the rule that is actually true, and a browser without it
   falls back to the two sitting 2rem apart, which is loose but not broken. No
   @supports guard for that reason — unlike the homepage overlap further up, where
   the fallback had to be the current layout. */
.grit-hero__lede:has(+ .grit-hero__lede) {
	margin-bottom: 0.75rem;
}

.grit-hero__actions {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem;
}

/* An optional form under the copy — the contact design's "Get in Touch".
   The markup is the provider's, so there is nothing here beyond the column it
   sits in: the fields, labels and submit button are already dressed by the Forms
   section further down this file, which matches on elements inside .wpcf7 /
   .mc4wp-form for exactly this reason. Same measure as the lede, so the form and
   the sentence above it share an edge.

   The margin is on the form rather than a gap on .grit-hero__content, because
   the lede already carries 2rem below it and the buttons carry none — a gap
   would double the first and the form would sit on the buttons in the other. */
.grit-hero__form {
	max-width: 38rem;
	margin-top: clamp(1.5rem, 3vw, 2rem);
}

/* THE ONE PLACE IN THIS FILE THAT NEEDS !important, and the reason is not
   sloppiness elsewhere: ActiveCampaign's JavaScript embed writes its own <style>
   block at runtime keyed on the form's generated id — `#_form_6A84…_ { margin:
   25px auto 0; padding: 20px }`. That is (1,0,0) and it lands after the theme's
   stylesheet, so no selector this file can write outranks it. An id cannot be
   matched either: it is regenerated per form, and the class beside it
   (`._form_3`) names one form in one ActiveCampaign account rather than the
   situation being corrected.

   What is being corrected is spacing the hero already owns. The wrapper above
   sets the gap under the copy and .grit-hero__content sets the gutters, so the
   embed's own 25px and 20px sit inside those and read as a form floating away
   from the column it belongs to. Zeroing both hands the spacing back to the
   hero.

   Matched by element inside the wrapper rather than by provider, the same
   bargain .grit-newsletter__form makes: a Contact Form 7 or Mailchimp form in a
   hero carries no margin or padding of its own, so this is a no-op for them and
   there is no provider name here to go stale. */
.grit-hero__form form {
	margin: 0 !important;
	padding: 0 !important;
}

/* A hero's lede has 2rem under it for the buttons' sake. With a form directly
   after it that reads as a gap nothing accounts for. */
.grit-hero__lede:has(+ .grit-hero__form) {
	margin-bottom: 0;
}

/* The pale details card under the media: a label above a value, twice.

   It keeps its own light fill on a navy hero, which is why it is listed with the
   cards in the dark-section rule near the top of this file — that re-points the
   heading, text and link tokens so nothing inside it draws white on near-white.
   --grit-bg-alt rather than white: the card is a quiet aside, and pure white
   beside a photograph reads as a second image. */
.grit-hero__details {
	margin: 0;
	padding: clamp(1.25rem, 3vw, 1.75rem);
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
}

.grit-hero__details dt {
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	color: var(--grit-section-text);
}

/* dd carries a browser default margin-inline-start of 40px, which would indent
   every value away from its own label. */
.grit-hero__details dd {
	margin: 0.15rem 0 0;
	color: var(--grit-section-heading);
	font-weight: 600;
}

/* The gap between the two pairs, and only between them: a margin on every dt
   would push the first one off the top of its own padding. */
.grit-hero__details dd + dt {
	margin-top: 1rem;
}

/* Stated rather than left to the browser. A link in here resolves to navy and so
   does the value beside it, so the underline is the only thing distinguishing a
   linked row from a plain one — the case WCAG 1.4.1 is about. Hover still turns
   it orange, from the base `a` rule. */
.grit-hero__details a {
	text-decoration: underline;
}

/* Below the split: stack rather than shrink. One column, image under the text
   (which is already the source order, so the headline, lede and CTA all come
   first), actions full-width so a thumb doesn't have to aim for a half-width
   button. .grit-hero--has-media is repeated because it is (0,2,0) and would
   otherwise keep winning over the bare class here. */
@media (max-width: 45em) {
	.grit-hero__grid,
	.grit-hero--has-media .grit-hero__grid,
	.grit-hero--has-details .grit-hero__grid {
		grid-template-columns: 1fr;
	}

	/* A wider, shorter crop. Carrying the 4:3 desktop crop down here would make
	   the image about 280px tall on a 375px screen and push everything below the
	   hero off the first screenful — the image is supporting the message, so it
	   gives up the height rather than the copy doing so. */
	.grit-hero {
		--grit-hero-media-ratio: 16 / 9;
	}

	.grit-hero__actions {
		flex-direction: column;
		align-items: stretch;
	}

	.grit-hero__actions .grit-btn {
		width: 100%;
	}
}

/* Above the split the hero gets its taller treatment. Same shape as the mobile
   formula, larger coefficients — one extra rule rather than a breakpoint per
   screen size. */
@media (min-width: 45.0625em) {
	.grit-hero {
		min-height: calc(18rem + 45vh);
		min-height: calc(18rem + 45svh);
	}

	/* Halved again, and it has to be restated here: the rule above and this one
	   are both a single class, so inside this query the base would otherwise win
	   on source order and put the full desktop height back. */
	.grit-hero--tight {
		min-height: calc(9rem + 22.5vh);
		min-height: calc(9rem + 22.5svh);
	}
}

/* Very short viewports — a landscape phone, or a desktop window dragged short.
   Last, so it overrides both formulas above: either would reserve more height
   than the window has and push the CTA out of view. Content height still
   applies, so the hero cannot collapse. */
@media (max-height: 30rem) {
	.grit-hero {
		min-height: 0;
	}
}

/* ---------------------------------------------------------------------------
   Homepage opening — the card row straddles the hero's bottom edge

   The design has the first card row on the homepage sitting half over the navy
   hero and half over the section below it, so the two read as one composed
   opening rather than as two stacked bands.

   Three things make that work, and all three are load-bearing:

   1. The cards section's own top padding goes to zero. A negative margin alone
      does nothing visible on desktop — --grit-section is up to 96px, more than
      the overlap, so the cards would move up and still land inside their own
      section without ever reaching the navy.
   2. Only .grit-cards moves, not the section. The section keeps its background
      where it is, so the band still starts at the hero's edge and the cards
      straddle the seam. Moving the section would just slide the seam down.
   3. The hero grows its bottom padding by the same amount, so the overlap eats
      padding instead of covering the CTA on a short viewport. The hero centres
      its content, so without this a long headline can collide with the cards.

   Scoped to .home, which WordPress puts on the front page and nowhere else, so
   an identical hero-then-cards pair on any other page is untouched.

   :first-child is the off switch. A cards section with its own <h2> would have
   the heading either dragged into the navy or overlapped by its own cards, so
   the overlap applies only when the grid is the first thing in the section.
   The :has() on the hero keeps the reserved padding in step with that switch,
   and the @supports guard means a browser without :has() falls back to today's
   stacked layout rather than to an overlap with no room reserved for it.

   This needs the hero and the cards to be siblings, which is the Sections
   template (template-sections.php renders every row into <main>). front-page.php
   puts the hero outside <main>, so a block-content homepage is unaffected.
   --------------------------------------------------------------------------- */
@supports selector(:has(*)) {
	.home .grit-hero:has(+ .grit-section > .grit-wrap > .grit-cards:first-child) {
		padding-bottom: calc(var(--grit-hero-pad) + var(--grit-hero-card-overlap));
	}

	.home .grit-hero + .grit-section:has(> .grit-wrap > .grit-cards:first-child) {
		padding-top: 0;
	}

	.home .grit-hero + .grit-section > .grit-wrap > .grit-cards:first-child {
		margin-top: calc(-1 * var(--grit-hero-card-overlap));
	}

	/* Below the split the hero stacks and its image sits directly above the
	   cards, so the overlap would land the row on a photo instead of on flat
	   navy — which reads as a mistake rather than as depth. Selectors are
	   repeated verbatim because :has() carries its argument's specificity;
	   anything shorter would lose. */
	@media (max-width: 45em) {
		.home .grit-hero:has(+ .grit-section > .grit-wrap > .grit-cards:first-child) {
			padding-bottom: var(--grit-hero-pad);
		}

		.home .grit-hero + .grit-section:has(> .grit-wrap > .grit-cards:first-child) {
			padding-top: var(--grit-section);
		}

		.home .grit-hero + .grit-section > .grit-wrap > .grit-cards:first-child {
			margin-top: 0;
		}
	}
}

/* Page header for interior pages without a hero image. */
.page-intro {
	max-width: var(--grit-measure);
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

/* ---------------------------------------------------------------------------
   Content
   --------------------------------------------------------------------------- */
.entry-content {
	max-width: var(--grit-measure);
}

/* Prose written in the Sections builder, which runs a little wider than the
   68ch a blog post is held to.

   A modifier and not a change to the base rule: .entry-content also wraps
   page.php, index.php, single-camp_host.php, front-page.php and the Sections
   template's own post_content fallback, and none of those asked to move. The
   text block keeps everything else the class carries — the vertical rhythm, the
   heading spacing, the prose-link underline — which is why it renders inside it
   in the first place. */
.entry-content--section {
	max-width: 90ch;
}

/* Text alignment for the Sections text block, per section.

   EACH ONE MOVES THE COLUMN TOO. These are a capped-width column inside a
   full-width wrap, so text-align alone would centre the words inside a column
   still pinned to the left of a wide screen — visibly not centred, and the same
   for right. The margin is what puts the column where the alignment claims it is.

   Modifiers of .entry-content rather than free-standing utilities, because that
   pairing only makes sense on an element that has a measure to be moved within.
   Left needs no class: it is what the column already does. */
.entry-content--center {
	text-align: center;
	margin-inline: auto;
}

.entry-content--right {
	text-align: right;
	margin-inline-start: auto;
}

.entry-content > * + * {
	margin-top: 1rem;
}

.entry-content > h2 {
	margin-top: 2.5rem;
}

/* An eyebrow names the heading under it, so the two are one unit and the rhythm
   above has to stop applying between them. Without this the 2.5rem lands between
   the label and the heading it labels, which reads as two stray fragments rather
   than as an eyebrow — .grit-hero__eyebrow's own 0.5rem margin-bottom is the gap
   that belongs there. Scoped to the pair, so a heading with no eyebrow above it
   keeps the full section rhythm. */
.entry-content > .grit-hero__eyebrow + h2 {
	margin-top: 0;
}

.entry-content > h3,
.entry-content > h4 {
	margin-top: 2rem;
}

/* Underline prose links, but never buttons. */
.entry-content a:not(.wp-block-button__link):not(.grit-btn) {
	text-decoration: underline;
}

/* ---------------------------------------------------------------------------
   Feature cards — the "Why Choose GRIT?" pattern
   --------------------------------------------------------------------------- */
.grit-cards {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
	padding: 0;
	margin: 0;
	list-style: none;
}

/* Borderless: the shadow and the white fill already separate the card from the
   section behind it, and the border was doubling that edge. */
.grit-card {
	background: #fff;
	border-radius: var(--grit-radius-md);
	padding: clamp(1.25rem, 3vw, 1.75rem);
	box-shadow: var(--grit-shadow);
	/* So a card's own button sits at the bottom of an equal-height row rather
	   than immediately under a short paragraph. */
	display: flex;
	flex-direction: column;
}

.grit-card h3 {
	font-size: var(--grit-text-h4);
}

.grit-card p:last-child {
	margin-bottom: 0;
}

/* The card's optional eyebrow. Same treatment as the hero's, so the two read as
   one component rather than two that happen to look similar. */
.grit-card__eyebrow {
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	margin: 0 0 0.4rem;
}

/* Guarded, for the reason .grit-hero__eyebrow above is. */
.grit-card__eyebrow:not([class*="grit-tc--"]) {
	color: var(--grit-primary);
}

/* margin-top: auto pins the button to the bottom of the card. */
.grit-card__action {
	margin: auto 0 0;
	padding-top: 1.25rem;
}

/* The SECTION's own button, under the whole grid — not a card's.

   Centred, because the heading above the grid is (.grit-centered in the
   renderer), and a left-aligned button under a centred heading reads as
   belonging to the first card rather than to the section.

   The gap is the theme's heading-row-to-grid number — the same clamp
   .grit-section__bar and the wide content grids use — rather than a fourth
   spacing value invented for one button. */
.grit-cards__action {
	margin: clamp(1.5rem, 4vw, 2.5rem) 0 0;
	text-align: center;
}

/* ---------------------------------------------------------------------------
   Numbered feature cards

   A counter, not markup, for the reason the numbered steps list uses one: the
   number has to follow the cards, and an editor-typed one stops being true the
   first time a card is reordered or removed. Cards that never render — a
   half-filled repeater row — never reach the DOM, so they cannot consume a
   number either.

   Its own counter name because grit-step is taken, and a shared one would
   continue across a page holding both sections.

   counter(), not decimal-leading-zero: the design draws a single digit, where
   the steps list draws 01. Two sections, two intentions.

   The badge is a ::before, so it is the card's first flex item and lands above
   the eyebrow without either knowing about the other. flex: 0 0 auto is
   load-bearing — a flex item stretches to the column's width by default, which
   would turn the circle into a lozenge.
   --------------------------------------------------------------------------- */
.grit-cards--numbered {
	counter-reset: grit-card;
}

.grit-cards--numbered .grit-card {
	counter-increment: grit-card;
}

.grit-cards--numbered .grit-card::before {
	content: counter(grit-card);
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.25rem;
	height: 2.25rem;
	margin-bottom: 0.9rem;
	border-radius: var(--grit-radius-full);
	background: var(--grit-primary);
	color: #fff;
	font-family: var(--grit-font-heading);
	font-weight: 700;
	font-size: var(--grit-text-body);
	line-height: 1;
}

/* Below the split the card button takes the full width of its card, so a thumb
   does not have to aim for a label-width target under a full-width card. Same
   three lines .grit-hero__actions and the CTA band already use, so every button
   on the page behaves alike.

   Note the grid stacks to one column at around 620px rather than at this 45em,
   so between the two the cards are two across with full-width buttons. The
   button still fills its own card, and 45em is the breakpoint the rest of the
   theme uses — a second one just for this would be harder to keep true. */
@media (max-width: 45em) {
	.grit-card__action .grit-btn {
		width: 100%;
	}
}

/* ---------------------------------------------------------------------------
   Value cards — the flatter "Why Choose GRIT?" row

   Same grid as .grit-cards, a different card: tinted fill instead of white,
   no shadow, and a short orange rule where feature cards put an eyebrow. It is
   its own component rather than a .grit-card modifier because every visual
   property differs; only the grid is shared, and that is one line.
   --------------------------------------------------------------------------- */
.grit-value-cards {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));

	/* The muted default lives on the GRID, not the card, so the optional
	   .grit-tc--* class the renderer puts on the card always wins — a class on
	   a descendant beats one on its ancestor regardless of file order, which a
	   same-element tie would not. */
	color: var(--grit-section-text);
}

/* The optional copy above the cards, and the optional button below them. The
   same three rules .grit-stats__intro carries, because it is the same
   arrangement: one block of copy, the thing itself, then where it leads.

   The clamp is the theme's heading-row-to-grid number, shared with
   .grit-section__bar and .grit-stats__intro. A heading that used to sit here on
   its own 0.6em margin gains a few pixels by joining them, which is the point:
   the gap under a heading should not depend on whether a description happens to
   follow it. */
.grit-value-cards__intro {
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-value-cards__intro > :last-child {
	margin-bottom: 0;
}

/* Only when nothing is pinned. .grit-tc--* is declared with the tokens hundreds
   of lines above every component and is also one class, so an unconditional
   colour here would quietly beat the editor's own choice. Same guard as
   .grit-stats__lede. */
.grit-value-cards__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* THE SECOND DESCRIPTION, under the grid rather than above it. Its own element
   rather than a second .grit-value-cards__lede, because the two sit on opposite
   sides of the cards and only one of them can own the gap to them.

   The margin is on top for that reason: the grid above it has none of its own
   below, and what follows is either .wp-block-buttons — which carries its own
   1.5rem — or the section's padding. Same clamp as .grit-value-cards__intro's
   bottom margin, so the copy sits the same distance from the cards either side.

   The colour guard is .grit-value-cards__lede's, and required for the same
   reason: without the :not(), this rule and the editor's .grit-tc--* choice are
   both one class, this one comes later in the file, and the colour field would
   silently do nothing. */
.grit-value-cards__outro {
	margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-value-cards__outro:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

.grit-value-cards__outro > :last-child {
	margin-bottom: 0;
}

/* .wp-block-buttons already carries the 1.5rem margin-block that separates it
   from the grid, so this only has to undo the bottom half of it: the section's
   own padding is what ends the section. */
.grit-value-cards__action {
	margin-bottom: 0;
}

.grit-value-card {
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
	padding: clamp(1.25rem, 3vw, 1.75rem);
	display: flex;
	flex-direction: column;
}

/* The orange rule. Decoration with no meaning, so it is a pseudo-element and
   never reaches the DOM or the accessibility tree. */
.grit-value-card::before {
	content: "";
	display: block;
	width: 1.75rem;
	height: 0.25rem;
	border-radius: var(--grit-radius-sm);
	background: var(--grit-primary);
	margin-bottom: clamp(0.85rem, 2vw, 1.25rem);
}

.grit-value-card h3 {
	font-size: var(--grit-text-h4);
	color: var(--grit-section-heading);
}

.grit-value-card p:last-child {
	margin-bottom: 0;
}

/* A tinted card on a light-grey (or any light-tone) section would vanish into
   it, so there it takes the page background instead. Both directions of the
   same idea: the card is always one step away from whatever is behind it. */
:is(.grit-bg--light, .grit-tone--light) .grit-value-card {
	background: var(--grit-bg);
}

/* Text alignment, per section. Left is what this section has always done and
   carries no class at all, so an existing row is untouched by all of this;
   centre and right are the two opt-ins.

   SCOPED TO THE COPY, AND THE CARDS KEEP THEIR LEFT EDGE. This was a bare
   text-align on the section, which took the cards with it and needed two hand-
   written margin-inline rules to drag each card's orange rule along. It is now
   the statistics row's arrangement instead: the intro, the closing line and the
   button move, and the grid below keeps the arrangement the design drew for it.
   A card holds a heading and a sentence at the left edge of a tinted panel, and
   a row of those centred inside their own boxes reads as four mistakes rather
   than as one choice.

   THE ORANGE RULE THEREFORE NEEDS NOTHING. It is a ::before with a declared
   width in a column flex container, so it sits at the card's left edge on its
   own — which is where it belongs now that the card's copy stays there too.

   The button needs answering separately, because it is not text: it is placed
   by justify-content on the flex row that wraps it. Same pair of properties the
   CTA band's and the statistics row's alignments use.

   .grit-value-card sets no text-align of its own, so the cards inherit from the
   section and land left with no rule here to put them there. */
.grit-value-cards--align-center .grit-value-cards__intro,
.grit-value-cards--align-center .grit-value-cards__outro {
	text-align: center;
}

.grit-value-cards--align-right .grit-value-cards__intro,
.grit-value-cards--align-right .grit-value-cards__outro {
	text-align: right;
}

.grit-value-cards--align-center .grit-value-cards__action {
	justify-content: center;
}

.grit-value-cards--align-right .grit-value-cards__action {
	justify-content: flex-end;
}

/* The orange rule, off. One class on the section and one rule here, because a
   ::before carrying no meaning has no business becoming conditional markup just
   to be removable — and (0,2,0) against the bar's own (0,1,0), so it wins on
   specificity rather than on sitting later in the file. */
.grit-value-cards--no-bar .grit-value-card::before {
	display: none;
}

/* ---------------------------------------------------------------------------
   Media cards — a tinted card opening with a full-bleed image

   The third and last card layout. Same tinted body as .grit-value-card, but led
   by an image instead of an orange rule and closed by a tertiary link, which is
   the "Contractors / Educators / Manufacturers" row.

   The image is flush to the card's top edge and its corners are clipped by the
   card, so nothing here needs its own radius.
   --------------------------------------------------------------------------- */
.grit-media-cards {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));

	/* On the grid, not the card, so the optional .grit-tc--* class the renderer
	   puts on the card body always wins — same reasoning as .grit-value-cards. */
	color: var(--grit-section-text);
}

.grit-media-card {
	/* The design measures nearer 2/1, but its images are placeholder blocks and
	   a 2/1 crop is unkind to photographs of people, which is what these will
	   be. One token, so it is a one-line change either way. */
	--grit-media-card-ratio: 16 / 9;

	display: flex;
	flex-direction: column;
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
	overflow: hidden;
}

.grit-media-card__media img,
.grit-resource-card__media img {
	width: 100%;
	aspect-ratio: var(--grit-media-card-ratio);
	object-fit: cover;
	/* Visible only while the image loads, and behind a transparent PNG after —
	   navy rather than nothing, so the card never flashes as a white gap. */
	background: var(--grit-surface-dark);
}

.grit-media-card__body,
.grit-resource-card__body {
	display: flex;
	flex-direction: column;
	flex: 1;
	padding: clamp(1.1rem, 3vw, 1.6rem);
}

.grit-media-card__body h3,
.grit-resource-card__body h3 {
	font-size: var(--grit-text-h4);
	color: var(--grit-section-heading);
}

.grit-media-card__body p:last-child,
.grit-resource-card__body p:last-child {
	margin-bottom: 0;
}

/* margin-top: auto pins the link to the bottom, so a row of cards with unequal
   copy still lines its links up. */
.grit-media-card__action,
.grit-resource-card__action {
	margin: auto 0 0;
	padding-top: 0.5rem;
}

/* ---------------------------------------------------------------------------
   A media card that is only a picture

   THE PICTURE TAKES THE SHARED 16/9 WELL, like every other card in the row.

   It used to keep the uploaded file's own ratio — `aspect-ratio: auto` on the
   well — on the reasoning that a wordless card has no headings or links to line
   up with its neighbours, so nothing was asking it to be cropped. What that
   actually shipped was a tile as tall as whatever an editor happened to upload:
   a phone photo is 3/4 or taller, so one wordless card ran to several times the
   height of the cards beside it and the row stopped reading as a row. A ratio
   the layout controls is worth more here than the uncropped file, and the crop
   is `object-fit: cover` on the shared rule, so the picture is centred rather
   than squashed.

   align-self stays. Grid rows stretch their items, so a wordless card beside one
   with two paragraphs would be as tall as that card and the leftover would show
   as a band of empty tint under the picture. Sizing to its content instead
   leaves the tile at the top of its cell with nothing beneath it.

   Rounding needs nothing of its own: .grit-media-card's border-radius and
   overflow: hidden clip all four of the image's corners once no body follows it.
   --------------------------------------------------------------------------- */
.grit-media-card--media-only {
	align-self: start;
}

/* The whole tile is the link when there is nowhere to put a label. Block, so it
   wraps the image as a box rather than shrink-wrapping it on a text baseline. */
.grit-media-card__tile {
	display: block;
	width: 100%;
}

/* A lightbox tile opens a dialog rather than navigating, so the cursor must not
   present a navigation affordance. */
.grit-media-card__tile[data-grit-lightbox] {
	cursor: zoom-in;
}

:is(.grit-bg--light, .grit-tone--light) .grit-media-card {
	background: var(--grit-bg);
}

/* ---------------------------------------------------------------------------
   Image band — three photographs, one of them large

   Not a card layout, which is why it sits after them rather than among them:
   a tile has no fill, no body and no link of its own, and the only thing it
   shares with .grit-media-card is the navy showing through while an image loads.
   An optional heading block sits above the band; the pictures are what the
   layout is.

   THE HEIGHT IS THE CONSTANT, NOT A RATIO. The three tops and bottoms have to
   line up whatever their files measure, and a ratio per slot cannot promise
   that: a 2fr track is two 1fr tracks PLUS the gap between them, so a 2/1 large
   slot and a 1/1 small one land half a gap apart and the row picks the taller.
   One row height, images filling it, and the arrangement is exact at every
   width. It is a clamp rather than a length because .grit-wrap has no max-width
   — a proportional band would be 570px tall on an ultrawide monitor, which is a
   hero, not a band.

   Two values, one token: the desktop band divides its height between three
   pictures, and the phone gives all of it to one, so the same number cannot
   serve both.
   --------------------------------------------------------------------------- */
/* THE OPTIONAL HEADING BLOCK ABOVE THE BAND — eyebrow, heading, description and
   button, in the same three rules .grit-stats__intro and .grit-partners__intro
   carry, because it is the same arrangement: one block of copy, then the thing
   itself.

   THE BLOCK OWNS THE GAP TO THE PICTURES, not whichever element happens to be
   last inside it. That is the whole reason it is a wrapper: the space above the
   band must not change depending on whether an editor filled in a description, a
   button, both or neither. The clamp is the theme's heading-row-to-grid number,
   shared with .grit-section__bar, .grit-stats__intro and
   .grit-value-cards__intro. */
.grit-image-band__intro {
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

/* Both ends, unlike the other two intros, and the first-child half is the one
   doing real work: this block can be nothing but a button, and
   .wp-block-buttons carries a 1.5rem margin-block. A first child's top margin
   collapses out of its parent and keeps going until something stops it — and
   .grit-wrap has no block padding, so the first thing that does is the section's
   own padding, which the 24px would then sit inside as a gap nothing asked for.
   The last-child half is tidiness by comparison: a bottom margin there collapses
   with this block's own 40px and loses to it. */
.grit-image-band__intro > :first-child {
	margin-top: 0;
}

.grit-image-band__intro > :last-child {
	margin-bottom: 0;
}

/* Only when nothing is pinned. .grit-tc--* is one class declared with the tokens
   hundreds of lines above every component, so an unconditional colour here would
   be a (0,1,0) selector later in the file beating the editor's own choice and the
   description colour field would silently do nothing. Same guard as
   .grit-stats__lede. */
.grit-image-band__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* Said on the button rather than left to its position, which is the same
   declaration .grit-stats__action makes. Today it is belt to the `> :last-child`
   braces above — and to margin collapsing, which would swallow the 1.5rem into
   this block's 40px anyway — but neither of those survives the block gaining a
   fill or a padding, and both stop being true the moment a field is added after
   the button. */
.grit-image-band__action {
	margin-bottom: 0;
}

.grit-image-band {
	/* One image, the full width of the wrap — the arrangement below the split. */
	--grit-image-band-height: clamp(13rem, 60vw, 20rem);

	display: grid;
	gap: clamp(0.75rem, 1.5vw, 1.25rem);
	grid-auto-rows: var(--grit-image-band-height);
}

.grit-image-band__item {
	border-radius: var(--grit-radius-md);

	/* What actually performs the crop, and what clips the picture to the
	   rounded corners — the same job it does on .grit-partners__card, and for
	   the same reason: object-fit: cover genuinely hangs the overflowing axis
	   outside the box, rounded corners included. */
	overflow: hidden;

	/* Visible only while the image loads, and behind a transparent PNG after —
	   navy rather than nothing, so the band never flashes as a row of white
	   gaps. The same fill .grit-media-card__media img uses, for the same
	   reason. */
	background: var(--grit-surface-dark);

	/* min-width: 0 IS LOAD-BEARING, and min-height: 0 is the belt to its braces.
	   A grid item's default automatic minimum size is content-based, and the
	   image inside is `width: 100%` — a percentage that resolves to `auto` when
	   min-content is being measured, so the contribution is the FILE's own width.
	   A 1600px photograph in a 1fr track would then claim a 1600px floor and
	   burst the tracks; `1fr` is flexible, so nothing clamps it. The row cannot
	   go the same way — `grid-auto-rows` is a fixed track size and the spec
	   clamps an automatic minimum to that — but the pairing is one declaration
	   and the partner logos already lost this argument once (a square mark stood
	   120px proud of a 101px card). */
	min-width: 0;
	min-height: 0;
}

.grit-image-band__item img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* ---------------------------------------------------------------------------
   The band on a phone: one image, and the other slots are gone

   REMOVED RATHER THAN STACKED. A band is a horizontal arrangement; stacked it
   is three unrelated pictures in a column, which is what the media cards layout
   already draws and draws better. Which one survives is the editor's
   `gf_mobile` — the widest photograph is often not the one that reads at 360px.

   display: none is also what should keep a phone from paying for the other two:
   an element with no box never intersects the viewport, so a lazy fetch has
   nothing to trigger it. That is engine behaviour rather than anything the spec
   promises, which is exactly why their `sizes` advertises 1px here — a browser
   that starts the fetch anyway starts the smallest candidate in the srcset
   rather than a full-width one. See gritfoundation_image_band().

   A max-width query, so the desktop rules below need no counterpart undoing it.
   --------------------------------------------------------------------------- */
@media (max-width: 45em) {
	.grit-image-band__item:not(.grit-image-band__item--mobile) {
		display: none;
	}
}

/* ---------------------------------------------------------------------------
   The band above the split: 2fr for the large slot, 1fr for the rest

   The large slot WIDENS WHERE IT SITS rather than moving to the front, so the
   order an editor arranged is the order that renders and the choice reads as a
   size instead of as a sort.

   One image needs no template at all — an implicit single column is already the
   whole row — and so needs no feature rule either. Every other pairing has one,
   which the clamp in gritfoundation_image_band() is what guarantees: without it
   a two-image band could store `third` and emit a class matching nothing, which
   is three equal tracks and a control that silently did nothing.
   --------------------------------------------------------------------------- */
@media (min-width: 45.0625em) {
	.grit-image-band {
		/* Three pictures dividing one band. 21vw lands the design's own 300px at
		   1440, holds a floor just above the split, and caps before an ultrawide
		   turns the band into a hero. */
		--grit-image-band-height: clamp(15rem, 21vw, 21rem);
	}

	.grit-image-band--2.grit-image-band--feature-1 {
		grid-template-columns: 2fr 1fr;
	}

	.grit-image-band--2.grit-image-band--feature-2 {
		grid-template-columns: 1fr 2fr;
	}

	.grit-image-band--3.grit-image-band--feature-1 {
		grid-template-columns: 2fr 1fr 1fr;
	}

	.grit-image-band--3.grit-image-band--feature-2 {
		grid-template-columns: 1fr 2fr 1fr;
	}

	.grit-image-band--3.grit-image-band--feature-3 {
		grid-template-columns: 1fr 1fr 2fr;
	}
}

/* ---------------------------------------------------------------------------
   Label pills — a small uppercase chip for metadata

   Distinct from .host-badge, which is a STATUS badge: "Upcoming", "Current
   host" — a fact about the thing at this moment. These are classification, and
   two of them sit side by side on one card. Same shape and type scale, and the
   accent tier deliberately reads the same two tokens as .host-badge--soft, so a
   palette change moves both rather than leaving one behind.
   --------------------------------------------------------------------------- */
.grit-pill {
	display: inline-block;
	font-size: 0.6875rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	padding: 0.25rem 0.6rem;
	border-radius: var(--grit-radius-full);
	white-space: nowrap;
}

/* The quiet orange: the label on a tenth-strength fill, so it reads as an
   accent beside a title rather than out-shouting it. */
.grit-pill--accent {
	background: var(--grit-primary-soft);
	color: var(--grit-primary);
}

.grit-pill--muted {
	background: var(--grit-bg-alt);
	color: var(--grit-text-muted);
}

/* ---------------------------------------------------------------------------
   Resource cards — the downloads grid

   The fourth card layout. Its insides are .grit-media-card's, shared by selector
   grouping above rather than restated, so the image well, the body and the
   bottom-pinned action cannot drift. What differs is the shell and the grid:

   BORDERED WHITE, not tinted. These sit on a light-grey section in the design,
   where a tint of the same family would be almost invisible and the border is
   what separates the card from the page. .grit-media-card's fill flips with the
   section; this one does not, because the design's card is white on both.

   TWO ACROSS, not auto-fit. The titles are short but the descriptions run two
   and three lines, and an auto-fit 17rem grid puts four columns on a wide
   monitor and turns each into a ribbon. Same fixed-two treatment, and the same
   45.0625em breakpoint, as .grit-people — one step above the theme's max-width:
   45em split so the two can never both apply.

   V2 IS NOT A GRID AT ALL — it is a scroll-snap track, so it shares the gap
   token below and nothing else. See the v2 block further down.
   --------------------------------------------------------------------------- */
.grit-resources {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: minmax(0, 1fr);

	/* On the grid, not the card, so the optional .grit-tc--* class the renderer
	   puts on the card body always wins — same reasoning as .grit-media-cards. */
	color: var(--grit-section-text);
}

@media (min-width: 45.0625em) {
	.grit-resources {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

.grit-resource-card {
	/* The same crop the media card uses, declared again because the image rule
	   the two share reads this token — and a token declared on .grit-media-card
	   is not in this card's inheritance chain, so resource images had no ratio
	   at all until this line. Invisible in review: every placeholder in the
	   preview harness is already 16/9. */
	--grit-media-card-ratio: 16 / 9;

	display: flex;
	flex-direction: column;
	background: var(--grit-bg);
	border: 1px solid var(--grit-section-border);
	border-radius: var(--grit-radius-md);

	/* What clips the image's top corners, so the image needs no radius of its
	   own and cannot leave a hairline of card showing through at the corner. */
	overflow: hidden;
}

/* Both pills on one line, wrapping together rather than the second one being
   pushed under the title. Same wrapper shape .host-row__title uses. */
.grit-resource-card__pills {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem;
	margin: 0 0 0.75rem;
}

/* ---------------------------------------------------------------------------
   Resources v2 — the same card without its picture, on a slider

   THE CARD IS THE ONE ABOVE, class for class, so there is nothing about it here
   beyond the width it takes in the track. Dropping the image is all that
   separates the two designs at card level, and an image the renderer never emits
   needs no rule to hide it.

   A SCROLL-SNAP TRACK, NOT A GRID, on both mobile and desktop. The overflow is
   the mechanism: dragging, swiping, a trackpad and the arrow keys all slide it
   with no script at all, and camp-carousel.js only adds the buttons — the same
   progressive enhancement, and the same component, the camp photo strip uses.
   That is also why there is no column count any more: a track has slide widths,
   and `flex-basis` answers "how many are in view" without the renderer having to
   emit anything.

   FOUR IN VIEW ON DESKTOP, TWO FROM THE SPLIT, AND A PEEK ON A PHONE. The 85%
   is the whole point of the mobile width: a card that filled the viewport would
   leave a slider looking like a single card, and the sliver of the next one is
   what says "this moves" before anyone has touched it. The widths are computed
   off the gap token rather than guessed, so retuning the gap cannot leave four
   cards overflowing a row they are supposed to fit.

   Two across from the split, four from 62rem — the same two breakpoints the stat
   cards and the v1 grid use, rather than a third set of numbers.
   --------------------------------------------------------------------------- */
.grit-resources-v2 {
	/* One gap for the track and for the arithmetic below, so a card's width and
	   the space beside it can never be computed from two different numbers. The
	   value is .grit-resources' own. */
	--grit-resources-v2-gap: clamp(1rem, 2.5vw, 1.75rem);
}

.grit-resources-v2__track {
	display: flex;
	gap: var(--grit-resources-v2-gap);
	margin: 0;

	/* Vertical padding, not decoration: the track clips its overflow, and a focus
	   ring on a card's own link sits a couple of pixels outside the card. */
	padding: 0.25rem 0;
	list-style: none;

	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;

	/* The scrollbar is suppressed because the arrows and the peeking card are the
	   affordance, and a permanent bar under four cards reads as a mistake. The
	   track keeps its keyboard focus ring and its tabindex, so this removes a
	   visual, never the control. */
	scrollbar-width: none;

	/* On the track, not the card, so the optional .grit-tc--* class the renderer
	   puts on the card body always wins — same reasoning as .grit-resources. */
	color: var(--grit-section-text);
}

.grit-resources-v2__track::-webkit-scrollbar {
	display: none;
}

/* The slide IS the card — the <li> carries .grit-resource-card, the way
   .host-tile does — so that `hidden` collapses the slide with it. A card hidden
   inside a slide of its own would leave the filter bar an empty column the width
   of a card, and the arrows would scroll through gaps. */
.grit-resources-v2__slide {
	flex: 0 0 auto;
	width: min(20rem, 85%);
	scroll-snap-align: start;
}

@media (min-width: 45.0625em) {
	.grit-resources-v2__slide {
		width: calc( ( 100% - var(--grit-resources-v2-gap) ) / 2 );
	}
}

@media (min-width: 62rem) {
	.grit-resources-v2__slide {
		width: calc( ( 100% - 3 * var(--grit-resources-v2-gap) ) / 4 );
	}
}

/* Smooth scrolling is the one piece of motion here, and it is declared in two
   places — this property and the script's scrollBy — so both have to be taken
   back off. The global reduce block further down only touches transitions and
   animations, which a scroll is neither. */
@media (prefers-reduced-motion: reduce) {
	.grit-resources-v2__track {
		scroll-behavior: auto;
	}
}

/* The left half of the heading bar: the eyebrow, the heading and the
   description, with the arrows and the "all resources" link opposite. Capped
   rather than left to run the whole band — .grit-wrap has no max-width, and a
   description measured across 1800px is not a paragraph anyone reads. 46rem is
   .grit-section__head's column and the locator's intro, so it is this
   stylesheet's answer to that question rather than a fourth number. */
.grit-resources-v2__intro {
	max-width: 46rem;
}

/* .grit-section__bar clears the trailing margin of a heading that is its own
   child; this column is that child, so the heading nested inside it is out of
   that rule's reach and the margin has to come off here instead. Otherwise the
   bar's own gap sits on top of it and the controls drift off the heading's centre. */
.grit-resources-v2__intro > :last-child {
	margin-bottom: 0;
}

/* ARROWS IN THE HEADING BAR, NOT OVER THE CARDS. Overlaid arrows are the commoner
   pattern and they were declined on purpose: they cover the card they sit on,
   they need a fill heavy enough to stay legible over whatever is beneath them,
   and they move with the cards on a phone. Up here they occlude nothing, they sit
   in one place at every width, and they are next to the heading that names what
   they scroll — which is also where the "all resources" link already was, so the
   whole right-hand side of the bar reads as one set of controls for one block.

   The link stays last, as the design draws it. */
.grit-resources-v2__controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem;

	/* Right-aligned by its own margin rather than by the bar's space-between,
	   which only reaches it while there is an intro opposite. A block whose
	   heading was left blank would otherwise move its arrows to the left edge,
	   and arrows that are somewhere else on some sections are arrows a visitor
	   has to look for. */
	margin-inline-start: auto;
}

/* The gap above is the arrows'; the link needs the wider one the bar itself uses
   between its two halves, or it reads as a third button. */
.grit-resources-v2__controls > .grit-btn {
	margin-inline-start: clamp( 0.5rem, 2vw, 1rem );
}

/* ---------------------------------------------------------------------------
   Resource filters — the search box and chip rows above the library

   THE FILTERED-OUT STATE IS THE `hidden` ATTRIBUTE, and these two rules are what
   make it stick. PHP marks non-matching cards hidden so the page works with no
   JavaScript, and the script writes the same attribute as you type — but
   `[hidden]`'s UA rule is `display: none` at the lowest possible specificity, and
   .grit-resource-card sets `display: flex`, which beats it. Adding the attribute
   to the selector costs one point of specificity and settles it without
   !important. The section is here for the same reason: a block the time chips
   exclude is removed whole.

   EVERY HOST SELECTOR IS HERE FOR THAT SAME REASON, and each was a real bug until
   it was. .host-row is `display: grid` and .host-tile `display: flex`, so both beat
   the UA rule — the region chips marked non-matching cards `hidden` and the cards
   stayed on screen regardless. The lists are named too, because an emptied block
   now swaps its <ul> for a message and .host-rows carries the borders that would
   otherwise draw an empty frame under it.

   WORTH KNOWING WHY THE TESTS MISSED IT: tools/preview/test-camp-locator.mjs runs
   in jsdom and asserts on the `.hidden` DOM property, which was correct all along.
   Nothing in the harness computes style. This rule is verified by looking at
   sections.html in a browser, which is why that step is written out in the
   README's checklist rather than left to whoever remembers.

   Chips are LINKS, so everything below has to survive being an anchor: no
   background on :hover alone (the active state is the fill), and the underline
   the global link rule adds is turned off explicitly.
   --------------------------------------------------------------------------- */
.grit-resource-card[hidden],
.host-row[hidden],
.host-tile[hidden],
.host-rows[hidden],
.host-tiles[hidden],
.grit-section[hidden] {
	display: none;
}

.grit-resource-filters__search {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.75rem;

	/* The design's box is a third of the row, not the whole of it. Capped rather
	   than set, so it still shrinks on a phone. */
	max-width: 34rem;
}

/* Overrides the shared field rule's width: 100% and its label-gap margin, both
   written for a field stacked under a visible label — the same override, for the
   same reason, as the newsletter's. */
.grit-resource-filters input[type="search"] {
	flex: 1 1 14rem;
	width: auto;
	min-width: 0;
	margin: 0;
}

.grit-resource-filters__facets {
	display: grid;
	grid-template-columns: 1fr;
	gap: clamp(1rem, 2.5vw, 1.5rem);
	margin-top: clamp(0.85rem, 2vw, 1.15rem);
}

/* Two vocabularies, two columns above the split. Below it they stack — the same
   breakpoint the search row uses, so the bar does not mix a two-column chip
   layout with a stacked search box. */
@media (min-width: 45.0625em) {
	.grit-resource-filters__facets {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

.grit-resource-filters__facet-label {
	margin: 0 0 0.5rem;
	font-size: var(--grit-text-small);
	font-weight: 600;
	/* The token, not a literal: #5A6B7A on a light band, and the on-dark grey
	   when the section is navy — the same swap every other muted label makes. */
	color: var(--grit-section-text);
}

.grit-chips {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem;
}

/* NOT .grit-pill, which is the label on a card: that one is uppercase, 11px and
   never interactive, and these are 14px controls with a hover and a pressed
   state. Same rounded silhouette, different job — and a shared class would mean
   every change to one had to be undone on the other. */
.grit-chip {
	display: inline-block;
	font-size: var(--grit-text-small);
	font-weight: 600;
	line-height: 1.25;
	color: var(--grit-navy);
	text-decoration: none;
	padding: 0.45rem 1.05rem;
	background: var(--grit-bg);
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-full);
	transition:
		background-color var(--grit-motion),
		border-color var(--grit-motion),
		color var(--grit-motion);
}

.grit-chip:hover,
.grit-chip:focus-visible {
	border-color: var(--grit-navy);
	text-decoration: none;
}

/* The navy fill the design gives the chip that is on. Also the only visual
   difference between the chips, so it carries aria-current in the markup — the
   fill is not something a screen reader can see.

   The second selector is the product page's attribute pills, which are the same
   control with a different state source: a real radio, so `checked` says which
   one is on and there is no class for JavaScript to write. Listed here rather
   than restated in the WooCommerce block, because a pill that stopped matching
   the library's chips would be two vocabularies for one shape. Not repeated in
   the dark-context rules below: the product summary is never on a dark band. */
.grit-chip.is-active,
.grit-variation-pills__input:checked + .grit-chip {
	background: var(--grit-navy);
	border-color: var(--grit-navy);
	color: #fff;
}

/* On a dark section the chips would otherwise be navy on navy: a white outline,
   and the active one inverts to a white fill. Keyed on the tone class rather
   than on the navy slug alone, like every other dark-context rule here. */
:is(.grit-bg--navy, .grit-tone--dark) .grit-chip {
	color: #fff;
	background: transparent;
	border-color: var(--grit-section-border);
}

:is(.grit-bg--navy, .grit-tone--dark) .grit-chip:hover,
:is(.grit-bg--navy, .grit-tone--dark) .grit-chip:focus-visible {
	border-color: #fff;
}

:is(.grit-bg--navy, .grit-tone--dark) .grit-chip.is-active {
	background: #fff;
	border-color: #fff;
	color: var(--grit-navy);
}

.grit-resource-filters__empty {
	margin: clamp(1rem, 3vw, 1.5rem) 0 0;
	color: var(--grit-section-text);
}

/* Below the split the field and the button each take the whole width — the same
   trade the newsletter row makes, because a half-width search box beside a
   button is not a thing anyone wants to type into on a phone. */
@media (max-width: 45em) {
	.grit-resource-filters__search {
		flex-direction: column;
		align-items: stretch;
		max-width: none;
	}

	/* flex: 0 0 auto is not tidying up. Turning the row into a column flips which
	   axis flex-basis sizes, so the field's 14rem basis would stop being a
	   minimum width and become a 224px-tall search box. */
	.grit-resource-filters input[type="search"],
	.grit-resource-filters__search .grit-btn {
		flex: 0 0 auto;
		width: 100%;
	}
}

/* ---------------------------------------------------------------------------
   Camp locator — the search box and region chips above the hosts blocks

   .grit-chips and .grit-chip are reused wholesale: a chip is a chip, and a
   second set of classes that looked identical would be two things to keep in
   step. What is here is this bar's own layout, which differs from the resources
   bar in the one way that matters — ONE facet, so the chip row is never the
   two-column grid that bar switches to above the split.

   There is no approved design for this section yet, so it deliberately borrows
   the resources bar's measurements rather than inventing its own: two filter
   bars on one site that agree by accident are worse than two that agree on
   purpose.
   --------------------------------------------------------------------------- */

.grit-locator__intro {
	max-width: 46rem;
	margin-bottom: clamp(1rem, 2.5vw, 1.5rem);
}

/* Under the heading rather than a paragraph's full gap from it. Grouped with
   the resources v2 intro rather than restated there: it is the same treatment of
   the same thing — a description directly beneath a section heading — and two
   copies of one declaration is how they stop being the same. */
.grit-locator__lede,
.grit-resources-v2__lede {
	margin: 0.5rem 0 0;
}

.grit-locator__search {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.75rem;

	/* A third of the row, as the resources box is. Capped rather than set, so it
	   still shrinks on a phone. */
	max-width: 34rem;
}

/* Overrides the shared field rule's width: 100% and its label-gap margin, both
   written for a field stacked under a visible label. */
.grit-locator input[type="search"] {
	flex: 1 1 14rem;
	width: auto;
	min-width: 0;
	margin: 0;
}

.grit-locator__facets {
	margin-top: clamp(0.85rem, 2vw, 1.15rem);
}

.grit-locator__facet-label {
	margin: 0 0 0.5rem;
	font-size: var(--grit-text-small);
	font-weight: 600;
	/* The token, not a literal: the muted grey on a light band, and the on-dark
	   grey when the section is navy. */
	color: var(--grit-section-text);
}

/* The bar's own message, and a block's. Grouped because they are the same
   sentence in two places — the block says it where the list was, the bar says it
   when no block is on screen to. Nothing about the treatment should be able to
   drift between them. */
.grit-locator__empty,
.host-empty {
	margin: clamp(1rem, 3vw, 1.5rem) 0 0;
	color: var(--grit-section-text);
}

/* No top margin on a block's message: it stands where the list stood, under the
   heading bar's own spacing, rather than being pushed off it. */
.host-empty {
	margin-top: 0;
}

/* Two facet rows now, and the second wants less air above it than the first does
   below the search box — they read as one control, not two. */
.grit-locator__facets + .grit-locator__facets {
	margin-top: clamp(0.6rem, 1.5vw, 0.85rem);
}

/* Below the split the field and the button each take the whole width — a
   half-width search box beside a button is not a thing anyone wants to type into
   on a phone. */
@media (max-width: 45em) {
	.grit-locator__search {
		flex-direction: column;
		align-items: stretch;
		max-width: none;
	}

	/* flex: 0 0 auto is not tidying up. Turning the row into a column flips which
	   axis flex-basis sizes, so the field's 14rem basis would stop being a
	   minimum width and become a 224px-tall search box. */
	.grit-locator input[type="search"],
	.grit-locator__search .grit-btn {
		flex: 0 0 auto;
		width: 100%;
	}
}

/* ---------------------------------------------------------------------------
   Numbered steps — the "Why Host a GRIT Camp?" list

   A real <ol> whose visible numbers come from a counter rather than from the
   markup, so they cannot drift from the order after a reorder or a deletion.
   decimal-leading-zero is what gives 01, 02, 03 rather than 1, 2, 3.
   --------------------------------------------------------------------------- */
.grit-steps {
	counter-reset: grit-step;
	list-style: none;
	margin: clamp(1.25rem, 3vw, 2rem) 0 0;
	padding: 0;
}

.grit-step {
	counter-increment: grit-step;
	display: grid;
	grid-template-columns: auto 1fr;
	gap: clamp(0.75rem, 2.5vw, 1.25rem);
	align-items: start;
}

.grit-step::before {
	content: counter(grit-step, decimal-leading-zero);
	font-family: var(--grit-font-heading);
	font-weight: 800;
	font-size: var(--grit-text-h3);
	line-height: 1.1;
	color: var(--grit-primary);
	/* Tabular-ish alignment without a fixed width: two digits is the whole range
	   a section like this uses, and min-content keeps 01 and 10 in one column. */
	min-width: 1.75em;
}

/* Between steps only. The design has no rule above the first or below the last. */
.grit-step + .grit-step {
	margin-top: clamp(1rem, 2.5vw, 1.5rem);
	padding-top: clamp(1rem, 2.5vw, 1.5rem);
	border-top: 1px solid var(--grit-section-border);
}

.grit-step h3 {
	font-size: var(--grit-text-h4);
	margin-bottom: 0.35rem;
	color: var(--grit-section-heading);
}

.grit-step__body > :last-child {
	margin-bottom: 0;
}

/* Content that runs full width, with prose still held to a readable measure. */
.entry-content--wide {
	max-width: none;
}

.entry-content--wide > p,
.entry-content--wide > h2:not(.grit-centered),
.entry-content--wide > h3,
.entry-content--wide > h4 {
	max-width: var(--grit-measure);
}

.grit-centered {
	max-width: 46rem;
	margin-inline: auto;
	text-align: center;
}

.entry-content--wide > .grit-cards,
.entry-content--wide > .grit-stats {
	margin-block: clamp(1.5rem, 4vw, 2.5rem);
}

/* ---------------------------------------------------------------------------
   Team grid — a card per person, avatar beside the name

   .grit-person is shared: template-parts/sections/team-grid.php,
   patterns/team-grid.php and the About Us content in data/pages.json all emit
   these classes in the same order — figure, h3, p.role, bio. The live About Us
   copy is in the database, where a theme change cannot reach it, so the card is
   built by PLACING those existing children rather than by wrapping them. A
   wrapper would have styled the section and left that page looking broken.

   Three explicit rows is what makes the avatar work without one: it lets the
   figure span 1/-1, which is unresolvable against a purely implicit grid (-1
   means the last EXPLICIT line, and with no explicit rows that is line 1). Those
   three are exactly the children every one of the three call sites emits; a
   fourth lands in an implicit row below, still in the text column.
   --------------------------------------------------------------------------- */
/* The section heading's left and right alignments.

   .grit-centered IS DECLINED, NOT OVERRIDDEN. It bundles max-width: 46rem and
   margin-inline: auto with its text-align, and it is shared with feature cards,
   the newsletter, the FAQ and a block pattern — so a text-align here would leave
   the heading in a column still centred on a wide screen, and touching the shared
   class would move four other layouts. team-grid.php emits this pair instead and
   re-declares the measure, so the heading reads at the same width whichever
   alignment it takes. Centred needs nothing: it keeps .grit-centered, which is
   what every row saved before the field existed already had. */
.grit-team__heading {
	max-width: 46rem;
}

.grit-team__heading--align-left {
	margin-inline: 0;
	text-align: left;
}

.grit-team__heading--align-right {
	margin-inline-start: auto;
	text-align: right;
}

.grit-people {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: minmax(0, 1fr);
	margin-block: clamp(1.5rem, 4vw, 2.5rem);

	/* The muted default lives on the GRID, not the card, so the optional
	   .grit-tc--* class the renderer puts on the card always wins — a class on a
	   descendant beats one on its ancestor whatever the file order, which a
	   same-element tie would not. The bio's colour used to be set on the card's
	   own paragraph, which quietly made gf_desc_color a no-op. */
	color: var(--grit-section-text);
}

/* Two across, never more. These bios run four and five lines, so the old
   auto-fit/minmax(15rem) put four columns on a wide screen and turned each into
   a thin ribbon of text. */
@media (min-width: 45.0625em) {
	.grit-people {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

.grit-person {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	grid-template-rows: repeat(3, min-content);
	column-gap: clamp(0.85rem, 2.5vw, 1.35rem);
	background: var(--grit-bg);
	border: 1px solid var(--grit-section-border);
	border-radius: var(--grit-radius-md);
	padding: clamp(1.1rem, 3vw, 1.6rem);
	text-align: left;
}

/* Everything that is not the avatar shares the text column. */
.grit-person > * {
	grid-column: 2;
}

.grit-person__photo {
	grid-column: 1;
	/* Spans the text rows so the avatar sits beside all of them; align-self stops
	   it stretching to their combined height when the bio runs long. */
	grid-row: 1 / -1;
	align-self: start;
	margin: 0;
	width: clamp(4.5rem, 12vw, 6rem);
}

.grit-person__photo img {
	width: 100%;
	aspect-ratio: 1;
	object-fit: cover;
	border-radius: var(--grit-radius-full);
	background: var(--grit-bg-alt);
}

.grit-person h3 {
	font-size: var(--grit-text-h4);
	margin-bottom: 0.15rem;
	color: var(--grit-section-heading);
}

/* Orange, not the neutral it used to be. The design draws the role as an
   eyebrow, and .grit-hero__eyebrow and .grit-card__eyebrow are already orange —
   so this joins that treatment instead of being a third thing. The layout also
   gained gf_eyebrow_color, so it can be pinned back to muted without code. */
.grit-person__role {
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	margin-bottom: 0.4rem;
}

/* Guarded, for the reason .grit-hero__eyebrow is — and this is the rule whose
   comment above promised the field worked. */
.grit-person__role:not([class*="grit-tc--"]) {
	color: var(--grit-primary);
}

/* :not() rather than p:last-child, which is what this used to be — a person with
   a role and no bio matched it, and (0,1,1) beat .grit-person__role at (0,1,0),
   so their role came out muted instead of orange. */
.grit-person > p:not(.grit-person__role) {
	font-size: var(--grit-text-small);
}

.grit-person > :last-child {
	margin-bottom: 0;
}

/* Below the split the avatar sits above the name, as the mobile design shows.
   grid-row has to be released or the figure keeps spanning three rows and lands
   on top of the text in a single-column grid. */
@media (max-width: 45em) {
	.grit-person {
		grid-template-columns: minmax(0, 1fr);
		grid-template-rows: none;
		row-gap: 0.5rem;
	}

	.grit-person > *,
	.grit-person__photo {
		grid-column: 1;
	}

	.grit-person__photo {
		grid-row: auto;
	}
}

/* ---------------------------------------------------------------------------
   Media + text split

   The two-column grid is declared here rather than inherited from core's
   block-library stylesheet. It used to rely on core, which meant this layout
   only held together when that stylesheet happened to be enqueued — fine for a
   block in post_content, not something to bet a PHP-rendered section on. Owning
   it also means the block pattern and the Sections renderer are laid out by the
   same rules and cannot drift apart.

   `has-media-on-the-right` is core's own class name, kept so the pattern and
   the renderer share one vocabulary.
   --------------------------------------------------------------------------- */
.grit-media-text {
	/* ONE HEIGHT FOR EVERY MEDIA COLUMN. The image used to run at whatever ratio
	   was uploaded, so two of these sections on one page sat at two different
	   heights and a portrait photo took most of a screen.

	   A height and not an aspect-ratio, unlike the hero and the media card: this
	   column's WIDTH changes with the split (50/50, a third, two thirds), so a
	   ratio would produce three different heights again — which is the thing
	   being fixed. Overridden below the stack, where 450px is most of a phone. */
	--grit-media-text-height: 450px;

	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: clamp(1.5rem, 4vw, 3rem);
	margin-block: clamp(2rem, 5vw, 3.5rem);
	align-items: center;
}

.grit-media-text > .wp-block-media-text__media {
	grid-column: 1;
	grid-row: 1;
	margin: 0;
}

.grit-media-text > .wp-block-media-text__content {
	grid-column: 2;
	grid-row: 1;
	padding-inline: 0;
}

.grit-media-text.has-media-on-the-right > .wp-block-media-text__media {
	grid-column: 2;
}

.grit-media-text.has-media-on-the-right > .wp-block-media-text__content {
	grid-column: 1;
}

/* No image: one column, rather than text squeezed into half the width beside a
   blank void that reads as a failed image load. */
.grit-media-text--no-media {
	grid-template-columns: 1fr;
}

/* Collapsing the track is only half of it. The placements above pin the content
   to grid-column 2 (or to 1 when the media is on the right), and a stale
   grid-column: 2 against a single-track grid does not fall back to track 1 — it
   creates an implicit second track and puts the content there. The result was
   text at 214px wide in the right-hand fifth of an 1120px section, with the real
   track left empty, which is the opposite of what --no-media is for.
   Both sides need answering, and the media-on-the-right selector is (0,3,0). */
.grit-media-text--no-media > .wp-block-media-text__content,
.grit-media-text--no-media.has-media-on-the-right > .wp-block-media-text__content {
	grid-column: 1;
}

/* Image / text ratios. `half` is the base 1fr 1fr above and carries no class, so
   a row with nothing stored renders as it always did.

   The :not() is load-bearing. These sit after --no-media and match the same
   element, so without it a section with no image would take two columns and leave
   the second empty.

   WHICH column is wide depends on the side the image is on, because the content
   is whichever column the image is not: media-left puts content second. That is
   why each ratio needs its has-media-on-the-right pair rather than one rule.

   minmax(0, …) rather than bare fractions: a grid track's default minimum is
   auto, so one long unbroken word would push its column past its share. */
/* The near-even ratio. Written in 45/55 rather than reduced to 9/11 so the
   numbers in the rule are the numbers in the dropdown, and `sizes` (45vw) can be
   read straight off it. */
.grit-media-text--split-45-55:not(.grit-media-text--no-media) {
	grid-template-columns: minmax(0, 45fr) minmax(0, 55fr);
}

.grit-media-text--split-45-55:not(.grit-media-text--no-media).has-media-on-the-right {
	grid-template-columns: minmax(0, 55fr) minmax(0, 45fr);
}

.grit-media-text--split-third:not(.grit-media-text--no-media) {
	grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
}

.grit-media-text--split-third:not(.grit-media-text--no-media).has-media-on-the-right {
	grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}

.grit-media-text--split-two-thirds:not(.grit-media-text--no-media) {
	grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
}

.grit-media-text--split-two-thirds:not(.grit-media-text--no-media).has-media-on-the-right {
	grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
}

/* --stretch-media USED TO SIZE THE IMAGE and no longer does. It set
   height: 100% so the steps layout's panel matched the list beside it, plus a
   min-height floor so a two-line list could not collapse it to a strip. A
   declared height on the base rule below makes both redundant, and the two
   together were worse than either: a stretched figure taller than a fixed image
   leaves a gap under it.

   The class stays because it still carries the mobile ordering — steps above
   their photo, the reverse of the media-led default — down in the media query. */

/* The video takes the identical treatment — same slot, same corners, same crop —
   because it stands in for the image rather than being a different object, and its
   poster is the file the <img> would have been. `display: block` is stated because
   <video> is inline by default and would otherwise sit on a text baseline, leaving
   a few pixels of section colour under the media box. The same pair of rules the
   hero carries, for the same reasons. */
.grit-media-text .wp-block-media-text__media :is(img, video) {
	display: block;
	width: 100%;
	height: var(--grit-media-text-height);
	/* Cover, so a source of any ratio fills the slot without distorting. The
	   cost is the top and bottom of a tall photograph, which is the same trade
	   the hero makes with its ratio. */
	object-fit: cover;
	border-radius: var(--grit-radius-md);
}

.grit-media-text > :last-child > :last-child {
	margin-bottom: 0;
}

/* THE SECOND BLOCK OF COPY. Its colour arrives as a .grit-tc--* class on this
   element and needs no rule here at all: `.grit-tc--* p { color: inherit }` up
   with the tokens is what carries it down to the paragraphs, and left on Default
   this wrapper has no colour of its own so they inherit the content column's —
   which is the first block's. That is the whole mechanism behind the field's
   "Default follows the description colour beside it".

   The closing margin is the one thing that does need saying. The rule above
   reaches the content column's last child, which is now this wrapper rather than
   a paragraph, and zeroing a wrapper's margin leaves the paragraph's inside it. */
.grit-media-text__text2 > :last-child {
	margin-bottom: 0;
}

/* THE VALUES ROW — up to three short statements under the copy, divided by a
   rule. The renderer caps the count at three and sends it as a class, the same
   bargain the statistics cards make: .grit-wrap has no max-width, so how many
   tracks to draw is something only the renderer knows.

   STACKED FIRST, side by side from 60rem. The row sits inside the content column,
   whose width depends on the split an editor chose — so the width that actually
   decides whether three columns fit is not the viewport's, and no media query can
   read it. 60rem is where even the narrowest split (a third of the page to the
   copy) leaves room for three columns of two or three words. Below it the
   statements stack and the divider turns horizontal, which is the same rule doing
   the same job on its side rather than a different treatment.

   THE DIVIDERS ARE ON THE ITEMS, NOT THE CONTAINER, because what the design
   shows is a rule BETWEEN statements: border-inline-end on all but the last, so
   the row ends on a statement rather than trailing off into a rule. A single
   value matches neither :not(:last-child) nor `* + *` and so draws nothing at
   all, which is right — one statement has nothing to be divided from. */
.grit-media-text__values {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	margin-block: clamp(1.25rem, 3vw, 1.75rem);
	padding: 0;
	list-style: none;
}

/* Heading colour rather than body: these are statements, and the design draws
   them in the same navy a heading takes — which means they flip to white on a
   dark section without this rule knowing anything about the background. */
.grit-media-text__value {
	margin: 0;
	font-weight: 700;
	line-height: var(--grit-lh-heading);
	color: var(--grit-section-heading);
}

.grit-media-text__value + .grit-media-text__value {
	border-block-start: 1px solid var(--grit-section-border);
	margin-top: 0.75rem;
	padding-top: 0.75rem;
}

@media (min-width: 60rem) {
	.grit-media-text__values--cols-2 {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	.grit-media-text__values--cols-3 {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}

	/* Undo the stacked divider before drawing the upright one, or an item on the
	   second row of a wrapped grid would carry both. */
	.grit-media-text__value + .grit-media-text__value {
		border-block-start: 0;
		margin-top: 0;
		padding-top: 0;
	}

	.grit-media-text__values > :not(:last-child) {
		border-inline-end: 1px solid var(--grit-section-border);
		padding-inline-end: clamp(1rem, 2.5vw, 1.5rem);
	}

	/* The first statement stays flush with the column's edge; every one after a
	   divider is held off it. */
	.grit-media-text__values > * + * {
		padding-inline-start: clamp(1rem, 2.5vw, 1.5rem);
	}
}

/* Stack below the split. Every selector that sets grid-template-columns above
   has to be answered here, at equal or greater specificity, or it keeps winning
   inside the media query — has-media-on-the-right is (0,3,0) and a split modifier
   is (0,2,0) on its own and (0,3,0) with the side class. A media query adds no
   specificity of its own, which is the whole trap. */
@media (max-width: 45em) {
	.grit-media-text,
	.grit-media-text.has-media-on-the-right,
	:is(.grit-media-text--split-45-55, .grit-media-text--split-third, .grit-media-text--split-two-thirds):not(.grit-media-text--no-media),
	:is(.grit-media-text--split-45-55, .grit-media-text--split-third, .grit-media-text--split-two-thirds):not(.grit-media-text--no-media).has-media-on-the-right {
		grid-template-columns: 1fr;
	}

	/* A much shorter slot below the stack. Full width now, so 450px is most of a
	   phone screen of photograph above copy nobody has started reading — the same
	   trade the hero makes when it swaps to a wider ratio down here. */
	.grit-media-text {
		--grit-media-text-height: 200px;
	}

	.grit-media-text > .wp-block-media-text__media,
	.grit-media-text.has-media-on-the-right > .wp-block-media-text__media {
		grid-column: 1;
		grid-row: 1;
	}

	.grit-media-text > .wp-block-media-text__content,
	.grit-media-text.has-media-on-the-right > .wp-block-media-text__content {
		grid-column: 1;
		grid-row: 2;
	}

	/* Stretched media stacks AFTER its content, unlike the media-led default
	   above. A media+text block leads with its image because the image is the
	   point; a numbered list leads with the heading that says what is being
	   numbered, and the design stacks its photo below the steps.
	   The has-media-on-the-right pair is repeated because the rules above are
	   (0,3,0) and would otherwise keep winning. */
	.grit-media-text--stretch-media > .wp-block-media-text__media,
	.grit-media-text--stretch-media.has-media-on-the-right > .wp-block-media-text__media {
		grid-row: 2;
	}

	.grit-media-text--stretch-media > .wp-block-media-text__content,
	.grit-media-text--stretch-media.has-media-on-the-right > .wp-block-media-text__content {
		grid-row: 1;
	}

}

/* Above the split, a section's media+text drops its own margin.

   .grit-media-text carries a margin-block because it is also a BLOCK PATTERN,
   dropped into the content column where nothing else would separate it from the
   paragraph above. As a SECTION it sits inside .grit-section, which already
   supplies the page's vertical rhythm as padding — and padding does not collapse
   with a margin, so the two stacked: up to 3.5rem of margin on top of up to 4rem
   of padding, at both ends, leaving this one layout floating in roughly twice the
   space every other section takes.

   Scoped to .grit-section for exactly that reason: the pattern keeps the margin
   it needs, and the section stops paying for it. Numbered steps emits the same
   class inside a section, so it is fixed by the same rule, which is right — the
   two layouts share a grid deliberately.

   Above the split only. Below it the numbers are far smaller (2rem of section
   padding against 2rem of margin) and the extra breathing room between a stacked
   image, its copy and whatever follows is doing real work. */
@media (min-width: 45.0625em) {
	.grit-section .grit-media-text {
		margin-block: 0;
	}
}

/* ---------------------------------------------------------------------------
   Map

   A fixed height, and the same pair of numbers the media+text image takes, so a
   map and a photograph in the same page read as one rhythm. A height rather than
   an aspect-ratio for the reason that layout gives: this box is full width and a
   ratio would make it as tall as the page is wide.

   display: block is load-bearing. An iframe is inline by default, so it sits on
   a text baseline and leaves a few pixels of section colour under the frame —
   the same note the hero's video rule carries, for the same reason.

   No border-radius on the iframe itself: Safari has historically painted an
   iframe's own content square over a rounded parent, so the radius and the clip
   both live on the frame around it.
   --------------------------------------------------------------------------- */
.grit-map {
	--grit-map-height: 450px;
}

.grit-map__intro {
	margin-bottom: clamp(1.25rem, 3vw, 2rem);
}

.grit-map__intro > :last-child {
	margin-bottom: 0;
}

/* Only when nothing is pinned. .grit-tc--* is declared hundreds of lines above
   every component and is also one class, so an unconditional colour here would
   quietly beat the editor's own choice. Same guard as .grit-social-band__lede. */
.grit-map__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

.grit-map__frame {
	overflow: hidden;
	border-radius: var(--grit-radius-md);
	/* Visible for as long as the embed takes to arrive, which on a slow
	   connection is seconds — navy rather than nothing, so the section does not
	   open as a white hole in a coloured band. */
	background: var(--grit-surface-dark);
}

.grit-map__frame iframe {
	display: block;
	width: 100%;
	height: var(--grit-map-height);
	border: 0;
}

@media (max-width: 45em) {
	/* The same step down the media+text image takes below the stack: at full
	   width 450px is most of a phone screen spent on a map nobody has asked to
	   look at yet. */
	.grit-map {
		--grit-map-height: 300px;
	}
}

/* ---------------------------------------------------------------------------
   Call-to-action band

   Two surfaces, one set of centring rules, because they sit in different
   wrappers and only one of them has a background to inherit:

   .grit-cta-section — the section's FULL WIDTH display, and its default. The
     colour is the section's own, chosen from the palette like every other
     section, and the vertical rhythm is .grit-section's padding. It paints no
     panel: a rounded card inside a coloured section meant two backgrounds
     competing for one band and a second layer of padding inside the first.
   .grit-cta-band — the inset card. Used by the block pattern, which is a group
     inside the content column where there is no section background to inherit,
     AND by the section's `card` display for a page that wants the panel rather
     than a band across the page. It paints its own navy either way, which is
     why the card display leaves the section's own background alone.

   Deliberately not a 100vw breakout either way. That technique overflows
   horizontally whenever a vertical scrollbar is present, and the usual fix
   (overflow-x on html) breaks position: sticky elsewhere. The full-width
   display is already the full width of the page; the card spans the content
   column, which carries the same weight without the bug.
   --------------------------------------------------------------------------- */
.grit-cta-band {
	background: var(--grit-surface-dark);
	color: #fff;
	border-radius: var(--grit-radius-md);
	padding: clamp(2.5rem, 6vw, 4rem) clamp(1.5rem, 5vw, 4rem);
	margin-block: clamp(2rem, 5vw, 3.5rem);
}

.grit-cta-band,
.grit-cta-section {
	text-align: center;
}

/* The card's own navy is invisible to the contextual defaults — it is not a
   .grit-bg--* / .grit-tone--* class — so its headings are pinned here, and the
   text token is re-pointed to the on-dark grey the tone flip would have given
   it. Without that, a card sitting on a white section would draw its
   description in #5A6B7A on navy, at roughly 2.5:1. The section's own headings
   are not pinned: the dark tone flip already handles them, and pinning would
   break a band an editor sets to light grey.

   The same technique as the light-card-on-dark rule further up, mirrored: it is
   the surface that knows its own tone, so the surface re-points the tokens. */
.grit-cta-band {
	--grit-section-text: var(--grit-text-on-dark);
}

.grit-cta-band :is(h2, h3) {
	color: #fff;
}

.grit-cta-band :is(h2, h3),
.grit-cta-section :is(h2, h3) {
	max-width: 34rem;
	margin-inline: auto;
}

/* …and the per-section opt-out of it. A short statement reads better broken over
   two or three lines, which is what 34rem buys; a longer one — or a band an
   editor wants to fill — should be allowed to use the width it is given.
   `margin-inline: auto` above becomes a no-op once there is no cap to centre
   against, so it does not need undoing.

   The modifier is on the band's own element rather than an ancestor, so this is
   (0,2,1) against the (0,1,1) above and wins on specificity rather than on
   source order — the rules above it are free to move.

   Both surfaces, because the choice is about the heading and not about which
   display the section is in. */
.grit-cta-band.grit-cta--wide-heading :is(h2, h3),
.grit-cta-section.grit-cta--wide-heading :is(h2, h3) {
	max-width: none;
}

/* Text alignment, per section, on both surfaces. Centred is what the band has
   always been and carries no class, so an existing row is untouched by all of
   this; left and right are the two opt-ins.

   EACH ONE MOVES THE MEASURES TOO, which is why this is more than one
   declaration. The heading is capped at 34rem and the description at 60rem, both
   centred with `margin-inline: auto` — so `text-align` alone would left-align the
   words inside a column still sitting in the middle of the band, which reads as a
   mistake rather than as a choice. The same thing the text block's alignment does
   with its column, for the same reason.

   The button needs answering separately because it is not text: it is centred by
   `justify-content` on the flex row that wraps it, not by `text-align`, so it
   would stay in the middle of a left-aligned band.

   Every selector here pairs the modifier with the band's own class, which puts it
   at (0,2,1) against the capped heading's (0,1,1) and (0,3,0) against the lede's
   and the buttons' (0,1,0) / (0,2,0) — so these win on specificity rather than on
   sitting later in the file, and the rules they override are free to move. */
.grit-cta-band.grit-cta--align-left,
.grit-cta-section.grit-cta--align-left {
	text-align: left;
}

.grit-cta-band.grit-cta--align-right,
.grit-cta-section.grit-cta--align-right {
	text-align: right;
}

.grit-cta-band.grit-cta--align-left :is(h2, h3),
.grit-cta-section.grit-cta--align-left :is(h2, h3),
.grit-cta-band.grit-cta--align-left .grit-cta__lede,
.grit-cta-section.grit-cta--align-left .grit-cta__lede {
	margin-inline: 0 auto;
}

.grit-cta-band.grit-cta--align-right :is(h2, h3),
.grit-cta-section.grit-cta--align-right :is(h2, h3),
.grit-cta-band.grit-cta--align-right .grit-cta__lede,
.grit-cta-section.grit-cta--align-right .grit-cta__lede {
	margin-inline: auto 0;
}

.grit-cta-band.grit-cta--align-left .wp-block-buttons,
.grit-cta-section.grit-cta--align-left .wp-block-buttons {
	justify-content: flex-start;
}

.grit-cta-band.grit-cta--align-right .wp-block-buttons,
.grit-cta-section.grit-cta--align-right .wp-block-buttons {
	justify-content: flex-end;
}

/* The optional description under the heading. Wider than the heading's measure:
   a heading breaks well at 34rem, and a paragraph of prose under it reads better
   with considerably more room than that. */
.grit-cta__lede {
	max-width: 60rem;
	margin-inline: auto;
	font-size: var(--grit-text-body-lg);
}

/* The default colour, and ONLY when nothing is pinned.

   It has to be conditional because of source order: .grit-tc--* is declared up
   with the tokens, hundreds of lines above every component, and both selectors
   are one class — so a component rule setting `color` here would silently beat
   the class the editor's own colour choice writes onto the same element. The
   :not() keeps this rule out of that fight entirely rather than escalating it.

   The token, not a literal: #5A6B7A on a light band, #B8C4CE on a dark one, and
   .grit-cta-band re-points it for the always-navy card. */
.grit-cta__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

.grit-cta__lede > :last-child {
	margin-bottom: 0;
}

.grit-cta-band > :last-child,
.grit-cta-section .grit-wrap > :last-child {
	margin-bottom: 0;
}

.grit-cta-band .wp-block-buttons,
.grit-cta-section .wp-block-buttons {
	justify-content: center;
	margin-bottom: 0;
}

/* Below the split the CTA takes the whole width, so a thumb does not have to aim
   for a label-width target in the middle of a band. Same three lines
   .grit-hero__actions already uses, so the page's two calls to action behave
   alike instead of one being full width and the other not. */
@media (max-width: 45em) {
	.grit-cta-band .wp-block-buttons,
	.grit-cta-section .wp-block-buttons {
		flex-direction: column;
		align-items: stretch;
	}

	.grit-cta-band .wp-block-button,
	.grit-cta-band .wp-block-button > .grit-btn,
	.grit-cta-band .wp-block-button > .wp-block-button__link,
	.grit-cta-section .wp-block-button,
	.grit-cta-section .wp-block-button > .grit-btn,
	.grit-cta-section .wp-block-button > .wp-block-button__link {
		width: 100%;
	}
}

/* ---------------------------------------------------------------------------
   Social band — heading left, outlined pill links right

   The buttons need nothing here. .grit-btn--secondary is the outlined tier,
   --pill rounds it, and the dark-context rule up with the buttons turns the
   outline white — which is exactly why the renderer defaults the section's
   background to navy rather than painting a dark background of its own: that
   rule is a descendant selector, and a bespoke dark class would leave a navy
   outline on navy.

   Not a modifier of .grit-section__bar, the theme's other heading-left row. That
   one assumes exactly two children, so three buttons would be spread across the
   row by space-between instead of grouped; and it has no last-child margin
   reset, so the lede's trailing paragraph margin would land on the row's gap.
   --------------------------------------------------------------------------- */
.grit-social-band__row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: clamp(1.25rem, 3vw, 2.5rem);
}

/* Holds the heading and its lede together as one flex item, so the buttons face
   the pair rather than the row distributing three separate things. */
.grit-social-band__intro {
	flex: 1 1 22rem;
}

.grit-social-band__intro > :last-child {
	margin-bottom: 0;
}

.grit-social-band__intro h2 {
	margin-bottom: 0.35rem;
}

.grit-social-band__lede {
	font-size: var(--grit-text-body);
}

/* Only when nothing is pinned — .grit-tc--* is declared hundreds of lines above
   every component and is also one class, so an unconditional colour here would
   quietly beat the editor's own choice. Same guard as .grit-cta__lede. */
.grit-social-band__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* Wraps as a group. flex-start rather than the row's centre, so a wrapped second
   line of buttons lines up under the first. */
.grit-social-band__links {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: flex-start;
	gap: 0.75rem;
}

/* The icon variant: the same outlined pill, collapsed to a circle around a mark.

   Equal width and height rather than padding, because padding around a 24-unit
   glyph gives a slightly different circle per platform — the X mark is wider
   than the LinkedIn one — and a row of not-quite-round buttons reads as sloppy
   rather than as variety.

   The label is not drawn here; it is on the anchor as aria-label. Nothing about
   the outline or the hover sweep changes, so the two kinds of button in one row
   still read as one set. */
.grit-social-band__icon {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.75rem;
	height: 2.75rem;
	padding: 0;
}

.grit-social-icon {
	width: 1.15rem;
	height: 1.15rem;
	/* The mark is the button's whole content, so it must not sit on a text
	   baseline that leaves it a pixel low inside its circle. */
	display: block;
}

/* ---------------------------------------------------------------------------
   Newsletter signup

   The form markup belongs to whichever provider is installed — ActiveCampaign
   through its plugin, and Mailchimp still in the footer — so every rule here
   matches on ELEMENT inside the scope rather than on any one plugin's class
   names. That way one set of rules dresses all of them, and none of it depends on
   markup this theme does not control.

   The exception worth knowing: ActiveCampaign's JavaScript embed ships its own
   stylesheet and inline styles, which these cannot reliably outrank. Its plugin
   shortcode, or a "raw HTML" form from ActiveCampaign, both give markup that can
   be styled.
   --------------------------------------------------------------------------- */
.grit-newsletter {
	max-width: 44rem;
	margin-inline: auto;
	text-align: center;
}

.grit-newsletter__lede {
	color: var(--grit-section-text);
	margin-bottom: clamp(1.25rem, 3vw, 1.75rem);
}

.grit-newsletter__lede > :last-child {
	margin-bottom: 0;
}

/* One row, centred, wrapping before it ever squashes the field.

   The width and max-width are load-bearing. The provider's <form> is a flex item
   of this wrapper, and a flex item sizes to its content — which for a nested
   wrapping flex container Chrome resolves to one item's width, collapsing the row
   to the input's intrinsic size and pushing the button onto a second line. Giving
   the form a definite width takes the guesswork out of it. */
/* Flex on the wrapper too, so a provider that emits bare inputs with no <form>
   around them still gets the row. */
.grit-newsletter__form {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 0.75rem;
}

.grit-newsletter__form form {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 0.75rem;
	width: 100%;
	max-width: 32rem;
	/* Providers ship their own padding on the <form> (Mailchimp's embed is the
	   worst offender). It offsets the row from the centred wrapper and eats into
	   the 32rem, so it goes. */
	padding: 0;
}

/* Plugins wrap fields in their own paragraphs and divs. Letting those be flex
   items too keeps the field and the button on one row whatever the nesting. */
.grit-newsletter__form form > p,
.grit-newsletter__form form > div {
	display: contents;
}

.grit-newsletter__form label {
	/* Providers ship a visible label above the field, which the design does not
	   have room for. Hidden rather than removed: the field still needs a name for
	   anyone not reading the placeholder. */
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* Overrides the shared field rule's width: 100% and its label-gap margin, both
   of which are written for a field stacked under a visible label. */
.grit-newsletter__form input[type="email"],
.grit-newsletter__form input[type="text"] {
	flex: 1 1 14rem;
	width: auto;
	min-width: 0;
	margin: 0;
}

.grit-newsletter__form input[type="submit"],
.grit-newsletter__form button {
	flex: 0 0 auto;
	margin: 0;
}

/* Below the split the field and the button each take the whole width, the same
   trade the hero and the CTA band make — a half-width email field is not a thing
   anyone wants to type into on a phone. */
@media (max-width: 45em) {
	.grit-newsletter__form,
	.grit-newsletter__form form {
		flex-direction: column;
		align-items: stretch;
	}

	/* flex: 0 0 auto is not tidying up. Turning the row into a column flips which
	   axis flex-basis sizes, so the field's 14rem basis stopped being a minimum
	   width and became a 224px-tall email box. */
	.grit-newsletter__form input[type="email"],
	.grit-newsletter__form input[type="text"],
	.grit-newsletter__form input[type="submit"],
	.grit-newsletter__form button {
		flex: 0 0 auto;
		width: 100%;
		max-width: none;
	}
}

/* ---------------------------------------------------------------------------
   Stat row — the 35% / 9 in 10 / 6% band
   --------------------------------------------------------------------------- */
.grit-stats {
	display: grid;
	gap: clamp(1.25rem, 3vw, 2.5rem);
	/* Explicit columns rather than auto-fit: with three figures, auto-fit leaves a
	   collapsed track and the row sits off-centre. */
	grid-template-columns: 1fr;
	padding: clamp(1.75rem, 4vw, 2.75rem);
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
}

/* 55rem = 880px. Later than the theme's usual 44rem split: side by side, three
   across, a 64px figure and a caption all have to fit in one third of the row,
   and below this the caption wraps to three or four lines and reads worse than
   the stacked arrangement it replaced. */
@media (min-width: 55rem) {
	.grit-stats {
		grid-template-columns: repeat(3, 1fr);
	}
}

/* Mobile: figure above caption. The desktop rule below turns this on its side.
   Flex rather than margins so one property flips the whole arrangement. */
.grit-stat {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 0.35rem;
	text-align: center;
}

/* Base (mobile) margin. Declared here, ABOVE the desktop media query below, so
   that query's `margin: auto` wins on desktop at equal specificity (0,0,1 vs
   0,0,1) purely by coming later in the file. Moving this block below the media
   query — where it previously sat — silently overrides `margin: auto` with
   `margin: 0` on every screen, which is exactly the bug this ordering avoids. */
.grit-stat__value {
	font-size: var(--grit-text-stat);
	font-weight: 700;
	line-height: 1.05;
	letter-spacing: -0.03em;
	color: var(--grit-primary);
	margin: 0;
}

.grit-stat__label {
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
	margin: 0;
}

/* Desktop: figure and caption side by side, baseline-aligned so the caption
   sits on the number's baseline rather than floating at its optical centre. */
@media (min-width: 55rem) {
	.grit-stat {
		flex-direction: row;
		align-items: baseline;
		gap: 0.75rem;
		text-align: left;
	}

	/* The figure is the fixed element; the caption takes the remaining width and
	   wraps. Without this the caption pushes the number narrow enough to wrap
	   mid-figure ("9 in" / "10"). */
	.grit-stat__value {
		flex: 0 0 auto;
		margin: auto;
	}

	.grit-stat__label {
		flex: 1 1 auto;
		margin: auto;
	}
}

/* The optional copy above the figures, and the optional button below them.

   Left with the grid rather than centred, because the figures themselves are
   left-aligned above 55rem and a centred heading over a left-aligned row reads
   as two sections that happen to touch. Same reasoning as .grit-value-cards and
   .grit-media-cards, which put their headings left for the same reason. */
.grit-stats__intro {
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-stats__intro > :last-child {
	margin-bottom: 0;
}

/* Only when nothing is pinned. .grit-tc--* is declared with the tokens hundreds
   of lines above every component and is also one class, so an unconditional
   colour here would quietly beat the editor's own choice. Same guard as
   .grit-cta__lede. */
.grit-stats__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* .wp-block-buttons already carries the 1.5rem margin-block that separates it
   from the grid, so this only has to undo the bottom half of it: the section's
   own padding is what ends the section. */
.grit-stats__action {
	margin-bottom: 0;
}

/* Text alignment for the copy, per section. Left is what the section has always
   done and carries no class at all, so an existing row is untouched by all of
   this; centre and right are the two opt-ins.

   SCOPED TO THE COPY, never to the section. The class rides on the <section>
   because that is the one element every layout here already builds through
   gritfoundation_section_attrs(), but a bare `text-align` there would also reach
   .grit-stat — whose own centring and its 55rem flip to a baseline-aligned row
   are the display's decision, not the copy's. Two descendant selectors keep the
   figures out of it.

   The button needs answering separately because it is not text: .grit-stats__action
   is a flex row, so it is justify-content and not text-align that moves it. Same
   pair of properties the CTA band's alignment uses, for the same reason. */
.grit-stats--align-center .grit-stats__intro {
	text-align: center;
}

.grit-stats--align-right .grit-stats__intro {
	text-align: right;
}

.grit-stats--align-center .grit-stats__action {
	justify-content: center;
}

.grit-stats--align-right .grit-stats__action {
	justify-content: flex-end;
}

/* ---------------------------------------------------------------------------
   Stat row, the cards display

   A figure per card instead of one tinted band holding all of them.

   THE BAND'S OWN PANEL COMES OFF. .grit-stats paints a fill, rounds its corners
   and pads itself, which is exactly right when it is the surface — and wrong the
   moment the cards are, because a tinted panel behind tinted cards is two fills
   competing for one band and a second layer of padding inside the first.

   THE COLUMN COUNT COMES FROM THE MARKUP, not from the CSS guessing. The
   renderer emits .grit-stats--cols-N with min( 4, number of figures ), because
   neither of the two things CSS could do on its own is right here: a fixed
   repeat(4, 1fr) leaves a quarter of the row empty when there are three cards —
   the collapsed-track problem the row display is explicit to avoid — and
   auto-fit cannot be capped, so on a wide monitor it would draw six, .grit-wrap
   having no max-width to stop it.

   Two across from the split, four from 62rem — the two breakpoints this
   stylesheet already uses, rather than a third set of numbers. Four figures need
   more room than the row display's three, which is why the step up is later than
   its 55rem.
   --------------------------------------------------------------------------- */
.grit-stats--cards {
	padding: 0;
	background: none;
	border-radius: 0;
	grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 45.0625em) {
	.grit-stats--cards {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}

	/* One figure is one card at full width, not one card in a half-width track
	   with a hole beside it. */
	.grit-stats--cards.grit-stats--cols-1 {
		grid-template-columns: minmax(0, 1fr);
	}
}

@media (min-width: 62rem) {
	.grit-stats--cards.grit-stats--cols-3 {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}

	.grit-stats--cards.grit-stats--cols-4 {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.grit-stat--card {
	padding: clamp(1.25rem, 3vw, 1.75rem);
	border-radius: var(--grit-radius-md);
}

/* The fill, and ONLY when the editor has not chosen one. gf_card_color resolves
   through the section-background list, so a chosen colour arrives as
   .grit-bg--* — one class, declared with the palette utilities hundreds of lines
   above this, which an unconditional `background` here would silently beat. The
   :not() keeps this rule out of that fight rather than escalating it, the same
   guard .grit-cta__lede uses. */
.grit-stat--card:not([class*="grit-bg--"]) {
	background: var(--grit-bg-alt);
}

/* Figure above caption, at every width. The 55rem rule above turns the row
   display on its side; a card is a column, and a 64px figure with its caption
   beside it inside a quarter-width card would leave the caption two words wide.
   Scoped to the grid so it is (0,2,0) against that rule's (0,1,0) and wins on
   specificity, which keeps it working wherever it sits in the file. */
.grit-stats--cards .grit-stat {
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: 0.35rem;
	text-align: center;
}

/* And the same for the two children, whose `margin: auto` inside that media
   query would otherwise push the pair apart inside a card taller than its
   contents. */
.grit-stats--cards .grit-stat__value,
.grit-stats--cards .grit-stat__label {
	flex: 0 0 auto;
	margin: 0;
}

/* ---------------------------------------------------------------------------
   List tables — two colour-coded lists side by side

   The "What you need to host" comparison: a coloured bar naming each list, a
   pale body under it, a bulleted line per item.

   THE FILL IS ON THE PANEL AND THE BODY PAINTS BACK OVER IT. The bar is not an
   element with a background of its own — it is the part of the panel the body
   does not cover. That arrangement is what lets the bullets AND the panel's own
   button echo the fill: the .grit-bg--* class publishes its colour as
   --grit-fill (see the palette utilities above), which inherits down to a list
   and a button the bar could never have reached.

   The body is a flex column, so everything a panel may hold — an eyebrow, a
   heading, a description, the list, a button — is spaced by one gap and pins to
   the bottom with one auto margin.

   overflow: hidden rather than a radius on the bar, so the bar's top corners are
   clipped by the panel and the two cannot drift apart.
   --------------------------------------------------------------------------- */
.grit-list-tables {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: minmax(0, 1fr);
}

/* The column count comes from the markup, capped at two — the same explicit
   count the stat row's cards carry, and for the same reason: .grit-wrap has no
   max-width, so auto-fit would draw a track for every 20rem of a wide monitor
   and leave two panels at a third of the width with the rest collapsed. */
@media (min-width: 45.0625em) {
	.grit-list-tables--cols-2 {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

.grit-list-table {
	border-radius: var(--grit-radius-md);
	overflow: hidden;
	display: flex;
	flex-direction: column;
}

/* Navy when the editor has not chosen, and ONLY then. A chosen colour arrives
   as .grit-bg--* — one class, declared with the palette utilities hundreds of
   lines above this, which an unconditional `background` here would silently
   beat. The :not() keeps this rule out of that fight rather than escalating it,
   the same guard .grit-stat--card uses. --grit-fill comes with it, so a default
   panel's bullets are navy rather than falling through to the section's. */
.grit-list-table:not([class*="grit-bg--"]) {
	background: var(--grit-navy);
	--grit-fill: var(--grit-navy);
}

/* The bar. Small, uppercase and tracked — the theme's label treatment, the same
   one .grit-hero__details dt and the resource pills use. */
.grit-list-table__title {
	margin: 0;
	padding: clamp(0.85rem, 2vw, 1.1rem) clamp(1.25rem, 3vw, 1.75rem);
	font-size: var(--grit-text-label);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);

	/* THE BAR'S TEXT FOLLOWS ITS OWN FILL, NOT THE SECTION'S, and that is why
	   these two rules exist rather than the contextual default being left to it.
	   The default is a token the SECTION sets, so a light panel on a navy
	   section inherits white and a heading on the theme's navy default — which
	   carries no tone class, because it carries no colour class to carry one —
	   inherits navy. Both are the same colour twice, and invisible.

	   Three of the four cases land on white: navy, orange, and anything an admin
	   adds that resolves dark. The fourth is the one class this can key on. */
	color: #fff;
}

.grit-list-table.grit-tone--light .grit-list-table__title {
	color: var(--grit-navy);
}

/* The body paints over the rest of the panel. The hairline is not decoration:
   on a white section a white body would be invisible below the bar, and the
   panel's own fill is behind it rather than around it.

   IT IS A COLUMN, AND THE GAP IS THE ONLY THING SPACING ITS PARTS. A panel may
   hold an eyebrow, a heading, a description, the list and a button, and every
   one of those arrives with a margin of its own — five numbers that would have
   to be kept in agreement against one that cannot disagree with itself. With
   nothing in it but a list, which is every panel saved before those fields
   existed, a one-child flex column renders exactly as the block did. */
.grit-list-table__body {
	flex: 1 1 auto;
	background: var(--grit-bg);
	border: 1px solid var(--grit-section-border);
	border-top: 0;
	padding: clamp(1.25rem, 3vw, 1.75rem);
	display: flex;
	flex-direction: column;
	gap: clamp(0.75rem, 2vw, 1rem);
}

/* …which is what this drops. A heading brings 0.6em, an eyebrow 0.4rem, a
   paragraph its own bottom margin, and each would ADD to the gap above rather
   than replace it. Block margins only: the list keeps its padding-left, which is
   what the markers hang in. */
.grit-list-table__body > * {
	margin-block: 0;
}

/* A panel whose bar is empty — its heading may now live inside the body instead
   — has no bar to hide the missing top edge, and the panel's fill is BEHIND the
   body rather than around it, so border-top: 0 leaves nothing there at all.
   :only-child is precisely that case: the bar is a sibling the renderer omits,
   so its absence is structural and needs no class to announce it. */
.grit-list-table__body:only-child {
	border-top: 1px solid var(--grit-section-border);
}

/* The panel's own heading, inside the body rather than in the bar. Its TAG is
   the renderer's choice — h4 under a bar, h3 without one, so the outline never
   skips a level — and its size is this class either way, which is what lets the
   tag move without the design moving with it.

   700 rather than the 600 h3/h4 carry: it is the loudest thing in the panel and
   the design draws it that way. */
.grit-list-table__heading {
	font-size: var(--grit-text-h3);
	font-weight: 700;
}

/* The panel's own description. --grit-section-text is the contextual muted grey,
   re-pointed for this body by the light-surfaces-inside-dark block far above, so
   this is right on a light section and on a navy one without either being named
   here. */
.grit-list-table__text {
	color: var(--grit-section-text);
}

/* A paragraph inside the description is a child of __text, not of the column, so
   the margin reset above cannot reach it — and the prose helper wraps every
   paragraph, including the last. */
.grit-list-table__text > p:last-child {
	margin-bottom: 0;
}

.grit-list-table__list {
	margin: 0;
	padding-left: 1.15rem;
	display: grid;
	gap: clamp(0.6rem, 1.5vw, 0.9rem);
}

.grit-list-table__list li {
	color: var(--grit-section-text);
}

/* The bullet echoes the bar. --grit-fill is inherited from whichever
   .grit-bg--* class the panel carries, so this needs no per-colour rule and
   picks up an admin-added colour for free.

   The fallback is load-bearing: gritfoundation_palette_compile_css() writes the
   token, and a custom colour compiled before it did so carries none until the
   palette screen is saved again. currentColor is then the bullet, which is the
   markerless-but-legible answer rather than an invisible one. */
.grit-list-table__list li::marker {
	color: var(--grit-fill, currentColor);
}

/* …except when the fill is a pale one. The body is white, so a light-toned bar
   echoed onto a bullet is a light-grey dot on white — the echo working exactly
   as designed and producing nothing anyone can see. The tone class is the one
   thing that already knows which fills those are, so the bullet gives up the
   echo and takes the text's own colour. */
.grit-list-table.grit-tone--light .grit-list-table__list li::marker {
	color: currentColor;
}

/* The panel's own button, pinned to the bottom — margin-top: auto in the column
   above, the same way .grit-card__action pins a card's — so two panels of
   different lengths still line their buttons up. It has to come AFTER the margin
   reset, which is the same specificity. */
.grit-list-table__action {
	margin-top: auto;
	padding-top: clamp(0.25rem, 1vw, 0.5rem);
}

/* THE BUTTON ECHOES THE BAR, by exactly the mechanism the bullet uses:
   --grit-fill is the panel's own colour, published as a token by whichever
   .grit-bg--* class it carries and inherited down here. The renderer stores a
   slug and never knows the hex, so there is no way to write this colour into the
   markup — and no .grit-btn--* tier to reach for either, because the fill is
   whatever the editor picked, an admin-added colour included.

   Full width because the panel is the button's container and a short button in a
   wide pale body reads as unfinished — the design draws it edge to edge.

   The fallback is the primary orange rather than currentColor, which is what the
   bullet falls back to: a marker with no colour is still a marker, but a button
   with no background is not a button. It is reachable — a custom colour compiled
   before gritfoundation_palette_compile_css() published the token carries none
   until the palette screen is saved again. */
.grit-list-table__btn {
	display: block;
	width: 100%;
	background: var(--grit-fill, var(--grit-primary));
	color: #fff;
	/* filter joins the base rule's transition list, which cannot know about it. */
	transition:
		filter var(--grit-motion),
		background-color var(--grit-motion),
		color var(--grit-motion);
}

/* …except when the fill is a pale one, and for the reason the bullet gives up
   the echo there: the body is white, so a light-toned fill is a pale button with
   white text on it, which is the echo working as designed and producing a box
   nobody can read. Orange is the one button colour the design system guarantees
   on a light surface, so that is where a light panel's button lands. */
.grit-list-table.grit-tone--light .grit-list-table__btn {
	background: var(--grit-primary);
	color: #fff;
}

/* Hover is a darkening, not a colour. Every other tier can name its hover pair
   because it knows its own colour; this one is handed a token that may hold an
   admin-added hex, so there is no --*-hover to reach for. Brightness works on
   all of them, orange included, and opacity is spoken for: it is what the
   disabled state uses, and a button that fades on hover reads as one going
   away. */
.grit-list-table__btn:hover,
.grit-list-table__btn:focus-visible {
	filter: brightness(0.9);
	color: #fff;
}

/* Only when nothing is pinned — same guard as .grit-stats__lede. */
.grit-list-tables__lede:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* The closing sentence under the lists — a different job from the opening one
   above them, which is the whole reason it is a second field rather than a
   longer first one.

   .grit-value-cards__outro is the same component in another layout and this is
   deliberately its ruleset, clamp and guard included: the margin is on top
   because the grid above carries none below it and what follows is either
   .wp-block-buttons with its own 1.5rem or the section's padding, and the :not()
   is what keeps an editor's .grit-tc--* choice from being silently outranked by
   a rule of equal weight sitting later in the file.

   NO text-align, which is what makes it left: it is a sibling of
   .grit-section__head rather than a child, so the centring above the grid never
   reached it and nothing here has to undo it. A `left` declaration would be a
   rule with nothing to override — and the one to add if a page ever wants this
   centred, rather than one to remove. */
.grit-list-tables__outro {
	margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-list-tables__outro:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

.grit-list-tables__outro > :last-child {
	margin-bottom: 0;
}

/* .wp-block-buttons already carries the 1.5rem margin-block that separates it
   from the grid, so this only has to undo the bottom half of it, and centre it
   under an intro that .grit-section__head has already centred. */
.grit-list-tables__action {
	justify-content: center;
	margin-bottom: 0;
}

/* ---------------------------------------------------------------------------
   Core button block layout

   The button *treatment* lives in the Buttons section above, which covers
   .wp-block-button__link and .wp-block-file__button directly. Only layout and
   the file block's spacing belong here.

   is-style-outline is unregistered in functions.php: core's own rule for it is
   (0,4,0) against anything the theme can reasonably write, so it always won on
   border and padding and rendered a different size from the solid button. The
   theme's own secondary tier replaces it.
   --------------------------------------------------------------------------- */
.wp-block-file__button {
	margin-left: 0.75rem;
}

.wp-block-buttons {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem;
	margin-block: 1.5rem;
}

/* ---------------------------------------------------------------------------
   Partner logo strip
   --------------------------------------------------------------------------- */
.partner-strip {
	display: grid;
	gap: clamp(1.5rem, 4vw, 3rem);
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 9rem), 1fr));
	align-items: center;
	padding: 0;
	margin: 0;
	list-style: none;
}

.partner-strip li {
	display: grid;
	place-items: center;
	min-height: 5rem;
}

.partner-strip img {
	max-height: 3.75rem;
	width: auto;
	object-fit: contain;
	/* Logos arrive in many treatments; muting them keeps the strip calm. */
	filter: grayscale(100%);
	opacity: 0.75;
	transition: filter 0.2s ease, opacity 0.2s ease;
}

.partner-strip li:hover img {
	filter: none;
	opacity: 1;
}

/* ---------------------------------------------------------------------------
   Partner logos — the Sections layout, in two displays

   NOT .partner-strip above. That one is the grit_partner post type's strip on
   front-page.php and it is 9rem auto-fill tracks; this is the editor-composed
   section and it is an explicit column count. Two sources, two surfaces, two
   class families ON PURPOSE — they share the muted-until-hover treatment and
   nothing else, and one shared class would mean a change to either silently
   restyling the other with no call site to warn anyone.

   ONE ROW, TWO MODES. .grit-partners__row is the wrapping, centring line of
   cells the grid draws. The marquee adds nowrap, a doubled track and an
   animation on top of it, and prefers-reduced-motion takes all three back off —
   which is why the marquee row carries --cols-N as well. With the animation gone
   it IS the grid, and re-declaring a whole component inside a media query is how
   two layouts drift apart.
   --------------------------------------------------------------------------- */
.grit-partners {
	--grit-partners-gap: clamp(1rem, 3vw, 3rem);
	--grit-partners-cell: clamp(6rem, 22vw, 10rem);

	/* EVERY LOGO GETS THE SAME CARD, and the logo fills it. The card is the
	   constant, not the picture: one ratio, one fill, one radius, so a wide
	   wordmark and a square badge are the same object on the page and only their
	   ink differs. Capping the logo's height instead — which is what this used to
	   do — left each mark at its own natural size floating in a much larger cell,
	   about an eighth of it, and no two alike.

	   The ratio is the design's own tile, near 5:2. The min-height is a floor for
	   a narrow phone, where a third of 360px at 5:2 would be a 38px-tall card;
	   every cell in a row is the same width either way, so the cards stay
	   identical whichever of the two is deciding. */
	--grit-partners-card-ratio: 5 / 2;
	--grit-partners-card-min-h: 3.5rem;
}

/* The theme's heading-row-to-grid number, shared with .grit-section__bar,
   .grit-stats__intro and .grit-value-cards__intro, so the gap under a heading
   does not depend on which section it is in. */
.grit-partners__intro {
	margin-bottom: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-partners__intro > :last-child {
	margin-bottom: 0;
}

/* THE SECOND DESCRIPTION, under the logos rather than above them. The margin is
   on top for .grit-value-cards__outro's reason: the row above has none of its own
   below it, and what follows is the section's padding. */
.grit-partners__outro {
	margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

.grit-partners__outro > :last-child {
	margin-bottom: 0;
}

/* Only when nothing is pinned. .grit-tc--* is one class declared with the tokens
   hundreds of lines above every component, so an unconditional colour here would
   be a (0,1,0) selector later in the file beating the editor's own choice — and
   both description colour fields would silently do nothing. Same guard as
   .grit-value-cards__lede. */
.grit-partners__lede:not([class*="grit-tc--"]),
.grit-partners__outro:not([class*="grit-tc--"]) {
	color: var(--grit-section-text);
}

/* FLEX, NOT GRID, and the reason is the short last row. Grid puts seven logos in
   a six-track row and leaves the seventh alone at the left of row two;
   justify-content: center puts it in the middle, which is what a logo wall wants
   and what grid cannot do without being told how many are on the last row. */
.grit-partners__row {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: var(--grit-partners-gap);
	margin: 0;
	padding: 0;
	list-style: none;

	/* Three across until the split; --cols-N re-points this and the basis below
	   follows. Written as a custom property so the calc is resolved where it is
	   used — against the row — rather than once here. */
	--grit-partners-cols: 3;
	--grit-partners-basis: calc(
		(100% - (var(--grit-partners-cols) - 1) * var(--grit-partners-gap)) / var(--grit-partners-cols)
	);
}

.grit-partners__item {
	display: grid;
	flex: 0 0 var(--grit-partners-basis);
}

/* THE CARD, and the whole reason the logo is not floating in space. It carries
   the ratio, the fill and the padding; the <li> around it carries only the
   column width, and in the marquee the spacing between cells as well. Keeping
   those on separate elements is what lets the marquee space its cells with
   padding — which it must, or the 50% translate lands half a gap out — without
   that padding being painted as part of the card.

   The fill and radius are the theme's card pair, the same two .grit-value-card
   and .grit-media-card use, so a partner tile is recognisably the same object as
   every other card on the site. */
.grit-partners__card {
	display: grid;
	place-items: center;

	/* SIZED TO THE COLUMN, and it has to be said rather than left to `stretch`.
	   The <li> is a grid container and the card is its only item, so the card
	   should stretch to the column — but a grid item's automatic minimum size is
	   `auto`, and here that resolves through the logo to something wider than a
	   narrow column. Three across on a 360px phone gave a 96px <li> holding a
	   140px card, which overflowed the column by nearly half and pushed the row
	   down to two per line.
	   It went unseen because the linked card was already correct by accident —
	   .grit-partners__link carries width: 100% — so a row of plain cards was
	   uniformly wrong and only a row mixing the two showed two different sizes. */
	width: 100%;
	min-width: 0;

	aspect-ratio: var(--grit-partners-card-ratio);
	min-height: var(--grit-partners-card-min-h);
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
	/* LOAD-BEARING with object-fit: cover below, not tidying. Cover scales the
	   mark until it covers the card, so the overflowing axis genuinely hangs
	   outside — and with no padding it hangs over the card's rounded corners as
	   well. This is what clips both. */
	overflow: hidden;
}

/* THE ONE BACKGROUND THE CARD GREY COLLIDES WITH. `Light grey` in the section
   palette is #EEF0F4, which is exactly --grit-bg-alt, so on that background the
   card is invisible. A covered card is fully painted by its mark, so this only
   shows in the moment before the images arrive and wherever one never does — but
   those are exactly the states where a row of tiles should still read as a row of
   tiles rather than as nothing at all.

   Scoped to the fill class and not to .grit-tone--light, because White is
   light-toned too and a white card on a white section would be the same
   disappearance one background over. */
.grit-bg--light .grit-partners__card {
	background: var(--grit-bg);
}

/* THE COUNT IS A NUMBER AND THE BASIS IS ONE FORMULA, rather than a flex-basis
   per column count. Three reasons, and the third is the one that was a bug:

   - adding a column is a number, not another calc to get the gap arithmetic
     right in;
   - the marquee's reduced-motion fallback re-uses the SAME formula, so it cannot
     lay out differently from the grid it is falling back to;
   - and that fallback is where a per-count rule went wrong. `.--marquee .item`
     and `.--cols-6 .item` are both two classes, so the later one in this file
     wins — which meant the reduced-motion override, written last, pinned the
     marquee to three across at every width while the real grid beside it went to
     six. Reading a variable has no such fight to lose.

   ONE AND TWO LOGOS ARE NOT A GRID OF THREE: a lone logo in a one-third track
   sits off at the left of the page with two empty tracks beside it. Given the
   whole row it centres instead, and the height cap further down is what stops it
   becoming a billboard — the picture never stretches, only the slot it is centred
   in changes size. Three is the phone default, so --cols-3 needs no entry here
   and neither do the counts above it: they are three across until the split. */
.grit-partners__row--cols-1 {
	--grit-partners-cols: 1;
}

.grit-partners__row--cols-2 {
	--grit-partners-cols: 2;
}

@media (min-width: 45.0625em) {
	/* The count the renderer measured, capped at six. Seven or more carry
	   --cols-6 and wrap, and the last row centres itself. The 45.0625em is the
	   other side of the 45em the rest of this theme stacks at, so the two
	   breakpoints cannot both apply to the same pixel. */
	.grit-partners__row--cols-4 {
		--grit-partners-cols: 4;
	}

	.grit-partners__row--cols-5 {
		--grit-partners-cols: 5;
	}

	.grit-partners__row--cols-6 {
		--grit-partners-cols: 6;
	}

	/* .grit-wrap has no max-width, so on a wide monitor a two-logo row would fling
	   one mark to each edge of the screen with a metre of nothing between them. A
	   cap on the ROW rather than on the cell keeps the short sets clustered in the
	   middle and leaves four or more alone. */
	.grit-partners__row--cols-1 {
		max-width: 22rem;
		margin-inline: auto;
	}

	.grit-partners__row--cols-2 {
		max-width: 40rem;
		margin-inline: auto;
	}

	.grit-partners__row--cols-3 {
		max-width: 58rem;
		margin-inline: auto;
	}
}

.grit-partners__logo {
	/* FILLS THE CARD, which max-width/max-height could not do. Those only ever
	   cap: an image smaller than its slot renders at its own natural size, so a
	   150px mark sat at 150px in a 253px-wide cell and every logo came out a
	   different size. Sizing the element to the card and letting object-fit place
	   the pixels inside means the card is always full and the mark is always as
	   large as its own shape allows.

	   `cover`, so the mark fills the card edge to edge with no margin and no
	   letterboxing: every tile is the same size AND the same fill, which is what
	   a uniform wall of partners wants.

	   THE COST IS REAL AND IS THE EDITOR'S TO MANAGE. Cover crops whichever axis
	   overflows — the ends off a mark wider than the card, the top and bottom off
	   one taller — so a file has to be supplied at roughly the card's own ratio or
	   it loses ink. The field instruction says so, because no CSS can decide which
	   part of a logo is expendable.

	   min-width/min-height: 0 IS LOAD-BEARING, not tidying. A grid item's default
	   `min-height: auto` is its automatic minimum size, and for a replaced element
	   that is the height its own aspect ratio asks for — so a 300x300 mark in a
	   221px-wide slot claimed a 221px floor and `height: 100%` lost to it. The
	   symptom was square logos standing 120px proud of a 101px card, which reads
	   as the card being wrong rather than the image. */
	width: 100%;
	height: 100%;
	min-width: 0;
	min-height: 0;
	object-fit: cover;
	/* Logos arrive in many treatments; muting them keeps the wall calm. The same
	   two values .partner-strip uses, so the two surfaces agree on what a partner
	   logo looks like. */
	filter: grayscale(100%);
	opacity: 0.75;
	transition: filter var(--grit-motion-sweep), opacity var(--grit-motion-sweep);
}

.grit-partners__item:hover .grit-partners__logo,
.grit-partners__link:focus-visible .grit-partners__logo {
	filter: none;
	opacity: 1;
}

/* A touch screen never hovers, so there the muted state would be the ONLY state
   and every logo would be permanently grey. The reveal is the point of the mute;
   with no way to trigger it there is nothing to reveal, so colour is the default
   instead. */
@media (hover: none) {
	.grit-partners__logo {
		filter: none;
		opacity: 1;
	}
}

/* ---------------------------------------------------------------------------
   The marquee — the only animation in this stylesheet

   There were no @keyframes in this file before this block, and that was not an
   accident: nothing else on the site moves on its own. This does, so it carries
   the whole cost of being the first, and the reduced-motion branch at the end is
   not decoration — it is what keeps the rest of the file's promise true.

   HOW THE SEAM IS HIDDEN. The row holds the set twice over and slides left by
   exactly half its own width, so the frame after the last cell is the first cell
   again.

   WHICH IS WHY THERE IS NO gap HERE. A row of 2n cells has 2n-1 gaps, so half of
   it is n-1 gaps and a 50% translate lands one half-gap out — the strip visibly
   stutters once per cycle, and that is an unpleasant thing to trace back to a CSS
   shorthand. The gap becomes padding inside each cell instead, which makes the
   row exactly 2n × (cell + gap) wide and 50% of it exactly one half.

   WHY THE SET IS REPEATED MORE THAN TWICE. Sliding by half only looks continuous
   while that half is wider than the viewport: six 13rem cells is ~1250px, and on
   a 2560px monitor a third of every cycle would be empty track. The renderer
   repeats the set until a half holds eighteen cells — see
   gritfoundation_partner_marquee() — which is ~3700px and clear of an ultrawide.

   WHY THE DURATION IS A MULTIPLICATION. Duration is per track, so one fixed value
   would slide forty logos three times faster than fourteen. Constant speed means
   duration proportional to length, and the renderer already counted the cells, so
   it passes the count and this multiplies. That inline custom property is the
   only one in the theme and it earns itself: the alternative is a class per
   length bucket, which makes the speed approximately constant instead of exactly.
   --------------------------------------------------------------------------- */
.grit-partners__viewport {
	position: relative;
	overflow: hidden;
	/* Fades the leading and trailing edges into whatever the section is painted
	   with. A mask rather than a gradient overlay, because an overlay would have
	   to know that fill and the renderer only ever knows the slug. No mask support
	   means hard edges, which is a downgrade and not a break. */
	-webkit-mask-image: linear-gradient(to right, transparent, #000 var(--grit-gutter), #000 calc(100% - var(--grit-gutter)), transparent);
	mask-image: linear-gradient(to right, transparent, #000 var(--grit-gutter), #000 calc(100% - var(--grit-gutter)), transparent);
}

.grit-partners__row--marquee {
	flex-wrap: nowrap;
	justify-content: flex-start;
	width: max-content;
	max-width: none;
	margin-inline: 0;
	gap: 0;
	/* ~3.5s per cell in every strip, so the speed does not depend on the count. */
	animation: grit-partners-slide calc(var(--grit-partners-cells, 18) * 3.5s) linear infinite;
	will-change: transform;
}

.grit-partners__row--marquee .grit-partners__item {
	flex: 0 0 auto;
	width: calc(var(--grit-partners-cell) + var(--grit-partners-gap));
	padding-inline: calc(var(--grit-partners-gap) / 2);
}

@keyframes grit-partners-slide {
	from {
		transform: translateX(0);
	}

	to {
		transform: translateX(-50%);
	}
}

/* Stops while it is being read. :focus-within covers the keyboard, which :hover
   cannot — a linked logo tabbed to inside a moving strip is a target that walks
   away from the pointer. The attribute is the pause button's, and it is on the
   same element so the three states share one rule rather than fighting. */
.grit-partners__viewport:hover .grit-partners__row--marquee,
.grit-partners__viewport:focus-within .grit-partners__row--marquee,
.grit-partners__viewport[data-grit-partners-paused] .grit-partners__row--marquee {
	animation-play-state: paused;
}

/* Injected by assets/partner-marquee.js, so it exists only where it works. It
   sits inside the viewport's own padding rather than over a logo, and it is the
   carousel arrow's shape because it is the same kind of control on the same kind
   of strip. */
.grit-partners__pause {
	position: absolute;
	right: 0.5rem;
	bottom: 0.5rem;
	display: grid;
	place-items: center;
	width: 2rem;
	height: 2rem;
	padding: 0;
	font-size: 0.75rem;
	line-height: 1;
	color: var(--grit-navy);
	background: #fff;
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-full);
	cursor: pointer;
	opacity: 0.55;
	transition: opacity var(--grit-motion);
}

.grit-partners__viewport:hover .grit-partners__pause,
.grit-partners__pause:focus-visible,
.grit-partners__pause[aria-pressed="true"] {
	opacity: 1;
}

/* ---------------------------------------------------------------------------
   Reduced motion — the marquee becomes the grid

   `animation: none`, and not merely a shorter duration. The blanket rule further
   down this file sets `animation-duration: 0.01ms !important` on everything,
   which is the right answer for a one-shot transition and exactly the wrong one
   here: this animation is `infinite`, so a 0.01ms duration does not stop it, it
   runs it a hundred thousand times a second. The result is a juddering blur —
   strictly worse than the motion it was meant to remove. Clearing the animation
   NAME is what actually stops it, and an important duration cannot undo that.

   The duplicate copies go with it. They exist only to hide the loop's seam, and
   with nothing looping they would be the same eight partners printed five times
   over. They are already aria-hidden for that reason, so the attribute is what
   selects them.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
	.grit-partners__viewport {
		overflow: visible;
		padding-inline: var(--grit-gutter);
		-webkit-mask-image: none;
		mask-image: none;
	}

	.grit-partners__row--marquee {
		flex-wrap: wrap;
		justify-content: center;
		width: auto;
		gap: var(--grit-partners-gap);
		animation: none;
		will-change: auto;
	}

	.grit-partners__row--marquee .grit-partners__item {
		width: auto;
		padding-inline: 0;
		/* The grid's own basis, formula and all, so the fallback cannot lay out
		   differently from the grid it is falling back to. --cols-N is on this
		   same row and has already chosen the count. */
		flex: 0 0 var(--grit-partners-basis);
	}

	.grit-partners__item[aria-hidden="true"] {
		display: none;
	}
}

/* ---------------------------------------------------------------------------
   Host grid + single host
   --------------------------------------------------------------------------- */
.host-grid {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
	padding: 0;
	margin: 0;
	list-style: none;
}

.host-card {
	display: flex;
	flex-direction: column;
	background: #fff;
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-md);
	overflow: hidden;
	box-shadow: var(--grit-shadow);
	transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.host-card:hover {
	transform: translateY(-2px);
	box-shadow: 0 6px 20px rgb(0 22 42 / 12%);
}

.host-card__logo {
	display: grid;
	place-items: center;
	padding: 1.5rem;
	min-height: 9rem;
	background: var(--grit-bg-alt);
}

.host-card__logo img {
	max-height: 6.5rem;
	width: auto;
	object-fit: contain;
}

.host-card__body {
	display: flex;
	flex-direction: column;
	gap: 0.3rem;
	flex: 1;
	padding: 1.1rem 1.25rem 1.35rem;
}

/* Four across on wide screens, matching the Wix host grid. */
.host-grid--quad {
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
}

@media (min-width: 62rem) {
	.host-grid--quad {
		grid-template-columns: repeat(4, 1fr);
	}
}

.host-grid + h2 {
	margin-top: clamp(2.5rem, 6vw, 4rem);
}

.host-card__title {
	font-size: 1rem;
	margin: 0 0 0.25rem;
	text-align: center;
}

.host-card__title a {
	color: var(--grit-navy);
	text-decoration: none;
}

.host-card__title a:hover,
.host-card__title a:focus-visible {
	color: var(--grit-primary);
}

.host-card__meta {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	gap: 0.3rem;
	font-size: var(--grit-text-label);
	color: var(--grit-text-muted);
}

.host-card__meta li {
	display: grid;
	grid-template-columns: 1.1rem 1fr;
	gap: 0.4rem;
	align-items: start;
}

.host-card__meta a {
	color: inherit;
	overflow-wrap: anywhere;
}

.grit-icon {
	color: var(--grit-text-muted);
	margin-top: 0.22rem;
}

.host-card__cta {
	align-self: center;
	margin-top: 1rem;
}

/* Upcoming camps aren't linked yet. This used to set only cursor: default, so
   it looked identical to a live button; it is a real disabled state now and
   picks up the shared treatment from .grit-btn--disabled. */
.host-card__cta--soon {
	cursor: default;
}

/* ---------------------------------------------------------------------------
   Host tiles — the `cards` display of the camp hosts section

   The three-up tile from the original design: pill, name, location, a text link.
   It was deleted when the section was rebuilt as rows, and is restored verbatim
   here because it is the same reviewed design and the section now offers both
   arrangements rather than one replacing the other. Which one a section uses is
   the gf_display toggle; rows is the default.

   Bordered rather than shadowed, unlike .grit-card: these sit on the page
   background by default, where a shadow would be the only thing separating white
   from white and would read as a lift the tile has not earned.
   --------------------------------------------------------------------------- */
.host-tiles {
	display: grid;
	gap: clamp(1rem, 2.5vw, 1.75rem);
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
	padding: 0;
	margin: 0;
	list-style: none;
}

/* Bordered rather than shadowed: these sit on the page background by default,
   where a shadow would be the only thing separating white from white and would
   read as a lift the tile has not earned. */
.host-tile {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 0.5rem;
	background: var(--grit-bg);
	border: 1px solid var(--grit-section-border);
	border-radius: var(--grit-radius-md);
	padding: clamp(1rem, 2.5vw, 1.35rem);
}

.host-tile__title {
	font-size: var(--grit-text-body-lg);
	margin: 0;
}

.host-tile__title a {
	color: var(--grit-section-heading);
	text-decoration: none;
}

.host-tile__title a:hover,
.host-tile__title a:focus-visible {
	color: var(--grit-primary);
}

.host-tile__meta {
	margin: 0;
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
}

.host-tile__action {
	margin: 0;
}

/* Sentence case, unlike the uppercase tertiary button the section heading row
   uses — the design draws the two links differently, so they are two things.
   The arrow is the tertiary tier's, which keeps one arrow in the theme. */
.host-tile__link,
.grit-resource-card__link {
	font-size: var(--grit-text-small);
	font-weight: 600;
	color: var(--grit-primary);
	text-decoration: none;
}

/* Scoped to anchors, the way the tertiary tier's arrow is: an arrow on the
   "Coming soon" <span> would promise a page that does not exist yet. */
a.host-tile__link::after,
a.grit-resource-card__link::after {
	content: "\2009\2192";
}

a.host-tile__link:hover,
a.host-tile__link:focus-visible,
a.grit-resource-card__link:hover,
a.grit-resource-card__link:focus-visible {
	color: var(--grit-primary-hover);
	text-decoration: underline;
}

/* Upcoming camps have no highlights page yet. A <span> can never match
   :disabled, so the state is carried by the attribute and this modifier — the
   same arrangement as .host-card__cta--soon. */
.host-tile__link--soon {
	color: var(--grit-text-muted);
	cursor: default;
}

.host-tile__foot {
	width: 100%;
	display: flex;
	flex-direction: column;
	gap: 0.35rem;
}

/* Below the theme's split the tile puts its meta and link on one line, the way
   the mobile design shows it, rather than spending two lines on a location and
   a two-word link. */
@media (max-width: 45em) {
	/* Inline flow, not a wrapping flex row. A flex row that does not fit puts
	   the link on a line of its own *after* the separator, leaving a "·"
	   stranded at the start of it; inline flow breaks the pair like a sentence
	   instead. (Flex items are blockified, so the container has to give up
	   display: flex for the children's display: inline to mean anything.) */
	.host-tile__foot {
		display: block;
	}

	.host-tile__meta,
	.host-tile__action {
		display: inline;
	}

	/* Generated, and only when there is something after it to separate from.
	   The leading character is a non-breaking space, which glues the separator
	   to the end of the meta line so a wrap can never orphan it. */
	.host-tile__meta:not(:last-child)::after {
		content: "\00a0·";
	}
}

/* ---------------------------------------------------------------------------
   Host rows — the list used by the camp hosts section

   Distinct from .host-card, which is the archive's card: a logo panel above icon
   meta rows above a solid button, three or four across. This is one bordered
   container of full-width rows, so a page section can list a few hosts compactly
   without turning into a second archive.

   The container paints white on both its allowed backgrounds. On the default
   (white) section the border is what separates it; on a light-grey one the fill
   does. One rule covers both rather than a modifier per background.
   --------------------------------------------------------------------------- */
.host-rows {
	padding: 0;
	margin: 0;
	list-style: none;
	background: var(--grit-bg);
	border: 1px solid var(--grit-section-border);
	border-radius: var(--grit-radius-md);
	/* So the first and last rows are clipped by the container's radius instead of
	   squaring off its corners from the inside. */
	overflow: hidden;
}

.host-row {
	display: grid;
	grid-template-columns: 1fr auto;
	align-items: center;
	gap: 0.35rem clamp(0.85rem, 2.5vw, 1.5rem);
	padding: clamp(0.85rem, 2vw, 1.15rem) clamp(1rem, 2.5vw, 1.35rem);
}

/* The logo rail only exists when the renderer found at least one logo in the
   set, so a run of hosts with no featured images gets no empty column. */
.host-rows--logos .host-row {
	grid-template-columns: auto 1fr auto;
}

/* Dividers between rows, never above the first or below the last — a border on
   every row would double up against the container's own. */
.host-row + .host-row {
	border-top: 1px solid var(--grit-section-border);
}

/* A well rather than a bare image: host logos arrive on every kind of
   background, and a consistent tinted box keeps a column of them calm. It holds
   its size when empty so one logo-less host does not knock the column out of
   alignment. */
.host-row__logo {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: clamp(3.75rem, 12vw, 5.5rem);
	aspect-ratio: 3 / 2;
	padding: 0.35rem;
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-sm);
}

.host-row__logo img {
	max-width: 100%;
	max-height: 100%;
	width: auto;
	object-fit: contain;
}

.host-row__body {
	min-width: 0;
}

/* inline-flex wraps the pill onto the title's own line if the name is long,
   instead of letting it hang past the meta column. */
.host-row__title {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem;
	font-size: var(--grit-text-body-lg);
	margin: 0 0 0.15rem;
}

.host-row__title a {
	color: var(--grit-section-heading);
	text-decoration: none;
}

.host-row__title a:hover,
.host-row__title a:focus-visible {
	color: var(--grit-primary);
}

.host-row__meta {
	margin: 0;
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
}

/* The website reads as part of the line rather than as a link, matching the
   design — the affordance is the hover, the same bargain .host-card__meta makes. */
.host-row__meta a {
	color: inherit;
	text-decoration: none;
	overflow-wrap: anywhere;
}

.host-row__meta a:hover,
.host-row__meta a:focus-visible {
	color: var(--grit-primary);
	text-decoration: underline;
}

/* Separators come from CSS, not from a joined string, so a host missing a date
   or an address never leaves a stray "·" behind. The leading character is a
   non-breaking space, gluing the separator to the item it follows so a wrap
   cannot orphan it at the start of the next line. */
.host-row__meta > :not(:last-child)::after {
	content: "\00a0·\00a0";
	color: var(--grit-section-text);
}

.host-row__action {
	margin: 0;
}

/* Sentence case, unlike the uppercase tertiary button the section heading row
   uses — the design draws the two links differently, so they are two things.
   The arrow is the tertiary tier's, which keeps one arrow in the theme. */
.host-row__link {
	font-size: var(--grit-text-small);
	font-weight: 600;
	color: var(--grit-primary);
	text-decoration: none;
	white-space: nowrap;
}

.host-row__link::after {
	content: "\2009\2192";
}

.host-row__link:hover,
.host-row__link:focus-visible {
	color: var(--grit-primary-hover);
	text-decoration: underline;
}

/* Upcoming hosts get the uppercase treatment the design draws for them. It is
   still a real link — an upcoming host has a page carrying its date and
   location, unlike the archive's card, which has nowhere to send anyone. */
.host-row__link--soon {
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	font-size: var(--grit-text-label);
}

/* Below the theme's split the action drops to its own full-width line. The
   mobile mockup keeps it beside the meta and truncates both, which is the thing
   to fix rather than reproduce — every element the desktop row carries is still
   here, it just gets a line of its own. */
@media (max-width: 45em) {
	.host-row,
	.host-rows--logos .host-row {
		grid-template-columns: auto 1fr;
	}

	.host-row__action {
		grid-column: 1 / -1;
	}

	/* Indented to the text column, so the title, the meta and the action share
	   one left edge instead of the action hanging back under the logo. */
	.host-rows--logos .host-row__action {
		grid-column: 2 / -1;
	}

	/* No logo rail to indent past, so the row is a single column. */
	.host-rows:not(.host-rows--logos) .host-row {
		grid-template-columns: 1fr;
	}
}

/* Pagination.

   These selectors used to assume paginate_links( type => 'list' ) markup —
   ul.page-numbers > li > a — but the archives call the_posts_pagination(),
   which emits div.nav-links > a.page-numbers. So the container rule was landing
   on each individual number (making every one a block-level flex container and
   stacking them vertically) and the child rules matched nothing at all.

   .nav-links is the container; .page-numbers are the items. */
.navigation.pagination {
	margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

.navigation.pagination .nav-links {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 0.4rem;
}

.navigation.pagination .page-numbers {
	display: grid;
	place-items: center;
	min-width: 2rem;
	min-height: 2rem;
	padding: 0 0.5rem;
	border-radius: var(--grit-radius-full);
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-decoration: none;
	color: var(--grit-text-muted);
}

.navigation.pagination a.page-numbers:hover,
.navigation.pagination a.page-numbers:focus-visible {
	background: var(--grit-bg-alt);
	color: var(--grit-navy);
}

/* Current page reads as a selected chip, matching the badge vocabulary. */
.navigation.pagination .page-numbers.current {
	background: var(--grit-navy);
	color: #fff;
}

.navigation.pagination .page-numbers.dots {
	min-width: 0;
	padding: 0 0.25rem;
}

/* Sliding photo strip */
.grit-carousel {
	position: relative;
	display: flex;
	align-items: center;
	gap: 0.5rem;
	padding-inline: var(--grit-gutter);
}

.grit-carousel__track {
	display: flex;
	gap: 1rem;
	margin: 0;
	padding: 0.25rem 0;
	list-style: none;
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	scrollbar-width: none;
	flex: 1 1 auto;
}

.grit-carousel__track::-webkit-scrollbar {
	display: none;
}

.grit-carousel__slide {
	flex: 0 0 clamp(14rem, 30vw, 22rem);
	scroll-snap-align: start;
}

.grit-carousel__slide img {
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	border-radius: var(--grit-radius-md);
}

/* 44px, which is the smallest target the WCAG 2.2 pointer-size criterion calls
   adequate — these were 40 when the photo strip was the only thing using them,
   and the resources slider puts a pair of them on a phone. */
.grit-carousel__nav {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: 2.75rem;
	height: 2.75rem;
	font-size: 1.5rem;
	line-height: 1;
	color: var(--grit-navy);
	background: #fff;
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-full);
	cursor: pointer;
	box-shadow: var(--grit-shadow);
}

.grit-carousel__nav:hover:not(:disabled) {
	background: var(--grit-bg-alt);
	border-color: var(--grit-navy);
}

/* WHAT MAKES `hidden` STICK ON THESE BUTTONS. `[hidden]`'s UA rule is
   `display: none` at the lowest possible specificity, and the rule above sets
   `display: grid`, which beats it — so an arrow the script had hidden went on
   rendering, and a strip that fits drew a pair of dead controls. Exactly the
   trap .grit-resource-card[hidden] is here for, and the same one-point fix.

   It was invisible while the camp photo strip was the only caller, because that
   template has no call site — nothing ever rendered a strip short enough to hide
   an arrow. */
.grit-carousel__nav[hidden] {
	display: none;
}

/* Not merely faded: an arrow at the end of the track is genuinely inoperable, so
   it says so to a screen reader through :disabled and to everyone else by losing
   the pointer cursor as well as the contrast. Left in place rather than hidden —
   a control that vanishes at the ends moves the other one under the cursor. */
.grit-carousel__nav:disabled {
	opacity: 0.35;
	cursor: default;
}

.host-badge {
	align-self: flex-start;
	margin-top: 0.75rem;
	font-size: 0.6875rem;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	padding: 0.25rem 0.6rem;
	border-radius: var(--grit-radius-full);
	background: var(--grit-primary);
	color: #fff;
}

.host-badge--upcoming {
	background: var(--grit-navy);
	color: #fff;
}

/* The quiet tier: orange label on a tenth-strength orange fill. Used where the
   badge is one element among several inside a card, and a solid orange block
   would out-shout the title it sits above. Resets the base badge's margin-top,
   which is tuned for the single-host header it was written for. */
.host-badge--soft {
	margin-top: 0;
	background: var(--grit-primary-soft);
	color: var(--grit-primary);
}

/* ---------------------------------------------------------------------------
   Host details — the page every camp and workshop host gets

   Two bands: the facts on white, the photographs on the light tint, and below
   them the shared sections from the footer page. Nothing here is per host.

   The old .host-header / .host-facts pair is gone with the definition list it
   dressed. The design puts the facts on one line under the name, which a <dl>
   of five term/description pairs cannot be talked into being.
   --------------------------------------------------------------------------- */
.host-detail__back {
	font-size: var(--grit-text-small);
	font-weight: 600;
	margin-bottom: clamp(1rem, 3vw, 1.75rem);
}

.host-detail__back a {
	color: var(--grit-primary);
	text-decoration: none;
}

.host-detail__back a::before {
	content: "\2190\2009";
}

.host-detail__back a:hover,
.host-detail__back a:focus-visible {
	text-decoration: underline;
}

/* The name and its facts on the left, the logo on the right, wrapping to one
   column when there is not room for both. The logo goes FIRST when it wraps —
   flex order, not source order, because the heading must stay first in the
   document for anyone reading it in order. */
.host-detail__row {
	display: flex;
	flex-wrap: wrap;
	gap: clamp(1.25rem, 4vw, 2.5rem);
	align-items: flex-start;
	justify-content: space-between;
}

.host-detail__intro {
	flex: 1 1 22rem;
}

.host-detail__intro h1 {
	margin-bottom: 0.5rem;
}

.host-detail__status {
	margin: 0 0 0.75rem;
}

/* The badge's own margin-top is tuned for the header it used to sit in, where
   it followed the heading. Here it leads. */
.host-detail__status .host-badge {
	margin-top: 0;
}

.host-detail__meta {
	color: var(--grit-text-muted);
	margin-bottom: 0.35rem;
}

.host-detail__site {
	font-size: var(--grit-text-small);
	font-weight: 600;
	margin-bottom: 0;
}

.host-detail__site a {
	color: var(--grit-primary);
	text-decoration: none;
}

.host-detail__site a::after {
	content: "\2009\2192";
}

.host-detail__site a:hover,
.host-detail__site a:focus-visible {
	text-decoration: underline;
}

.host-detail__logo {
	flex: 0 0 auto;
	display: grid;
	place-items: center;
	width: min(100%, 16rem);
	padding: 1.25rem;
	background: var(--grit-bg-alt);
	border-radius: var(--grit-radius-md);
}

.host-detail__body {
	margin-top: clamp(1.5rem, 4vw, 2.5rem);
}

/* Small, muted and uppercase — a label for the block rather than a heading
   competing with the host's name above it. Still an h2, because it is one. */
.host-detail__photos-label {
	font-size: var(--grit-text-label);
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	color: var(--grit-text-muted);
	margin-bottom: clamp(1rem, 3vw, 1.5rem);
}

/* Three across, two on a tablet, one on a phone — matched by the sizes
   attribute the template passes, which has to agree with these breakpoints or
   the browser fetches for a layout that never renders. */
.host-gallery {
	display: grid;
	gap: clamp(0.75rem, 2vw, 1.25rem);
	grid-template-columns: 1fr;
	padding: 0;
	margin: 0;
	list-style: none;
}

@media (min-width: 45.0625em) {
	.host-gallery {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 62em) {
	.host-gallery {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

.host-gallery a {
	display: block;
	border-radius: var(--grit-radius-md);
	overflow: hidden;
}

.host-gallery img {
	aspect-ratio: 3 / 2;
	object-fit: cover;
	width: 100%;
	/* Visible while the photograph loads, so the grid does not open as a page
	   of white holes on the tinted band. */
	background: var(--grit-border);
	transition: transform var(--grit-motion);
}

.host-gallery a:hover img,
.host-gallery a:focus-visible img {
	transform: scale(1.03);
}

/* ---------------------------------------------------------------------------
   The photo lightbox

   A native <dialog>, so the focus trap, the inert background and Escape are the
   browser's rather than ours. Everything below is paint.
   --------------------------------------------------------------------------- */
.grit-lightbox {
	max-width: min(96vw, 72rem);
	max-height: 92vh;
	padding: 0;
	border: 0;
	border-radius: var(--grit-radius-md);
	background: var(--grit-navy);
	color: #fff;
	overflow: hidden;
}

.grit-lightbox::backdrop {
	background: rgb(0 22 42 / 82%);
}

.grit-lightbox__frame {
	display: grid;
	place-items: center;
	/* Leaves room for the bar below without the image ever pushing it off the
	   bottom of a short window. */
	max-height: calc(92vh - 4rem);
	overflow: hidden;
}

.grit-lightbox__image {
	display: block;
	max-width: 100%;
	max-height: calc(92vh - 4rem);
	width: auto;
	height: auto;
}

.grit-lightbox__bar {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1rem;
	padding: 0.75rem 1rem;
}

.grit-lightbox__count {
	font-size: var(--grit-text-small);
	color: var(--grit-text-on-dark);
	min-width: 4rem;
	text-align: center;
}

.grit-lightbox__nav,
.grit-lightbox__close {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	padding: 0;
	font-size: 1rem;
	color: #fff;
	background: rgb(255 255 255 / 12%);
	border: 0;
	border-radius: var(--grit-radius-full);
	cursor: pointer;
	transition: background-color var(--grit-motion);
}

.grit-lightbox__nav:hover,
.grit-lightbox__nav:focus-visible,
.grit-lightbox__close:hover,
.grit-lightbox__close:focus-visible {
	background: rgb(255 255 255 / 24%);
}

.grit-lightbox__close {
	position: absolute;
	top: 0.75rem;
	right: 0.75rem;
}

/* ---------------------------------------------------------------------------
   Forms (Contact Form 7, Mailchimp for WordPress)
   --------------------------------------------------------------------------- */
.wpcf7 form,
.mc4wp-form {
	max-width: 34rem;
}

.wpcf7 label,
.mc4wp-form label {
	display: block;
	font-weight: 600;
	font-size: var(--grit-text-small);
	margin-bottom: 1rem;
}

.wpcf7 input[type="text"],
.wpcf7 input[type="email"],
.wpcf7 input[type="tel"],
.wpcf7 input[type="url"],
.wpcf7 input[type="number"],
.wpcf7 input[type="date"],
.wpcf7 textarea,
.wpcf7 select,
.mc4wp-form input[type="text"],
.mc4wp-form input[type="email"],
.grit-newsletter__form input[type="text"],
.grit-newsletter__form input[type="email"],
.grit-resource-filters input[type="search"],
.grit-locator input[type="search"],
.woocommerce form .form-row :is(input.input-text, textarea, select),
.woocommerce form.woocommerce-ordering select,
.woocommerce .quantity input.qty,
.woocommerce-cart-form input.input-text,
.woocommerce form.checkout_coupon input.input-text {
	display: block;
	width: 100%;
	margin-top: 0.35rem;
	padding: 0.7rem 0.8rem;
	font: inherit;
	font-weight: 400;
	color: var(--grit-text);
	background: #fff;
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-sm);
	transition: border-color var(--grit-motion), box-shadow var(--grit-motion);
}

.wpcf7 ::placeholder,
.grit-newsletter__form ::placeholder,
.grit-resource-filters ::placeholder,
.grit-locator ::placeholder,
.mc4wp-form ::placeholder,
.woocommerce ::placeholder {
	color: var(--grit-text-muted);
	opacity: 1; /* Firefox dims placeholders by default. */
}

/* Focus. Deliberately :focus and not :focus-visible — the global
   :focus-visible ring at the top of this file never fires for a field you
   clicked into and are typing in, which is exactly when it is wanted. */
.wpcf7 input:focus,
.wpcf7 textarea:focus,
.wpcf7 select:focus,
.grit-newsletter__form input:focus,
.grit-resource-filters input:focus,
.grit-locator input:focus,
.mc4wp-form input:focus,
.woocommerce form :is(input, textarea, select):focus {
	outline: none;
	border-color: var(--grit-navy);
	box-shadow: 0 0 0 3px rgb(0 22 42 / 12%);
}

/* Error. CF7 adds .wpcf7-not-valid to the field itself; previously only the tip
   text turned red and the field carried no treatment at all.

   The element is named explicitly because the base field rule above is
   `.wpcf7 input[type="email"]` — (0,2,1). A bare `.wpcf7 .wpcf7-not-valid` is
   (0,2,0) and quietly loses the border-color it is trying to set. */
.wpcf7 input.wpcf7-not-valid,
.wpcf7 textarea.wpcf7-not-valid,
.wpcf7 select.wpcf7-not-valid {
	border-color: var(--grit-error);
}

.wpcf7 input.wpcf7-not-valid:focus,
.wpcf7 textarea.wpcf7-not-valid:focus,
.wpcf7 select.wpcf7-not-valid:focus {
	border-color: var(--grit-error);
	box-shadow: 0 0 0 3px rgb(196 43 16 / 15%);
}

/* Success is form-level, not per-field: CF7 has no per-field success hook, so
   the design's green field state can only be expressed once the whole form has
   sent. */
.wpcf7 form.sent input[type="text"],
.wpcf7 form.sent input[type="email"],
.wpcf7 form.sent input[type="tel"],
.wpcf7 form.sent textarea {
	border-color: var(--grit-success);
}

.wpcf7 input:disabled,
.wpcf7 textarea:disabled,
.grit-newsletter__form input:disabled,
.mc4wp-form input:disabled {
	background: var(--grit-bg-alt);
	color: var(--grit-text-muted);
	cursor: default;
}

.wpcf7 textarea {
	min-height: 9rem;
	resize: vertical;
}

/* Checkboxes and radios were entirely unstyled and inherited the block/full
   width rules above, which stretched them across the form. */
.wpcf7 input[type="checkbox"],
.wpcf7 input[type="radio"] {
	width: auto;
	margin-right: 0.4rem;
	accent-color: var(--grit-primary);
}

.wpcf7 .wpcf7-list-item {
	margin: 0 1rem 0 0;
	display: inline-block;
}

.wpcf7 .wpcf7-list-item-label {
	font-weight: 400;
}

/* Submit buttons take their whole treatment from the Buttons section. */

.wpcf7-not-valid-tip {
	font-size: var(--grit-text-small);
	font-weight: 400;
	color: var(--grit-error);
}

.wpcf7 .wpcf7-response-output {
	margin: 1rem 0 0;
	padding: 0.75rem 1rem;
	border: 1px solid var(--grit-border);
	border-left-width: 4px;
	border-radius: var(--grit-radius-sm);
	font-size: var(--grit-text-small);
}

.wpcf7 form.sent .wpcf7-response-output {
	border-left-color: var(--grit-success);
	color: var(--grit-success);
}

.wpcf7 form.invalid .wpcf7-response-output,
.wpcf7 form.failed .wpcf7-response-output {
	border-left-color: var(--grit-error);
	color: var(--grit-error);
}

/* ---------------------------------------------------------------------------
   Footer
   --------------------------------------------------------------------------- */
.site-footer {
	background: var(--grit-surface-dark);
	color: rgb(255 255 255 / 72%);
	padding-block: clamp(2.5rem, 6vw, 4rem);
	font-size: var(--grit-text-small);
}

.site-footer__grid {
	display: grid;
	gap: clamp(1.5rem, 4vw, 3rem);
	/* Two tracks, not an auto-fit: About takes the row and the two short link
	   columns sit beside each other, which is what the width is worth spending on.
	   `minmax(0, 1fr)` rather than `1fr` so a long menu label cannot push its own
	   track wider than half. Three tracks from 60rem, further down. */
	grid-template-columns: repeat(2, minmax(0, 1fr));
	margin-bottom: 2.5rem;
}

.site-footer__about {
	grid-column: 1 / -1;
}

/* The Contact column is conditional (footer.php: an empty <div> in the grid would
   leave the legal line under a gap nothing accounts for). Without it, Navigation
   is the last child and would sit in a half-width track with nothing beside it,
   so it takes the row instead. */
.site-footer__grid > div:nth-child(2):last-child {
	grid-column: 1 / -1;
}

.site-footer__about p {
	max-width: 34rem;
}

/* The editor-chosen footer logo, or the site's own when none is set.

   Capped by width rather than by height, unlike .site-brand img in the header:
   the header is a fixed-height bar and the footer column is not, so here the
   logo should sit at a consistent WIDTH in a 2fr column whatever its aspect
   ratio. `height: auto` is not decoration — wp_get_attachment_image() writes real
   width and height attributes, and constraining one axis without releasing the
   other distorts the image. */
.site-footer__logo {
	display: block;
	width: auto;
	max-width: 12rem;
	height: auto;
	margin-bottom: 1.25rem;
}

/* The social row reuses .grit-social-band__links for its layout — same design
   object, one set of rules. This only places it in the column: the band sits in
   a row beside its heading and needs no margin, and here it follows a paragraph
   and precedes the signup form. */
.site-footer__social {
	margin-block: 1.25rem;
}

@media (min-width: 60rem) {
	.site-footer__grid {
		grid-template-columns: 2fr 1fr 1fr;
	}

	/* Three tracks, one child each. The spans above are the mobile layout. */
	.site-footer__about,
	.site-footer__grid > div:nth-child(2):last-child {
		grid-column: auto;
	}
}

.site-footer h2 {
	font-size: var(--grit-text-label);
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	color: #fff;
	margin-bottom: 0.9rem;
}

.site-footer a {
	color: rgb(255 255 255 / 82%);
	text-decoration: none;
}

.site-footer a:hover,
.site-footer a:focus-visible {
	color: #fff;
}

.site-footer ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: 0.4rem;
}

.site-footer__legal {
	border-top: 1px solid rgb(255 255 255 / 15%);
	padding-top: 1.5rem;
	font-size: var(--grit-text-label);
	color: rgb(255 255 255 / 55%);
}

.site-footer__legal p {
	margin: 0 0 0.4rem;
}

/* ---------------------------------------------------------------------------
   Floating donate action, as on the Wix site
   --------------------------------------------------------------------------- */
.grit-donate-float {
	position: fixed;
	right: clamp(0.75rem, 2vw, 1.5rem);
	bottom: clamp(0.75rem, 2vw, 1.5rem);
	z-index: 50;
	box-shadow: 0 4px 16px rgb(0 22 42 / 25%);
}

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		transition-duration: 0.01ms !important;
		animation-duration: 0.01ms !important;
	}
}

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

   Built on <details>/<summary> so it opens, closes and is keyboard- and
   screen-reader-operable with no JavaScript at all. The only work here is
   hiding the native disclosure marker and drawing our own.

   THE SELECTORS BELOW SPLIT ON THE ELEMENT, not on a modifier class, because
   the section has a second mode: with the Question behaviour field set to "Always
   open" the template drops <details> entirely and renders an <h3> per question (a
   list of answers is not a disclosure widget, and a <summary> stays operable
   from the keyboard however it is styled). .grit-accordion__q therefore carries
   only the look, which both elements want; everything interactive — the
   pointer, the marker suppression, the hover and the plus/minus — is qualified
   `summary.grit-accordion__q`, so the always-open mode drops it by being a
   different element rather than by undoing rule after rule.
   --------------------------------------------------------------------------- */
.grit-accordion {
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-md);
	overflow: hidden;
	margin-block: clamp(1.5rem, 4vw, 2.5rem);
	background: #fff;
}

.grit-accordion__item + .grit-accordion__item {
	border-top: 1px solid var(--grit-border);
}

.grit-accordion__q {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 1rem;
	padding: clamp(0.9rem, 2vw, 1.15rem) clamp(1rem, 3vw, 1.5rem);
	/* An <h3> brings a margin of its own; a <summary> does not. */
	margin: 0;
	font-family: var(--grit-font-heading);
	font-weight: 600;
	font-size: var(--grit-text-body-lg);
	line-height: var(--grit-lh-heading);
	color: var(--grit-navy);
}

summary.grit-accordion__q {
	cursor: pointer;
	/* Both are needed: `list-style` covers Firefox and modern Chrome, the
	   pseudo-element covers older WebKit. */
	list-style: none;
}

summary.grit-accordion__q::-webkit-details-marker {
	display: none;
}

summary.grit-accordion__q:hover {
	background: var(--grit-bg-alt);
}

summary.grit-accordion__q::after {
	content: "+";
	flex: 0 0 auto;
	font-size: 1.5rem;
	line-height: 1;
	color: var(--grit-primary);
}

.grit-accordion__item[open] > summary.grit-accordion__q::after {
	/* Minus sign, not a hyphen — it lines up with the plus optically. */
	content: "\2212";
}

/* 100ch rather than the shared --grit-measure (68ch). An answer is read once,
   inside a panel the reader opened on purpose, and at 68ch a three-line answer
   became six inside a full-width accordion with the rest of the row empty.

   A literal, NOT a change to the token: --grit-measure also sets .page-intro,
   .entry-content and the prose children of .entry-content--wide, so moving it
   would widen every page and post on the site to answer a question about FAQs. */
.grit-accordion__a {
	padding: 0 clamp(1rem, 3vw, 1.5rem) clamp(1rem, 2.5vw, 1.35rem);
	max-width: 100ch;
	color: var(--grit-text);
}

.grit-accordion__a > :last-child {
	margin-bottom: 0;
}

/* Answers take links — the prose allowlist has always permitted an anchor, and
   the field now says so. This makes the underline deliberate: today it is the
   browser's default, which nothing removes but nothing guarantees either, and
   .entry-content states the same rule explicitly a few hundred lines up. Buttons
   are excluded there and here for the same reason. */
.grit-accordion__a a:not(.grit-btn) {
	text-decoration: underline;
}

/* ---------------------------------------------------------------------------
   WooCommerce

   THE TWO LAYOUTS WOO'S OWN SHEET USED TO DO. inc/woocommerce.php drops
   `woocommerce-layout` because it floats the product grid into percentage
   columns and floats the gallery against the summary — the same two jobs the
   design system does with grid, and the theme was beating it only by
   specificity accident. Those two layouts are rebuilt here; removing that sheet
   without this block leaves the grid as one stacked column.

   `woocommerce-general` and `woocommerce-smallscreen` are still enqueued, so
   everything below is an override of a rule that exists, and the selectors match
   Woo's own specificity rather than exceeding it — this stylesheet loads after
   the plugin's, so a tie goes to the theme. That is also why the column-class
   selectors below carry [class*="columns-"]: the small-screen sheet sets widths
   through it at (0,4,2), and a shorter selector here would silently lose under
   768px only.

   BUTTONS AND FIELDS ARE NOT HERE. Woo's selectors are on the theme's own button
   and form rules above, because a second set of button declarations is how the
   three near-copies this file already merged came about in the first place. Only
   Woo's `.alt` tier takes the orange fill — that tier is add-to-cart, place-order
   and proceed-to-checkout, and orange is reserved for calls to action, so plain
   `.button` (update cart, apply coupon) gets the outline treatment below.
   --------------------------------------------------------------------------- */

/* The product grid. Three across on a desktop, two on a tablet, one on a phone —
   counted rather than fitted. auto-fit was giving five tracks on a wide screen,
   and a merch catalog of six items reads as a catalog at three and as a spill at
   five. Woo's own columns-N class is therefore ignored, as it already was. */
.woocommerce ul.products {
	display: grid;
	gap: clamp(1rem, 2.5vw, 2.5rem);
	grid-template-columns: minmax(0, 1fr);
	padding: 0;
	margin: 0;
	list-style: none;
}

@media (min-width: 560px) {
	.woocommerce ul.products {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
}

@media (min-width: 900px) {
	.woocommerce ul.products {
		grid-template-columns: repeat(3, minmax(0, 1fr));
	}
}

/* WOO'S CLEARFIX IS A PHANTOM CARD UNDER GRID. woocommerce-general gives the list
   `::before`/`::after` with `display: table` to clear its own floats; as grid
   children those become two empty items, and the first one takes a whole cell —
   an empty bordered card at the top of the catalog. The floats they cleared are
   in the layout sheet this theme dequeues, so there is nothing left for them to
   do. */
.woocommerce ul.products::before,
.woocommerce ul.products::after {
	content: none;
	display: none;
}

/* Woo prints its column count as a class on the list and its widths as
   percentages on the items. The percentages are not inert under grid — a width
   on a grid item shrinks it inside its own track — so they are cleared here.
   position: relative is for the sale badge, which is absolutely positioned and
   would otherwise be placed against whatever ancestor happens to be positioned. */
.woocommerce ul.products li.product,
.woocommerce ul.products[class*="columns-"] li.product {
	width: auto;
	float: none;
	clear: none;
	margin: 0;
	position: relative;
	display: flex;
	flex-direction: column;
	background: var(--grit-bg);
	border: 1px solid var(--grit-border);
	border-radius: var(--grit-radius-md);
	padding: clamp(1rem, 2.5vw, 1.25rem);
}

/* BORDERED, WHERE .grit-card IS NOT. That card is borderless on purpose — the
   note above it says the shadow and the white fill already separate it and a
   border doubled the edge. A product card takes the border and drops the shadow
   instead: same job, and it is the treatment the catalog design asks for. The two
   are not meant to look identical, so neither shares the other's class. */
.woocommerce ul.products li.product {
	box-shadow: none;
}

/* The whole of a card above the button is one link — image, title and price —
   so the theme's prose underline would land on all three. Woo's own sheet sets
   this too; it is stated here so the card does not depend on a plugin stylesheet
   to not be underlined. */
.woocommerce ul.products li.product > a {
	text-decoration: none;
	display: block;
}

.woocommerce ul.products li.product a img {
	margin-bottom: 0.75rem;
	border-radius: var(--grit-radius-md);
}

.woocommerce ul.products li.product .woocommerce-loop-product__title {
	font-family: var(--grit-font-heading);
	font-size: var(--grit-text-body-lg);
	font-weight: 700;
	line-height: var(--grit-lh-heading);
	color: var(--grit-section-heading);
	padding: 0;
	margin: 0 0 0.35rem;
}

/* Woo's price green is the one colour in its palette with no equivalent here.
   On a card the price is secondary to the name, so it is muted body text — the
   navy weight belongs to the title above it. On the product page it is the
   headline number and gets navy at h3, below. Never orange: a price is
   information, and orange is the CTA. */
.woocommerce ul.products li.product .price {
	color: var(--grit-text-muted);
	font-weight: 400;
	font-size: var(--grit-text-body);
	margin: 0 0 0.6rem;
}

.woocommerce div.product p.price,
.woocommerce div.product span.price {
	color: var(--grit-section-heading);
	font-weight: 700;
	font-size: var(--grit-text-body);
}

.woocommerce div.product p.price,
.woocommerce div.product span.price {
	font-size: var(--grit-text-h3);
}

/* Was-price. del carries the strike itself, so this only needs to step it back
   out of the way; ins is not an edit here and should not be underlined. */
.woocommerce :is(ul.products li.product, div.product) .price del {
	color: var(--grit-text-muted);
	font-weight: 400;
	opacity: 1;
}

.woocommerce :is(ul.products li.product, div.product) .price ins {
	text-decoration: none;
}

/* The card's button sits at the bottom of an equal-height row rather than
   immediately under a short title — the same reason .grit-card is a column. */
.woocommerce ul.products li.product a.button {
	margin-top: auto;
	align-self: flex-start;
}

/* Sale badge. A pill, like every other status label on the site, and orange
   because this one genuinely is a promotion. */
.woocommerce span.onsale {
	position: absolute;
	top: 0.75rem;
	left: 0.75rem;
	right: auto;
	min-height: 0;
	min-width: 0;
	padding: 0.3rem 0.7rem;
	margin: 0;
	line-height: 1.4;
	font-family: var(--grit-font);
	font-size: var(--grit-text-label);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	background: var(--grit-primary);
	color: #fff;
	border-radius: var(--grit-radius-full);
}

/* Plain .button is Woo's secondary action. A flat outline rather than the
   sweeping overlay .grit-btn--secondary uses: half of these are <input>, which
   has no ::before to sweep, and a tier that animates on some buttons and not
   others reads as a bug. */
.woocommerce :is(a, button, input).button {
	background: transparent;
	color: var(--grit-navy);
	border-color: var(--grit-navy);
}

.woocommerce :is(a, button, input).button:hover,
.woocommerce :is(a, button, input).button:focus-visible {
	background: var(--grit-navy);
	color: #fff;
	border-color: var(--grit-navy);
}

/* The count and the sort dropdown. Only what they look like — where they sit is
   .grit-collection__controls below, and every page that prints them goes through
   archive-product.php, so they are always inside it. The select's field treatment
   comes from the shared form rule; this is the width the row wants.

   width: auto, because that shared rule sets 100% for the fields it was written
   for and a full-bleed sort dropdown is not one of them. */
.woocommerce .woocommerce-result-count {
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
	margin: 0;
}

.woocommerce .woocommerce-ordering {
	margin: 0;
}

.woocommerce form.woocommerce-ordering select {
	width: auto;
	min-width: 12rem;
}

/* The collection page's own rhythm --------------------------------------- */

/* The title band and the grid band read as one run, not two. The head drops its
   lower padding entirely and the grid takes a flat 15px above — a literal, not
   --grit-section or its half, because this is the one gap on the site that is a
   fixed distance between a heading and the thing it heads rather than a step in
   the fluid section rhythm. */
.grit-collection__head {
	padding-bottom: 0;
}

.grit-collection {
	padding-top: 15px;
}

/* The result count and the sort dropdown on one row, count left, sort right.
   They are siblings with no container of their own, so archive-product.php wraps
   the hook that prints them. */
.grit-collection__controls {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 0.75rem;
	margin-bottom: clamp(1rem, 2.5vw, 1.75rem);
}

/* Notices come through the same hook, at priority 10. They take the whole row
   rather than joining it — a cart confirmation squeezed beside a sort dropdown
   is the wrong shape for the one message on the page that has to be read. */
.grit-collection__controls > .woocommerce-notices-wrapper,
.grit-collection__controls > :is(.woocommerce-message, .woocommerce-info, .woocommerce-error) {
	flex: 1 0 100%;
	margin-bottom: 0;
}

.grit-collection__controls > .woocommerce-notices-wrapper:empty {
	display: none;
}

/* The short description under a collection's title. On a category it is the
   term's own Description field; on /shop it is the Shop page's content. Woo wraps
   them in different classes and prints them through the same hook, so both are
   named — and both are clamped to the measure, because this is prose and the band
   around it is not. */
.woocommerce .term-description,
.woocommerce .page-description,
.woocommerce-page .term-description,
.woocommerce-page .page-description {
	max-width: var(--grit-measure);
	color: var(--grit-section-text);
	margin-top: -0.5rem;
}

.woocommerce .term-description p:last-child,
.woocommerce .page-description p:last-child,
.woocommerce-page .term-description p:last-child,
.woocommerce-page .page-description p:last-child {
	margin-bottom: 0;
}

/* Single product: the gallery beside the summary, everything after them across
   both columns. Woo floated these 48/48; grid means the tabs and the related
   products below cannot end up in a third track. */
.woocommerce div.product {
	position: relative;
	display: grid;
	gap: clamp(1.5rem, 4vw, 3rem);
	grid-template-columns: minmax(0, 1fr);
}

.woocommerce div.product :is(.woocommerce-product-gallery, div.images, div.summary) {
	width: auto;
	float: none;
	margin: 0;
}

@media (min-width: 900px) {
	.woocommerce div.product {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		align-items: start;
	}

	.woocommerce div.product > :not(.woocommerce-product-gallery, div.images, div.summary) {
		grid-column: 1 / -1;
	}
}

.woocommerce div.product .product_title {
	margin-bottom: 0.4em;
}

.woocommerce div.product .woocommerce-product-rating {
	margin-bottom: 1rem;
}

.woocommerce div.product .quantity {
	margin-right: 0.75rem;
}

.woocommerce .quantity input.qty {
	width: 5rem;
	text-align: center;
}

/* Tabs. Woo builds its own rounded-tab chrome out of ::before and ::after on
   every item; those are switched off rather than restyled, because the design
   system's tab is a flat row with an underline on the current one. */
.woocommerce div.product .woocommerce-tabs ul.tabs {
	display: flex;
	flex-wrap: wrap;
	gap: 0.25rem;
	padding: 0;
	margin: 0 0 1.5rem;
	border-bottom: 1px solid var(--grit-section-border);
}

.woocommerce div.product .woocommerce-tabs ul.tabs::before,
.woocommerce div.product .woocommerce-tabs ul.tabs li::before,
.woocommerce div.product .woocommerce-tabs ul.tabs li::after {
	content: none;
	display: none;
}

.woocommerce div.product .woocommerce-tabs ul.tabs li {
	background: transparent;
	border: 0;
	border-radius: 0;
	margin: 0;
	padding: 0;
}

.woocommerce div.product .woocommerce-tabs ul.tabs li a {
	display: block;
	padding: 0.6rem 1rem;
	font-family: var(--grit-font-heading);
	font-size: var(--grit-text-small);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: var(--grit-track-label);
	color: var(--grit-text-muted);
	border-bottom: 2px solid transparent;
	text-decoration: none;
}

.woocommerce div.product .woocommerce-tabs ul.tabs li.active a,
.woocommerce div.product .woocommerce-tabs ul.tabs li a:hover {
	color: var(--grit-section-heading);
	border-bottom-color: var(--grit-primary);
}

/* The panel under the tabs. It carries `entry-content`, so it is already clamped
   to --grit-measure by the Content block — what it had no rule for is content
   that will not break INSIDE that measure and runs out past the column: a pasted
   URL, a product code, copy dropped into a Preformatted block, a spec table
   wider than the page.

   Every declaration here is a wrap of last resort, on the panel rather than on
   the description alone, because Additional information and Reviews take the
   same class and can hold the same offenders. What it deliberately does NOT do
   is force `white-space` on the panel itself: an editor who pasted a nowrap
   style into the description has written that inline, no stylesheet outranks it,
   and clearing the formatting in wp-admin is the fix. */
.woocommerce div.product .woocommerce-Tabs-panel {
	overflow-wrap: break-word;
}

/* anywhere rather than break-word for links, the same treatment .host-card__meta
   gives a URL in a narrow column: a bare address has no break opportunity at all
   and break-word would leave it hanging off the edge. */
.woocommerce div.product .woocommerce-Tabs-panel a {
	overflow-wrap: anywhere;
}

/* THE DESCRIPTION IS INSIDE A <pre>, and the UA sheet gives that monospace at a
   smaller size with no wrapping — so a rule that only wraps it leaves the page's
   main run of prose looking like a code sample that happens to fit. Both halves
   are here: pre-wrap for the wrapping, and the type inherited from the panel so
   it reads as the paragraph it is.

   THE COST, SAID OUT LOUD: a genuine code sample in some future description
   loses its monospace too. That is the trade for not needing an editor to
   convert the block, and it is reversible the moment the content is a paragraph
   rather than a Preformatted block — which is where this copy belongs. */
.woocommerce div.product .woocommerce-Tabs-panel pre {
	white-space: pre-wrap;
	font-family: inherit;
	font-size: inherit;
	line-height: inherit;
	color: inherit;
	background: none;
	padding: 0;
	margin: 0;
	border: 0;
}

.woocommerce div.product .woocommerce-Tabs-panel :is(img, table) {
	max-width: 100%;
}

/* A table that genuinely needs the width scrolls in its own box rather than
   stretching the page — the one thing here that is not a wrap, because a spec
   table with its columns squeezed to nothing is not readable either.

   THE DESCRIPTION PANEL ONLY, and the modifier class is why. `display: block` on
   a table hands its width to an anonymous table box that shrinks to fit, which is
   right for something an editor pasted and wrong for Additional information —
   `.shop_attributes` is 100%-wide by design and already fits, so it is left where
   Woo put it. */
.woocommerce div.product .woocommerce-Tabs-panel--description table {
	display: block;
	overflow-x: auto;
}

/* Notices. Woo's own layout is kept — the icon, the padding, the inline button —
   and only the accent colour changes, per type, which its sheet paints one
   purple for all three. */
.woocommerce .woocommerce-message,
.woocommerce .woocommerce-info,
.woocommerce .woocommerce-error {
	background: var(--grit-bg-alt);
	color: var(--grit-text);
	border-top-color: var(--grit-primary);
	border-radius: var(--grit-radius-sm);
}

.woocommerce .woocommerce-message::before {
	color: var(--grit-success);
}

.woocommerce .woocommerce-message {
	border-top-color: var(--grit-success);
}

.woocommerce .woocommerce-error {
	border-top-color: var(--grit-error);
}

.woocommerce .woocommerce-error::before {
	color: var(--grit-error);
}

.woocommerce .woocommerce-info::before {
	color: var(--grit-navy);
}

/* Breadcrumb. Small and muted: it is orientation, not content. */
.woocommerce .woocommerce-breadcrumb {
	font-size: var(--grit-text-small);
	color: var(--grit-text-muted);
	margin-bottom: clamp(1rem, 2.5vw, 1.5rem);
}

.woocommerce .woocommerce-breadcrumb a {
	color: var(--grit-text-muted);
}

/* Pagination. Squares on the page's own border colour, current page in orange —
   the same treatment the resources library's pager uses. */
.woocommerce nav.woocommerce-pagination ul {
	border: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 0.35rem;
	margin: clamp(1.5rem, 4vw, 2.5rem) 0 0;
}

.woocommerce nav.woocommerce-pagination ul li {
	border: 0;
	margin: 0;
	overflow: visible;
}

.woocommerce nav.woocommerce-pagination ul li :is(a, span) {
	display: block;
	min-width: 2.5rem;
	padding: 0.5rem 0.65rem;
	font-size: var(--grit-text-small);
	font-weight: 700;
	color: var(--grit-navy);
	background: transparent;
	border: 1px solid var(--grit-section-border);
	border-radius: var(--grit-radius-sm);
}

.woocommerce nav.woocommerce-pagination ul li span.current,
.woocommerce nav.woocommerce-pagination ul li a:hover {
	background: var(--grit-primary);
	border-color: var(--grit-primary);
	color: #fff;
}

/* Cart, checkout and my-account are ordinary pages, so page.php renders them
   inside .entry-content — which is clamped to --grit-measure. That is right for
   prose and wrong for a two-column checkout and a cart table, and it is the one
   place the measure has to be lifted rather than worked around. */
.woocommerce-cart .entry-content,
.woocommerce-checkout .entry-content,
.woocommerce-account .entry-content {
	max-width: none;
}

/* The headings Woo prints for related products, upsells and reviews are h2s in
   the middle of a page whose own h2s open sections. Stepped down so a product
   page has one visual hierarchy rather than two competing ones. */
.woocommerce div.product :is(.related, .upsells, #reviews) h2 {
	font-size: var(--grit-text-h3);
}

/* ---------------------------------------------------------------------------
   WooCommerce — product detail

   Separate from the WooCommerce block above so the catalog rules and the product
   page's rules stay separately findable; both override a `woocommerce-general`
   rule that exists, so the selectors match Woo's specificity rather than
   exceeding it and rely on this stylesheet loading after the plugin's.

   TWO THINGS HERE ARE DRIVEN BY assets/product-form.js, and both are keyed on
   `.grit-pills-ready` — the one class that script writes, on the form, once it
   has actually built the pills. Nothing is hidden on the promise that JavaScript
   will run: a page where the script never loaded shows Woo's own select and its
   own variation price, and works.
   --------------------------------------------------------------------------- */

/* The image, and the tiles under it --------------------------------------- */

.woocommerce div.product .woocommerce-product-gallery__image img {
	border-radius: var(--grit-radius-md);
}

/* EVERY SELECTOR BELOW CARRIES `div.images`, AND THAT IS THE LOAD-BEARING PART.
   woocommerce-general styles these at `.woocommerce div.product div.images
   .flex-control-thumbs li` — (0,4,3) — with `width: 25%; float: left`, and dims
   the inactive ones through an equally long selector. A shorter one here loses,
   which is what left every tile a quarter of its own track and the inactive ones
   dimmed despite this block claiming to have replaced both. Matching Woo's
   selector rather than exceeding it is the same bargain the catalog block makes
   with [class*="columns-"]: this stylesheet loads after the plugin's, so a tie
   goes to the theme.

   ONE LINE THAT SCROLLS, NOT A GRID THAT WRAPS. A grid re-flows a fifth image
   onto a second row, and on a phone it divides the column into tiles too small
   to aim at. A no-wrap flex row keeps every tile the same size and puts the
   overflow somewhere a thumb can reach it. That is also why Woo's
   `overflow: hidden` on this element has to be beaten rather than left alone —
   it would clip the scroll away and leave the extra images unreachable. */
.woocommerce div.product div.images .flex-control-thumbs {
	/* The two numbers the item rule's calc() reads. Written once rather than
	   twice: a gap that drifts from the one inside that calc is a row which
	   overflows by a few pixels for no reason anybody can see. */
	--grit-thumb-gap: 0.5rem;
	--grit-thumbs-per-view: 4;

	display: flex;
	flex-wrap: nowrap;
	gap: var(--grit-thumb-gap);
	margin: 0.75rem 0 0;
	padding: 0 0 0.25rem;
	list-style: none;
	overflow-x: auto;
	overflow-y: hidden;
	/* A swipe that runs out of tiles should stop, not hand the gesture to the
	   page behind it. */
	overscroll-behavior-x: contain;
	scroll-snap-type: x proximity;
	scrollbar-width: thin;
}

/* grow 1, shrink 0, basis exactly one view's share. That triple is what lets one
   rule cover all three cases: four tiles fill the column exactly, fewer than four
   grow to fill it, and more than four keep their size and overflow into the
   scroll. The percentage resolves against the scroller's VISIBLE width rather
   than its scroll width, which is why a fifth tile does not shrink the first
   four. */
.woocommerce div.product div.images .flex-control-thumbs li {
	flex: 1 0 calc(
		(100% - (var(--grit-thumbs-per-view) - 1) * var(--grit-thumb-gap)) /
		var(--grit-thumbs-per-view)
	);
	width: auto;
	float: none;
	margin: 0;
	list-style: none;
	scroll-snap-align: start;
}

/* border-box is not tidying up: the ring is 2px of border on the image itself,
   so under content-box every tile would sit 4px past the basis above and a row
   of exactly four would overflow. */
.woocommerce div.product div.images .flex-control-thumbs li img {
	display: block;
	box-sizing: border-box;
	width: 100%;
	height: auto;
	/* Square, whatever arrives. The registered size is a 1:1 crop, so this is
	   normally a no-op — it earns its place on the images uploaded before that
	   size existed, which fall back to their full-size original at whatever
	   shape it happens to be until someone regenerates them. */
	aspect-ratio: 1;
	object-fit: cover;
	opacity: 1;
	border: 2px solid transparent;
	border-radius: var(--grit-radius-sm);
	cursor: pointer;
	transition: border-color var(--grit-motion);
}

.woocommerce div.product div.images .flex-control-thumbs li img.flex-active,
.woocommerce div.product div.images .flex-control-thumbs li img:hover {
	opacity: 1;
	border-color: var(--grit-navy);
}

/* The summary column ------------------------------------------------------ */

.woocommerce div.product .product_title {
	font-size: var(--grit-text-h2);
	color: var(--grit-section-heading);
	margin-bottom: 0.5rem;
}

.woocommerce div.product .summary p.price {
	margin: 0 0 1rem;
}

.woocommerce div.product .woocommerce-product-details__short-description {
	color: var(--grit-section-text);
	margin-bottom: 1.5rem;
}

.woocommerce div.product .woocommerce-product-details__short-description p:last-child {
	margin-bottom: 0;
}

/* Attributes: the label above its pills ----------------------------------- */

/* Woo renders this as <table class="variations" role="presentation">, so
   restacking its rows to blocks is semantically free — that role is already
   telling assistive technology the table is layout and not data. The selectors
   name `.variations` and its parts rather than the elements, so a future Woo
   that drops the table needs a smaller revisit than a rewrite. */
.woocommerce div.product .variations,
.woocommerce div.product .variations tbody,
.woocommerce div.product .variations tr,
.woocommerce div.product .variations th,
.woocommerce div.product .variations td {
	display: block;
	width: auto;
	padding: 0;
	margin: 0;
	border: 0;
	text-align: left;
}

.woocommerce div.product .variations tr {
	margin-bottom: 1.25rem;
}

.woocommerce div.product .variations .label label {
	display: block;
	/* The right padding is the Clear link's berth. That link is positioned into
	   the top right of this block, so without it a long attribute name would run
	   underneath rather than wrap short of it. */
	padding-right: 4.5rem;
	font-size: var(--grit-text-small);
	font-weight: 700;
	color: var(--grit-section-heading);
	margin-bottom: 0.5rem;
}

/* The select WooCommerce actually drives. Clipped rather than removed — it is
   what resolves the variation, enables the button and holds the option list —
   and only once .grit-pills-ready says something is standing in for it. */
.woocommerce div.product form.grit-pills-ready .variations select {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.woocommerce div.product .grit-variation-pills {
	margin: 0;
}

/* The radio carries the state, the label is the pill. Clipped rather than
   display:none so it can still take focus, and the ring is drawn on the label
   where it can be seen. */
.grit-variation-pills__input {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.grit-variation-pills .grit-chip {
	cursor: pointer;
	user-select: none;
}

.grit-variation-pills__input:focus-visible + .grit-chip {
	outline: 2px solid var(--grit-navy);
	outline-offset: 2px;
}

/* A combination Woo has ruled out as the choices narrowed. Still legible — it is
   information, not decoration — but plainly not choosable, because a fully drawn
   pill that does nothing when clicked is worse than one that says why. */
.grit-variation-pills__input:disabled + .grit-chip {
	opacity: 0.4;
	cursor: not-allowed;
	text-decoration: line-through;
}

.grit-variation-pills__input:disabled + .grit-chip:hover {
	border-color: var(--grit-border);
}

/* Clear. OUT OF FLOW ON PURPOSE, and that is the entire point of this rule.

   Woo prints the link inside the LAST attribute's `td.value` and toggles it with
   `visibility`, not `display` — so in normal flow it holds a full blank line
   above Add to cart at all times, whether or not anything is chosen. That was the
   white gap on the page. Absolutely positioned it costs zero pixels in both
   states, which removes the gap and means revealing it shifts nothing: the same
   layout before and after a pill is clicked.

   `visibility` is deliberately not declared here. wc-add-to-cart-variation.js
   sets it inline, so anything this file said about it would be overruled the
   moment that script ran — and it is what makes the link appear at all.

   `form.cart` is in both selectors because Woo's own is
   `.woocommerce div.product form.cart .reset_variations` at (0,4,2). The rule
   this replaced stopped at (0,4,1) and lost its font-size to Woo's .83em. */
.woocommerce div.product form.cart .variations {
	position: relative;
}

.woocommerce div.product form.cart .variations .reset_variations {
	position: absolute;
	top: 0;
	right: 0;
	margin: 0;
	font-size: var(--grit-text-small);
	line-height: 1.4;
	color: var(--grit-text-muted);
	text-decoration: underline;
}

/* Quantity, and the button ------------------------------------------------ */

/* woocommerce-general floats all three of these; the layout here is normal flow
   and a float would take the full-width button out of it. */
.woocommerce div.product form.cart div.quantity,
.woocommerce div.product form.cart .variations_button,
.woocommerce div.product form.cart .single_add_to_cart_button {
	float: none;
}

/* QUANTITY IS WOOCOMMERCE'S OWN NUMBER INPUT, STYLED — not a control of ours.
   It already takes the theme's border, radius, focus ring and placeholder
   treatment from the form block above, because its selector is on that rule's
   list; all that is left is to size it and give it its own row, since the button
   below spans the column.

   The browser's spinners stay. They are the platform's stepper, and a pair of
   buttons drawn over them is two controls for one number — plus a second copy of
   the min/max/step arithmetic that Woo rewrites for every variation's stock, and
   which would go stale silently because it rewrites them with .attr() and fires
   no event. Wider than the catalog's field to leave room for the spinners. */
.woocommerce div.product form.cart div.quantity {
	margin: 0 0 1rem;
}

.woocommerce div.product form.cart div.quantity input.qty {
	width: 6rem;
}

/* The one place in this design a button spans its column. It is the page's
   single action, and the fill is already the primary tier's from the button
   block above — only the width and the height are new. */
.woocommerce div.product form.cart .single_add_to_cart_button {
	display: block;
	width: 100%;
	padding-block: 1rem;
}

/* State, and the small print ---------------------------------------------- */

/* Woo's own copy of the chosen variation's price, hidden only while
   product-form.js is mirroring it into the page's single price slot. Without the
   script this is the only place the resolved price appears, which is why it is
   not hidden outright. */
.woocommerce div.product form.grit-pills-ready .woocommerce-variation-price {
	display: none;
}

.woocommerce div.product .woocommerce-variation-availability,
.woocommerce div.product .woocommerce-variation-description,
.grit-product-note {
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
	margin: 0.75rem 0 0;
}

/* p.stock, not .stock: woocommerce-general paints it green at (0,3,2) and a
   shorter selector here loses. Covers the single product's own line and the one
   inside a variation's availability, which is the same element. */
.woocommerce div.product p.stock {
	font-size: var(--grit-text-small);
	color: var(--grit-section-text);
	margin: 0.75rem 0 0;
}

.woocommerce div.product p.stock.out-of-stock {
	color: var(--grit-error);
	font-weight: 600;
}

/* SKU, categories and tags. The design has no slot for them, so they read as
   small print under a rule rather than as another row of content. */
.woocommerce div.product .product_meta {
	margin-top: 1.5rem;
	padding-top: 1.25rem;
	font-size: var(--grit-text-small);
	color: var(--grit-text-muted);
	border-top: 1px solid var(--grit-section-border);
}

.woocommerce div.product .product_meta > span {
	display: block;
}

/* ---------------------------------------------------------------------------
   Utilities
   --------------------------------------------------------------------------- */
.screen-reader-text {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

.skip-link:focus {
	position: static;
	width: auto;
	height: auto;
	margin: 0.5rem;
	padding: 0.5rem 1rem;
	clip: auto;
	background: #fff;
	color: var(--grit-navy);
}
