/* ==========================================================================
   Momentum Care — main.css
   Becomes style.css at the WordPress port.

   Structure:
     1. Tokens
     2. Reset and base
     3. Typography
     4. Utilities and components (buttons, links, lists, cards, placeholders)
     5. Shell (header, nav, footer)
     6. Sections
     7. Decoration
     8. Motion
     9. Review mode
    10. Responsive

   Two rules that are not negotiable:
     - No hex literals outside the token block (T29 may still change a value).
     - --brand-green / --green never carries text and never sits under white text (T32).
       Measured 2.07:1 and 2.50:1 on white. Green is fills, rules, icons and decoration.
   ========================================================================== */

/* ---------------------------------------------------------------- 1. tokens */

:root {
  /* brand tier — match the logo artwork exactly, do not tune (brand.md) */
  --brand-teal:  #00656E;
  --brand-green: #8BC53F;

  /* ui tier — tuned for contrast */
  --ink:         #294647;   /* body text — 10.19:1 on white, AAA */
  --muted:       #5C6862;   /* secondary text — AA on white */
  --green:       #8AB053;   /* fills — and, by client instruction, `.accent-script` only (D74) */
  --green-dark:  #789D46;   /* hover/active on accent fills */
  --green-light: #CDDFA9;   /* soft fills, borders */
  --green-pale:  #F2F7EA;   /* section backgrounds */
  --border:      #E8EDE2;
  --white:       #FFFFFF;
  --teal-tint:   #CFE0E0;   /* soft teal wash, placeholder panels only */

  /* dark surfaces — from the designer's export, both carry white text well */
  --teal-900:    #1E3C3D;   /* 11.6:1 with white */
  --teal-950:    #173032;

  /* text colours that are safe on the dark surfaces */
  --on-dark:          rgba(255, 255, 255, 0.92);
  --on-dark-muted:    rgba(255, 255, 255, 0.72);
  --on-dark-hairline: rgba(255, 255, 255, 0.14);
  --on-dark-fill:     rgba(255, 255, 255, 0.08);

  /* accent that IS readable on dark, unlike --green — used for links in the footer and CTA band */
  --green-on-dark: #B9D77F;   /* measured 7.40:1 on --teal-900, 8.70:1 on --teal-950 */

  /* Accent TEXT is teal, with exactly one logged exception.
     Measured on white: --brand-green 2.07:1, --green 2.50:1, --green-dark 3.13:1. Even the darkest
     green in the family only clears the 3:1 threshold for non-text graphics. Pushing it to 4.5:1
     lands around #5E8131, which is olive and no longer reads as the brand. So the rule from
     brand.md stands as written: green is fills, rules and icons; anything a person reads at label
     size is teal or ink. --green-dark below is for ICON STROKES only. (T32)

     ⚠️ The exception is `.accent-script`, the two handwritten `~phrase~` accents, which are
     --green #8AB053 on the client's instruction (D74). It is a known 2.29:1 and it is the only
     failing text on the site. Nothing else moved: --accent-text below is unchanged and is still
     what every link, nav marker and label uses. */
  --accent-text: var(--brand-teal);   /* 6.80:1 on white, 6.24:1 on --green-pale */

  /* review-mode chrome — never client-facing copy, but still tokenised */
  --review-tag-bg: #00656E;
  --verify-tag-bg: #8A3B12;   /* 6.9:1 with white */

  /* type */
  --font-body:    'Figtree', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-accent:  'Caveat', cursive;

  /* scale — clamped so nothing needs a media query to stay sane */
  --step-h1:  clamp(2.125rem, 1.35rem + 3.4vw, 4rem);
  --step-h2:  clamp(1.625rem, 1.2rem + 1.9vw, 2.5rem);
  --step-h3:  clamp(1.25rem, 1.1rem + 0.7vw, 1.5rem);
  --step-h4:  clamp(1.0625rem, 1rem + 0.35vw, 1.3125rem);
  --step-body: 1rem;            /* 16px floor, accessibility (brand.md) */
  --step-lead: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);
  --step-sm:   0.875rem;
  --step-xs:   0.8125rem;

  /* layout */
  --max:  1200px;
  /* The header runs wider than the page (D83). It is a single row of logo, six nav items and a
     button, so it fills its width instead of setting a measure, and at `--max` it read as pinched
     on a wide screen. The only width on the site that is not `--max`; sections keep that. */
  --max-header: 1400px;
  --max-prose: 65ch;
  --pad:  clamp(1.25rem, 0.75rem + 2vw, 2.25rem);
  --gap:  clamp(1.5rem, 1rem + 2vw, 2.75rem);
  --section-y:    clamp(2.75rem, 2rem + 3.5vw, 4.5rem);
  --section-y-sm: clamp(2rem, 1.5rem + 2.5vw, 3rem);

  --radius:    14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  --shadow-sm: 0 4px 14px rgba(30, 60, 60, 0.05);
  --shadow-md: 0 10px 28px rgba(30, 60, 60, 0.09);

  /* motion (§8, D77). One rise distance and one curve for the whole site, so every reveal on every
     page is the same gesture at the same speed. The rise is deliberately under a line of body text:
     the eye should register that something settled, not watch it travel. */
  --motion-rise: 0.6rem;
  --motion-in:   0.5s;
  --motion-ease: cubic-bezier(0.22, 0.61, 0.36, 1);   /* ease-out: fast to start, soft to land */
}

/* ---------------------------------------------------- 2. reset and base */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* Anchor targets clear the sticky header (D128). `.site-header` is `position: sticky` over a 4.5rem
   row, so without this every `#s-` link lands with the target's heading hidden behind the header and
   the reader starting mid-section. `scroll-margin-top` on the target rather than padding on the
   section, so nothing about the page's own spacing changes; the extra 1rem is breathing room, not
   part of the measurement. Measured working: `/contact/#s-what-to-send` puts the H2 at 169px with the
   header ending at 73px.

   ⚠️ **This fixes where an anchor lands, not whether it fires.** A *same-page* jump (the `#s-apply`
   and `#s-why-us` links inside a page) works. A **cross-page** one — arriving at `/contact/#s-what-to-
   send` from another page, which is exactly what the header's Referrals button now does — does not
   scroll at all in Chrome: `scroll-behavior: smooth` above animates the load-time fragment scroll and
   the animation is cancelled by the layout shifts during load, leaving the reader at the top. Removing
   `smooth` makes it fire but land wrong (it scrolled to the foot of `/careers/`, the document having
   been taller when the scroll was computed). **Pre-existing and site-wide** — it affects the eight
   `#s-` links already in `data/`, not just the new one — and open as **T45**. Do not "fix" it by
   deleting `smooth` here without reading that entry. */
section[id] { scroll-margin-top: calc(4.5rem + 1rem); }

/* The blanket reduced-motion guard. It flattens durations, which neutralises every transition and
   every time-based animation on the site.
   ⚠️ It does NOT neutralise a scroll-driven animation, whose progress comes from scroll position and
   never from `animation-duration`. Squashing the duration of one of those leaves the element frozen
   at 0% — which for a fade-in means permanently invisible copy for exactly the users who asked for
   less movement. Everything in §8 is therefore declared inside `prefers-reduced-motion:
   no-preference` and does not exist at all here, rather than being overridden back out. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-body);
  line-height: 1.6;
  color: var(--ink);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
}

/* A component that sets its own `display` (.card is flex, .btn is inline-flex) beats the browser's
   default [hidden] rule, so the homes filter would leave a "hidden" card on the page. Anything the
   filter hides has to be gone from the layout and from the accessibility tree, not just quieter. */
[hidden] { display: none !important; }

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

a { color: var(--ink); text-decoration-color: var(--green-dark); text-underline-offset: 0.18em; }
a:hover { text-decoration-color: currentColor; }

/* Visible focus everywhere. Part of the audience navigates by keyboard. */
:focus-visible {
  outline: 3px solid var(--brand-teal);
  outline-offset: 2px;
  border-radius: 3px;
}
/* ⚠️ ...but --brand-teal is a dark colour and the dark surfaces are dark, so on those it was a ring
   nobody could see. Measured: **2.05:1 on --teal-950** (the footer) and **1.74:1 on --teal-900** (the
   CTA band, the trust bar, the quicklinks bar), against the 3:1 that WCAG 1.4.11 asks of a focus
   indicator. --green-on-dark is 8.70:1 and 7.40:1 on the same two grounds — it is already the token
   for "accent that survives on dark" and it is what the links there use.
   Found while reworking the footer's link states (D79); it is a keyboard dead end on every dark band
   on the site, not a footer problem. The light-surface ring is unchanged. */
:is(.section--dark, .trustbar, .cta-band, .quicklinks, .site-footer) :focus-visible {
  outline-color: var(--green-on-dark);
}

.container { max-width: var(--max); margin-inline: auto; padding-inline: var(--pad); }

.skip-link {
  position: absolute; left: 0; top: 0; z-index: 100;
  transform: translateY(-120%);
  background: var(--brand-teal); color: var(--white);
  padding: 0.75rem 1.25rem; font-weight: 700;
}
.skip-link:focus { transform: translateY(0); }

.visually-hidden {
  position: absolute !important; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* ------------------------------------------------------- 3. typography */

h1, h2, h3, h4, h5 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-variation-settings: 'SOFT' 0, 'WONK' 1;
  color: var(--ink);
  line-height: 1.12;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
h1 { font-size: var(--step-h1); }
h2 { font-size: var(--step-h2); }
h3 { font-size: var(--step-h3); }
h4 { font-size: var(--step-h4); line-height: 1.2; }

p { margin: 0 0 1em; max-width: var(--max-prose); }
p:last-child { margin-bottom: 0; }

.lead { font-size: var(--step-lead); color: var(--muted); }

.eyebrow {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--accent-text);
  margin: 0 0 0.5rem;
  display: flex; align-items: center; gap: 0.5rem;
}
/* The green identity in an eyebrow comes from a rule, not from the letters. */
.eyebrow::before {
  content: ''; flex: none;
  width: 1.5rem; height: 3px; border-radius: 2px;
  background: var(--brand-green);
}

/* --- inline display treatments inside a heading (D41) --- */

/* ==phrase== — the designer's highlighter wash. This is green doing the one job it is allowed to do
   near words: sitting behind them as a fill, with the text still --ink. Measured 7.88:1 (T32). */
/* The wash is a layer of its own, not a background on the span, because at 1.12 leading the lines of
   a display serif interlock: the g of "Helping" hangs 8px into the line box below it, lower than the
   ascenders of "independently" start. A background painted on the span therefore has to cover one or
   the other, and since a later line paints over an earlier one, it swallows the g.
   The wash goes in the heading's own negative layer instead — below every letter in the heading,
   above the section behind it. The heading is isolated so it stays inside that heading and does not
   fall behind a pale hero band (D47). */
h1:has(.hl), h2:has(.hl), h3:has(.hl) { isolation: isolate; }

.hl {
  position: relative;
  padding: 0.02em 0.2em;
}
.hl::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--green-light);
  border-radius: 3px;
}

/* D63's failure again, in the other narrow heading box. A `prose`/`split` heading sits in a 22rem
   rail — narrower than the hero card that first exposed this — and "Digital ==from day one==" broke
   it: "from day" on one line, "one" on the next, and Chrome does not paint the second fragment of a
   split inline, so the mark silently disappeared. The rail measures 352px and the longest wash on
   the site measures 273px inside it, so holding the phrase together cannot push a heading wider than
   its rail. Same guarantee as the hero rule below: a wash is one short phrase by the rule that
   creates it (D41). */
:is(.split__head, .section__head) .hl { white-space: nowrap; }

/* ~phrase~ — the handwritten accent on a heading's closing phrase.

   ⚠️ **This is the one text colour on the site that fails contrast, and it is a client instruction
   (D74).** The colour is `--green` #8AB053, matching the export's `.script` on "People come first,
   *always*" — measured **2.50:1 on white, 2.29:1 on --green-pale**, against a 3:1 large-text
   threshold. It is deliberate, it is logged, and it is the documented exception to D33 rather than a
   drift away from it. **D33 otherwise stands unchanged**: every other piece of text on the site is
   ink or teal, and green stays fills, rules and icon strokes. Do not read this rule as permission to
   put green on anything else, and do not "fix" it back to teal without checking D74 first.

   Both accents sit on light surfaces — Home's `s-intro` on white, About's `s-our-name` on the pale
   band — so the pale-band figure is the one that governs. `.section--dark` below still overrides to
   --green-on-dark; that path is unaffected and still passes.

   The swoosh beneath it is an SVG used as a mask rather than an <img>, so the rule takes a token
   colour and no hex is baked into the artwork. That file carries preserveAspectRatio="none" so the
   curve stretches to the width of the phrase instead of being letterboxed to a stub in the middle of
   it. */
.accent-script {
  position: relative;
  display: inline-block;
  font-family: var(--font-accent);
  font-weight: 700;
  font-size: 1.3em;
  line-height: 0.95;
  letter-spacing: normal;
  color: var(--green);
  vertical-align: -0.04em;
  /* Clears descenders off the swoosh — Caveat's g/p/y hang a long way below the baseline. */
  padding-bottom: 0.16em;
}
.accent-script::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 0.16em;
  background: var(--brand-green);
  -webkit-mask: url('../img/underline-swoosh.svg') center / 100% 100% no-repeat;
  mask: url('../img/underline-swoosh.svg') center / 100% 100% no-repeat;
}

/* --- style U: the swoosh alone (D127) -------------------------------------------------------------
   Client instruction 2026-08-25, named by them: "underline only with no text / font change". It is
   `.accent-script` with the font and the colour taken away — same face as the heading around it, same
   ink, and the swoosh underneath.

   ✅ **The only one of the three treatments that costs nothing in text contrast.** The words keep the
   heading's own colour — ink on a light section, white on a dark one — so nothing a person reads is
   weakened. That makes style U the treatment to reach for when a heading wants a mark and the page
   has already spent the D74 exception. Do not "improve" it by tinting the text: that turns it into
   the thing D33 forbids and D74 has already made one exception to.

   **The swoosh itself is decoration and measures accordingly.** --brand-green #8BC53F is **5.74:1 on
   --teal-900** and **2.07:1 on white**. It carries no meaning and duplicates nothing, so 1.4.11 does
   not bite, and the same figures already apply to `.accent-script`'s swoosh everywhere on the site.
   But do not use style U as a *quiet* mark on white and expect it to be seen: on a light section it
   is a faint rule, and on the dark band — where its first use, `s-contact-details`, sits — it is
   crisp. If style U is ever asked for on a pale section and needs to read, raise it rather than
   darkening the green here; that token is shared with fills, rules and icon strokes site-wide.

   Less bottom padding than `.accent-script` because Fraunces and Figtree hang shallower descenders
   than Caveat does. */
.uline {
  position: relative;
  display: inline-block;
  padding-bottom: 0.1em;
}
.uline::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 0.12em;
  background: var(--brand-green);
  -webkit-mask: url('../img/underline-swoosh.svg') center / 100% 100% no-repeat;
  mask: url('../img/underline-swoosh.svg') center / 100% 100% no-repeat;
}

/* No treatment is used on a dark surface today, but the partials are reusable and the CTA band is
   dark. Failing readably beats failing silently: teal accent text vanishes on --teal-900, and a pale
   wash behind white text inverts it. Style U needs no override — its text is whatever the heading
   around it is, and the swoosh reads on both surfaces. */
.section--dark .accent-script { color: var(--green-on-dark); }
.section--dark .hl { background: var(--on-dark-fill); }

ul, ol { margin: 0; padding: 0; }
ul { list-style: none; }

/* Missing values (D16). Deliberately conspicuous: a visible gap prompts a question,
   an invented value becomes a false claim nobody catches. */
.ph-value {
  background: repeating-linear-gradient(-45deg, var(--green-light) 0 6px, transparent 6px 12px);
  border-bottom: 2px dotted var(--green-dark);
  padding: 0 0.25em;
  font-style: normal;
  white-space: nowrap;
}
.ph-value::before { content: '['; }
.ph-value::after  { content: ']'; }

/* --- justified body copy, Home only (D194) ------------------------------------------------------
   Client instruction, 2026-09-02: *"justify the content on the homepage"*. Scoped to `.page-home`,
   which `data/home.php` sets through `'body_class'` — no partial and no other page is touched.

   ⚠️ **It reaches wide, multi-line body copy and nothing else, and the exclusions are the point.**
   A justified line spreads its spare space between the words on it, so the fewer words a line holds
   the wider each gap opens. Measured at 1280 with a normal word space of 3.9px: the two prose
   sections run 734px wide, the FAQ answers 666, the commissioner list 583, the careers band 532 and
   the service cards 497, and across the 45 justified lines two gaps pass 8px, the worst 11.6. The
   excluded blocks are the narrow ones — the hero standfirst is 372px of `--step-lead`, the six
   journey cards and the three referral cards 287 to 290, the quicklinks 207 to 246. At that measure
   the same rule opens rivers of white down the middle of a card.
   ⛔ Do not extend this to `.step p`, `.hero__lead` or `.quicklinks__text p`.

   ⚠️ **Two gates, and the second one is not a tidier version of the first.** A paragraph's measure
   here is not monotonic in the viewport, because these sections change column count at different
   widths: `.prose__body p` is 571px at a 620 viewport, 750 at 960 and 734 at 1280, while
   `.card__body p` — two-up from 48rem — collapses to **295px at 768** before climbing back to 415
   at 1024. So the prose, the FAQ, the commissioner list and the careers band justify from 40rem
   (they are 506px or wider everywhere above it), and the service cards wait for 62rem, which is
   where the card clears 400px again. Below 40rem every one of them is a single column under 340px
   and the page keeps the ragged right edge it should have.

   ⚠️ **`hyphens: auto` is load-bearing, not a flourish.** With it, two lines in 45 open a gap over
   8px and one passes 10px; without it, seven and two. `<html lang="en-GB">` in `head.php` is what
   picks the dictionary — if that attribute ever changes, measure this again.
   ⚠️ **`hyphenate-limit-chars: 8 4 4` is the other half of it**: unbounded, the browser will break
   after two or three letters, and "pro-fessionals" over a line end is the one thing that reads as a
   typographic accident rather than a setting. Four letters minimum on each side of the break, and
   no word under eight letters broken at all.

   🟡 Justified text is a **WCAG 2.4.8 (AAA)** exception — the criterion asks for text that is not
   justified, and the uneven spacing is hardest on dyslexic readers, who are part of this site's own
   audience. Nothing at AA is affected and the client asked for it, so it ships. **Raise it at
   review**: undoing it is deleting this block. */
@media (min-width: 40rem) {
  .page-home :is(.prose__body, .faq-item__answer, .feature-list) p,
  .page-home .band-card > * > p:not(.eyebrow, .band-card__cta) {
    text-align: justify;
    hyphens: auto;
    hyphenate-limit-chars: 8 4 4;
  }
}
@media (min-width: 62rem) {
  .page-home .card__body p {
    text-align: justify;
    hyphens: auto;
    hyphenate-limit-chars: 8 4 4;
  }
}

/* ------------------------------------- 4. utilities and components */

/* --- buttons --- */
.btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.875rem 1.5rem;
  border: 2px solid transparent;
  border-radius: var(--radius-pill);
  font-family: var(--font-body); font-weight: 700; font-size: var(--step-sm);
  text-decoration: none; cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.btn .ic { width: 1em; height: 1em; flex: none; }

.btn--primary { background: var(--brand-teal); color: var(--white); }
.btn--primary:hover { background: var(--teal-900); }

/* Outlined, not filled green: white on --green is 2.07:1 and fails (T32). */
.btn--secondary { background: var(--white); color: var(--ink); border-color: var(--border); }
.btn--secondary:hover { border-color: var(--green-dark); }

.btn--on-dark { background: var(--white); color: var(--ink); }
.btn--on-dark:hover { background: var(--green-pale); }

.btn--ghost { background: transparent; color: var(--ink); border-color: var(--border); }
.btn--ghost:hover { border-color: var(--green-dark); }

/* --- arrow link --- */
/* Label in teal, arrow in green: the accent colour rides the icon, which only has to clear the
   3:1 graphics threshold, while the words a person reads clear 6.8:1. */
.link-arrow {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-weight: 700; font-size: var(--step-sm);
  color: var(--accent-text);
  text-decoration: none;
}
.link-arrow .ic {
  width: 0.9em; height: 0.9em;
  --stroke-0: var(--green-dark);
  transition: transform 0.15s ease;
}
.link-arrow:hover { color: var(--ink); text-decoration: underline; }
.link-arrow:hover .ic { transform: translateX(3px); }

/* --- tick list --- */
.check-list { display: grid; gap: 0.7rem; }
.check-list li { display: flex; gap: 0.65rem; align-items: flex-start; }
.check-list .ic {
  width: 1.05rem; height: 1.05rem; flex: none; margin-top: 0.3rem;
  --stroke-0: var(--green-dark);
}

/* --- cards --- */
.card {
  display: flex; flex-direction: column;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.card__body { padding: 1.25rem 1.375rem 1.5rem; display: flex; flex-direction: column; flex: 1; gap: 0.6rem; }
.card__body > .link-arrow { margin-top: auto; padding-top: 0.75rem; }
.card--link { transition: transform 0.18s ease, box-shadow 0.18s ease; }
.card--link:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }

.card-grid { display: grid; gap: var(--gap); grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); }
/* A card's state pill — "Coming soon" on the homes that have not opened (D90).
   Teal text on the pale green fill, which measures 6.24:1 (the `--accent-text` note in §1). Not
   green text on green: no green on this site carries text at any size (D33). Not the `[Placeholder]`
   chip treatment either — a chip means "we do not know", this means "not yet", and two states that
   mean different things must not look the same.

   ⚠️ Selector is `.card .card__status`, not `.card__status`. The pill is a `<p>`, and `.card p`
   three lines down sets `--muted` and `--step-sm` at (0,1,1) — it beats a lone class and silently
   repaints the pill as ordinary secondary text at the wrong size. Caught by measuring the rendered
   colour, not by reading the file. */
.card .card__status {
  align-self: start; justify-self: start;
  margin: 0 0 0.5rem;
  padding: 0.2rem 0.65rem;
  background: var(--green-pale);
  border: 1px solid var(--green-light);
  border-radius: var(--radius-pill);
  font-size: var(--step-xs); font-weight: 700; letter-spacing: 0.01em;
  color: var(--accent-text);
}

.card__meta { display: grid; gap: 0.35rem; }
.card__meta li { display: flex; align-items: center; gap: 0.5rem; font-size: var(--step-sm); color: var(--muted); }
.card__meta .ic { width: 0.9rem; height: 0.9rem; flex: none; --stroke-0: var(--green-dark); }
/* `h2` as well as `h3` because a card's heading level is the page's outline talking, not the card's
   size (D178): `/our-care/`'s grid is the hub's whole content and its titles are `h2`. A card title
   is the same size wherever it appears, so the two are styled together and nothing on that page
   moved by a pixel. */
.card :is(h2, h3) { font-size: var(--step-h4); }
.card p { font-size: var(--step-sm); color: var(--muted); }

.section__footer-link, .steps__cta, .band-card__cta { margin-top: var(--gap); }
/* The line that closes a list and qualifies all of it: `pills`'s body, and `features`'s note on
   Careers. Same treatment either side, because it is the same thing on the page. */
.pills__body, .list-note { margin-top: 1.5rem; }
.pills__body p, .list-note p { color: var(--muted); }

/* --- image placeholders (T06) --- */
.ph-image {
  position: relative; display: grid; place-items: center;
  aspect-ratio: var(--ratio, 4 / 3);
  border-radius: var(--radius-lg); overflow: hidden;
  background:
    repeating-linear-gradient(135deg, rgba(255,255,255,0.5) 0 14px, transparent 14px 28px),
    linear-gradient(135deg, var(--green-light), var(--green-pale) 60%, var(--teal-tint));
  border: 1px dashed var(--green-dark);
}
.ph-image__label {
  display: flex; align-items: center; gap: 0.5rem;
  max-width: 22ch; padding: 0.55rem 0.9rem;
  background: var(--white); border-radius: var(--radius-pill);
  font-size: var(--step-xs); font-weight: 700; color: var(--ink);
  text-align: center; box-shadow: var(--shadow-sm);
}
.sample-value {
  /* Sample content under D17 — a real-looking value standing in for one nobody has confirmed. It is
     quieter than a [Placeholder] chip on purpose: a placeholder marks an absence and should
     interrupt, a sample marks a value that reads correctly and only needs to stay identifiable. */
  border-bottom: 1px dashed var(--green-dark);
  cursor: help;
}
body[data-review] .sample-value::after {
  content: "sample";
  margin-left: 0.4rem; padding: 0.05rem 0.35rem;
  background: var(--review-tag-bg); color: var(--white);
  border-radius: 4px;
  font-size: 0.625rem; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase;
}

.ph-image__icon { width: 1rem; height: 1rem; flex: none; --stroke-0: var(--green-dark); --fill-0: var(--green-dark); }

/* Inside a card the placeholder is flush to the top edge — the card already has the corner
   radius and the border, and two dashed outlines nested inside each other read as a bug. */
.card .ph-image.card__media { border-radius: 0; border-width: 0 0 1px; }

/* --- tile cards: a picture, a title, and the whole card is the link ---
   Client instruction 2026-09-03. Home's `s-services` was media, title, a paragraph and a text link;
   it is media and title now, and the `<article>` is an `<a>` (see partials/sections/cards.php).

   ⚠️ **The lift, the shadow and the photograph's 1.04 all come free** — the tile carries
   `.card--link`, so §8's existing card hover already applies and nothing new was written for it.
   That is the point of reusing the class rather than inventing a `--cta` treatment: one card
   language on the site, and a tile behaves exactly like the cards a reader has already met. */
.card--tile { text-decoration: none; color: inherit; }
/* The body is a single row — title left, arrow right — rather than the stacked column every other
   card uses, because there is nothing under the title to stack. `flex: 0 0 auto` stops it taking
   the column's leftover height: without it a short title floats in the middle of a tall strip.
   ⚠️ `justify-content: space-between` and not a `margin-left: auto` on the arrow, so a title that
   wraps to two lines still keeps the arrow beside it rather than under it. */
.card--tile .card__body {
  flex: 0 0 auto; flex-direction: row; align-items: center; justify-content: space-between;
  gap: 1rem;
  padding: 1.125rem 1.375rem;
}
.card--tile :is(h2, h3) { margin: 0; }
/* Teal, matching `.link-arrow`'s: on this card the arrow is the only thing saying the tile goes
   anywhere, so it is the site's link colour and not a decorative green. */
.card__arrow { width: 1.15rem; height: 1.15rem; flex: none; --stroke-0: var(--accent-text); }
/* The global ring is `border-radius: 3px`, drawn for inline links. On a card-sized target that reads
   as a rounded rectangle traced around a squarer one, so the ring follows the card's own corner. */
.card--tile:focus-visible { border-radius: var(--radius); }

/* --- split cards: two-up, media in a side column (D54) ---
   For grids where the picture is the point rather than a decoration on a routing card — the homes
   hub, where a photo of the house is the first thing a family reads. Two per row buys an image
   roughly twice the width of the three-up card, which is the whole reason for the layout. */
.card-grid--split { grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr)); }
@media (min-width: 78rem) {
  /* Above this width auto-fit would find room for a third column and undo the point. */
  .card-grid--split { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 48rem) {
  .card--split { display: grid; grid-template-columns: 45% 1fr; align-items: stretch; }
  /* Two things set the size of the picture, and both have to be here or the layout loses to the
     three-up it replaced. The column is 45%, and the panel carries a floor of 16rem — without it the
     card collapses to the height of four short meta lines and the "bigger" image comes out smaller
     than the full-width band it replaced. Above the floor it stretches to whatever the copy needs,
     so two cards in a row always match. */
  .card--split .ph-image.card__media {
    height: 100%; aspect-ratio: auto; min-height: 16rem;
    border-width: 0 1px 0 0;
  }
  .card--split .card__body { padding: 1.5rem 1.625rem; justify-content: center; }
}

/* The same slot holding a real photograph instead of a label. It keeps the box and the ratio and
   drops the waiting-for-artwork chrome: no dashed border, no gradient, nothing showing through. The
   image fills the slot and is cropped to it, so swapping a placeholder for a photograph cannot
   change the height of anything around it. */
.ph-image--photo { border: 0; background: none; }
/* Absolute, not just `height: 100%`. A percentage height inside the ratio box resolves against a row
   the image is itself sizing, so a portrait file wins and the tile grows to fit it — three landscape
   photographs and one portrait in a gallery row then stand at different heights. Taking the image
   out of flow makes `--ratio` the only thing that decides the tile, whatever shape the file is.
   Found on Careers' training rooms (T19); it would have found us again at the property shoot (T06). */
.ph-image--photo > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* --- section scaffolding --- */
.section { position: relative; padding-block: var(--section-y); }
.section--tight { padding-block: var(--section-y-sm); }
.section--pale { background: var(--green-pale); }
.section--dark { background: var(--teal-900); color: var(--on-dark); }
.section--dark h2, .section--dark h3, .section--dark h4 { color: var(--white); }
.section--dark p { color: var(--on-dark-muted); }
.section--dark .eyebrow { color: var(--green-on-dark); }
.section--dark .link-arrow { color: var(--green-on-dark); }
.section--dark .link-arrow:hover { color: var(--white); }

.section__head { max-width: 46rem; margin-bottom: var(--gap); }
.section__head .lead { margin-top: 0.75rem; }

/* `centred` (D162): the head, and the closing note under the section, centred in the column.
   ⚠️ **It is a utility, opted into from `data/` through the `class` key** — the same door the decor
   masks use (D67) — because it is orthogonal to a partial's `layout`. `steps` already spends `layout`
   on `cards`, so a `layout => 'centred'` there would have to be parsed out of a string; and the two
   sections that want it on `/referrals/` are different section types.

   One consumer today: `/referrals/`'s `s-what-to-send` and `s-referral-process` (D162). Every other
   section on that page — the closing band, the FAQ accordion and the locations band — was centred
   already, so those two were the whole of the page's disagreement with itself, and with the reference
   page, which centres every section head it has.

   ⚠️ **The children are centred individually, not just the block.** `p` carries `max-width:
   var(--max-prose)` from §2, so a lead inside a centred 46rem head still sits against the left edge of
   it — off-centre by the difference. `.faq--centred` (D160) has the same shape and no lead to show it.

   ⛔ Not for a section whose head sits in a `split` rail: that column is 22rem and left-anchored by
   construction, and centring inside it reads as a mistake rather than a decision. */
.section--centred .section__head { margin-inline: auto; text-align: center; }
.section--centred .section__head > * { margin-inline: auto; }
.section--centred :is(.grouplist__note, .steps__note) { margin-inline: auto; text-align: center; }
/* ⚡ **The body copy of a centred `prose` section is centred with its head (D224, 2026-09-02)** —
   *"center align the second paragraph"*, `/activity-hub/`'s `s-a-space-to-belong`. Until today
   `.section--centred` centred the head and left everything under it hard left, which on a `prose`
   section is the utility half-applied: a centred heading over four paragraphs pinned to the left of
   the same column reads as a mistake.

   ⚠️ **Both declarations are needed, for the reason documented above.** `p` carries `max-width:
   var(--max-prose)` from §2, so `text-align: center` alone centres each line inside a block that is
   itself sitting against the left edge of a wider column — off-centre by the difference.

   ⚠️ **One consumer, and it is the only centred running copy on the site.** Ragged on both edges is
   harder to read than ragged right, which is why nothing else takes it; a section opts in from
   `data/` and it is one key to drop. ✅ It cannot reach the two `features--icons` sections that emit
   `section--centred` themselves — they render `.icongrid__body`, not `.prose__body`, and their leads
   are already centred by the head rule above. */
.section--centred .prose__body { text-align: center; }
.section--centred .prose__body > * { margin-inline: auto; }

/* two-column split: narrow heading rail beside content */
.split { display: grid; gap: var(--gap); align-items: start; }
@media (min-width: 62rem) {
  .split { grid-template-columns: minmax(15rem, 22rem) 1fr; }
  .split--wide-first { grid-template-columns: 1fr minmax(15rem, 22rem); }
  .split--even { grid-template-columns: 1fr 1fr; }
  .split__head { position: sticky; top: 1.5rem; }
}

/* --------------------------------------------------------- 5. shell */

/* --- header --- */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: var(--white);
  border-bottom: 1px solid var(--border);
}
.site-header__inner { display: flex; align-items: center; gap: 1.25rem; min-height: 4.5rem; }
/* Wider than the page container (D83), and the width lives on `.container` rather than on `__inner`
   so the padding, the centring and the mobile behaviour all still come from the one rule in §2.
   Below 1200px nothing changes — the header was already viewport-width there. Between 1200 and 1400
   it runs the full viewport while the page stays capped, so the logo sits left of the content column
   across the whole desktop range, not just above 1400. That offset is the known cost of this rule;
   the footer is deliberately still at `--max`. The border-bottom and the scroll-progress bar are on
   `.site-header` itself and stay full-bleed. */
.site-header .container { max-width: var(--max-header); }

/* Logo stays on white. There is no white-on-dark variant and the raster cannot be
   recoloured in CSS (T28) — filter:invert() turns the teal orange. */
.site-logo { display: inline-flex; align-items: center; flex: none; }
.site-logo img { width: auto; height: 2.5rem; }
@media (min-width: 48rem) { .site-logo img { height: 2.875rem; } }

.main-nav { margin-left: auto; }
.main-nav__list { display: flex; align-items: center; gap: 0.25rem; }
.main-nav__link {
  position: relative;
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.6rem 0.7rem; border-radius: 8px;
  font-weight: 600; font-size: 0.9375rem; text-decoration: none;
  white-space: nowrap;
}
.main-nav__link:hover { background: var(--green-pale); }
.main-nav__link .ic { width: 0.6rem; height: 0.6rem; opacity: 0.6; }

/* Current page marker.
   Was a 2px inset rule across the whole padded link, which the 8px radius clipped into a curved
   trough and which read as a stray border under an item that is otherwise a pill. The marker is now
   the same short green rule the eyebrow uses (§3), so "you are here" is said in the one piece of
   vocabulary this site already has for it — a green rule beside a teal label. Colour is not doing
   the work on its own: aria-current carries the state, and the rule carries it visually.
   Teal, not bolder, because a weight change would resize the item and nudge its neighbours. */
.main-nav__link[aria-current='page'] { color: var(--accent-text); }
.main-nav__link[aria-current='page']::after,
.main-nav__link[data-nav='section']::after {
  content: '';
  position: absolute; left: 50%; bottom: 0.3rem;
  width: 1.15rem; height: 3px; margin-left: -0.575rem;
  border-radius: 2px;
  background: var(--brand-green);
}
/* A parent of the open page, e.g. Our care while a specialism page is showing. The page itself is
   marked inside the dropdown, which is closed, so without this the nav says nothing about where you
   are. Paler and unteal: it is a route, not the destination. */
.main-nav__link[data-nav='section']::after { background: var(--green-light); }
/* The chevron is part of the link box, so a rule centred on the box sits right of the label it
   belongs to. Pull it back by half the icon and its gap. */
.has-children .main-nav__link::after { margin-left: -1.025rem; }

.submenu a[aria-current='page'] { background: var(--green-pale); color: var(--accent-text); }

.has-children { position: relative; }
/* ⚠️ **`z-index` here is load-bearing and is not a tidy-up.** The header's scroll-progress bar is
   `.site-header::after` (§8) — full-bleed, 3px, `bottom: -1px`, so it occupies y71–74 of a 73px
   header. An open dropdown starts at the nav row's baseline, ~y57, so the bar's band falls *inside*
   the panel, about 13px down from its top edge. Both were `z-index: auto`, and a pseudo-element
   paints after every child of its own element, so the bar drew a green line straight across the
   menu. Raising the panel one step is the fix: the bar cannot be pushed down (it sits on the border
   line by design) and cannot be sent behind (`z-index: -1` would put it under the header's own white
   background and delete it). ⛔ Do not "simplify" this back to `auto`. */
.submenu {
  position: absolute; left: 0; top: 100%; min-width: 15rem; z-index: 1;
  display: grid; padding: 0.5rem;
  background: var(--white);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  opacity: 0; visibility: hidden; transform: translateY(-6px);
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}
.has-children:hover > .submenu,
.has-children:focus-within > .submenu { opacity: 1; visibility: visible; transform: translateY(0); }
.submenu a {
  padding: 0.55rem 0.7rem; border-radius: 8px;
  font-weight: 600; font-size: 0.9rem; text-decoration: none;
}
.submenu a:hover { background: var(--green-pale); }

.header-cta { flex: none; }

/* mobile nav — <details> so it works with JS off */
.nav-toggle { display: none; }
.nav-toggle > summary {
  display: flex; align-items: center; gap: 0.5rem;
  list-style: none; cursor: pointer;
  padding: 0.6rem 1rem; margin-left: auto;
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  font-weight: 700; font-size: var(--step-sm);
}
.nav-toggle > summary::-webkit-details-marker { display: none; }
.nav-toggle__bars { display: grid; gap: 3px; }
.nav-toggle__bars span { display: block; width: 1rem; height: 2px; background: var(--ink); }

/* --- footer --- */
/* --- the footer's curved top edge (D237, 2026-09-03) ---------------------------------------------
   *"do the wave on the footer."* The mirror of the hero's arc (D236): the same shape and the same
   height clamp, drawn so the **footer rises into the section above it** rather than the section
   dipping into the footer.

   ★ **It is `--teal-950`, the footer's own colour, and that is the one structural difference from a
   hero's arc.** A hero paints its arc in the colour of what follows; the footer cannot, because it is
   not a sibling of the sections — `partials/layout.php` puts them in `<main>` and the footer after it
   — so no selector can reach the last section's surface, and an arc guessing at it would go wrong the
   first time a page's closing band changed. Painting the footer's own colour upward needs to know
   nothing about what is above it, and is right on every page.

   ⚠️ **It sits outside the footer's box** (`top` is minus its own height, plus 1px of overlap to close
   the hairline) and paints over the foot of the last section. That is safe because the footer comes
   after `<main>` in tree order and therefore paints later than every section in it — §8 promotes each
   `.section` to its own stacking context, and this is the one place on the site where something has to
   paint over one of them. ⛔ Do not reach for a `z-index`: it needs the right paint step, not a layer
   number, and a number here would lift the arc over the footer's own content too.

   ⚠️ **The surface it rises into is white on ten of the eleven active pages, not dark**, and that is
   worth knowing before anybody "softens" it. Ten close on the referral strip, and since D199/D203 that
   section is **transparent** — the dark surface is a rounded *card* inside the container, not the
   section's background — so the arc meets white either side of the card. `/careers/` ends on white
   outright (D205 took its closing band off). ⚠️ **`/contact/` is the only page where the arc is dark
   on dark**, #173032 rising into the `--teal-900` of `cta-band--map`, and there it is nearly
   invisible; that is honest, because there is no edge to mark. ✅ Verified by hit-testing 6px above
   the footer's top edge: the footer's own pseudo at the centre of the span, the section behind it at
   the margin — which is the arc's shape. Checked at 375, 768 and 1280. */
.site-footer { position: relative; background: var(--teal-950); color: var(--on-dark-muted); padding-top: var(--section-y); }
.site-footer::before {
  content: "";
  position: absolute; inset-inline: 0;
  height: clamp(0.875rem, 0.625rem + 1vw, 1.5rem);
  top: calc(1px - clamp(0.875rem, 0.625rem + 1vw, 1.5rem));
  background: var(--teal-950);
  border-start-start-radius: 60% 100%;
  border-start-end-radius: 60% 100%;
}
.site-footer h2, .site-footer h3 { color: var(--white); }
.site-footer a { color: var(--on-dark-muted); text-decoration: none; }
.site-footer a:hover { color: var(--white); }

/* The footer's hover mark is a rule that draws in from the left, not an underline (D79).
   An underline appearing under a link in a column of links is the one hover treatment that changes
   the shape of the list while you read it, and on a five-column footer the effect is a row of text
   twitching. The rule is drawn under the link instead, in the site's own vocabulary — the same
   left-anchored stroke as the eyebrow rule, the highlighter wash and the journey track (D78).

   Three things this is doing besides looking better:
   - It is a **shape**, not a colour, so hover is not signalled by colour alone. The white text swap
     stays as well, so there are two indicators rather than one.
   - It is on `:focus-visible` too, so a keyboard user gets the same mark a mouse user gets.
   - The `transform` sits here, not in §8, so it still works with reduced motion — that user gets the
     rule instantly instead of drawn. §8 adds only the easing. A hover state that lives entirely
     inside a motion block disappears for the people most likely to need the affordance.

   The logo is excluded: it is a link wrapping an image and a rule under it is a rule under nothing. */
:is(.footer-col, .footer-contact, .site-footer__bottom) a { position: relative; }
:is(.footer-col, .footer-contact, .site-footer__bottom) a::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: -0.18em;
  height: 2px; border-radius: 2px;
  background: var(--green-on-dark);
  transform: scaleX(0);
  transform-origin: left center;
}
:is(.footer-col, .footer-contact, .site-footer__bottom) a:is(:hover, :focus-visible)::after {
  transform: scaleX(1);
}

/* Brand block plus the three link columns and the contact block: five slots on desktop, so the
   footer is one row rather than an orphaned second row. */
.site-footer__top {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
  padding-bottom: var(--section-y-sm);
}
@media (min-width: 62rem) {
  .site-footer__top { grid-template-columns: 1.5fr repeat(4, 1fr); }
  .site-footer__brand { max-width: 26rem; }
}

/* No white-on-dark artwork exists, so the logo is flattened to a white silhouette for dark grounds
   (D55). brightness(0) takes every colour to black; only then can invert(1) give white. invert(1)
   alone would keep the luminance relationships and turn the teal orange. */
.site-logo--mono img { filter: brightness(0) invert(1); }
.site-footer__brand .site-logo img { height: 2.5rem; width: auto; }

.site-footer__blurb { margin-top: 1rem; font-size: var(--step-sm); }

/* The two social profiles under the blurb (D105). Circular plates rather than bare glyphs, matching
   the medallions the quicklinks and trust bars use, so the footer's one graphic element belongs to
   the set. The drawn-rule hover above is deliberately not extended to them — a 2px rule under a
   circle reads as a defect — so these get their own state: the plate lifts to `--green-on-dark`,
   which is the same colour the rule uses and passes on this ground. 2.75rem clears the 44px tap
   target with the gap around it. */
.footer-social { display: flex; gap: 0.6rem; margin-top: 1.25rem; }
.footer-social a {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  --stroke-0: var(--on-dark-muted); --fill-0: var(--on-dark-muted);
}
.footer-social .ic { width: 1.35rem; height: 1.35rem; }
.footer-social a:is(:hover, :focus-visible) {
  background: rgba(255, 255, 255, 0.16);
  --stroke-0: var(--green-on-dark); --fill-0: var(--green-on-dark);
}

.footer-col__heading {
  font-family: var(--font-body); font-weight: 700; font-size: var(--step-sm);
  color: var(--white); margin: 0 0 0.9rem;
}
.footer-col ul { display: grid; gap: 0.65rem; }
.footer-col li { font-size: var(--step-sm); }

.footer-contact { display: grid; gap: 0.75rem; font-size: var(--step-sm); }
/* ⚡ **The phone number is set like the rest of the footer, client instruction 2026-09-02 (D198)** —
   *"The talk to us number: 0116 502 2975 → Make it the same font"*. It was `--font-display` at
   1.3rem, the one place in the footer where Fraunces carried something that is not a heading; both
   declarations are gone, so it inherits `--step-sm` in `--font-body` from `.footer-contact`, exactly
   like the email address under it.

   ⚠️ **The white is deliberate, and it is a fix rather than a new idea.** This rule has said
   `color: var(--white)` since it was written and the number has never been white: `.site-footer a`
   is a class plus a type selector and outranks a bare class, so on a linked number the declaration
   was dead. With the display face gone, colour is the only thing left to say that the phone is the
   primary route in this block — so the selector is raised to `.footer-contact a.footer-contact__value`
   and the number is now the brightest line in the column, by contrast rather than by scale.
   ⛔ If the client meant the typeface only and wants the size back, that is one declaration
   (`font-size: 1.3rem`) — do not bring `--font-display` back with it. */
.footer-contact a.footer-contact__value { color: var(--white); }
.footer-contact a.footer-contact__value:hover { color: var(--green-on-dark); }
.footer-contact address { font-style: normal; }

/* Icon column on each contact row, client instruction 2026-09-02.
   ⚠️ **The sizing rule is load-bearing, not decoration.** There is no base `.ic` rule anywhere in
   this file — `.footer-social`, `.factgrid__ic`, `.quicklinks__ic` and every other consumer names its
   own width and height — and the SVGs in `assets/img/icons/` carry a `viewBox` with no `width` or
   `height` attribute. An `icon()` call in a slot with no sizing rule therefore renders at the
   browser's default replaced-element size and blows the column apart. If this block is ever
   refactored, the width/height goes with it.

   `align-items: start` plus the icon's own `margin-top` keeps the pin on the first line of the
   four-line address instead of centred against the whole block; the fixed first track keeps all four
   icons on one optical line whatever their aspect ratio. The strokes are `--on-dark-muted`, the
   colour of the text they label, rather than the `--green-on-dark` the social plates take: these sit
   inline with the copy, and a brighter stroke would make the icon the loudest thing in a column that
   is mostly address. The phone stays the primary route by being the only white value (D198). */
.footer-contact__row {
  display: grid; grid-template-columns: 1.1rem 1fr; gap: 0.6rem;
  align-items: start;
}
.footer-contact__row .ic {
  width: 1.1rem; height: 1.1rem; margin-top: 0.15em;
  --stroke-0: var(--on-dark-muted); --fill-0: var(--on-dark-muted);
}
/* ⚠️ **`justify-self: start` is what keeps the hover mark the same mark the rest of the footer uses.**
   A grid item is blockified, so putting these links in a row grid stretches each one to the full
   column and the drawn rule (D79) — `left: 0; right: 0` against the link's own box — goes with it: a
   201px rule under a 13-character phone number, where every link in the three columns beside it has
   one that stops with the text. Shrinking the item to its content restores that. On the address it
   settles under the longest of the four lines, which is the same behaviour on a block of lines.

   The address also has to be a block for a second reason: left inline, its `::after` would resolve
   against the union of all four line boxes and strike a rule through the middle of the block rather
   than sitting under it. Blockification gives that for free — it is only the stretch that had to go. */
.footer-contact__row > a { justify-self: start; }

/* Centred on the client's instruction 2026-09-02; was `space-between`, which parked the copyright
   hard left against an empty right half because `legal` is still an empty list (T34). `text-align`
   is the other half of it: centring the flex item only centres the box, so the line would go back to
   ragged-left the moment the strapline wraps on a phone. */
.site-footer__bottom {
  display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding-block: 1.25rem;
  font-size: var(--step-xs);
  text-align: center;
}
.site-footer__bottom ul { display: flex; flex-wrap: wrap; gap: 1.25rem; }

/* ------------------------------------------------------ 6. sections */

/* --- hero --- */
.hero { padding-block: clamp(2rem, 1.25rem + 3vw, 3.5rem) var(--section-y); }
.hero__grid { display: grid; gap: var(--gap); align-items: center; }
.hero__lead { margin-top: 1.25rem; font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.1875rem); color: var(--muted); }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }
.hero__aside { display: grid; gap: 1rem; }

/* small commissioner shortcut under the hero actions */
.hero__jump { margin-top: 1.25rem; font-size: var(--step-sm); color: var(--muted); }
.hero__jump a { font-weight: 700; color: var(--accent-text); }

/* --- three-column hero (Home only) ---
   Client instruction 2026-08-04: match the export's index.html, which runs copy | photo | contact
   card. The export does it with a 486px fixed column and a card absolutely positioned over the
   photo; this is the same shape as a grid, so it reflows instead of overlapping at narrow widths.

   The card column is the narrowest of the three and the copy column gives up the width — the H1 is
   four words now and no longer needs 1.05fr. Two breakpoints, not one: below 75rem the card would
   squeeze the photo below its 5:4 ratio, so it drops under the copy and the hero goes back to two
   columns before it goes to one. */
@media (min-width: 62rem) {
  .hero__grid { grid-template-columns: 1.05fr 0.95fr; }
  .hero__grid--triple { grid-template-columns: 1fr 0.95fr; }
}
@media (min-width: 75rem) {
  .hero__grid--triple { grid-template-columns: 0.95fr 1fr 0.72fr; }
}

.hero__contact {
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: 1.5rem 1.375rem;
  align-self: center;
}
.hero__contact-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
.hero__contact-head h2 { font-size: var(--step-h3); line-height: 1.15; }
.hero__contact-head .ic { width: 1.25rem; height: 1.25rem; flex-shrink: 0; margin-top: 0.2rem; }
.hero__contact p { margin-top: 0.75rem; font-size: var(--step-sm); color: var(--muted); }

/* --- the card's two contact tiles, to the client's design (D107) ---
   Each is a bordered row on the card's own white: medallion, a label over what you get, chevron. The
   `.contact-line` component the card used before belongs to the dark closing band and had to have
   both its colours overridden to appear here at all; a tile with a border, a second line and an
   affordance at the end is a different object, so it is written rather than forked.

   🔴 **The number is ink, and the design sets it in brand green.** Green measures 2.07:1 on white and
   the site's oldest rule (D33, T32) is that no green carries text at any size, so it shipped teal
   (D107) and is now body ink on instruction (D109). The medallion beside it keeps the pale-green fill
   and the green stroke: an icon may be green, a phone number may not. T43 is closed.

   No `transition` on any of this. §8 sets a blanket one and `transition` is a shorthand, so a rule
   here naming it would replace §8's rather than add to it (the trap `.card--link` documents). */
/* The rule sits under the tiles rather than over the button: the button is a filled pill, and a
   border-top on it would draw inside its own shape. Same line either way, one element earlier. */
.hero__contact-tiles {
  display: grid; gap: 0.625rem;
  margin-top: 1.125rem; padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--border);
}
.hero__contact-tile {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.75rem 0.875rem;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--white); text-decoration: none;
}
.hero__contact-ic {
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; flex-shrink: 0;
  border-radius: 50%; background: var(--green-pale);
  --stroke-0: var(--green-dark); --fill-0: var(--green-dark);
}
.hero__contact-ic .ic { width: 1.15rem; height: 1.15rem; }
.hero__contact-text { display: grid; gap: 0.1rem; min-width: 0; }
.hero__contact-label { font-family: var(--font-display); font-weight: 600; color: var(--ink); line-height: 1.2; }
/* D109. Was --accent-text teal. The number is body ink now, on instruction: it is a fact under a
   label, not a link, and the tile is already the affordance. Contrast goes up, not down (10.19:1
   against teal's 6.80:1), so T43 closes without a trade — the design's green is off the table either
   way, and this is now further from it than the teal was. */
.hero__contact-value { color: var(--ink); font-size: var(--step-sm); }
.hero__contact-note { color: var(--muted); font-size: var(--step-sm); }
/* The chevron is the affordance, so it is the thing that moves on hover — 3px, the same nudge the
   arrow in a `.btn` takes, and no colour change on the label under it. */
.hero__contact-chev { width: 1.1rem; height: 1.1rem; flex-shrink: 0; margin-left: auto; --stroke-0: var(--ink); }
.hero__contact-tile:is(:hover, :focus-visible) { border-color: var(--green-dark); background: var(--green-pale); }
.hero__contact-tile:is(:hover, :focus-visible) .hero__contact-chev { transform: translateX(3px); }
.hero__contact-tile:is(:hover, :focus-visible) .hero__contact-ic { background: var(--white); }
.hero__contact-cta { margin-top: 1.125rem; width: 100%; justify-content: center; }

/* --- service hero (D47) ---
   Home's hero is copy beside a portrait visual on white. A child page gets a pale band instead: the
   trail, then one measured column of copy, then a wide landscape visual under it. Different shape,
   different surface, same data fields — so a reader arriving from a search result can see they are
   one level inside a site rather than on a second front page.

   The H1 is held to 30ch rather than made smaller. It is still the page's largest type; it just stops
   running to 1200px, which is what made it read as a homepage banner. */
/* The stacked shape belongs to both of the child-page heroes: copy and visual in one column rather
   than side by side. The property hero then reorders it (D56), which needs the grid to exist. */
.hero__stack { display: grid; gap: var(--gap); }
.hero--service { padding-block: clamp(1.75rem, 1.25rem + 2vw, 2.75rem) clamp(2rem, 1.5rem + 2vw, 3.25rem); }
.hero--service h1 { max-width: 30ch; }
.hero--service .hero__lead { max-width: 60ch; }
/* The visual is decoration here, not the subject, so it is a shallow band rather than Home's portrait
   panel. 3:1 is right at 1200px and a 110px strip on a phone, so it opens up below 48rem. */
@media (max-width: 47.999rem) {
  .hero--service .hero__image { aspect-ratio: 16 / 9; }
}

/* --- property hero (D56, rebuilt to the ivolve shape D115) ---
   A home's own page. Two columns on a pale band: the copy on the left — breadcrumb, pill eyebrow,
   H1, location line, address, standfirst, actions, then the two fact grids — and a tall portrait
   photograph on the right. A curved divider closes the band into the white below it.

   **This replaces D56's stacked treatment** (picture wide across the top, copy on a white card
   pulled up over its lower edge). D56's argument was that the building is the subject of the page so
   it should lead. That still holds and the new shape still honours it — the photograph is the
   largest single element in the band — but it no longer costs the whole first screen: the name, the
   location, what the house has and what it is at a glance are now all above the fold together, which
   is what the reference page does and what a family comparing five homes actually needs.

   D56's other point survives intact and is the reason the copy is not set over the photograph: white
   text on an unknown image is a contrast failure waiting for the shoot. Here the copy is simply
   beside it, on the band, so both states are legible — today's labelled placeholder and tomorrow's
   photograph. */
.hero--property {
  padding-block: clamp(1.75rem, 1.25rem + 2vw, 2.75rem) clamp(3rem, 2rem + 3vw, 4.5rem);
  position: relative;
}
.hero__split { display: grid; gap: var(--gap); align-items: start; }
@media (min-width: 60rem) {
  /* Ivolve splits 50/50. Ours gives the copy the larger share: their left column is a paragraph and
     two four-item grids, ours adds a breadcrumb, an address and longer labels, and at 50% the
     four-column grids drop to two and the band grows a couple of hundred pixels. */
  .hero__split { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: clamp(2rem, 1rem + 3vw, 3.5rem); }
  /* ⚡ **A wider copy column wherever a fact row is drawn in it, and every number here is a
     measurement.** At 1.15fr the column is 574px. *Amenities* is 553px of items and three 12.8px
     gaps — **591** — so at 574 the last label drops to a line of its own, which is what the client
     saw: *"it doesn't fit ... don't have it in diff lines"*. At **1.45fr the column is 635** and both
     rows sit on one line each (*Our Expertise* needs 544). The photograph pays for it, 499px → 438,
     which is the client's own suggested trade: *"maybe resize the image on the right"*.

     ⚡ **Keyed on `.hero__panel--row`, not on `.hero__strips` (D209).** It was written for D186, when
     the only row in the column was the one D185 held back from the strips. D208 put both rows in the
     column and left no strips at all, so the `:has(.hero__strips)` test stopped matching and took the
     width with it. The row itself is the thing that needs the width, so the row is what the rule
     asks for.
     ⛔ **Still scoped, and deliberately.** `.hero__split` is also `/referrals/` and the seven
     specialism pages (D136, D166): they emit no panels, so they cannot match this and their columns
     are unchanged. ⚠️ **The photograph stays the largest single element in the band** — D56's point
     and the floor under this ratio. ⛔ **Do not widen it again for a longer list**: Lonsdale's six
     amenities are 843px and no column that leaves a photograph on the band will hold them. */
  .hero--property:has(.hero__panel--row) .hero__split {
    grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  }
}
.hero--property h1 { max-width: 16ch; }
.hero--property .hero__lead { margin-top: 0.85rem; max-width: 46ch; }

/* The eyebrow as a filled pill, which is the reference page's treatment of it. Teal fill rather than
   green: it carries text (D33). Everywhere else on the site `.eyebrow` is a rule and a small label,
   and that is untouched — this is one extra class on one layout. */
.eyebrow--pill {
  display: inline-block;
  margin-bottom: 0.9rem; padding: 0.4rem 0.9rem;
  background: var(--accent-text); color: var(--white);
  border-radius: var(--radius-pill);
  font-size: var(--step-xs); font-weight: 600; letter-spacing: 0.02em; text-transform: none;
}
.eyebrow--pill::before { content: none; }   /* the rule the default eyebrow draws */

/* The location line under the name. A label, not a sentence: teal, tight, and it sits closer to the
   H1 than the standfirst does. */
.hero__sub {
  margin-top: 0.5rem;
  font-family: var(--font-body); font-weight: 700; font-size: var(--step-lead);
  color: var(--accent-text);
}
.hero__address {
  margin-top: 0.35rem;
  font-style: normal; font-size: var(--step-sm); color: var(--muted); line-height: 1.6;
}

/* The two fact grids under the actions. */
.hero__panel { margin-top: clamp(1.5rem, 1rem + 1.5vw, 2.25rem); }
.hero__panel-head {
  margin: 0 0 0.9rem;
  font-family: var(--font-display); font-size: var(--step-h4); font-weight: 600; color: var(--ink);
}
/* ⚡ **Tiles since D161, not bare icon-over-label cells.**
   The reference page runs four narrow columns and so did this, which worked when the panel held nine
   short room names. The client's own amenity list (`20260827-PropertyLocation-amenities.docx`) is
   four to six longer labels — *Close to Local Amenities* is 24 characters — and in a 7.5rem column
   every one of them wrapped to three lines under a floating icon.

   So each item is a card now: white on the pale band, with the label beside a rounded-square icon
   plate. ⚠️ **This is not a new component.** It is the tile `.hero__contact-tile` and
   `.cta-band--list .contact-line` already use — same 2.5rem plate, same `--green-pale` fill,
   `--green-dark` strokes, `--border` hairline and `var(--radius)` — so a home page gains no visual
   language the site did not already have. Two across in the hero's left column, one on a phone. */
.factgrid {
  display: grid; gap: 0.7rem;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.factgrid__item {
  display: flex; align-items: center; gap: 0.8rem;
  padding: 0.7rem 0.9rem;
  background: var(--white);
  border: 1px solid var(--border); border-radius: var(--radius);
}
/* Rounded square rather than the circle the compact contact tiles use, for the reason the feature
   band gives: at this size a circle wastes its corners, and the square lines a stacked column of
   items up on one left edge with the panel heading above them. */
.factgrid__ic {
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; flex: none;
  border-radius: var(--radius);
  /* `--green-light` outline as well as the `--green-pale` fill, which the circular medallions
     elsewhere do without. They sit on white cards on a white section; this tile sits on a white card
     on the pale band, where a `--green-pale` plate is within 1.05:1 of the card under it and simply
     disappears. The outline is the `.pill-list` treatment exactly, which is also what ties this panel
     to the pills in the panel below it. */
  background: var(--green-pale); border: 1px solid var(--green-light);
  --stroke-0: var(--green-dark); --fill-0: var(--green-dark);
}
.factgrid__ic .ic { width: 1.25rem; height: 1.25rem; }
/* Ink, not the teal the bare cells used. The label is a fact inside a tile, the same role the
   contact tile's label plays, and ink measures 15.4:1 on white against teal's 6.80:1 — the tile is
   what carries the accent now, so the text does not need to. */
.factgrid__label {
  font-family: var(--font-display); font-weight: 600;
  font-size: var(--step-sm); line-height: 1.3; color: var(--ink);
}
/* A [Placeholder] chip inside the label needs to stay legible without stretching the row. */
.factgrid__item .ph-value { font-size: inherit; }

/* The second panel runs `.pill-list` instead (D161), and it needs one thing the section-level list
   does not: a top margin is already on `.hero__panel`, but the pills sit tighter to their heading
   than a run of tiles does. */
.hero__panel .pill-list { gap: 0.5rem; }
.hero__panel .pill-list li { font-size: var(--step-sm); }

/* --- the fact rows (D184) ---
   `panels_layout => 'strips'`. The same two panels, taken out of the copy column and laid across the
   foot of the band as full-bleed rows.

   The problem they solve is competition, not decoration. Eight labelled items under the buttons is
   the tallest single thing in this hero: it pushes the H1 and the client's own sentence about the
   house into the top third, it sets the height the photograph has to fill, and it puts two headings
   and eight icons inside the same measure as the copy. Read left to right under the band instead,
   the facts stop competing with the name and start doing what a specification row does.

   ⚠️ **The hairlines are full-bleed and the content is not.** That is the whole visual idea and it
   is why `.hero__strip` sits outside `.container` with a `.container` of its own inside it: a rule
   that stops at the page measure reads as a box around the row, and a row in a box is the tile grid
   again. */
.hero__strips { margin-top: clamp(1.75rem, 1.25rem + 2vw, 3rem); }
/* ⚠️ **`--green-light`, not `--border`, and that is measured rather than chosen.** The band is
   `--green-pale` #F2F7EA and `--border` is #E8EDE2: 1.05:1 against it, which is a rule nobody can
   see. It is the same finding `.factgrid__ic` records a few rules up and it is answered the same
   way, with the one hairline token that reads on the pale surface. ⛔ Do not "normalise" these two
   borders back to `--border` — the strips lose their only structure. */
.hero__strip { border-top: 1px solid var(--green-light); }
.hero__strip > .container { padding-block: clamp(1rem, 0.8rem + 0.7vw, 1.4rem); }
.hero__strip-head {
  margin: 0;
  font-family: var(--font-body); font-weight: 700;
  font-size: var(--step-body); line-height: 1.3; color: var(--ink);
}

.factrow { display: grid; gap: 0.85rem 0; }
.factrow__item { display: flex; align-items: center; gap: 0.8rem; }
/* Body sans, not the display serif the tiles use. A tile is a card with a heading above it and the
   serif reads as a small title there; a row of facts under a band is a caption, and Fraunces at
   label size across four columns is the busyness this layout exists to take out. */
.factrow__label {
  font-family: var(--font-body); font-weight: 500;
  font-size: var(--step-body); line-height: 1.3; color: var(--ink);
}
.factrow__item .ph-value { font-size: inherit; }
/* A circle, where `.factgrid` uses a rounded square. The square exists there to line a stacked
   column of tiles up on one left edge under a heading; nothing is stacked here, and the circle is
   the medallion the journey steps and the compact contact tiles already use. The `--green-light`
   hairline is kept for `.factgrid`'s reason: `--green-pale` on the pale band is within 1.05:1 of the
   surface behind it and disappears without it. */
.factrow__ic {
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; flex: none;
  border-radius: 50%;
  background: var(--green-pale); border: 1px solid var(--green-light);
  --stroke-0: var(--green-dark); --fill-0: var(--green-dark);
}
.factrow__ic .ic { width: 1.2rem; height: 1.2rem; }

@media (min-width: 48rem) {
  .factrow:not(.factrow--compact) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 60rem) {
  /* `auto-flow: column` rather than a counted `repeat()`: a panel is the client's list and its
     length is theirs to change. Four items today on all five homes, and five or six would lay out
     the same way without anybody having to find this rule. */
  .factrow:not(.factrow--compact) {
    grid-template-columns: none;
    grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  }
  .factrow:not(.factrow--compact) .factrow__item { justify-content: center; padding-inline: 1rem; }
  /* ⚠️ The dividers are here and only here, because this is the one width at which the row is
     guaranteed to be a single row. Below it the items wrap, and a `+` rule cannot tell the first
     item of the second row from the middle of the first — it would draw a rule hard against the
     left margin. The gap does that job at those widths. */
  .factrow:not(.factrow--compact) .factrow__item + .factrow__item {
    border-inline-start: 1px solid var(--green-light);
  }
}

/* The lighter row. Its heading stays on the screen and sits on the same line as its items — the
   labels below are conditions rather than facilities, and without the words *Our Expertise* in
   front of them a row reading "Autism, Acquired Brain Injury" states something nobody wrote. */
.hero__strip--compact > .container {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.6rem clamp(1.25rem, 0.75rem + 1.6vw, 2.25rem);
}
.hero__strip--compact .hero__strip-head { flex: none; }
.factrow--compact {
  display: flex; flex-wrap: wrap;
  gap: 0.6rem clamp(1.25rem, 0.75rem + 1.6vw, 2.25rem);
}
/* No plate and no chip: the glyph alone at label size. `.pill-list` would have worked and is
   deliberately not used — a row of outlined chips directly under a row of medallions is two
   containers where the point of the strip is none. */
.factrow--compact .factrow__ic {
  width: auto; height: auto;
  background: none; border: 0; border-radius: 0;
}
.factrow--compact .factrow__ic .ic { width: 1.1rem; height: 1.1rem; }
.factrow--compact .factrow__item { gap: 0.5rem; }
.factrow--compact .factrow__label { font-size: var(--step-sm); }

/* A row held back from the strips and drawn in the copy column instead (D185,
   `panels[].place => 'body'`). It is the same row: same heading, same bare glyphs, same labels.

   ⚡ **Since D208 this is the only place a fact row is drawn.** Both panels on all five home pages
   set `place => 'body'`, so nothing renders `.hero__strips` and the block above — with the three
   `:has(.hero__strips)` rules further down — has no consumer. ⛔ **None of it is dead code to
   delete**: one key in `data/` puts a panel back under the band, and the strip is what it lands in.

   ⚡ **The heading sits over its items and the items hold one line, which took both halves of D209.**
   With the strips gone the split reverted to 1.15fr and the 574px column broke *Amenities* across two
   lines; the width is keyed on this row now, so the column is 635 again. And at 635 the two panels
   disagreed about their headings — *Our Expertise* fits beside its own, *Amenities* does not — so the
   heading takes the full line in both. ⛔ **Do not undo either half independently**: the width without
   the heading rule gives two rows that look different, and the heading rule without the width still
   breaks the amenity list.

   ⚡ **One line, heading included (D186).** It was heading-over-items until the client asked for a
   single line, and the column was widened to 1.45fr above so that the line exists to sit on. ⚠️ The
   gaps here are **tighter than the strip's** and that is what buys the fit: 99px of heading and
   468px of items leave about 60px for four gaps in a 635px column, where the strip has a whole
   viewport to spend. ⛔ **Do not unify the two gap values** — the strip loses its air, or this wraps.

   ⚠️ **It still wraps between 60rem and about 79rem**, where the container is narrower than its
   1200px cap and the column with it. That is honest and unavoidable: flex wrap puts the heading on
   its own line again rather than overflowing, and the row reads correctly either way. */
.hero__panel--row {
  margin-top: clamp(1.5rem, 1rem + 1.5vw, 2.25rem);
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.55rem 0.8rem;
}
/* ⚡ **The heading takes the whole line, so the items always start on the next one (D209).** Left to
   wrap on its own it goes one way on one panel and the other way on the next: at the 635px column
   *Our Expertise* (99px of heading and 544 of items) fits beside its heading and *Amenities* (73 and
   591) does not, so one row read as a heading over a list and the row under it as a label in front of
   one. **The client asked for the two to be identical** (D208), and heading-over-items is also the
   arrangement of the reference page they named. ⛔ **Do not put `flex: none` back** without measuring
   both panels of all five homes at 1280 — that is the state this replaces. */
.hero__panel--row .hero__strip-head { flex: 0 0 100%; }
.hero__panel--row .factrow--compact { gap: 0.55rem 0.8rem; }

/* --- the curved divider under every hero (D236) --------------------------------------------------
   A shallow light arc painted over the foot of the band, so the hero reads as closing into the
   section below it. Purely decorative, and aria-hidden by construction rather than by attribute — it
   is a background on a pseudo-element, so there is nothing in the accessibility tree to hide.
   ⛔ Do not rebuild it as an element or an SVG to make it "editable": no partial and no data file
   knows it exists, which is what lets it land on the whole site without touching a page.

   ⚡ **2026-09-03, five client instructions in one session. Read them in order — three of the five
   are reversals, and the reasoning only makes sense as a sequence:**

   1. *"I like this wavy thing you've done - go through all pages and after the hero -> do this on all
      pages. By all pages i mean all active pages."* It was `.hero--property::after`: the five home
      pages, the seven specialisms and `/referrals/` (D136).
   2. *"undo the wavy thing - i only want light wavy thing across all pages - very light wavy."*
   3. *"Remove wavy from - index, /about/, supported-living/, activity-hub/, /homes/ /careers/,
      /contact."*
   4. *"reduce the curve of the pages that have it - its too curvy - reduce it."*
   5. *"nice - this amount of curve - have it on all active pages."*

   ★ **So the depth was the objection all along, not the coverage.** Instruction 3 took the arc off
   seven pages that instruction 5 put back; what changed in between was instruction 4, which more than
   halved it. ⛔ **Do not read 3 as a standing preference about those seven pages** — it was superseded
   four instructions later, by the client, with the shallower arc in front of them.

   ★ **`.hero::after`, so every hero on the site draws it** — `default`, `service`, `overlay` and
   `property`/`split` alike. All fourteen active pages carry it (see the table in `CLAUDE.md`).

   ⚠️ **The seven stale pages draw it too and that is the selector, not a decision.** ⛔ Do not add
   `:not()` exceptions to hold them back: a stale page looking like the rest of the site costs
   nothing, and a selector carrying a list of exceptions goes wrong the day one of them is linked.

   ★ **Two light values, and the choice is made against the HERO, not against the section below** —
   the arc sits on the band, so it has to differ from the band:

   | Hero surface | Pages | Arc |
   |---|---|---|
   | `.section--pale` | `/about/` `/our-care/` `/careers/` `/homes/` `/referrals/`, the five homes, the seven specialisms | `--white` |
   | a photograph (`hero--overlay`) | `/supported-living/` `/activity-hub/` `/contact/` | `--white` |
   | white (Home's default layout) | `/` | `--green-pale` |

   ⚠️ **Home is the exception and it is the whole reason the `:not()` pair exists.** Its hero is plain
   white, so `--white` would draw an invisible arc; `--green-pale` (#F2F7EA) is the lightest tint the
   site has and is what instruction 2's *"very light"* means. ⛔ Do not deepen it to `--green-light` to
   make it show — a stronger curve there reads as a section band that is not one.

   ⚡ **Where the section below is dark, so is the arc (D237, 2026-09-03)** — *"landing wave has light
   green instead of dark green, contact/ as well."* Home and `/contact/` are the only two pages whose
   second section is `--teal-900`, and on both the light arc read as a stripe floating between two
   dark surfaces rather than as a section rising into the band. They take `--teal-900`, which is that
   section's own colour, so the arc merges into it.

   ⚠️ **This reverses instruction 2 for exactly these two pages and nothing else.** An early build
   coloured *every* arc from its neighbour, at more than twice the present depth, and the client
   rejected it; they have now asked for it back on the two pages where it is the difference between a
   stripe and a curve. ⛔ **Do not widen it into a general "colour from the neighbour" rule** — the
   twelve pale-and-white pages are light-on-light and were named in neither instruction.
   ⛔ **And do not read it as permission for a dark arc on a light hero elsewhere**: it works here
   only because there is a dark band directly beneath it to disappear into.

   ⚠️ **`--teal-900` is the reading of *"dark green"* that makes the instruction true.** It is the
   dark green-teal of the band below on both pages; an actual green (`--green-dark` #789D46) would
   draw an olive stripe across a dark band, which is the thing being complained about. 🟡 If the
   client meant a literal green, it is one token.

   ★ **`border-start-*-radius: 60% 100%` is what makes it one continuous arc and it is not a
   curviness dial.** Two top corners each asking for 60% of the width is 120%, so the browser scales
   every radius on the box by 1/1.2 — each corner lands at exactly 50% of the width and the two
   ellipses meet in the middle with **no straight segment between them**. ⛔ Drop either number and
   the shape changes rather than softening: a smaller horizontal radius opens a flat top with rounded
   shoulders, and a smaller vertical one pulls the arc's *ends* down the box instead of lowering its
   apex, which reads as tighter, not gentler. ⛔ **The height is the only honest control**, because
   the ends are pinned to the bottom edge and the apex to the top. It was
   `clamp(1.5rem, 1rem + 2vw, 2.75rem)` and instruction 4 more than halved it: **22.8px at 1280**,
   14px at 375, capped at 24px from about 1400px up. */
.hero::after {
  content: "";
  position: absolute; inset-inline: 0; bottom: -1px;
  display: block; height: clamp(0.875rem, 0.625rem + 1vw, 1.5rem);
  background: var(--white);
  border-start-start-radius: 60% 100%;
  border-start-end-radius: 60% 100%;
}
/* ⚡ **Home and `/contact/`: the arc is the dark band below it (D237).** ⛔ This replaces the
   `--green-pale` arc Home carried for one round — that value existed only because a white arc on a
   white hero is an invisible one, and the question stopped being "how is it made visible" the moment
   the client asked for it to match the band underneath. ⛔ Do not restore `--green-pale` here. */
.hero:has(+ .section--dark)::after { background: var(--teal-900); }
/* ⚠️ **The overlay hero needs a layer number and both halves are load-bearing.** Its photograph is
   `position: absolute; inset: 0`, so without the `1` the arc paints under the picture and vanishes;
   and its copy card sits in a `.container` at `z-index: 2` (set in the `.hero--overlay` block below,
   raised from `1` for exactly this), because an equal-numbered pseudo later in tree order would paint
   over the card on a short viewport. ⛔ Neither half works alone. ⚠️ `.hero--overlay`'s
   `overflow: hidden` clips the arc's `bottom: -1px`, which is harmless — that 1px exists only to
   close the hairline against the next section, and a clipped edge closes it too. */
.hero--overlay::after { z-index: 1; }
/* ⛔ **A band that ends in strips does not take the curve, and it is not a taste call** (D184). The
   curve is drawn in the colour of the section below and sits over the bottom of the band, so it
   would cut a white arc through the last fact row. The row's own hairline is the edge now, and the
   band's bottom padding goes with it — each strip carries its own. */
.hero--property:has(.hero__strips) { padding-bottom: 0; }
.hero--property:has(.hero__strips)::after { content: none; }

@media (max-width: 59.999rem) {
  /* Stacked, and the picture goes first — which is D56's original order, and the right one on a
     phone where there is no column to put it beside. */
  .hero--property .hero__aside { order: -1; }
  .hero--property .hero__image { aspect-ratio: 4 / 3; }
  .hero--property h1 { max-width: none; }
}

/* --- overlay hero (D63) ---
   For a page whose job is to get somebody to act, not to read: the photograph goes full-bleed behind
   the whole band and the copy sits on a white card over it. Contact is the case it was built for —
   the phone number is the page, and on the service hero it sat under a 3:1 band and a standfirst.

   The copy is on a card rather than set directly over the photograph, for the same reason the
   property hero uses one (D56): white text over an unknown image is a contrast failure waiting for
   the next image swap, and nobody re-checks it once the file changes in `data/`. On the card, the
   type is ink on white whatever is behind it, and the scrim below is only there so the card's
   shadow still separates it from a bright frame.

   The band sets a min-height rather than an aspect-ratio. A ratio that frames the photograph well at
   1200px turns into a 200px letterbox at 400px, and the card, not the picture, is what has to fit. */
.hero--overlay {
  position: relative;
  display: grid; align-items: center;
  min-height: clamp(23rem, 17rem + 18vw, 31rem);
  padding-block: var(--section-y-sm);
  overflow: hidden;
  /* What shows in the moment before the file arrives, and behind it if it never does. */
  background: var(--teal-900);
}
/* The image slot, unwrapped: same helper and same markup as every other photograph on the site
   (`media_image`), with the ratio box turned off so it fills the band instead of setting its height. */
.hero--overlay .hero__media {
  position: absolute; inset: 0;
  aspect-ratio: auto; height: 100%;
  border-radius: 0;
}
.hero--overlay .hero__media::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(100deg, rgba(23, 48, 50, 0.55), rgba(23, 48, 50, 0.14) 62%, rgba(23, 48, 50, 0.3));
}
/* `width: 100%` is load-bearing. The band is a grid and the container is its only in-flow child, and
   a grid item carrying `margin-inline: auto` opts out of stretch — so without this the container
   shrink-wraps the card and centres it, and the card no longer lines up with the columns of every
   section under it. */
.hero--overlay .container { position: relative; z-index: 2; width: 100%; }
.hero__card {
  max-width: 38rem;
  padding: clamp(1.5rem, 1.1rem + 1.8vw, 2.25rem);
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
/* --step-h1 runs to 4rem, which is right across a 1200px column and wrong inside a 38rem card. No
   measure on top of that: the card already is the measure, and a `max-width` in ch on top of it turns
   a real heading — "Specialist supported living for adults with complex needs" — into a tall narrow
   column with white space beside it. */
.hero--overlay h1 { font-size: clamp(1.75rem, 1.4rem + 1.7vw, 2.5rem); }
/* D41's highlighter mark only works as one continuous sweep. Across a 1200px column the washed
   phrase always fitted on a line and this never came up; inside the card it wraps mid-phrase, and a
   wash split over two lines does not read as a marker — in Chrome the fragmented inline does not
   even paint the second half. So the phrase holds together and the rest of the heading wraps around
   it. The mark is short by the rule that created it (one phrase, chosen to fit a line), so this
   cannot push a heading wider than the card. */
.hero--overlay .hl { white-space: nowrap; }

/* D63's rule, promoted from the hero card to every hero H1 (2026-08-25). Every hero on the site now
   carries a wash, and the moment they did, `/our-care/pmld/` reproduced the same failure in the
   widest box on the site: "profound and multiple" straddled the line break in a 30ch heading, and a
   fragmented inline does not paint its second half in Chrome — the mark vanished with nothing to
   show it had. Width is not what protects a wash; only holding the phrase together is. */
.hero__body h1 .hl { white-space: nowrap; }
.hero--overlay .hero__lead { margin-top: 0.875rem; font-size: var(--step-lead); }
.hero--overlay .hero__actions { margin-top: 1.5rem; }
.hero__points { margin-top: 1.375rem; font-size: var(--step-sm); }
@media (max-width: 47.999rem) {
  /* Below this the card is the full width of the container and the photograph is a frame around it
     rather than a scene beside it, so the band stops reserving height the picture cannot use. */
  .hero--overlay { min-height: 0; padding-block: var(--section-y-sm); }
}

/* --- breadcrumb ---
   Built from the same array as the page's BreadcrumbList (D47), so the visible route and the
   structured one cannot drift. Separators are generated, never typed into the data. */
.breadcrumb { margin-bottom: 1.25rem; }
.breadcrumb ol { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
.breadcrumb li { display: flex; align-items: center; gap: 0.5rem; font-size: var(--step-sm); }
.breadcrumb li + li::before {
  content: ''; flex: none;
  width: 0.3rem; height: 0.3rem; border-radius: 50%;
  background: var(--green-dark);
}
.breadcrumb a { color: var(--muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--ink); text-decoration: underline; }
.breadcrumb [aria-current='page'] { font-weight: 700; color: var(--ink); }

/* --- quicklinks (the dark routing bar under the hero, D43) ---
   Three items, so the grid goes straight from one column to three at 52rem. There is no two-column
   middle state: it would leave one item alone on a second row directly under a hairline, which reads
   as a broken layout rather than a deliberate one. */
.quicklinks__list { display: grid; }
.quicklinks__item {
  display: flex; gap: 1rem; align-items: flex-start;
  padding-block: 1.5rem;
  border-top: 1px solid var(--on-dark-hairline);
}
.quicklinks__item:first-child { border-top: 0; }

@media (min-width: 52rem) {
  .quicklinks__list { grid-template-columns: repeat(3, 1fr); gap: var(--gap); }
  .quicklinks__item {
    padding-block: 0; padding-left: var(--gap);
    border-top: 0; border-left: 1px solid var(--on-dark-hairline);
  }
  .quicklinks__item:first-child { padding-left: 0; border-left: 0; }
}

.quicklinks__ic {
  display: grid; place-items: center; flex: none;
  width: 2.75rem; height: 2.75rem; border-radius: 50%;
  background: var(--on-dark-fill);
}
.quicklinks__ic .ic {
  width: 1.25rem; height: 1.25rem;
  --stroke-0: var(--green-on-dark); --fill-0: var(--green-on-dark);
}
.quicklinks__text { display: grid; gap: 0.3rem; justify-items: start; }
.quicklinks__text h3 { font-size: var(--step-h4); }
.quicklinks__text p { font-size: var(--step-sm); }
.quicklinks__text .link-arrow { margin-top: 0.25rem; font-size: var(--step-sm); }
/* The medallion is the item's only decoration, so it carries the hover instead of a card lift. */
.quicklinks__item:hover .quicklinks__ic { background: var(--on-dark-hairline); }

/* --- trust bar --- */
.trustbar { background: var(--teal-900); color: var(--on-dark); padding-block: clamp(1.5rem, 1rem + 1.5vw, 2rem); }
.trustbar__list {
  display: grid; gap: 1.25rem var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
}
.trustbar__item { display: flex; align-items: center; gap: 0.85rem; }
.trustbar__ic {
  display: grid; place-items: center; flex: none;
  width: 2.75rem; height: 2.75rem; border-radius: 50%;
  background: var(--on-dark-fill);
}
.trustbar__ic .ic { width: 1.25rem; height: 1.25rem; --stroke-0: var(--green-on-dark); --fill-0: var(--green-on-dark); }
.trustbar__label { font-weight: 600; font-size: var(--step-sm); color: var(--white); line-height: 1.35; }

/* --- prose --- */
.prose__body { font-size: var(--step-lead); color: var(--muted); }
.prose__body p { max-width: var(--max-prose); }

/* --- feature list (why us) --- */
.feature-list { display: grid; gap: 0; border-top: 1px solid var(--border); }
.feature-list li {
  display: flex; gap: 1rem; align-items: flex-start;
  padding-block: 1.125rem;
  border-bottom: 1px solid var(--border);
}
.feature-list .ic { width: 1.15rem; height: 1.15rem; flex: none; margin-top: 0.35rem; --stroke-0: var(--green-dark); }
.feature-list p { margin: 0; font-size: var(--step-sm); color: var(--muted); }
.feature-list strong { display: block; font-size: var(--step-body); color: var(--ink); margin-bottom: 0.15rem; }

/* --- features, `icons` layout: the client's own live-site blocks, laid out graphically (D169) ---
   `/activity-hub/`'s "What We Offer" and "Developmental & Educational Activities". The list layout
   above is a hairline stack in the right half of a `.split`; this is a centred grid where each item
   carries its own icon, so it reads as a menu of things offered rather than as evidence for a claim.

   ⚠️ **The medallion is a teal ring around a teal glyph, and green was never a candidate.** It is a
   graphic, so 3:1 would have been enough for `--green-dark`, but the closing marks on this site put
   `--brand-teal` around numerals (`.steps--cards`) and behind icons (`.quicklinks__ic`), and a green
   ring here would be the only one of its kind. `--brand-teal` on white is 6.80:1 (T32, D33).

   ⚠️ **No two-column middle state**, for `.quicklinks`' reason and it applies harder here: the
   primary consumer has three items, and two columns leave one of them alone on a second row under
   nothing, which reads as a broken grid rather than a deliberate one. The four-item consumer wraps
   one item onto row two at three columns, which is the shape the client's own mockup shows.

   ⚠️ **`p` carries `max-width: var(--max-prose)` from §2**, so a centred item's lines would sit
   against the left edge of a wide column without the narrower cap and the auto margins below — the
   same trap `.section--centred` documents. */
.icongrid { display: grid; gap: var(--gap) calc(var(--gap) * 0.75); }
@media (min-width: 62rem) {
  /* ⚠️ **The grid is drawn on twice as many half-columns as it shows, and that is what centres a
     short last row** (client instruction, 2026-09-02, D225). A full row is identical either way:
     with six 1fr tracks and a two-track item, the item measures `(W - 5g)/3 + g`, which is exactly
     the `(W - 2g)/3` a three-track grid gives it, and the gap between two neighbours is one `g`
     both ways. What the extra tracks buy is the odd offset: starting a trailing item on track 2 or
     3 shifts it by `col + g = (W + g)/6`, precisely half a column-plus-gap, so a row of one or two
     sits centred under a row of three. ⛔ **Not a `transform`** — §8 animates `transform` on a
     revealing item and would replace the offset for the length of the draw (D179, D181). */
  .icongrid { grid-template-columns: repeat(calc(var(--icongrid-cols, 3) * 2), 1fr); }
  .icongrid__item { grid-column: span 2; }
  /* A trailing item that would otherwise sit alone at the start of a fresh row — four groups in a
     three-column grid ("Developmental & Educational Activities"), seven, and so on — is pulled into
     the centre so the wrap reads as deliberate rather than as a dropped tile (client instruction,
     2026-08-31). `:not(:first-child)` keeps a lone single-item grid, which has one column not
     three, out of it; three items land on `3n` and never match. `--icongrid-cols: 3` is the only
     multi-column state this layout has (D169: no two-column middle state), so track 3 is always the
     middle. */
  .icongrid__item:last-child:nth-child(3n + 1):not(:first-child) { grid-column: 3 / span 2; }
  /* And a trailing PAIR — five groups, eight, and so on — starts half a column in, so the two
     straddle the centre. Only the first of the two is placed; the second auto-flows into the tracks
     beside it. `:not(:first-child)` keeps a two-item grid, which has two columns not three, out of
     it. */
  .icongrid__item:nth-last-child(2):nth-child(3n + 1):not(:first-child) { grid-column: 2 / span 2; }
}
.icongrid__item { display: grid; justify-items: center; text-align: center; gap: 0.6rem; align-content: start; }
.icongrid__ic {
  display: grid; place-items: center; flex: none;
  width: 3.25rem; height: 3.25rem; border-radius: 50%;
  border: 2px solid var(--brand-teal);
}
.icongrid__ic .ic { width: 1.5rem; height: 1.5rem; --stroke-0: var(--brand-teal); --fill-0: var(--brand-teal); }
.icongrid__item h3 { font-size: var(--step-h4); margin: 0; }
.icongrid__body p { max-width: 24rem; margin-inline: auto; font-size: var(--step-sm); color: var(--muted); }
.icongrid__body p + p { margin-top: 0.5rem; }
/* The note under an icons grid is centred with it, the same way `.section--centred` centres the
   notes under `grouplist` and `steps`. */
.features--icons .list-note { text-align: center; }
.features--icons .list-note p { margin-inline: auto; }

/* --- pill list (who we support) --- */
.pill-list { display: flex; flex-wrap: wrap; gap: 0.6rem; }
.pill-list li {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.6rem 1rem;
  background: var(--green-pale); border: 1px solid var(--green-light);
  border-radius: var(--radius-pill);
  font-weight: 600; font-size: var(--step-sm);
}
.pill-list .ic { width: 0.95rem; height: 0.95rem; --stroke-0: var(--green-dark); }

/* A second list in the same section, for statements too long to be pills (the situations people
   arrive from, on Supported Living). Reuses .feature-list rather than inventing a third list. */
.pills__group { margin-top: 1.75rem; }
.pills__group > p { color: var(--muted); margin-bottom: 0.75rem; }
.pills__group .feature-list li { padding-block: 0.75rem; }

/* --- grouplist (is this the right service · what people do here) ---
   Three groups, styled identically on purpose. Behind the eligibility use sits a green/amber/red
   decision tool that is never published, so nothing here may read as a rating: no colour per group,
   no tick or cross, no ordering cue beyond the order of the sentences themselves. The marker is the
   same green square in all three (D44).

   Cards, not a rail, because a reader scans the three headings before reading any line. Two states:
   stacked, then three columns at 62rem. There is no two-column middle state — it would leave the third
   group alone on a second row, which reads as the important one. */
.grouplist { display: grid; gap: 1rem; }
@media (min-width: 62rem) { .grouplist { grid-template-columns: repeat(3, 1fr); gap: 1.25rem; } }
/* One group is legitimate and is what a home page's eligibility block is: a single set of things we
   look at, not three parallel sets. Left in a three-column grid it would sit in the first third of
   the page with two empty columns beside it, which reads as a layout that lost its other two cards.
   It gets a measure of its own instead. The styling is otherwise identical, deliberately — the
   neutrality rule in D44 applies to one group exactly as it does to three. */
@media (min-width: 62rem) {
  .grouplist:has(.grouplist__group:only-child) { grid-template-columns: minmax(0, 48rem); }
}

.grouplist__group {
  padding: clamp(1.25rem, 1rem + 1vw, 1.75rem);
  background: var(--white);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.section--pale .grouplist__group { box-shadow: none; }
.grouplist__group h3 { font-size: var(--step-h4); }

.grouplist__items { display: grid; gap: 0.7rem; margin-top: 1rem; }
.grouplist__items li {
  position: relative; padding-left: 1.4rem;
  font-size: var(--step-sm); color: var(--muted);
}
.grouplist__items li::before {
  content: ''; position: absolute; left: 0; top: 0.5em;
  width: 0.5rem; height: 0.5rem; border-radius: 2px;
  background: var(--green);
}
.grouplist__note {
  max-width: var(--max-prose); margin-top: var(--gap);
  font-size: var(--step-lead); color: var(--muted);
}

/* `filled` (D160): the same card on the site's pale green instead of white. Six white cards on a white
   section are held apart by a 1px `--border` and nothing else, which is a grid the reader has to go
   looking for; filled, the six blocks are the first thing on the section and the colour is the brand's.
   ⚠️ **Every group takes it or none does** — a fill that varied by group would rebuild the green/amber/
   red decision tool that is never published (D44, D20). The border goes because the fill is the edge:
   left on, it draws a line a shade off the fill it surrounds. */
.grouplist--filled .grouplist__group {
  background: var(--green-pale);
  border-color: transparent;
  box-shadow: none;
}

/* --- numbered steps ---
   The column count comes from the partial as --steps-cols, because it is a fact about the data, not
   about the viewport: three steps on Home, four on Contact (D49). Four would otherwise leave one step
   alone on a second row, which reads as the important one. The md value is separate so four goes 2x2
   on a tablet rather than three-up with an orphan. */
/* `list-style: none` because the numbers are drawn as medallions below. A grid container does not
   suppress the markers on its items — each `.step` is still a `list-item` — so without this every
   step reads "1. 1. Look at what is open", the browser's marker beside our own. It is still an <ol>,
   which is what carries the order to a screen reader. Found on Careers (T19); it was on Home and
   Contact too. */
.steps { display: grid; gap: var(--gap); counter-reset: step; list-style: none; }
@media (min-width: 48rem) { .steps { grid-template-columns: repeat(var(--steps-cols-md, 3), 1fr); } }
@media (min-width: 62rem) { .steps { grid-template-columns: repeat(var(--steps-cols, 3), 1fr); } }
.step { counter-increment: step; position: relative; padding-top: 3.25rem; }
.step::before {
  content: counter(step);
  position: absolute; top: 0; left: 0;
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; border-radius: 50%;
  background: var(--green-light); color: var(--ink);
  font-family: var(--font-display); font-weight: 600; font-size: 1.125rem;
}
.step h3 { font-size: var(--step-h4); }
.step p { margin-top: 0.4rem; font-size: var(--step-sm); color: var(--muted); }
/* The line that qualifies the whole route, matched to .grouplist__note so the two closing notes on
   Contact read as the same thing. */
.steps__note {
  max-width: var(--max-prose); margin-top: var(--gap);
  font-size: var(--step-lead); color: var(--muted);
}

/* --- steps, `cards` layout: the referral process laid out graphically (D136) ---
   Built for `/referrals/` from the reference page's "Our Referral Process". Their version is four
   flat white cards with a large green "Step N" label; ours keeps the card and replaces the label with
   the medallion the rest of the site already numbers things with, then threads the medallions
   together with the dashed rule from `.journey` so the row reads as one route.

   ⚠️ **The medallion is teal with a white numeral.** The default `.step::before` is `--green-light`
   behind `--ink`, which is fine on white at 2.5rem; at this size and weight it is the loudest thing
   in the card, and green is not a colour this site puts text on (T32, D33). White on `--brand-teal`
   is 6.80:1. Do not swap it back to a green fill to "match" the reference page.

   The connecting rule is drawn INSIDE THE GRID GAP ONLY — `right: 100%` and `width: var(--gap)` — so
   it never crosses a card, never sits over a rounded corner, and needs no z-index against the cards.

   ⚡ **It wraps now (D144).** The rule is drawn into the gap to an item's LEFT, so the one item that
   must never have it is the first of each row — otherwise it dangles off the edge of the grid. That
   used to be enforced by only emitting `.steps--track` when nothing wrapped; the partial now also
   emits `.steps--track-{cols}` and the `:nth-child` rules below take the rule off each row start.
   `/referrals/`'s four steps are unchanged by it: at four columns `4n+1` is item 1, which is already
   excluded by `:not(:first-child)`. Adding a new column count means adding its `n+1` rule here. */
.steps--cards .step {
  padding: 1.75rem;
  padding-top: 4.5rem;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.steps--cards .step::before {
  top: 1.75rem; left: 1.75rem;
  width: 3rem; height: 3rem;
  background: var(--brand-teal); color: var(--white);
  font-size: 1.375rem;
}
.steps--cards .step h3 { font-size: var(--step-h3); }
.steps--cards .step p { margin-top: 0.55rem; }

/* Medallion centre: 1.75rem of padding + half of a 3rem circle. */
@media (min-width: 62rem) {
  .steps--track .step:not(:first-child)::after {
    content: '';
    position: absolute;
    top: 3.25rem; right: 100%;
    width: var(--gap);
    border-top: 2px dashed var(--green-light);
  }
  /* Row starts, per column count. `4n+1` is item 1 only at four-up, so /referrals/ is untouched. */
  .steps--track-2 .step:nth-child(2n + 1)::after,
  .steps--track-3 .step:nth-child(3n + 1)::after,
  .steps--track-4 .step:nth-child(4n + 1)::after { content: none; }
}

/* --- steps, `feature`: the card weight the reference page gives its own process (D162) ---
   `s-referral-process` is the section `/referrals/` exists for, and on the reference page it is the
   tallest block on the page. Ours was the shortest: four 251x294 cards under a left-aligned heading,
   with the body copy set at `--step-sm`. `feature` is the same opt-in `cta` took at D160 and it does
   the same job — the treatment a page's centrepiece needs, without moving the other consumer.

   ⚠️ **The other consumer is Home's six-stage journey** (`s-journey`, D144), which is the client's own
   flow chart and is deliberately not a feature block. Everything here is scoped to `--feature` for
   that reason; nothing may be lifted onto `.steps--cards`.

   ⚠️ **The body copy goes to `--step-body`, the site's 16px accessibility floor** (brand.md). `.step p`
   sets `--step-sm` for the default layout, where a step is a line of prose in a full-width column;
   inside a 251px card that is 14px in a 22-character measure, which is what made the section read as
   a footnote to the page rather than the point of it. The reference sets 18px in the same box.

   ⚡ **Redrawn 2026-08-28 (D164)** — *"inside the card there's too much whitespace, the numbers and
   spacing looks wrong, redo the cards."* Both halves of that are one cause. The medallion was
   absolutely positioned in the card's top-left corner and the card reserved a 4.75rem band of padding
   above the title to clear it, so every card opened with an empty strip and a number floating in it;
   and at four-up on a 1200px grid the card is 249px, which is a 22-character measure, so the longest
   step ran to eight lines, the shortest to four, and the grid stretched all four to the tallest.

   **The number is a grid item now, on the title's own line.** `position: static` on the `::before`
   puts it back in flow as the card's first child, `auto 1fr` gives it a left rail, and the body copy
   sits under the title in the second column so the card reads as one numbered entry rather than a
   badge with a paragraph below it. The reserved padding goes with it: the card is `1.75rem` on every
   side and its height is what its content needs.

   ⚠️ **The column count is the other half and it lives in `data/`** — `/referrals/` passes
   `'cols' => 2` (D164). This block does not force it: `steps` is a grid keyed off `--steps-cols`, and
   a feature section that wants four columns can still have them. It will read the way this one did.

   ⚠️ **Row 1 is centred, so it must stay one line.** The medallion and the title are aligned to each
   other's centre; if a title wraps in one card and not its neighbour, that card's medallion drops and
   the dashed track no longer meets it. At two-up there is ~450px of title, which is three times the
   longest of these. **Check the track if you add a longer title or narrow the columns.**

   ⚠️ **The track's `top` is back to the shared 3.25rem** — padding plus half of a 3rem circle, which
   is what `.steps--cards` sets and what `.steps--track` already draws at. The D162 override that
   pushed it to 3.5rem is deleted rather than left to rot. Change the padding here and that number
   moves with it, in `.steps--track` above. */
.steps--feature .step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  /* ⚠️ **`align-content`, not `align-items`, is what keeps the copy under its title.** The cards are
     stretched to the tallest in the row, so without this the two rows share the slack out between
     them and a short card opens a gap between the title and the paragraph it belongs to — the
     whitespace this was asked to remove, moved rather than removed. `start` puts the slack at the
     foot of the card, where an unequal amount of copy is allowed to show. `align-items: center` is
     still what lines the number up with the title inside row 1. */
  align-content: start;
  column-gap: 1rem;
  row-gap: 0.7rem;
  padding: 1.75rem;
}
/* Back in flow, so it occupies the rail rather than hovering over reserved space. `.steps--cards`'s
   `top`/`left` are inert on a static box and are left where they are — they are that layout's, and
   this rule is the exception to it. */
.steps--feature .step::before { position: static; }
.steps--feature .step h3 { margin: 0; }
/* Column 2, so the copy hangs under the title and off the rail. `margin-top` is `row-gap`'s job here;
   a second paragraph still needs its own space and `.step p + p` has nothing else setting it. */
.steps--feature .step p { grid-column: 2; margin-top: 0; font-size: var(--step-body); }
.steps--feature .step p + p { margin-top: 0.75rem; }

/* --- steps, `flow`: the numbered-card weight for a long journey (D179) ---
   Home's `s-journey` only. Six stages at three-up is a shape the `cards` layout was never drawn for,
   and measured on the built page it failed in four ways at once: 73px of empty card above every
   title with the medallion floating in it; titles wrapping in some cards and not others, so the
   paragraphs in one row began at 109px, 136px and 136px; the stretch slack dumped at the foot in
   unequal amounts (78px, 51px, 29px); and the body copy at `--step-sm`, 14px in a 348px card, under
   the site's own 16px floor.

   ⚠️ **This is not `feature`, and must not become it.** `feature` is `/referrals/`'s centrepiece
   weight (D162, D164) and D144 is explicit that Home's journey is not a feature block. They share the
   diagnosis, not the treatment: `feature` centres the medallion against the title and therefore
   depends on the title staying on one line — a promise two wide columns can keep and three narrow
   ones cannot. Everything below is scoped to `--flow`; `/referrals/` does not set it and does not
   move.

   ★ **Top alignment is the whole idea.** The medallion is `align-self: start` in a left rail, so its
   centre is always `padding-top + half the disc` from the card's top edge — a constant, whatever the
   title does underneath it. That is what lets the connector below sit at a fixed `top` and still meet
   all six medallions, which is precisely what `feature`'s centred row cannot promise here.

   ⚠️ **The title's `padding-top` is an optical correction, not a spacer.** A 2.5rem disc top-aligned
   against a 1.12em line box puts the numeral's centre 20px down and the title's first line's centre
   at 0.56em; the calc closes that gap and stays closed as `--step-h4`'s clamp grows, because
   `min-height` and `padding` resolve `em` against the h3's own font size. Replace it with a flat rem
   and the pair drifts apart across the viewport range.

   ⚠️ **`min-height: 2.24em` is exactly two lines** (2 x the 1.12 line-height) and it is what makes
   every paragraph in a row start level. It is scoped to the three-up breakpoint on purpose: at 2-up
   the cards are ~440px and these titles all fit on one line, where forcing a second would add a dead
   line to all six rather than removing a misalignment. */
.steps--flow .step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  /* The cards stretch to the tallest in the row; `start` puts that slack at the foot of the card,
     where unequal copy is allowed to show, instead of sharing it out between the title and the
     paragraph it belongs to. Same reasoning as `.steps--feature`, same trap (D164). */
  align-content: start;
  column-gap: 1rem;
  row-gap: 0.9rem;
  padding: 1.75rem;
}
/* Back in flow as the card's first child, so it occupies the rail instead of hovering over a
   reserved strip. `.steps--cards`'s `top`/`left` are inert on a static box and are left alone —
   they belong to that layout and this is the exception to it. Quieter than `--cards`'s 3rem, which
   was the loudest thing in a 348px card. */
.steps--flow .step::before {
  position: static;
  grid-column: 1; grid-row: 1;
  width: 2.5rem; height: 2.5rem;
  font-size: 1.125rem;
}
.steps--flow .step h3 {
  grid-column: 2; grid-row: 1;
  margin: 0;
  /* `--step-h3` is 24px in a 258px title column, which is two words a line. `h4` gives the card a
     calmer hierarchy against a body that has just gone up to 16px. */
  font-size: var(--step-h4);
  line-height: 1.12;
  padding-top: calc(1.25rem - 0.56em);
}
/* Full card width rather than hanging off the rail: 314px is a 39-character measure at 16px against
   the rail's 32, and the medallion reads as a header badge for the stage, which is what it is. */
.steps--flow .step p {
  grid-column: 1 / -1;
  margin-top: 0;
  font-size: var(--step-body);
}
.steps--flow .step p + p { margin-top: 0.75rem; }

/* Two lines of title reserved, so every paragraph in a row starts level whatever its title does.
   ⚠️ **The padding is inside the box**, so the floor has to carry it: the optical correction above
   plus 2 x the 1.12 line-height. Written as `2.24em` alone it under-measures by the padding and a
   one-line title still sits 8px short of a two-line one, which is the misalignment this rule exists
   to remove.

   ⚠️ **62rem, and measured rather than assumed.** At three-up the column is ~347px and three of these
   six titles wrap, so the reserved line buys an aligned row from a genuinely split set. At the 2-up
   tablet the columns are ~355px of title and only *Person-Centred Assessment* wraps: reserving a line
   there costs five cards a dead line to close a 12px gap in the sixth, so it stays off. Below 48rem
   the grid is a single column and there is nothing to align to at all. */
@media (min-width: 62rem) {
  .steps--flow .step h3 { min-height: calc(1.25rem + 1.68em); }

  /* The connector is `wave-rule.svg` (D175), the smooth sine the client asked for on `s-referral`
     (D172) — so Home now threads both of its process sections with the same rule instead of a wave
     in one and a dashed hairline in the other. It keeps `.steps--track`'s geometry exactly: drawn
     into the grid gap to an item's LEFT, `right: 100%` and `width: var(--gap)`, so it never crosses
     a card, never sits on a rounded corner and needs no z-index against one.

     `top` is the medallion's centre — padding (1.75rem) plus half the disc (1.25rem) — less half the
     strip's own height. Change the card's padding, the disc's size or this height and that number
     moves with them.

     ⛔ **The offset is in `top`, never in a `transform`.** This connector takes §8's `draw`, and
     `draw` is `scaleX(0) → scaleX(1)` on `transform`; a `translateY(-50%)` here would be replaced
     rather than composed with, so the rule would sit 6px low for the length of its own draw and land
     somewhere else again. `.steps--wavy` has the same constraint and meets it the same way.

     Row starts carry no connector or it dangles off the edge of the grid — the `.steps--flow-{cols}`
     + `:nth-child` pair mirrors `.steps--track` and `.steps--wavy`, and adding a new column count
     means adding its `n+1` rule here. */
  .steps--flow .step:not(:first-child)::after {
    content: '';
    position: absolute; z-index: 0;
    top: calc(3rem - 0.375rem); right: 100%;
    width: var(--gap); height: 0.75rem;
    background: var(--green-light);
    -webkit-mask: url('../img/wave-rule.svg') left center / auto 100% repeat-x;
    mask: url('../img/wave-rule.svg') left center / auto 100% repeat-x;
  }
  .steps--flow-2 .step:nth-child(2n + 1)::after,
  .steps--flow-3 .step:nth-child(3n + 1)::after,
  .steps--flow-4 .step:nth-child(4n + 1)::after { content: none; }
}

/* --- steps, `wavy` layout: icon medallions on a smooth wavy connector (D172, D175) ---
   Home's `s-referral` only. The client asked for this three-stage route to "use some icons / wavy
   assets to connect it". It is the `.journey` treatment on a `steps` list: a white icon medallion
   per stage, the numeral kept as a small teal badge on its shoulder, and a wavy rule drawn into the
   gap between medallions in place of `.journey`'s ruled dash.

   ⚡ **The connector is `wave-rule.svg` (D175)** — a smooth two-period sine, tiled with `repeat-x`.
   It started as the `squiggle` asset (D172), which the client found too jagged; `squiggle` is still
   `.journey`'s heading rule and its draw animation is measured off that exact path, so a dedicated
   mask-only file was drawn rather than editing it.

   ⛔ Scoped entirely to `.steps--wavy`. `.steps--cards` — `/referrals/` and Home's own `s-journey`
   (D144) — must not move, so nothing here touches `.step` unqualified beyond overriding the two
   properties the medallion layout needs back (`padding-top`, and the `::before` position).

   The connector is a masked box tucked under the medallions at `z-index: 0`, so the wave tiles along
   it instead of `.journey`'s ruled border.

   ⚡ **It is drawn RIGHTWARD, from each step toward the next (D181)** — `left: 50%; right: -50%`,
   where `.journey__step::before` and `.steps--track` both draw leftward into the item they arrive at.
   ⚠️ **That is not a style choice and reversing it reintroduces a bug.** These medallions are 3.75rem
   circles sitting proud of the column, so a connector reaching a step's centre overlaps the previous
   step's circle by ~10px — and `z-index` cannot arbitrate it, because **each `.step` gets its own
   stacking context** from §8's scroll-driven `reveal` (Chrome promotes an element with an active
   scroll-timeline animation even while its `transform` computes to `none`). A stacking context is
   atomic against its siblings, so step 2's pseudo-element paints above *everything* in step 1
   whatever its z-index says, and the wave cut across the first circle. Drawn rightward, a connector
   only ever overlaps its own step's medallion — where the badge's `z-index: 1` does win, same
   context — and the next one, which paints later and covers it. ✅ Confirmed with
   `elementFromPoint` at the overlap before and after.

   ⛔ **The suppression moved with the direction**: it is now each row's LAST item that carries no
   connector, not its first, or the rule trails off the right edge of the grid. `.steps--wavy-{cols}`
   + `:nth-child({cols}n)`, and `:not(:last-child)` for the end of the list. */
.steps--wavy { gap: 2rem 1.25rem; }
.steps--wavy .step {
  padding-top: 0;
  display: flex; flex-direction: column; align-items: center;
  gap: 0.75rem; text-align: center;
}
.steps--wavy .step__badge { position: relative; z-index: 1; }
.steps--wavy .step__ic {
  display: grid; place-items: center;
  width: 3.75rem; height: 3.75rem; border-radius: 50%;
  background: var(--white); border: 1px solid var(--border); box-shadow: var(--shadow-sm);
}
.steps--wavy .step__ic .ic {
  width: 1.5rem; height: 1.5rem;
  --stroke-0: var(--green-dark); --fill-0: var(--green-dark);
}
/* The numeral, now a badge on the medallion's shoulder rather than the 2.5rem disc — teal with a
   white figure, never green under text (T32, D33), the same motif as `.journey__badge::after`.

   ⚡ **`z-index: 2`, 2026-09-01 (D180)** — *"the wavy lines at the backmost, then the circle with the
   icons, then the numbers."* ⚠️ **The badge was at the BACK of the three, not the front.** `.step` is
   `position: relative` with `z-index: auto`, so it opens no stacking context of its own and all three
   of these paint into the section's: the medallion holder is `z-index: 1`, the connector is `0`, and
   this was `auto` — which puts it at level 0 *and before the connector in tree order*, so the circle
   covered its lower-left corner and gave every badge a clipped, teardrop look. Naming the third level
   is what fixes it; the other two were already in the client's order. */
.steps--wavy .step::before {
  z-index: 2;
  top: -0.1rem; left: auto; right: calc(50% - 2.35rem);
  width: 1.4rem; height: 1.4rem;
  background: var(--brand-teal); color: var(--white);
  font-family: var(--font-body); font-weight: 700; font-size: 0.7rem;
}
.steps--wavy .step h3 { font-size: var(--step-h4); }
.steps--wavy .step p { max-width: 32ch; margin-inline: auto; }
/* Everything in this layout is centred — the medallion, the title and the copy — so a left-aligned
   button under it sat off the axis the whole section is built on (D180). `:has()` because
   `.steps__cta` is the list's SIBLING, not its child: the modifier lives on the `<ol>` and the button
   is a `<p>` after it. ⛔ Scoped through `.steps--wavy`, so the default and `cards` routes keep their
   left-aligned CTA.

   ⚠️ **`margin-inline` is half the job and `text-align` alone is a trap.** `.steps__cta` is a `<p>`,
   so §2's blanket `p { max-width: var(--max-prose) }` caps it: centring the text inside it centres the
   button in the prose column, which still sits hard left in the container. `.section--centred`'s note
   rule pairs the two for the same reason. */
.section:has(.steps--wavy) .steps__cta { margin-inline: auto; text-align: center; }

@media (min-width: 62rem) {
  .steps--wavy .step:not(:last-child)::after {
    content: '';
    position: absolute; z-index: 0;
    top: 1.35rem; left: 50%; right: -50%;
    height: 1.05rem;
    background: var(--green-light);
    -webkit-mask: url('../img/wave-rule.svg') left center / auto 100% repeat-x;
    mask: url('../img/wave-rule.svg') left center / auto 100% repeat-x;
  }
  .steps--wavy-2 .step:nth-child(2n)::after,
  .steps--wavy-3 .step:nth-child(3n)::after,
  .steps--wavy-4 .step:nth-child(4n)::after { content: none; }
}

/* --- steps, `timeline` layout: the client's own referral flow chart (D216) ---
   `/referrals/` only, and it is a transcription rather than a design: the client sent a photograph of
   their brochure panel — a dark teal ground, a column of pentagons joined by a vertical rule, the
   step's words beside each one — and asked for "a similar flowchart visual". The pentagon, the single
   column and the rail are theirs. The colours, the numeral and the type are this site's.

   ⚠️ **The pentagon carries the step number and the picture's pentagons carry nothing.** The client's
   own written copy numbers the five steps 1 to 5, this is an `<ol>`, and a numbered route with the
   numbers taken out reads as a list of five unrelated words. The numeral goes in the shape the
   picture gives us rather than beside it.

   ⚠️ **`--green-on-dark` behind `--teal-900`, which is 7.40:1 and is not the green rule being bent.**
   T32/D33 forbid a green that carries text at low contrast — the three brand greens measured on
   white, 2.07:1 to 3.13:1. This is the inverse: a dark numeral on a light green plate, which is
   exactly what the default `.step::before` already does on a white surface (`--green-light` behind
   `--ink`). ⛔ Do not "fix" it to teal-with-white to match `.steps--cards`: that medallion is teal
   because it sits on a **white** card, where a green plate would be the low-contrast case.

   🔴 **The rail is five per-item segments and it must stay that way.** §8 gives every `.step` a
   scroll-driven reveal, which promotes it to its own stacking context, and a stacking context is
   atomic against its siblings — the finding D181 spent a diagnosis on. A single rule reaching from
   one step down into the next is painted *over* by that next step whatever z-index it carries. So the
   list has **no `gap`**, each item pays for its own spacing in `padding-bottom`, and each item draws
   its own segment edge to edge inside its own box. The boxes touch, so the five segments read as one
   line. ⛔ Put a `gap` back and the line breaks into five dashes with nothing to say so.

   ⛔ **No `draw` on the rail.** `draw` is `scaleX` and this rule is vertical, so animating it means a
   `scaleY` — a fourth gesture, which D80 says the site does not have. The rail arrives with the step
   it belongs to and that is enough.

   The geometry, so it can be re-derived: the plate is 2.5rem, so its centre is 1.25rem from the top
   of the row and the rail sits at `1.25rem - half its own 3px width`. The title is pushed down
   0.35rem so that the centre of its FIRST line lands on the same 1.25rem — top-aligned, the way the
   client's panel aligns its three-line step. ⚠️ If the plate size, the rail width or `line-height`
   changes, all four numbers move together. */
.steps--timeline {
  gap: 0;
  grid-template-columns: 1fr;
  max-width: 42rem;
  margin-inline: auto;
  text-align: left;
}
.steps--timeline .step {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  column-gap: 1.25rem;
  align-items: start;
  padding-top: 0;
  padding-bottom: 2.25rem;
}
.steps--timeline .step:last-child { padding-bottom: 0; }

/* Back in flow as the row's first cell. The base rule's `top`/`left` are `0`, which is inert on a
   relatively positioned box, so they are left alone. `z-index` is what keeps the plate above the
   rail: both are pseudo-elements of the same `.step`, so this is arbitrated inside one stacking
   context and a level is all it takes (D181's own escape). */
.steps--timeline .step::before {
  position: relative;
  z-index: 1;
  border-radius: 0;
  clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}
/* ⚡ D218 made this section white, so the plate and the rail are keyed on the surface rather than on
   the layout. On white the pentagon keeps the site's default medallion — `--green-light` behind
   `--ink`, straight off `.step::before` — and the rail is `--green-light` at full strength, which is
   what 45% of `--green-on-dark` composited to on `--teal-900`. ⛔ Do not hard-code either colour into
   the `.steps--timeline` rules: the dark panel comes back with one class in `data/`, and these two
   selectors are what makes that a one-word change. */
.section--dark .steps--timeline .step::before {
  background: var(--green-on-dark);
  color: var(--teal-900);
}
.steps--timeline .step h3 {
  margin: 0;
  padding-top: 0.35rem;
  line-height: 1.35;
  font-size: var(--step-h4);
}
/* No step in the client's five carries body copy; the key still works, and it lands under the title
   rather than under the plate. */
.steps--timeline .step p {
  grid-column: 2;
  margin-top: 0.45rem;
  font-size: var(--step-body);
}
.steps--timeline .step::after {
  content: '';
  position: absolute;
  top: 0; bottom: 0;
  left: calc(1.25rem - 1.5px);
  width: 3px;
  background: var(--green-light);
}
.section--dark .steps--timeline .step::after {
  background: var(--green-on-dark);
  opacity: 0.45;
}
/* The first segment starts at its own plate's centre, the last one stops at its plate's centre, and
   a one-item timeline has nothing to join and draws nothing. */
.steps--timeline .step:first-child::after { top: 1.25rem; }
.steps--timeline .step:last-child::after  { bottom: auto; height: 1.25rem; }
.steps--timeline .step:only-child::after  { content: none; }

/* --- journey track (how we work) --- */
/* Five stops with a dashed rule threaded behind the medallions, from the export's care-steps block.
   The heading sits above the track rather than in a rail beside it: a 22rem rail leaves ~146px per
   stop, and this site has a 16px body floor, so the export's 12.5px column does not survive here
   (D42).

   Two states only, and the switch is late (70rem) on purpose. Five columns need ~190px each before a
   step title stops wrapping mid-phrase, and a 3+2 middle state would break the dashed rule, which is
   drawn per row. Below that it is a stacked list: medallion left, title and line right. */
.journey { display: grid; gap: 1.75rem 1.25rem; counter-reset: jstep; }

.journey__step {
  counter-increment: jstep;
  position: relative;
  display: flex; align-items: center; gap: 1rem;
}

@media (min-width: 70rem) {
  .journey { grid-template-columns: repeat(5, 1fr); gap: var(--gap) 1rem; }
  .journey__step { flex-direction: column; align-items: center; text-align: center; gap: 0.9rem; }
  /* Drawn back towards the previous stop, so the last one has nothing dangling off its right. The
     ends tuck under the two medallions, which sit above it on z-index. */
  .journey__step:not(:first-child)::before {
    content: '';
    position: absolute; z-index: 0;
    top: 1.875rem; right: 50%; left: -50%;
    border-top: 2px dashed var(--green-light);
  }
  .journey p { margin-inline: auto; }
}

.journey__badge { position: relative; z-index: 1; flex: none; }
.journey__ic {
  display: grid; place-items: center;
  width: 3.75rem; height: 3.75rem; border-radius: 50%;
  background: var(--white);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}
.journey__ic .ic { width: 1.5rem; height: 1.5rem; --stroke-0: var(--green-dark); --fill-0: var(--green-dark); }

/* The numeral is teal on white, not white on green — green under white text is 2.50:1 (T32). */
.journey__badge::after {
  content: counter(jstep);
  position: absolute; top: -0.15rem; right: -0.15rem; z-index: 2;
  display: grid; place-items: center;
  width: 1.5rem; height: 1.5rem; border-radius: 50%;
  background: var(--brand-teal); color: var(--white);
  font-family: var(--font-body); font-weight: 700; font-size: 0.75rem; line-height: 1;
}

.journey h3 { font-size: var(--step-h4); }
.journey p { margin-top: 0.35rem; font-size: var(--step-sm); color: var(--muted); max-width: 24ch; }

/* Hand-drawn rule under the section heading, as in the export. Decoration, so it is pale green. */
.journey-squiggle { width: 5rem; height: 1.5rem; margin-top: 0.9rem; --stroke-0: var(--green-light); }

/* --- FAQ --- */
.faq-list { display: grid; border-top: 1px solid var(--border); max-width: 52rem; }
.faq-item { border-bottom: 1px solid var(--border); }
.faq-item > summary {
  display: flex; justify-content: space-between; align-items: center; gap: 1rem;
  list-style: none; cursor: pointer;
  padding-block: 1.125rem;
  font-family: var(--font-display); font-weight: 600; font-size: 1.0625rem;
}
.faq-item > summary::-webkit-details-marker { display: none; }
.faq-item__toggle {
  display: grid; place-items: center; flex: none;
  width: 1.75rem; height: 1.75rem; border-radius: 50%;
  background: var(--green-pale); color: var(--ink);
  font-size: 1.25rem; line-height: 1;
  transition: transform 0.15s ease;
}
.faq-item[open] .faq-item__toggle { transform: rotate(45deg); background: var(--green-light); color: var(--ink); }
.faq-item__answer { padding-bottom: 1.25rem; color: var(--muted); }

/* `centred` (D160): the head and the list centred in the column rather than set against its left edge.
   The list keeps its 52rem measure — that number is what stops a question and its `+` sitting at
   opposite ends of 1200px — so on a wide page the only question is which side of it the empty space
   falls. Centred, it falls on both and reads as a measure; left, it reads as a section that lost its
   right-hand column. ⛔ **Home's FAQ is the one that keeps the left edge** (D67): the space beside it
   is where the figure the client removed used to stand, and it stays empty. */
.faq--centred .section__head { max-width: 52rem; margin-inline: auto; text-align: center; }
.faq--centred .faq-list { margin-inline: auto; }

/* --- CTA band --- */
.cta-band { background: var(--teal-900); color: var(--on-dark); }
.cta-band__grid { display: grid; gap: var(--gap); align-items: center; }
@media (min-width: 55rem) { .cta-band__grid { grid-template-columns: 1fr auto; } }
.cta-band p { color: var(--on-dark-muted); }

/* The one referral button, under the copy (D130). `btn--on-dark` is white-on-teal: `btn--primary` is
   teal and would be invisible here. Top margin rather than a gap on the parent, because the parent is
   a plain <div> holding an h2 and a paragraph, not a grid. */
.cta-band__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.5rem; }
.cta-band__actions .ic { --stroke-0: currentColor; }

.cta-band__contacts { display: grid; gap: 0.875rem; }
.contact-line { display: flex; align-items: center; gap: 0.875rem; text-decoration: none; }
.contact-line__ic {
  display: grid; place-items: center; flex: none;
  width: 2.75rem; height: 2.75rem; border-radius: 50%;
  background: var(--on-dark-fill);
}
.contact-line__ic .ic { width: 1.15rem; height: 1.15rem; --stroke-0: var(--green-on-dark); --fill-0: var(--green-on-dark); }
/* `min-width: 0` on the middle column, because a flex item's default `min-width: auto` refuses to
   shrink below its content — and `referrals@momentumcare.co.uk` set in the display face is wider than
   a 390px row once the D156 chevron takes its place at the end. Without both of these the chevron is
   pushed off the edge of the band. */
.contact-line__body { min-width: 0; }
.contact-line__value { overflow-wrap: anywhere; }
.contact-line__label { display: block; font-size: var(--step-xs); color: var(--on-dark-muted); }
.contact-line__value { display: block; font-family: var(--font-display); font-size: 1.15rem; color: var(--white); }
a.contact-line:hover .contact-line__value { color: var(--green-on-dark); text-decoration: underline; }

/* The address line (D50). Four short lines rather than one value, so it sits at body size and the
   row aligns to the top of the block instead of to its centre. It is not a link and gets no hover. */
.contact-line--static { align-items: flex-start; }
.contact-line__address {
  font-family: var(--font-display); font-style: normal;
  font-size: 1.05rem; line-height: 1.4; color: var(--white);
}

/* Promoted to the top of Contact as its own section, where it is the point of the page rather than
   the closing ask, so the copy column does not need to hold the contacts to a corner. */
@media (min-width: 55rem) {
  .cta-band--details .cta-band__grid { grid-template-columns: 1fr 1fr; align-items: start; }

  /* ⚡ **One heading across the top, then two columns under it (D157).** It was heading-over-map with
     a second heading over the contacts (D156); the client centred it and cut the second one, so the
     `copy` area now spans both columns and the map and the list start level beneath it.

     `align-items: stretch` overrides `--details`'s `start` above — deliberately, and it is what lets
     the list match the map's height rather than ending halfway up it. `1fr` on the second row gives
     both columns the same box to fill. `--gap` is a column gap of nearly 3rem and too much under a
     heading, so the row gap is set on its own. */
  .cta-band--map .cta-band__grid {
    grid-template-areas: "copy copy" "map contacts";
    grid-template-rows: auto auto;
    row-gap: var(--band-rowgap);
    align-items: stretch;
  }
  .cta-band--map .cta-band__copy     { grid-area: copy; }
  .cta-band--map .cta-band__contacts { grid-area: contacts; }
  .cta-band--map .cta-band__map      { grid-area: map; }

  /* ⚠️ **The contacts set the height of the row and the map grows into it, not the other way round.**
     The four rows are content-sized — the address is four lines and cannot be squeezed — so forcing
     equal `1fr` rows only pushed the list past the bottom of the map. Stretched instead, the map
     column takes the row's full height and the frame flexes into whatever the directions link leaves,
     which lands the two columns exactly level and makes the map as big as the content allows. */
  .cta-band--map .cta-band__map { display: flex; flex-direction: column; }
  .cta-band--map .map__frame {
    height: auto;
    flex: 1;
    /* A floor, for the case the rows are ever shorter than this — without it a map with nothing to
       match would collapse to its own content, which for an iframe is nothing. */
    min-height: clamp(18rem, 34vw, 30rem);
  }
}

/* --- the deeper band, and its one centred heading (D157) ---
   The client asked for more vertical room so the map can be bigger. This is the only section on the
   site that overrides `--section-y`: it is the whole of `/contact/` below the hero, and the page has
   nothing else to give it rhythm. Everywhere else the band is a sign-off and keeps the site's spacing.

   The heading is centred across both columns and holds a measure — a display heading running the full
   1200px would be a line nobody scans. `margin-inline: auto` rather than a grid `justify-self`, so it
   stays centred when the band stacks. */
/* ⚡ **Scoped to `.cta-band--list` since D160, not to `.cta-band--map`.** The deeper band, the centred
   heading and the ruled rows are the treatment the client liked on `/contact/`; the map is one of the
   things that can sit inside it, not the thing that defines it. `/referrals/`'s "Prefer to Speak to
   Someone?" carries the same three and no map (`'feature' => true`, see `partials/sections/cta.php`).
   ⚠️ **Both classes are emitted together on `/contact/`** — `--map` still owns the grid areas and the
   frame below, and nothing there moved. */
.cta-band--list {
  /* ⚡ Reduced at D159 from `clamp(4rem, 3rem + 4vw, 7rem)` — 99px above the heading and 99px below
     it was balanced but airy, and the client asked for it tighter. **This is the only number that
     moved**: the map is sized by the contact rows, not by this, so the band lost about 70px of
     whitespace without the map losing a pixel. ~64px at 1280, ~45px on a phone; still above the
     site's own `--section-y`, which is the point of the override. */
  --band-pad:    clamp(2.75rem, 2.25rem + 2.2vw, 4.5rem);
  --band-rowgap: clamp(1.75rem, 1.25rem + 1.4vw, 3rem);
  /* The row's own padding, named because two rules need the same number: the rows set it, and a band
     whose rows do not start with a rule (the feature band, whose first row opens a column rather than
     a list) has to put the top padding back. */
  --row-pad:     clamp(1rem, 0.7rem + 1.2vw, 2rem);
  padding-block: var(--band-pad);
}
.cta-band--list .cta-band__copy {
  text-align: center;
  max-width: 46rem;
  margin-inline: auto;
  /* ⚠️ **The heading sits between two equal spaces, and this is what makes that true (D158).** The
     band's top padding is `--band-pad` and the grid's row gap is `--band-rowgap`, so without this the
     heading had 99px above it and 38px below — it read as pushed down rather than centred. Topping
     the gap up here rather than raising `row-gap` keeps the stacked layout tight: on a phone the same
     row gap also sits between the map and the first contact row, and that one should stay small. */
  padding-bottom: calc(var(--band-pad) - var(--band-rowgap));
}

/* --- the map inside the details band (D155) ---
   Not a section of its own: the client asked for the embed in the empty space under "How to Reach
   Us", so it is a third grid child of that band and the grid names three areas instead of two. The
   copy column is short — a heading and nothing else — and the contacts column is four rows deep, so
   the space the map fills is space the band already had.

   **Source order is heading, map, contacts, and it is the mobile order too.** Since D156 both columns
   carry a heading, so each one has to stay with the thing it names — stacked, the band reads *How to
   Reach Us* → map → directions → *Get in Touch* → the four rows. No `order` juggling: the DOM order
   and the visual order are the same at every width, which is what keeps focus order and reading order
   honest (WCAG 1.3.2, 2.4.3). Above 55rem the grid areas lift the contacts back into the right column.

   ⚠️ **The phone number is not buried by putting the map first on a phone.** The hero card directly
   above this band opens with *Call 0116 502 2975* and *Email us*, so the first tap target on the page
   is still the number — which is what made this ordering safe to choose on adjacency instead.

   The mount is `--on-dark-fill` with a hairline, not the white card a light band would take: white
   here is a lit panel on a dark surface and pulls the eye off the phone number. `overflow: hidden` is
   load-bearing — an iframe does not inherit its parent's `border-radius`, so without it Google's
   square corners sit proud of the rounded mount.

   A height, not an `aspect-ratio`. ⚠️ **`aspect-ratio` with a `max-height` shrinks the width too** —
   a block box with `width: auto` re-solves its width to hold the ratio once the height is capped, so
   the mount stops short of its column and floats in the middle of it. `clamp()` keeps it full width
   at every size, and the cap keeps the band roughly as deep as its own contact rows. */
.map__frame {
  position: relative;
  padding: 0.4rem;
  background: var(--on-dark-fill);
  border: 1px solid var(--on-dark-hairline);
  border-radius: var(--radius);
  /* ⚡ Taller again at D157, on top of D156's own increase — *"so that the map can look bigger."* It
     runs past 4:3 in its column now, and the contact rows stretch to meet it rather than the map
     being held down to them (see the grid above). The floor is what a map is still usable at on a
     phone; the cap stops it eating a laptop screen whole. */
  height: clamp(17rem, 38vw, 32rem);
  overflow: hidden;
}
.map__frame iframe {
  display: block;
  position: absolute;
  inset: 0.4rem;
  width: calc(100% - 0.8rem);
  height: calc(100% - 0.8rem);
  border: 0;
  border-radius: calc(var(--radius) - 5px);
  /* Google's tiles paint on their own schedule; a soft teal ground means the box is not a white
     flash on a dark band while they arrive. */
  background: var(--teal-tint);
}

/* --- the contacts column as a ruled list (D156) ---
   The client's design: a heading of its own, then one full-width row per contact, hairline-separated,
   each with a rounded-square icon tile and a chevron at the far end. It is the same four rows in the
   same order carrying the same values — what changed is that they now read down the column as a list
   rather than sitting as a block in the corner of the band.

   Scoped to `.cta-band--map`, which is `/contact/` and only `/contact/`. Every other closing band on
   the site keeps the compact rows it has always had; this is the one page where the band *is* the
   page.

   ⚠️ **The chevron is only ever on a row that goes somewhere.** With a map present the address row
   becomes a link to the directions URL (see `cta.php`) — the mark is earned before it is drawn. */
/* `gap: 0` because the rows are separated by their own borders now, not by space. ⚠️ No
   `align-content` here — the desktop block above sets `stretch`, and a declaration at this point in
   the file would be later in source order at equal specificity and would silently win. */
.cta-band--list .cta-band__contacts { gap: 0; }

/* ⚡ **The row padding is what sizes the map (D157).** The contacts column is content-sized and the
   map stretches into whatever height it sets, so *"more vertical spacing so the map can look bigger"*
   is one change, not two: deeper rows make a taller column, and the map grows to meet it. */
.cta-band--list .contact-line {
  gap: 1.1rem;
  padding-block: var(--row-pad);
  border-top: 1px solid var(--on-dark-hairline);
}
/* The first row opens the list and takes no rule; with D157's heading centred above both columns
   there is no longer a sibling heading to key off, so it is `:first-child` again. Its top padding
   goes too, which is what lands the first icon level with the top of the map beside it. */
.cta-band--list .contact-line:first-child { border-top: 0; padding-top: 0; }

/* Rounded squares rather than the circles the compact band uses: at this size a circle wastes its
   corners, and the square tile lines the four rows up on one left edge with the heading. */
.cta-band--list .contact-line__ic {
  width: 3rem; height: 3rem;
  border-radius: var(--radius);
  border: 1px solid var(--on-dark-hairline);
}
.cta-band--list .contact-line__ic .ic { width: 1.3rem; height: 1.3rem; }

/* ⚠️ **A phone has one column and 335px of row, and `referrals@momentumcare.co.uk` did not fit in
   it** — the value sat at 236px of space and wanted 244, so it broke after `.co.u` and put a lone `k`
   on the second line. Noticed on `/referrals/` while its band was being changed (D164) and fixed for
   both bands, because it was never `/referrals/`'s own: `/contact/` has done the same thing since
   D156. The row gives the 8px back out of its own furniture rather than out of the type — the value
   stays at the 16px floor. ⚠️ **At 320px it still wraps**; there is no honest way to fit that address
   in 203px, and a wrap is better than shrinking the one line a reader has to read accurately. */
@media (max-width: 54.999rem) {
  .cta-band--list .contact-line { gap: 0.9rem; }
  .cta-band--list .contact-line__ic { width: 2.5rem; height: 2.5rem; }
}

/* The longest value on the site is `referrals@momentumcare.co.uk`, and an email has no space to break
   at — at 1.15rem it wrapped mid-domain on a 390px row once the chevron took its place at the end.
   Scaled with the viewport it sits on one line from 320px up, and `overflow-wrap` above stays as the
   last-resort net so nothing ever escapes the band. Scoped here: every other closing band lays its
   contacts out in a narrower `auto` column and is unaffected. */
.cta-band--list .contact-line__value { font-size: clamp(1rem, 0.85rem + 0.6vw, 1.15rem); }

/* Pushed to the far end by `auto` rather than by a fixed column, so a long value never collides with
   it. Centred on the row even where the row is the four-line address. */
.contact-line__go { margin-left: auto; align-self: center; flex: none; }
.contact-line__go .ic {
  width: 1rem; height: 1rem;
  --stroke-0: var(--on-dark-muted);
  transition: transform 0.15s ease;
}
a.contact-line:hover .contact-line__go .ic { --stroke-0: var(--green-on-dark); transform: translateX(3px); }

/* The address row is a link here, so it takes the hover the other three have. Its value is the
   `<address>` block rather than a `.contact-line__value`, which is what the §6 hover rule targets. */
.cta-band--map a.contact-line:hover .contact-line__address { color: var(--green-on-dark); }

/* `.section--dark .link-arrow` already recolours the label to --green-on-dark (§6). The arrow itself
   defaults to --green-dark, which clears the 3:1 graphics threshold on --teal-900 but sits a shade
   dull beside the contact rows' icons — matched to them here rather than left to chance (D79, T32). */
.map__foot { margin-top: 0.9rem; }
.cta-band__map .link-arrow .ic { --stroke-0: var(--green-on-dark); }

/* --- the feature band: `/contact/`'s treatment without a map (D160) ---
   The client's own summary of what they liked about `/contact/` was *"the colours and the simplicity"*,
   and this is that band with the one thing removed that does not travel. `/referrals/`'s "Prefer to
   Speak to Someone?" was the compact closing band — heading and a line of copy on the left, two small
   contact rows squeezed into an `auto` column on the right, about 230px deep. It carries `--list` and
   `--feature` now: the deeper padding, the centred heading and the ruled rows with their square tiles,
   and the two rows laid across the full width instead of held to a corner.

   ⛔ **This is the second band on the site to take it and there is not a third by default.** Nine other
   pages close on the compact `cta`, where the band is a sign-off under a page that has already said
   everything; it opts in from `data/` (`'feature' => true`) on a page where the band is a destination.

   ⚠️ **No map, and no third-party request** — the iframe stays on `/contact/`, which is the one page
   the client asked for it on and the only page that makes a request to Google (D155, D50, T30, T53). */
.cta-band--feature .cta-band__grid {
  grid-template-columns: minmax(0, 1fr);
  row-gap: var(--band-rowgap);
}
/* Rows side by side once there is room, rather than a short list centred under a wide heading — the
   band is 1200px across and two rows stacked in the middle of it is the emptiness this was asked to
   fix. Below 55rem they stack and become the ruled list `/contact/` shows at every width.

   ⚡ **`auto-fit` since D164, not `1fr 1fr`.** The client added a third row — *General enquiries*
   after *Referrals* — and a fixed two-column track puts the third one alone under the first with an
   empty cell beside it. The track counts itself instead: two rows go two-up, three go three-up on a
   desktop, and where three will not fit the same rule drops them to two and then to the stacked list.
   ⚠️ **The 21.5rem floor is one string's doing.** `referrals@momentumcare.co.uk` is the longest value
   on the site and an email address has nowhere to break, so the minimum is the narrowest column that
   still holds it whole — see the four numbers below. Widen it and a laptop gets two columns and an
   orphaned third row; narrow it and the third column breaks the address mid-domain. */
@media (min-width: 55rem) {
  .cta-band--feature .cta-band__contacts {
    grid-template-columns: repeat(auto-fit, minmax(21.5rem, 1fr));
    column-gap: clamp(1.75rem, 1rem + 2vw, 2.5rem);
  }
  /* ⚠️ **Four numbers here exist to keep one string on one line, and they move together.** A third
     column is about 344px wide at a laptop, and the row spends 92px of it before the value gets any:
     the tile, the gap on each side of the copy and the chevron. `referrals@momentumcare.co.uk` needs
     244px at 16px and 256px at `--list`'s 18.4px, so the value steps down to the site's 16px floor
     and the tile, the row gap and the column gap each give back a few pixels. That leaves ~8px of
     slack at the narrowest width this band shows three columns at.

     ⛔ **`/contact/` is untouched by all four** — its rows are a single column beside the map, at the
     larger size, with the 3rem tiles. ⚠️ **If a longer address is ever added to `data/site.php`,
     check this band first**: it is the only place on the site where a contact value is width-bound.
     The honest fallback if one no longer fits is `minmax(28rem, 1fr)`, which drops this band to two
     columns and stacks the third row, not a smaller value. */
  .cta-band--feature .contact-line { gap: 1rem; }
  .cta-band--feature .contact-line__ic { width: 2.75rem; height: 2.75rem; }
  .cta-band--feature .contact-line__value { font-size: 1rem; }
  /* ⚠️ **Both rows open a column here, so both keep their rule.** `--list` drops it on the first row
     because there it opens a list and a rule above the first item is a line under nothing; side by
     side, the same rule would leave one row ruled and one not. The padding comes back with it, from
     the same `--row-pad` the rows are set in, so the two never drift. */
  .cta-band--feature .contact-line:first-child {
    border-top: 1px solid var(--on-dark-hairline);
    padding-top: var(--row-pad);
  }
}

/* --- the strip band: Home's closing band as one horizontal row (D199) ---
   Client instruction with a mockup, 2026-09-02 — *"Redo the CTA on homepage like this."* Two things
   change and nothing else does: the dark surface becomes a **rounded card inside the container**
   instead of a full-bleed band, and the three contact rows become **three centred columns divided by
   hairlines**, with the heading and the button holding the first cell.

   ⚠️ **One consumer, and it is an exception to D176.** Thirteen other pages carry this partial and
   render the compact band byte-identically; the client named the homepage. ⛔ Do not lift any of this
   onto `.cta-band` — `--strip` is opt-in from `data/` (`'layout' => 'strip'`), the same way `--map`
   and `--feature` are.

   ⚠️ **The columns are the `quicklinks` item stood upright**, not a new component: same 2.75rem
   `--on-dark-fill` medallion, same `--on-dark-hairline` divider, same `--green-on-dark` icon. That is
   deliberate — the bar under the hero and the band at the foot of the page are the two places Home
   lays routes out side by side, and they should read as one pattern. */

/* The section stops being the dark surface and the grid starts. Specificity: `.section--dark` (§4)
   and `.cta-band` both set this background as a single class, so the override needs two — hence the
   doubled selector rather than an `!important`. `.section--dark` is still on the element and still
   correct for everything inside it: every descendant sits on the teal card. */
.cta-band.cta-band--strip { background: transparent; }
/* ⚡ **The one container on the site that is not 1200px wide (D200)** — *"make the design of the strip
   wider… the text and things look too small."* The two halves of that are one problem: the row holds
   four cells and `referrals@momentumcare.co.uk`, so the only way the type grows is for the card to
   find the width first. 90rem is 1440, which is the whole of a laptop screen inside the page padding
   and still short of the point where a single row of contact details reads as a rule across a
   window. ⛔ **Scoped to this band and it stays that way** — `--max` is what holds every other
   section on Home to one measure, and a closing band that starts wider than the section above it is
   the client's instruction, not a new page width. */
.cta-band--strip .container { max-width: 90rem; }
.cta-band--strip .cta-band__grid {
  background: var(--teal-900);
  border-radius: var(--radius-lg);
  /* ⚠️ **The low end of this clamp is a phone measurement, not a taste.** A card takes width away
     from the rows inside it that a full-bleed band never had: at 28px of padding, 390px of phone
     leaves the contacts 274px, and `referrals@momentumcare.co.uk` breaks after `.co.u` and puts a
     lone `k` on the next line — the same break `--list` was fixed for at D164. ⚡ **16px since D201**,
     down from 20px: the body face is about 7% wider than Fraunces at the same size, which moved that
     break from 320px up into 360px, a width real phones are sold at. ⛔ Do not raise the minimum to
     match the maximum. */
  padding: clamp(1rem, 0.5rem + 2vw, 2.75rem);
}
/* ⚠️ **The value grows with the viewport between the two sizes this site already uses (D200):** the
   16px floor at the breakpoint, `--list`'s 1.15rem from 1440 up. It was flat at the floor, which is
   what *"the text looks too small"* was — but the floor is still the floor, because a column that has
   to hold `referrals@momentumcare.co.uk` whole cannot be given type it has no room for. The slope is
   set by those two points and nothing else: 16px at 1152 and 18.4px at 1440.
   ⛔ **Never below 16px**, and ⛔ never a fixed size — a fixed 1.15rem breaks the address at the
   breakpoint, which is exactly what the flat 1rem was there to prevent. ⚠️ At 320px the address still
   wraps; there is no honest way to fit it in 182px, and every band on the site has wrapped there
   since D164 for the same reason. */
.cta-band--strip .contact-line__value { font-size: clamp(1rem, 0.55rem + 0.66vw, 1.15rem); }
/* ⚡ **The number and the two addresses are set in the body face, client instruction 2026-09-02
   (D201)** — *"use a normal font in the numbers and emails."* `.contact-line__value` is
   `--font-display` everywhere on the site, and Fraunces is a display serif: it is drawn for headings,
   and a phone number and an email address are neither. **This is D198's move in a second place** —
   the client took the footer's phone number out of the same face on the same day, for the same
   reason.

   ✅ **The size is untouched and must stay that way.** D200 raised it one instruction ago because
   *"the text looks too small"*, so ⛔ **do not read this as the whole setting the way D198 read its
   own instruction** — there the size came off because 1.3rem of Figtree still would not have matched
   the column around it; here the size is the client's own most recent request. 🟢 Figtree has the
   larger x-height of the two, so at the same size the values read slightly bigger, not smaller.

   ⚠️ **Colour is what marks the value as the primary line, and it already did** — `--white` against
   the label's `--on-dark-muted`. That is the hierarchy D198 ended up relying on in the footer, and it
   is why no weight is added here: a bolder value would be a third signal doing the same job.

   ⛔ **Scoped to this band.** `/contact/` and `/referrals/` still set their contact values in
   Fraunces, and the client has not been asked about those — see the note in `decisions.md`. */
.cta-band--strip .contact-line__value { font-family: var(--font-body); }
/* The heading's own margin is what the card's padding is measured against, so it goes: the h2 is the
   first thing in the cell and its top space is the card's, not the type's. */
.cta-band--strip .cta-band__copy h2 { margin-top: 0; }
.cta-band--strip .cta-band__actions { margin-top: 1.25rem; }

/* ⚠️ **The four cells only exist above 72rem, and that number was measured rather than chosen.**
   It was 67rem while the value sat flat at the 16px floor — the column holding
   `referrals@momentumcare.co.uk` clears the 214px that needs at about 1054px, so 1072 was the first
   round figure past it. ⚡ **D200 widened the card and let the type grow**, and both ends of that move
   the floor: the wider container gives the columns about 80px back at 1280, and the larger medallion,
   label and value take more than that at the bottom of the range. Re-measured with the new sizes, the
   row wants **about 1130px** before the address is safe, so the breakpoint is **72rem (1152)**.
   This is the third band the same string has sized (see `--feature` above and `--list` before it),
   and it is why a 1024px tablet — and now a 1152px window — gets the stacked band instead: below the
   breakpoint the layout falls back to what every other page shows, heading and button, then the three
   rows in an `auto` column, icon beside label. ⛔ **Do not lower this to get the row onto a smaller
   laptop.** The cell that gives way is the one holding the address the referral route depends on. */
@media (min-width: 72rem) {
  .cta-band--strip .cta-band__grid {
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: clamp(1.5rem, 0.5rem + 2vw, 2.5rem);
  }
  /* ⚠️ **`auto` tracks, not three equal `1fr`.** The three values are 13 characters, 28 and 24, so
     equal thirds give the phone number 100px it does not use and take the same 100px off
     `referrals@momentumcare.co.uk`, which then breaks mid-domain — the string that sets the column
     floor on the feature band above, doing it again. `auto` sizes each column to its own content
     first and then shares what is left over equally, which is also what the client's mockup shows:
     its dividers are not evenly spaced. ⛔ Do not "tidy" these to `1fr`. */
  .cta-band--strip .cta-band__contacts {
    grid-template-columns: repeat(3, auto);
    gap: 0;
  }
  /* Upright: medallion, then the label, then the value, centred on the column. `justify-items` rather
     than `text-align` alone, because the medallion is a grid item of its own and would otherwise sit
     at the left edge of a centred block of text. */
  .cta-band--strip .contact-line {
    flex-direction: column;
    align-items: center;
    gap: 0.9rem;
    text-align: center;
    padding-inline: clamp(1rem, 0.4rem + 1.2vw, 2rem);
  }
  /* ⚠️ **Every column takes the rule, including the first** — the divider between the heading and
     *Call Us* is one of the three in the client's mockup, and it is the one that separates the ask
     from the routes. That is the opposite of `.quicklinks__item:first-child`, which drops it because
     nothing precedes it. The rule is inset from the card's padding rather than run to its edge, so it
     reads as a divider between cells and not as a border on the card. */
  .cta-band--strip .contact-line {
    border-left: 1px solid var(--on-dark-hairline);
    align-self: stretch;
    justify-content: center;
  }
  /* ⚠️ **If a longer address is ever added to `data/site.php`, this is the third band to check**, and
     the value above already bottoms out at the floor — the honest fallback here is the stacked band at
     a wider breakpoint, never smaller type.

     ⚡ **The medallion and the label go up with it (D200).** A 2.75rem circle over 18.4px of address
     reads as a marker beside the type rather than the top of the cell, and `--step-xs` under a plate
     that size disappears. 3.25rem and `--step-sm` are both sizes the site already uses — the tile on
     `--list` is 3rem and every other small label on a dark surface is `--step-sm`. ⚠️ **Neither
     touches the column width**: the plate is 52px and the label is shorter than the address, so the
     one string that sizes this band is unaffected by both. */
  .cta-band--strip .contact-line__ic { width: 3.25rem; height: 3.25rem; }
  .cta-band--strip .contact-line__ic .ic { width: 1.35rem; height: 1.35rem; }
  .cta-band--strip .contact-line__label { font-size: var(--step-sm); margin-bottom: 0.15rem; }
  .cta-band--strip .contact-line__body { text-align: center; }
}

/* ⚡ **Below the row, the stacked band gives the address 12px back out of its own furniture (D201).**
   Same trade `--list` made at D164 and for the same string: the smaller plate and the tighter gap are
   invisible at this width, and a lone `k` on a second line is not. ⚠️ **It is the body face that made
   this necessary** — at 16px Figtree the address measures 229px against Fraunces's 214, so a 360px
   phone was 7px short. ⛔ The type does not give any of it back; 16px is the floor. */
@media (max-width: 71.999rem) {
  .cta-band--strip .contact-line { gap: 0.75rem; }
  .cta-band--strip .contact-line__ic { width: 2.5rem; height: 2.5rem; }
}

/* The medallion carries the hover as well as the value, because on a centred column it is the top of
   the target rather than a marker beside it — the same move `.quicklinks__item:hover` makes. */
.cta-band--strip a.contact-line:hover .contact-line__ic { background: var(--on-dark-hairline); }

/* --- related links (D51) ---
   A heading and a row of sibling links, side by side once there is room. The heading is set at H4
   size: this is navigation at the end of a page, and an H2 at full size would announce it as a
   section of the argument. */
.related { display: grid; gap: 1rem; align-items: baseline; }
@media (min-width: 48rem) { .related { grid-template-columns: auto 1fr; gap: 1.5rem; } }
.related__heading { font-family: var(--font-body); font-size: var(--step-h4); font-weight: 700; }
.related__list { display: flex; flex-wrap: wrap; gap: 0.75rem; }

/* --- facts: label-and-value pairs about one thing (D57) ---
   Three uses on a home page, two layouts. `panel` is the summary under the hero and the address
   block ("Where: [Area], Leicester") — read top to bottom, one thing per line. `grid` is what the
   property has — read by scanning for the one row you care about, which is why it is tiles rather
   than a longer list.

   Almost every value is a [Placeholder] chip today, so both layouts have to survive a value that is
   wider than its label and wraps. The label column is capped rather than sized to content for that
   reason: content sizing would let one long label push every value on the page out of alignment. */
.facts--panel {
  display: grid; gap: var(--gap);
  padding: clamp(1.25rem, 1rem + 1.5vw, 2rem);
  background: var(--white);
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.section--pale .facts--panel { box-shadow: none; }
@media (min-width: 55rem) {
  /* Address beside the rows, but only when there is an address — the at-a-glance panel has none and
     would otherwise put five rows in a column half the width of the card. */
  .facts--panel:has(.facts__address) { grid-template-columns: minmax(0, 18rem) 1fr; gap: var(--gap); }
}

.facts__list { display: grid; }
.facts--panel .facts__row {
  display: grid; gap: 0.15rem 1.25rem;
  padding-block: 0.85rem;
  border-top: 1px solid var(--border);
}
.facts--panel .facts__row:first-child { border-top: 0; padding-top: 0; }
.facts--panel .facts__row:last-child { padding-bottom: 0; }
@media (min-width: 32rem) {
  .facts--panel .facts__row { grid-template-columns: minmax(0, 10rem) 1fr; align-items: baseline; }
}

.facts__label {
  margin: 0;
  font-family: var(--font-body); font-weight: 700; font-size: var(--step-sm);
  color: var(--muted);
}
.facts__value {
  margin: 0;
  font-size: var(--step-body); font-weight: 600; color: var(--ink);
}

.facts__address address { font-style: normal; font-size: var(--step-body); color: var(--ink); line-height: 1.7; }
.facts__address .facts__label { margin-bottom: 0.35rem; }

/* Tiles. Pale rather than white so the grid reads as one block on a white page, and bordered rather
   than shadowed so eight of them do not turn the section into a field of floating cards. */
.facts--grid .facts__list {
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  gap: 1rem;
}
.facts--grid .facts__row {
  display: grid; gap: 0.3rem;
  padding: 1.1rem 1.25rem;
  background: var(--green-pale);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.section--pale .facts--grid .facts__row { background: var(--white); }
.facts--grid .facts__label { font-size: var(--step-xs); letter-spacing: 0.06em; text-transform: uppercase; }

.facts__note {
  max-width: var(--max-prose); margin-top: var(--gap);
  font-size: var(--step-lead); color: var(--muted);
}

/* --- prose, `media` layout: copy beside a photograph (D115) ---
   A home page's description section, in the shape the reference location page uses. The heading, the
   prose and the button read as one block on the left; the picture sits alongside at 4:5.

   ⚠️ **This replaced `.specs`**, the tile grid added the same day under D111. Its content — what the
   house has — moved into the hero as a fact grid when the page went to five sections, so the type
   had no consumer left and came out rather than sitting unused. The tiles live on as `.factgrid`.

   The picture is `align-self: start` so it keeps its own height instead of stretching to a long
   column of copy: at 4:5 a stretched placeholder becomes a very tall grey panel.

   ⚡ **The split is the hero band's, to the pixel, since 2026-09-02 (D220)** — client instruction:
   *"Make the height of the things on the hero section and supported living section the same - the
   images are too big and things - limit that."* It was `1.25fr / 1fr`, which put this photograph at
   **477 × 596** against the hero's **438 × 438** one section above it: the larger of the two, and the
   tallest thing on the page. `1.45fr / 1fr` here is the same ratio `.hero--property:has(.hero__panel--row)
   .hero__split` takes under D209, and this section already shares the hero's container width and its
   54.4px gap, so the two grids now compute identically — **copy 635, picture 438**. With the partial's
   ratio at `1 / 1` (also D220) the two photographs are **the same 438 × 438 square**, which is the
   whole of what was asked for.

   ⛔ **Do not re-tune one of the two on its own.** The instruction is that they match; a change to
   either the hero's ratio or this one has to be made in both places or the page stops answering it.
   ⚠️ **438 is D56's floor** and the hero has been sitting on it since D209 — the photograph is still
   the largest single element in its band. There is no room to shrink these further without going
   under it.
   ✅ **No other page moves**: `.prose-media` has exactly five consumers and they are the five home
   pages. */
.prose-media { display: grid; gap: var(--gap); }
@media (min-width: 60rem) {
  .prose-media { grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: clamp(2rem, 1rem + 3vw, 3.5rem); align-items: start; }
}
.prose-media__body .section__head { margin-bottom: 1rem; }
.prose-media__body .prose__body { max-width: var(--max-prose); }
.prose-media__action { margin-top: var(--gap); }
.prose-media__aside { align-self: start; }
@media (max-width: 59.999rem) {
  /* Picture under the copy, not over it. The H2 names the place and is the reason somebody is
     reading the section; an image between the heading above and the prose below just pushes it
     down. Source order already puts the copy first, so this only needs the ratio opening up.
     ⚠️ **Unchanged by D220.** Stacked, there is no hero photograph beside this one to match — the
     two are a screen apart — and 16:10 is what stops a full-width picture filling a phone. The
     square is a two-column arrangement's answer and it is scoped to one by the partial's `--ratio`
     losing to this rule. */
  .prose-media__image { aspect-ratio: 16 / 10; }
}

/* --- gallery and lightbox (D58, redesigned D88, unified to one grid D173) ---
   Two states: real photographs, and labelled placeholder slots naming the shots T06 waits for. Both
   are the same row grid now, at the same 4:3 — the point of D173 — so a page reads identically whether
   its gallery holds nine real photographs or seven placeholder slots, and at any count in between. */

/* **Photographs: a row grid, every tile 4:3.** D88 ran a `columns` masonry here, each frame at its own
   `width / height`, which kept every photograph true to how it was shot — and cost the layout the one
   thing a photo grid needs most: a clean bottom edge. `columns` balances by *estimated total height*,
   not by row, so whichever column drew the fewest or shortest items finished short and the grid's foot
   read as unaligned — visible on Weymouth's nine photographs at three columns, on client review.
   `display: grid` fixes it by construction: a row is genuinely a row, so the last one lines up
   regardless of item count or the camera's own aspect ratio. `object-fit: cover` on
   `.ph-image--photo > img` (unchanged, D88) does the cropping this trades away — the frame is shown as
   4:3, not as it was shot.
   Column *width*, not count, so the number of columns still follows the container with no media query. */
.gallery {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
}
.gallery__item { margin: 0; }
/* One item, opted in from `data/` (`'feature' => true`), spans every column. `grid-column: 1 / -1` is
   the grid equivalent of D88-era `column-span: all` on the old masonry — first line to last, whatever
   the generated column count is at this width — so the tile reads as a lead image the rest of the grid
   sits under, not the largest cell in the mix. Its own ratio (16:9, wider than the grid's 4:3) is set
   in the partial, not here. */
.gallery__item--feature { grid-column: 1 / -1; }

/* Slots keep the same shape the photographs now do. A placeholder has no intrinsic shape to respect
   either way, and a ragged column of dashed panels reads as a broken layout rather than as a
   deliberate gap (D35). Its own `minmax` floor is 16rem against the photographs' 19rem: a placeholder
   carries no caption, so a narrower tile still reads cleanly where a captioned photograph would feel
   cramped. */
.gallery--empty {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
}
.gallery--empty .gallery__item { margin: 0; }

/* Two rows, and **all three children are placed explicitly**. They have to be: `.gallery__zoom` is
   an explicitly positioned overlay, explicit placement happens before auto-placement, and an
   auto-placed `.ph-image` would then find row 1 occupied and drop itself to row 2 — putting the
   photograph underneath its own scrim. Naming all three costs two lines and removes the ordering
   dependency entirely. */
.gallery__link {
  display: grid; grid-template-columns: 1fr; gap: 0.6rem;
  text-decoration: none;
}
.gallery__link .ph-image { grid-area: 1 / 1; }
.gallery__caption { grid-area: 2 / 1; }
.gallery__link .ph-image { transition: transform 0.18s ease, box-shadow 0.18s ease; }
/* ⚠️ **Hover and focus are written out as two selectors, not `:is(:hover, :focus-visible)`.**
   Measured, not assumed: with `:is()` wrapping the two pseudo-classes, the browser reported the
   element as matching (`el.matches()` returned true) while the declarations never reached the
   computed style — the keyboard ring, written plainly, applied on the same element in the same
   frame. Two selectors always resolve, cost one line, and read the same as every other hover rule in
   this file. Do not "tidy" these back into `:is()`. */
.gallery__link:hover .ph-image,
.gallery__link:focus-visible .ph-image { transform: translateY(-3px); box-shadow: var(--shadow-md); }
/* The focus ring goes on the frame, not the <a>: the link is a grid box that includes the caption,
   so an outline on it draws a rectangle round the text as well and reads as two things selected. */
.gallery__link:focus-visible { outline: none; }
.gallery__link:focus-visible .ph-image { outline: 3px solid var(--accent-text); outline-offset: 3px; }

/* A bare grid — no caption under the tile (`'captions' => false`) — loses the one thing that said
   these were more than decoration. The gutter closes up to suit, and the affordance below carries
   the "you can open this" on its own.

   ⚡ **`margin-bottom: 0.85rem` was removed here, 2026-09-03 (D234, found during D233's
   vertical-rhythm pass).** The comment above says the gutter closes up; the declaration opened it.
   `.gallery__item` is `margin: 0` above and the grid's own `gap` is `1rem`, so **on a grid an item
   margin does not replace the gap, it adds to it** — and it adds on one axis only. Measured on
   `/homes/weymouth/` at 1280: **columns 16px apart, rows 33px**, so a bare grid was looser
   than the captioned grid it was written to tighten.
   Rows are 16px now and the two axes match. ⛔ Do not put a margin back on a grid item to open the
   rows up: `row-gap` is the declaration that means that, and it would not silently skew one axis.
   ⚠️ Three consumers, all bare: `/gallery/`, `/activity-hub/` and `/homes/weymouth/`. */
.gallery--bare .gallery__link { gap: 0; }

/* **The open affordance.**
   A scrim and a mark, both resting at 0 and both arriving on hover *and* on keyboard focus. It is
   `opacity` rather than `display`, so there is a state to ease in §8 and nothing to reflow.
   The scrim is the ink token at low alpha, which is dark enough for a white mark to clear 4.5:1 on
   every photograph in the set including the two bright training rooms.
   ⚠️ It is placed at `grid-area: 1 / 1`, **not** `position: absolute; inset: 0`. The link is a grid
   of two rows, frame then caption, and an absolutely positioned overlay on it covers both — dimming
   the caption along with the photograph. Stacking it into the first track instead means it is
   exactly the size of the frame whatever shape that frame turns out to be, which in a masonry column
   is not knowable up front. */
.gallery__zoom {
  grid-area: 1 / 1;
  display: grid; place-items: center;
  border-radius: var(--radius-lg);
  background: rgba(23, 48, 50, 0.34);
  opacity: 0;
  pointer-events: none;
}
.gallery__zoom-ic {
  width: 1.55rem; height: 1.55rem;
  color: var(--white);
  /* The mark sits on a photograph, not on a flat surface. A soft drop shadow keeps it legible over
     a blown-out skylight, which is the one thing a flat white stroke loses against. */
  filter: drop-shadow(0 1px 3px rgba(23, 48, 50, 0.55));
}
.gallery__link:hover .gallery__zoom,
.gallery__link:focus-visible .gallery__zoom { opacity: 1; }

/* **The hover label** (`'hover_labels' => true`, Weymouth only).
   The name of the room, inside the frame, arriving with the scrim rather than sitting under the tile
   like a caption. Same two triggers as everything else on this link — hover *and* `:focus-visible` —
   so a keyboard user gets the name too; a label that only exists for a mouse is a caption you have
   hidden from half the people who need it most on this site.

   ⚠️ **It is placed at `grid-area: 1 / 1` with `align-self: end`, not `position: absolute`**, for
   exactly the reason `.gallery__zoom` above it is: the link is a two-row grid, frame then caption,
   and an absolutely positioned label on it would measure itself against both rows and hang below the
   photograph. Stacking it into the first track means it is the width and height of the frame at any
   ratio, which matters here because the `feature` tile is 16:9 and the rest are 4:3.

   ⚠️ **The gradient is not decoration and must not be flattened to the scrim's colour.** `.gallery__zoom`
   is a flat 0.34 over the whole frame, which is measured for a white *mark* with a drop shadow on it
   — it is not enough behind white *text* on a blown-out window or a pale wall, and every bedroom in
   this set has one. The gradient lands ~0.88 of `--ink` under the words and fades to nothing a third
   of the way up, so the photograph is still readable above it. Deepening the scrim instead would dim
   all nine tiles to protect one line of text.

   ⚠️ **The bottom corners are rounded to `--radius-lg` to match the frame** — the label reaches both
   edges, so square corners cut two visible notches out of the tile. */
.gallery__label {
  grid-area: 1 / 1;
  align-self: end;
  /* 🔴 **`position: relative` is load-bearing and there is no way to guess it from the markup.**
     `.ph-image` is `position: relative` with `z-index: auto`, so it paints in the positioned-descendants
     step; a static sibling — however late in the DOM — paints in the in-flow step *before* it and ends
     up **underneath the photograph**. Confirmed with `elementFromPoint` at this element's own centre:
     it returned the `<img>`. Positioning the label puts it in the same step and tree order decides,
     which is what the DOM already reads as. ⛔ Do not "simplify" this away, and do not reach for a
     `z-index` instead — nothing here needs a layer number, only the right paint step.
     ⚠️ `.gallery__zoom` above has the identical problem and is deliberately not changed here: its
     scrim covers every gallery on the site, including two pages the client emptied of per-image text
     on purpose. It is reported, not fixed in passing. */
  position: relative;
  padding: 3.25rem 1rem 1.15rem;
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  /* ⚠️ **The plate is `--brand-teal`, not the near-black `--teal-950` it was** (D212) — *"have more
     of our brand colour"*. #00656E is the logo's own teal, so the label now reads as Momentum Care
     rather than as a generic photo scrim. It is written as an `rgba()` literal because the stops need
     alpha, which is the same thing `.gallery__zoom` above does; ⚠️ **if `--brand-teal` is ever
     retuned, these three stops do not follow it** — they are the one place on the site that restates
     it. ⛔ Do not swap it back to a neutral to "calm it down".
     ✅ **Measured, not assumed.** White on flat #00656E is 6.80:1; at the 0.88 stop under the words,
     composited over the worst case in this set (a blown-out white wall), it is **5.21:1** — clear of
     AA for normal text, and this text is large. */
  background: linear-gradient(to top, rgba(0, 101, 110, 0.94) 0%, rgba(0, 101, 110, 0.78) 42%, rgba(0, 101, 110, 0) 100%);
  color: var(--white);
  /* ⚠️ **Centred and much larger on instruction** (D212). The label is one or two words, so it is
     read at a glance rather than scanned, and it now has the frame's full width to sit in.
     ⚠️ The clamp tops out at 30px: *Modern Kitchen*, the longest of the nine, measures ~218px against
     the 333px a 4:3 tile leaves inside its padding at 1280, and ~160px against 303px on a 375px
     phone. ⛔ **Check that string first if the scale is raised** — it is the one that decides whether
     a label wraps, and a wrapped room name reads as a sentence.
     ⚠️ Still not `--font-display`. Bigger did not turn it into a heading: it is the name of what you
     are looking at, and Fraunces at this size on a photograph reads as a section title (D198, D201). */
  text-align: center;
  font-size: clamp(1.375rem, 1.1rem + 1.1vw, 1.875rem);
  font-weight: 600;
  line-height: 1.25;
  opacity: 0;
  /* The rest position and the arrival are both here in §5, never in §8 (D79): a reduced-motion user
     gets the label instantly rather than not at all. §8 adds the curve and nothing else. */
  transform: translateY(0.5rem);
  pointer-events: none;
}
/* **The green half of the brand, drawn as a rule** (D212). `--brand-teal` alone is one colour of the
   two, and the reference the client pointed at was green — but ⛔ **no green on this site carries
   text, at any size** (brand.md, T32, D33): `--brand-green` is 2.07:1 on white and worse over a
   photograph. A rule is what green is *for* here — fills, strokes and rules — so the mark under the
   room name is green and the word itself stays white on teal. ⛔ Do not "simplify" this by colouring
   the text green or by making the plate green with dark text; both are the failure the token comment
   in §1 exists to prevent.
   ⚠️ It is `::after` on the label, so it fades and rises with it as one thing rather than arriving
   as a second gesture (D80). */
.gallery__label::after {
  content: "";
  display: block;
  width: 2.25rem; height: 3px;
  margin: 0.55rem auto 0;
  border-radius: 2px;
  background: var(--brand-green);
}
.gallery__link:hover .gallery__label,
.gallery__link:focus-visible .gallery__label { opacity: 1; transform: translateY(0); }
/* The caption sits in row 2, outside the scrim, so it is never the thing being dimmed. */
.gallery__caption { font-size: var(--step-sm); color: var(--muted); }
.gallery__link:hover .gallery__caption,
.gallery__link:focus-visible .gallery__caption { color: var(--ink); }

.lightbox {
  max-width: min(72rem, 92vw); max-height: 92vh;
  padding: 0; border: 0; border-radius: var(--radius-lg);
  background: var(--white); color: var(--ink);
  overflow: visible;
}
.lightbox::backdrop { background: rgba(23, 48, 50, 0.82); }

/* **The frame is a flex column and the photograph is the part that gives way** (D88).
   It used to be `max-height: 72vh` on the image and nothing else, which worked while the only things
   under it were a caption, a counter and two buttons. The thumbnail rail is a fifth row, and on a
   900px-tall window the total ran past the dialog's own `max-height: 92vh` — the rail spilled out
   below the white card and sat on the backdrop. Flexing the figure instead means the image takes
   whatever is left after the rows that have a fixed height, at any window size, with no magic number.

   ⚠️ `display: flex` is on `.lightbox[open]`, **never on `.lightbox`**. The UA stylesheet hides a
   closed dialog with `display: none`, and any author `display` on the element beats it whatever the
   specificity — an unqualified rule here leaves the lightbox permanently open on every page. */
.lightbox[open] { display: flex; }
/* ⚠️ `min-width: 0` is what keeps the dialog on the screen on a phone, and it is not decoration.
   The frame is a flex item of `.lightbox[open]`, so it takes `min-width: auto` — its automatic
   minimum size is its own min-content width, and it will not shrink below it however narrow the
   dialog gets. Its min-content is set by the thumbnail rail: `overflow-x: auto` zeroes a scroll
   container's *automatic minimum size*, but the rail is a stretched item in a column, not the thing
   being clamped, and its min-content is still the eleven thumbs laid out end to end. Measured at
   375px: the dialog resolved to 345 (`92vw`) and the frame to **824**, so the photograph and the
   rail hung 479px past the white card, out over the backdrop and off the right of the screen. It is
   invisible above ~880px because the dialog is wider than 824 there, which is why it only ever
   showed up on a phone. With the floor removed the frame is 345, the image fits, and the rail
   scrolls — which is the behaviour it was written for (D88). Same reason `min-height: 0` is beside
   it for the column axis; the two are a pair, and neither is safe to tidy away. */
.lightbox__frame {
  display: flex; flex-direction: column; min-height: 0; min-width: 0; flex: 1;
  padding: clamp(1rem, 0.75rem + 1vw, 1.5rem);
}
.lightbox__figure { margin: 0; display: flex; flex-direction: column; min-height: 0; }
.lightbox__figure img {
  width: 100%; min-height: 0; flex: 1 1 auto; object-fit: contain;
  border-radius: var(--radius);
  background: var(--green-pale);
}
.lightbox__figure figcaption { margin-top: 0.9rem; font-size: var(--step-sm); color: var(--muted); max-width: var(--max-prose); }
/* ⚠️ `.lightbox__count` carries `.visually-hidden` in the markup since D235 — the client asked for
   "Photograph 1 of 7" not to be shown — so it has no visual treatment of its own any more. The
   element stays because it is the lightbox's only position announcement (see the partial). ⛔ Do
   not give it a size or a colour here; that would not bring it back on screen, it would just be a
   rule with nothing to style. */
.lightbox__controls { display: flex; gap: 0.5rem; margin-top: 1rem; }
.lightbox__btn, .lightbox__close {
  display: grid; place-items: center;
  width: 2.75rem; height: 2.75rem;
  border: 1px solid var(--border); border-radius: 50%;
  background: var(--white); color: var(--ink);
  font-size: 1.1rem; cursor: pointer;
}
.lightbox__btn:hover, .lightbox__close:hover { border-color: var(--green-dark); background: var(--green-pale); }
/* Outside the frame's top-right corner, where it cannot land on the photograph. On a small screen
   there is no room outside, so it comes back in. */
.lightbox__close { position: absolute; top: -1.25rem; right: -1.25rem; box-shadow: var(--shadow-md); }
@media (max-width: 47.999rem) {
  .lightbox__close { top: 0.5rem; right: 0.5rem; }
}

/* --- the lightbox thumbnail rail (D88) ---
   Eleven photographs is past the point where → nine times is a reasonable way to reach the last one.
   A horizontal scroller, because a wrapping rail changes the dialog's height as you move through it
   and the photograph above jumps with it. */
.lightbox__thumbs {
  display: flex; gap: 0.5rem;
  /* `flex: none` so the rail keeps its full height and the photograph above it is what shrinks. */
  flex: none;
  margin: 1rem 0 0; padding: 0 0 0.25rem;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: thin;
  /* Each thumb settles under the pointer instead of stopping halfway across one. */
  scroll-snap-type: x proximity;
}
.lightbox__thumbs li { scroll-snap-align: center; }
.lightbox__thumb {
  display: block; padding: 0;
  width: 4.5rem; height: 3.375rem;
  border: 2px solid transparent; border-radius: var(--radius);
  background: var(--green-pale);
  overflow: hidden; cursor: pointer;
  /* Dimmed at rest so the lit one reads as lit. Not so dim that the rail becomes unreadable — a
     person scanning it for "the one with the table tennis" has to be able to see the table tennis. */
  opacity: 0.55;
}
.lightbox__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lightbox__thumb:hover,
.lightbox__thumb:focus-visible { opacity: 1; border-color: var(--green-dark); }
.lightbox__thumb:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 2px; }
/* The current one. Colour is not carrying this on its own — it is also the only one at full opacity
   and the only one the script sets `aria-current` on. */
.lightbox__thumb.is-current { opacity: 1; border-color: var(--accent-text); }

/* --- careers band --- */
.band-card {
  display: grid; gap: var(--gap); align-items: center;
  padding: clamp(1.75rem, 1.25rem + 2vw, 3rem);
  background: var(--green-pale); border-radius: var(--radius-lg);
}
@media (min-width: 55rem) { .band-card { grid-template-columns: 1.15fr 1fr; } }
/* ⚡ **`/about/`'s "Our Commitment" is 60/40 and nothing else is** (D167). The client asked for the
   wider copy column when they asked for the picture, and 1.5fr/1fr is that; the shared 1.15fr/1fr
   above is 53/47 and stays, because the other four bands are a shout-out beside a photograph rather
   than a page's closing statement. The class is set in `data/about.php` through the section's own
   `class` key, so the partial knows nothing about it. */
@media (min-width: 55rem) { .band--60-40 .band-card { grid-template-columns: 1.5fr 1fr; } }
/* ⚠️ **This rule has no consumer as of 2026-08-28 and it stays.** It was written for the two bands
   with no media — `/referrals/`'s "See Our Locations", hidden by D165, and `/about/`'s "Our
   Commitment", which took a photograph in D167. ⛔ **Do not delete it**: it is what the page does the
   day T06 pulls the interim photography back out, and it answers itself in both directions.

   ⚠️ **Two of the six bands on the site have no media** — `/referrals/`'s "See Our Locations" and
   `/about/`'s "Our Commitment" — and in a two-column grid their copy sat in the left 1.15fr with the
   other column empty, which read as a card whose picture had failed to load rather than as a card that
   never had one (D160). With nothing to sit beside, the copy takes a measure of its own and centres:
   a heading, a sentence and one button, deliberately placed. `:has()` rather than a flag in `data/`,
   because the condition is *is there a picture here*, which the markup already knows — and it answers
   itself the day T06's photography lands. */
@media (min-width: 55rem) {
  .band-card:not(:has(img, .ph-image)) {
    grid-template-columns: minmax(0, 46rem);
    justify-content: center; justify-items: center; text-align: center;
  }
}
/* The band is used with an eyebrow (Home) and without one (the Activity Hub's "arranging a place"),
   and without it the H2 sat directly on the first line of copy. */
.band-card :is(h2, h3) + p { margin-top: 0.75rem; }

/* --- the CQC register mark (D206) ---
   ⚡ Client instruction 2026-09-02: the Care Quality Commission logo *"besides the See All locations
   button"* on `/supported-living/`'s "Our Locations" band, linking to their own register entry. The
   artwork, the destination and the claim it makes are all governed by `partials/cqc-badge.php` and
   `data/site.php`'s `cqc_url` — **read those before changing anything here.**

   ⚠️ **The action row only becomes a flex row when a badge is in it.** `:has()` rather than a
   blanket `display: flex` on `.band-card__cta`, because five other bands carry that class with a
   lone button in it and there is no reason for them to change layout mode. It answers itself: the
   day the badge is removed from `data/`, the row goes back to what the other five do. */
.band-card__cta:has(.cqc-badge) {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 1rem 1.25rem;
}

/* ⛔ **The dark plate is not decoration — it is the surface the artwork was drawn for.** The client
   supplied CQC's **white** variant and every `fill` in it is `#FFFFFF`; the band card is
   `--green-pale` #F2F7EA, so the mark laid bare on it is invisible. `--teal-900` is the site's own
   dark-surface token and the mark measures as designed against it. ⛔ **Do not "simplify" this to
   `fill: currentColor` and an `--ink` mark sitting flat on the card** — recolouring a regulator's
   logo is a brand alteration nobody here has permission to make. If a flat version is wanted, ask
   the client for CQC's dark variant.

   ⚠️ It sits beside a `--brand-teal` #00656E button, so two dark teals are adjacent. That is
   deliberate and it reads: the button is a green-flavoured pill with a label and an arrow, this is a
   rounded plate holding a logo, and nothing about them is confusable. */
.cqc-badge {
  display: inline-flex; align-items: center;
  padding: 0.5625rem 0.9375rem;
  background: var(--teal-900); border-radius: var(--radius);
  text-decoration: none;
  transition: background-color 0.15s ease;
}
/* State in §5, not §8 (D79), so a reduced-motion reader still gets the response — instantly rather
   than eased. ⛔ No lift: `.btn`'s 1px translate is what answers a press on this site, and this is a
   link to a regulator's page, not an action on ours. */
.cqc-badge:hover { background: var(--teal-950); }

/* ⚠️ **`aspect-ratio` is load-bearing and matches the file's `viewBox`.** The supplied SVG carries
   `width="170.083px" height="53.739px"` as attributes; those map to presentation declarations at
   specificity 0, so the `width: auto` here beats them — but the ratio is what then sizes the box, and
   naming it is what stops a browser falling back to the intrinsic 170px width and overflowing the
   plate. **If the artwork is ever replaced, copy the new `viewBox`'s numbers here.**
   The clamp keeps the wordmark legible on a phone without letting it outgrow the button beside it:
   28px tall at 320px, 34px from ~1050px up. */
.cqc-badge__mark {
  display: block; width: auto;
  height: clamp(1.75rem, 1.5rem + 0.8vw, 2.125rem);
  aspect-ratio: 170.083 / 53.739;
}

/* **The light variant** (D213) — CQC's purple full-colour artwork, bare on a white surface.
   ⛔ **The plate comes off and must**: the purple file is what CQC draw *for* a light background, so
   putting it on `--teal-900` would be using the wrong variant on the wrong surface, which is the
   mirror of the mistake the plate exists to prevent for the white one. Neither is a recolour — they
   are two files the regulator publishes.
   ⚠️ Its own `aspect-ratio`, because this one is a **raster** at 3840×1228 rather than the SVG's
   viewBox. **If the file is replaced, copy the new numbers here and into the `<img>`'s attributes in
   `partials/cqc-badge.php`.**
   ⚠️ It stands where three lines of ticked copy stood, so it is set larger than the badge in the
   band: 40px tall on a phone, 52px from ~1280 up, against that one's 28–34. */
.cqc-badge--light {
  padding: 0;
  background: none;
  border-radius: 0;
  /* `transition` is a shorthand, so this replaces the base rule's `background-color` transition
     rather than adding to it — which is right, because this variant has no background to ease. */
  transition: opacity 0.15s ease;
}
/* State in §5 (D79), so a reduced-motion reader still gets the response. ⛔ No filter, no tint: the
   mark itself may not be altered, and opacity moves the whole thing evenly. */
.cqc-badge--light:hover { background: none; opacity: 0.82; }
.cqc-badge--light .cqc-badge__mark {
  height: clamp(2.5rem, 2rem + 1.4vw, 3.25rem);
  aspect-ratio: 3840 / 1228;
}

/* The slot the hero's tick lines held. Same top margin as `.hero__points` so nothing else in the
   card moved when the swap happened. */
.hero__cqc { margin-top: 1.375rem; }

/* --- card-grid filter (D11, D54, D85) ---
   A labelled dropdown per group plus a search box, narrowing the grid below them. It was checkbox
   chips until 2026-08-10; the chip styles went with them rather than staying as dead rules.
   Everything here is a native control with a visible, permanent <label>. Part of this audience uses
   keyboard or switch access, and a native select is the one control that is already correct for
   them on every platform, including the phone's own picker. Nothing is `appearance: none` except the
   select's arrow, which is redrawn below so it can take a brand colour. */
.filter {
  padding: clamp(1.25rem, 1rem + 1.5vw, 2rem);
  background: var(--green-pale);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
/* ⚡ **A flex row weighted towards the search box, client instruction 2026-09-03 (redesign)** —
   was `grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr))`, equal thirds, which was right
   for three controls and wrong for two. With "Service type" gone the row is one dropdown and a
   search field, and equal halves gave a **519px-wide select holding a single option** beside a
   search box the same size. Weighted 1:3, the dropdown reads as a qualifier and the search reads as
   the control, which is what the section now is.
   ⚠️ It still wraps to one full-width column on a phone, and a third group dropping in still needs
   no media query — `flex-basis` does what `minmax()` did. */
.filter__controls {
  display: flex; flex-wrap: wrap; gap: 1rem;
}
@media (min-width: 55rem) { .filter__controls { gap: var(--gap); } }

/* `<p>` wrapping label + control, so the two are one block with no margin of their own.
   ⚠️ Both `flex-grow` values are load-bearing on a phone: a field with `flex: 0 …` would sit at its
   basis on its own wrapped line and leave the rest of the row empty. */
.filter__field { display: flex; flex-direction: column; gap: 0.4rem; margin: 0; flex: 1 1 14rem; }
.filter__field--search { flex: 3 1 18rem; }
.filter__label { font-size: var(--step-sm); font-weight: 700; color: var(--ink); }

.filter__control {
  width: 100%;
  /* 0.75rem block, not 0.7rem, which lands the control on 44px. Under it a select is below the
     touch target minimum (WCAG 2.5.8) for an audience where some people have limited fine motor
     control, and it is the first thing on the page anyone is asked to hit. */
  padding: 0.75rem 0.9rem;
  background: var(--white);
  border: 1px solid var(--border); border-radius: var(--radius);
  font: inherit; font-size: var(--step-sm); line-height: 1.3; color: var(--ink);
  transition: border-color 0.15s ease;
}
.filter__control:hover { border-color: var(--green-dark); }
.filter__control:focus-visible { outline: 3px solid var(--accent-text); outline-offset: 2px; }

/* The arrow is an inline SVG data URI in `background-image`, not an icon file: it is the only place
   on the site that needs a mark inside a form control, and a background cannot be an <svg> element.
   Stroke is `--green-dark`, which is decoration on a control whose state is already carried by its
   text — no fact rides on the colour, so the 3:1 rule for icons is met and not leaned on. */
.filter__select {
  appearance: none; -webkit-appearance: none;
  padding-right: 2.4rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23789D46' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.9rem center;
  background-size: 1rem 1rem;
  cursor: pointer;
}
/* The magnifier, drawn the same way and for the same reason as the select's arrow above: a
   background cannot be an `<svg>` element, and this is the second and last control on the site that
   needs a mark inside it. Same `--green-dark` stroke, same decoration-only argument — the field is
   labelled "Search" in text directly above it, so nothing rides on the glyph. Added with the
   two-control redesign (2026-09-03): with the row weighted towards it, the search box is the widest
   thing in the section and a bare rounded rectangle read as an empty text field rather than a
   search. ⚠️ `padding-left` and `background-position` are a pair — move one and the placeholder
   runs under the glyph. */
.filter__search {
  padding-left: 2.7rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23789D46' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.6-3.6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: left 0.95rem center;
  background-size: 1.05rem 1.05rem;
}
/* Safari draws its own clear button and Chrome does not; neither is styleable to the brand, and the
   form's own "Clear filters" button already covers the job for everyone. */
.filter__search::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.filter__foot {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
  margin-top: 1.25rem;
}
/* ⚠️ `margin: 0` is the alignment, not a tidy-up. The status is a `<p>` and §2 gives every
   paragraph a bottom margin; in a `align-items: center` flex row the margin box is what gets
   centred, so 14px underneath lifted the text **7px above** the "Clear filters" button beside it —
   visible the moment a result count appears, which is the only time both are on screen. Measured
   before: status box top 869 in a 868–905 row. ⛔ Do not "fix" this with `align-items: baseline`;
   the button's own padding puts its baseline somewhere else again. */
.filter__status { margin: 0; font-size: var(--step-sm); font-weight: 600; color: var(--ink); }
.filter__status:empty { display: none; }
.filter__clear { padding-block: 0.5rem; }
.filter__empty {
  margin-top: 1.25rem; padding: 1rem 1.25rem;
  background: var(--white); border: 1px solid var(--border); border-radius: var(--radius);
}
.filter__empty p { color: var(--muted); font-size: var(--step-sm); }

/* ------------------------------------------------- 7. decoration (D67)

   The designer's export decorates its white space with a small, repeated set of flat marks. This is
   that set, taken from `index.html` specifically — its inner pages use a different, heavier
   vocabulary (a blob and a dot field bled off the hero corners) that does not appear on the home
   page and is not reproduced here.

   What index.html actually uses, and where:

     star + heart      pinned on the corners of the hero visual, overlapping it
     sparkle           inline after a handwritten `.script` heading — and nowhere else
     squiggle          under the "how we work" heading            (already built: .journey-squiggle)
     swoosh            under the handwritten accent               (already built: .accent-script)
     heart, larger     above the heading of the small advice card
     questions figure  beside the FAQ accordion

   How it is built, and each of these is load-bearing:

   - **Masks, not images.** Every shape in assets/img/decor/ is one opaque colour and is painted by
     `mask` + `background-color`, exactly as `.accent-script::after` paints the swoosh (§3). Colour
     comes from a token, so no hex is baked into the artwork and one file works on light and dark.
     The one exception is the FAQ figure, which is full-colour artwork and is a background-image.
   - **Pseudo-elements, not markup.** Nothing here reaches the DOM, so nothing here reaches the
     accessibility tree, the reading order or the copy decks. Removing the whole part leaves every
     page saying exactly what it said before — which is the test for whether something is decoration.
     (The export gives its FAQ figure the alt text "Illustration of a person with questions", which
     is a sentence a screen reader has to sit through to learn nothing.)
   - **Never near a letter.** `.decor > .container` is lifted to z-index 1 and the marks sit under
     it. These are small saturated shapes in the margins, not washes behind text, so green stays a
     fill and never becomes a letter (D33, T32).
   - **Placement is data, not markup.** A section opts in through its `class` key in `data/`, which
     `section_open()` already passes through. The partials are untouched and every section type
     stays reusable undecorated.

   Sizes are in rem, so a mark scales with the type rather than the viewport. The hero pair turns off
   below 48rem, as it does in the export, because at that width it lands on the copy. */

.decor { overflow: clip; }              /* `.section` is already position: relative */
.decor > .container { position: relative; z-index: 1; }

/* Shapes are ink on paper at 300dpi and paper is expensive. */
@media print {
  .decor [class*='decor']::before, .decor::before, .decor::after,
  .decor--spark :is(.section__head, .split__head, .cta-band__grid) h2::after { display: none; }
}

/* --- hero: a star off one corner of the visual and a heart off the other ---
   The export pins both to the hero photograph rather than to the section, so they read as marks on
   the picture. Ours sits on `.hero__aside`, which is the same slot whether it is holding a
   photograph or today's labelled placeholder. Pulled slightly outside the frame on both axes: fully
   inside they look like a caption, fully outside like debris. */
.decor--hero .hero__aside { position: relative; }
.decor--hero .hero__aside::before,
.decor--hero .hero__aside::after {
  content: '';
  position: absolute;
  z-index: 1;
  pointer-events: none;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
}
.decor--hero .hero__aside::before {
  left: -0.9rem; top: -0.9rem;
  width: 2.75rem; height: 2.75rem;
  background-color: var(--brand-green);
  -webkit-mask-image: url('../img/decor/star.svg'); mask-image: url('../img/decor/star.svg');
}
.decor--hero .hero__aside::after {
  right: -0.75rem; top: 3.25rem;
  width: 2rem; height: 1.8rem;
  background-color: var(--accent-text);
  -webkit-mask-image: url('../img/decor/heart.svg'); mask-image: url('../img/decor/heart.svg');
}

/* --- the same two marks on a hero that has no picture to pin them to ---
   D67 put the star and the heart on `.hero__aside`, which was right while Home was the only page
   carrying them. Across the whole site that slot is missing twice over: the three `overlay` heroes
   have no aside at all (the visual is the full-bleed band *behind* everything), and eleven `service`
   heroes carry no visual whatsoever until T06 lands — breadcrumb, H1, standfirst, pale band, and
   nothing else on the largest section of the page.

   So the anchor is chosen per layout, in one priority order, and exactly one of these three matches
   any given hero:

     overlay              → the white card, the only object in the band
     anything with an aside → the aside, unchanged from D67
     everything else      → the copy column, where the marks take the empty right-hand third that
                            `h1 { max-width: 30ch }` and `.hero__lead { max-width: 60ch }` leave

   `:not(:has(.hero__aside))` is what keeps the third rule off a service page that *does* have a
   picture, so a page gains a photograph at T06 and the marks move back onto its frame with no edit
   here. Appearance is inherited from the block above; these rules set position only. */
.hero--overlay.decor--hero .hero__card,
.decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack { position: relative; }

.hero--overlay.decor--hero .hero__card::before,
.hero--overlay.decor--hero .hero__card::after,
.decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::before,
.decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::after {
  content: '';
  position: absolute;
  z-index: 1;
  pointer-events: none;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
}
/* The overlay card takes the same two offsets as the aside: the card is the object in that band the
   way the picture is the object in Home's, so the marks read the same way against it. */
.hero--overlay.decor--hero .hero__card::before {
  left: -0.9rem; top: -0.9rem;
  width: 2.75rem; height: 2.75rem;
  background-color: var(--brand-green);
  -webkit-mask-image: url('../img/decor/star.svg'); mask-image: url('../img/decor/star.svg');
}
/* ⚠️ The heart is `--green-on-dark` here and `--accent-text` everywhere else, and that is the same
   swap `.section--dark .accent-script` and the dark contact band's spark already make: the shape
   stays, the colour changes to the one that survives the ground. On Home the marks straddle a pale
   placeholder panel and teal reads cleanly; on an overlay hero the ground is a photograph dark
   enough to carry white text, and the teal heart all but disappeared into it. */
.hero--overlay.decor--hero .hero__card::after {
  right: -0.75rem; top: 3.25rem;
  width: 2rem; height: 1.8rem;
  background-color: var(--green-on-dark);
  -webkit-mask-image: url('../img/decor/heart.svg'); mask-image: url('../img/decor/heart.svg');
}
/* On a picture-less hero there is no frame to sit off the edge of, so the marks go in a channel of
   their own down the right-hand side of the copy column.

   ⚠️ The `padding-right` is what makes this safe, and it is not cosmetic. `.hero--service h1` is
   capped at `30ch`, which reads like it leaves a right-hand column free — at this type scale 30ch is
   *wider* than the 1128px container, so it caps nothing and the H1 runs the full width. Placed
   against that assumption the heart landed on the last word of the H1 on `/our-care/pmld/`,
   `/our-care/learning-disabilities/` and `/our-care/sensory-needs/`, and would have landed on any
   future page whose heading happened to run long. Reserving the channel means the copy stops before
   the marks by construction rather than by luck, at every width and for every heading.

   Held inside the container rather than pulled outside it: with no frame to overlap, a mark in the
   band's outer padding is just a shape floating in the margin. */
@media (min-width: 48rem) {
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack { padding-right: 5.5rem; }
}
.decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::before {
  right: 0; top: -0.25rem;
  width: 2.75rem; height: 2.75rem;
  background-color: var(--brand-green);
  -webkit-mask-image: url('../img/decor/star.svg'); mask-image: url('../img/decor/star.svg');
}
.decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::after {
  right: 0.5rem; top: 3.75rem;
  width: 2rem; height: 1.8rem;
  background-color: var(--accent-text);
  -webkit-mask-image: url('../img/decor/heart.svg'); mask-image: url('../img/decor/heart.svg');
}

/* The export hides both at its own mobile breakpoint. One column, and they land on the standfirst.
   The two anchors added above need it for the same reason and more urgently: below this width the
   overlay card is the full width of the band and the service heading runs to the container edge, so
   there is no empty column left for a mark to sit in. */
@media (max-width: 47.999rem) {
  .decor--hero .hero__aside::before, .decor--hero .hero__aside::after,
  .hero--overlay.decor--hero .hero__card::before,
  .hero--overlay.decor--hero .hero__card::after,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::before,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::after { display: none; }
}

/* --- the brush sparks after a handwritten accent ---
   The export puts these on `.script` headings and nowhere else, which is the whole reason they read
   as a flourish rather than as a bullet. Sized in `em` so they track the heading's own clamp.

   Two rules, not one, and the split is a wrapping problem rather than a stylistic choice.

   An inline mark is a 42px word the heading has to find room for. In `prose`'s 22rem heading rail
   that is 12px more than the line has left, so the flourish drops onto a line of its own under the
   accent and the section grows 55px — which is the one arrangement that makes it look like a
   mistake. Where the heading carries a `~handwritten accent~` the mark is therefore hung off the end
   of that phrase instead: out of flow, so it cannot wrap and cannot re-measure the heading, and
   pinned to the phrase it belongs to rather than to a corner of the box.

   Where there is no accent — the closing contact band, the export's "Ready to take the next step?" —
   there is nothing to hang it on, so it stays inline after the last word. Those headings sit in a
   full-width column with room to spare, which is what makes that safe there and not in the rail.
   `:has()` is how the heading tells the two apart; the same test already routes the highlighter
   wash in §3. */
.decor--spark h2 .accent-script::before,
.decor--spark :is(.section__head, .split__head, .cta-band__grid) h2:not(:has(.accent-script))::after {
  content: '';
  background-color: var(--brand-green);
  -webkit-mask: url('../img/decor/sparkle.svg') center / contain no-repeat;
  mask: url('../img/decor/sparkle.svg') center / contain no-repeat;
}
/* Hung off the accent's top-right corner. `em` here is the accent's own 1.3em, so the mark stays
   proportional to the handwriting rather than to the heading around it. `.accent-script` is already
   position: relative for the swoosh it carries on ::after (§3). */
.decor--spark h2 .accent-script::before {
  position: absolute;
  right: -1.05em; top: -0.1em;
  width: 0.72em; height: 0.55em;
}
/* Raised off the baseline with `position: relative`, not `vertical-align`. Vertical-align moves the
   box inside the line and the line grows to keep it — 9px taller on the contact band, which is 9px
   of the gap under the heading gone. A relative offset paints the mark higher and leaves the line
   measuring what it measured before. This is what the export does too. */
.decor--spark :is(.section__head, .split__head, .cta-band__grid) h2:not(:has(.accent-script))::after {
  display: inline-block;
  position: relative; top: -0.3em;
  width: 1.05em; height: 0.8em;
  margin-left: 0.3em;
}
/* --brand-green is a 2.07:1 mark on --teal-900 and disappears. Same rule the accent script follows
   on a dark surface (§3): the shape stays, the colour changes to the one that survives.
   Selectors are the full-length ones on purpose — a shorter `.section--dark.decor--spark h2::after`
   is a class lighter than the rule it is overriding and silently loses. */
.section--dark.decor--spark h2 .accent-script::before,
.section--dark.decor--spark :is(.section__head, .split__head, .cta-band__grid) h2:not(:has(.accent-script))::after {
  background-color: var(--green-on-dark);
}

/* --- the heart above the advice card's heading ---
   The export's "Not sure what support you need?" card carries a 36px outline heart above its
   heading, and it is the only thing separating that card from a plain box of text. */
.decor--heart .band-card > div:first-child::before {
  content: '';
  display: block;
  width: 2.25rem; height: 2rem;
  margin-bottom: 1.1rem;
  background-color: var(--brand-green);
  -webkit-mask: url('../img/decor/heart.svg') left center / contain no-repeat;
  mask: url('../img/decor/heart.svg') left center / contain no-repeat;
}

/* --- removed: the questions figure beside the FAQ (D75) ---
   `.decor--faq` painted a full-colour cartoon person in the empty column beside Home's FAQ
   accordion. It was the only artwork on the site depicting a person, it was the one part of the
   export's decorative set nobody had asked for, and D67 flagged it 🟡 for exactly that reason. The
   client has now said take it out, so the rule, the opt-in in `data/home.php` and the asset
   `img/decor/faq-questions.svg` are all gone rather than left dormant.

   `.faq-list` is still capped at 52rem so an answer stays readable, which still leaves a ~19rem
   column of nothing beside the accordion — the largest single piece of white space on the page.
   **That is intended.** It is the gap the figure was filling and the reason somebody will want to
   fill it again. Read D75 first, and do not put a person in it. */

/* ------------------------------------------------------ 8. motion (D77)

   Every page fades its content up as it arrives. Four rules govern the whole of it:

   1. **No JavaScript.** Eleven of the fourteen pages carry no script at all and that stays true. The
      reveals are CSS scroll-driven animations (`animation-timeline: view()`), so nothing is added to
      any page's `$page['scripts']` and the WordPress port inherits this as one stylesheet section.
   2. **Content can never end up hidden.** This is a care provider's site and the failure mode of a
      reveal is invisible copy, so the whole part is inside `@supports (animation-timeline: view())`.
      A browser that does not implement scroll timelines is not given the opening `opacity: 0` in the
      first place — it renders the page exactly as it did before this section existed. There is no
      `.js` class to arrive, no observer to fire, and nothing to fail to fire.
   3. **Reduced motion means absent, not fast.** See the note in §2: `animation-duration` does not
      apply to a scroll-driven animation, so the blanket guard cannot flatten these. Everything here
      is nested inside `prefers-reduced-motion: no-preference`.
   4. **Print is a third state.** Paper has no scroll container, so a scroll timeline is inactive and
      the spec says an inactive timeline applies no effect — but "the copy is visible when the client
      prints the page" is not a thing to infer from a spec sentence. It is asserted at the bottom.

   Nothing here reaches the DOM, `data/` or a partial: motion is a property of a section *type*, not
   of a section's content, so it is selected structurally the same way §6 styles the same elements.
   That is also why it needs no opt-in key — unlike §7's decoration, which is placed per section.

   No content moves more than --motion-rise (0.6rem, under one line of body text) and nothing scales,
   rotates or slides in from a side. Part of this audience has a visual or cognitive impairment and
   reads slowly; movement across the page competes with the words for exactly those readers. The
   brief was subtle and smooth, and the transform budget is where that is spent. */

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

  /* --- the hero, on load ---
     Time-based, not scroll-driven, because the hero is already in view when the page opens and there
     is no scroll to drive it. It runs everywhere, including in browsers that skip everything below.

     The stagger is a sixteenth of a second between items, which is felt as one gesture settling
     rather than as a queue. The H1 has no delay: it is the LCP element on most of these pages and
     nothing should hold its first paint. */
  @keyframes rise-in {
    from { opacity: 0; transform: translateY(var(--motion-rise)); }
    to   { opacity: 1; transform: none; }
  }

  .hero__body > *,
  .hero__aside {
    animation: rise-in var(--motion-in) var(--motion-ease) both;
  }
  .hero__body > :nth-child(2) { animation-delay: 0.06s; }
  .hero__body > :nth-child(3) { animation-delay: 0.12s; }
  .hero__body > :nth-child(4) { animation-delay: 0.18s; }
  .hero__body > :nth-child(n+5) { animation-delay: 0.24s; }
  .hero__aside { animation-delay: 0.12s; }

  /* The fact rows are part of the hero's one gesture and continue its count, so they take `rise-in`
     rather than a scroll timeline (D184) — they are on the first screen at 1280 and there is no
     scroll to drive them. ⚠️ **The row animates, never its items.** `.factrow__item` is not in the
     `.pill-list, .factgrid__item` group in §8 for that reason: a child fading inside a fading parent
     lands at 0.25 opacity halfway through. */
  .hero__strip { animation: rise-in var(--motion-in) var(--motion-ease) both; }
  .hero__strip:nth-child(1) { animation-delay: 0.30s; }
  .hero__strip:nth-child(n+2) { animation-delay: 0.36s; }

  /* The overlay hero's visual is the full-bleed surface the card sits on, not an object beside the
     copy. It fades and does not travel — a background that slides is a background that draws the eye
     away from the phone number the layout exists to put in front of someone (D63). */
  .hero--overlay .hero__media {
    animation: fade-in var(--motion-in) var(--motion-ease) both;
  }
  @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

  /* --- the two gestures the rest of this section is built from ---

     `draw` is the whole idea. Four of this site's marks are strokes that happen to be drawn with
     CSS: the highlighter wash behind part of a heading (D41), the swoosh under a handwritten accent
     (D41), the short green rule before an eyebrow (§3), and the dashed rule threading the journey
     medallions (D42). Every one of them is a line with a beginning and an end, and a pen draws a
     line from its start. So all four scale up on X from `transform-origin: left` and nothing else
     happens to them — no fade, no travel. The wash arriving as a stroke rather than as a rectangle
     is the single most on-brand thing on the page, because it is what a highlighter actually does.

     `pop` is for the marks that are dots rather than strokes — the counters and the two decorative
     shapes on the hero. They scale from 0.8, not from 0: a mark that grows from nothing reads as a
     notification badge, and these are meant to have been on the page all along. */
  @keyframes draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  @keyframes pop  { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }

  /* `wipe` is the third and last gesture: the same left-to-right direction as `draw`, applied to a
     block instead of a line. Used on section headings and on photographs.

     ⚠️ The insets are negative on three sides and that is load-bearing, not tidiness. `clip-path`
     clips to the border box, and this site sets display headings at 1.12 line-height where Fraunces'
     descenders hang below it — a plain `inset(0)` end state would shave the tail off every g, y and
     p on the page and keep it shaved, because the end state is what the heading rests at. The extra
     0.15em bottom and 0.05em side give the letters room to sit outside the clip entirely. Anything
     new that takes `wipe` needs the same allowance, or a box that is known to contain its ink. */
  @keyframes wipe {
    from { clip-path: inset(-0.15em 100% -0.15em -0.05em); }
    to   { clip-path: inset(-0.15em -0.05em -0.15em -0.05em); }
  }

  /* The image reveal is a mask sweep rather than a `clip-path` one, and the reason is corners. A
     `clip-path: inset()` end state squares off `--radius-lg` unless the radius is restated in the
     clip — and `.ph-image` is used at several radii, flush inside a card and rounded on its own, so
     there is no single value to restate. A mask only controls alpha; the element's own
     `border-radius` and `overflow: hidden` still clip the painted result, so the corners survive
     untouched. Mask is 200% wide, half opaque and half transparent: parked right, the element sits
     under the transparent half and is hidden; travelling to left, the opaque half crosses it. */
  @keyframes sweep {
    from { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
    to   { -webkit-mask-position: 0 0;    mask-position: 0 0; }
  }

  /* The H1's wash, on load. Time-based like the rest of the hero, and deliberately started at 0.3s
     — while the heading itself is still settling, not after it has finished. Sequenced strictly
     after, it reads as two events; overlapped, it reads as one hand putting the heading down and
     then striking through it. 0.45s is about the speed of a real marker pen across three words. */
  .hero__body h1 .hl::before {
    transform-origin: left center;
    animation: draw 0.45s var(--motion-ease) 0.3s both;
  }

  /* The star and the heart pinned to the hero visual (D67). They come in last, after the copy and
     the picture have both settled, because they are a flourish on a finished thing. `.hero__aside`
     is itself animating `rise-in`, so these are riding a moving parent — which is fine, and is why
     they carry no travel of their own. */
  .decor--hero .hero__aside::before,
  .decor--hero .hero__aside::after,
  .hero--overlay.decor--hero .hero__card::before,
  .hero--overlay.decor--hero .hero__card::after,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::before,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::after {
    animation: pop 0.4s var(--motion-ease) both;
  }
  .decor--hero .hero__aside::before,
  .hero--overlay.decor--hero .hero__card::before,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::before { animation-delay: 0.36s; }
  .decor--hero .hero__aside::after,
  .hero--overlay.decor--hero .hero__card::after,
  .decor--hero:not(.hero--overlay):not(:has(.hero__aside)) .hero__stack::after  { animation-delay: 0.46s; }

  /* --- the shell, on load ---
     The header is the first thing painted and was the only thing on the page that simply appeared.
     The logo fades; the current-page marker draws itself with the same stroke as every other rule on
     the site, so "you are here" is stated in the vocabulary rather than just switched on. */
  .site-logo { animation: fade-in 0.4s var(--motion-ease) both; }
  .main-nav__link[aria-current='page']::after,
  .main-nav__link[data-nav='section']::after {
    transform-origin: left center;
    animation: draw 0.4s var(--motion-ease) 0.25s both;
  }

  /* --- everything below the fold, on scroll --- */
  @supports (animation-timeline: view()) {

    @keyframes reveal {
      from { opacity: 0; transform: translateY(var(--motion-rise)); }
      to   { opacity: 1; transform: none; }
    }

    /* `entry 0%` is the moment the element's top edge appears at the bottom of the window; `cover
       18%` ends it 18% of the way through the element's full pass across the window. The end is
       measured against `cover` rather than `entry` on purpose: `entry` is scaled by the element's own
       height, so a tall card grid would still be fading while it sat in the middle of the screen,
       whereas `cover` is scaled by the element plus the window and stays roughly constant. In
       practice everything finishes settling while it is still in the lower third.

       `both` is load-bearing. Before the range the element holds the from-state; after it, the
       to-state. Anything already scrolled past — a section reached by an in-page anchor, or by a
       browser restoring scroll on a back-navigation — is past its range and therefore fully visible,
       not stuck at zero. */
    .reveal-target,
    .section:not(.hero) .section__head,
    .section:not(.hero) .split__head,
    .prose__body,
    .pills__body,
    .list-note,
    .grouplist__group,
    .facts__list,
    .facts__address,
    .related__heading,
    .related__list,
    .band-card,
    /* Since D155 this carries the map as well, and it stays one reveal: `.cta-band__grid` is the
       animating container, so nothing inside it animates separately. A child fading inside a fading
       parent multiplies to 0.25 opacity at the midpoint (D80). */
    .cta-band__grid,
    .section__footer-link,
    .steps__cta {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 18%;
    }

    /* --- rows ---
       A grid's items share a Y position, so `view()` alone gives them one identical timeline and they
       arrive as a block. Offsetting the range start walks them across instead. The cycle is three
       because three is the widest these grids get; `auto-fit` means the real column count is unknown
       to CSS, so on a two-column layout the pattern crosses rows rather than aligning with them. At
       4% of a scroll pass the difference between the two cases is not perceptible, and the
       alternative — a container query per grid per breakpoint — buys nothing a reader would notice.

       Vertical lists (steps, journey, FAQ items) are deliberately not in here: they are already
       staggered by their own position on the page, and adding a second offset on top of that reads
       as lag rather than as rhythm. */
    .card-grid > *,
    .quicklinks__item,
    .trustbar__item,
    .gallery__item,
    .journey__step,
    .step {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry calc(var(--reveal-i, 0) * 4%) cover calc(18% + var(--reveal-i, 0) * 4%);
    }
    .card-grid > :nth-child(3n + 2),
    .quicklinks__item:nth-child(3n + 2),
    .trustbar__item:nth-child(3n + 2),
    .gallery__item:nth-child(3n + 2) { --reveal-i: 1; }

    .card-grid > :nth-child(3n),
    .quicklinks__item:nth-child(3n),
    .trustbar__item:nth-child(3n),
    .gallery__item:nth-child(3n) { --reveal-i: 2; }

    /* A `.section__head` sitting inside a sticky `.split__head` rail would re-run its reveal as the
       rail travels. The rail is the element that scrolls, so it takes the timeline and its contents
       do not. */
    .split__head .section__head { animation: none; }

    /* --- the same four strokes, drawn on scroll ---
       Each mark takes its own `view()` timeline rather than its section's, so it draws when *it*
       arrives rather than when the block around it does. The range is shorter than a section reveal
       (`cover 12%`): a stroke that takes as long as a whole section to land stops reading as a
       stroke. `both` again, so a mark scrolled past is drawn, never a half-line. */
    .eyebrow::before,
    .section:not(.hero) h2 .hl::before,
    .accent-script::after,
    .journey__step:not(:first-child)::before,
    /* The wavy referral connector (D172) is the journey's dashed rule in a different asset, so it
       draws the same way: left to right, from the medallion it leaves toward the one it reaches —
       which since D181 is also the element it hangs off, so `transform-origin: left center` is now
       literally the medallion it starts at. `s-journey`'s connector (D179) is the same asset in the
       same role and takes the same draw. */
    .steps--wavy .step:not(:first-child)::after,
    .steps--flow .step:not(:first-child)::after {
      transform-origin: left center;
      animation: draw 0.5s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 12%;
    }

    /* The swoosh waits for the handwriting above it. It is an underline, and an underline that
       arrives with the words it underlines reads as a border; arriving after them, it reads as
       somebody having gone back over the phrase. The delay is expressed as a later range start,
       because `animation-delay` means nothing on a scroll-driven animation — progress comes from
       scroll position, so "later" has to be said in scroll distance. This is the same trap as the
       reduced-motion one at the top of the section, in a different disguise. */
    .accent-script::after { animation-range: entry 6% cover 18%; }

    /* --- the counters ---
       The numeral on a journey medallion and the numbered disc on a referral step are the same
       motif in two places, so they get the same 0.8 scale. Both sit on an element that is already
       revealing, so this is a mark appearing on a card that is itself arriving — hence no travel,
       and a range that finishes fractionally after its parent's. */
    .journey__badge::after,
    .step::before {
      animation: pop 0.42s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 4% cover 20%;
    }

    /* --- the squiggle under "how we work" actually draws ---
       It is the one mark on the site that is a real stroked SVG path rather than a mask or a box, so
       it is the one that can be drawn the way it was hand-drawn: `stroke-dashoffset` walking the dash
       along the path. 81.4 units measured with `getTotalLength()`, rounded up to 82 — the dash has to
       be at least the path length or the tail never gets covered. Slower than the other marks (0.9s)
       because this one is genuinely being drawn rather than swept, and a five-hump squiggle at 0.5s
       reads as a flicker.
       If the artwork in `img/icons/squiggle.svg` is ever redrawn, re-measure. A dasharray shorter
       than the path leaves a permanent gap in the line. */
    .journey-squiggle path {
      stroke-dasharray: 82;
      animation: draw-line 0.9s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 16%;
    }

    /* --- medallions and decorative marks take the same `pop` as the counters ---
       Every circular icon holder on the page arrives the same way, so the quicklinks bar, the trust
       bar and the contact band all read as the same component doing the same thing. */
    .quicklinks__ic,
    .trustbar__ic,
    .contact-line__ic,
    /* The referral steps' icon medallions (D172) are the same circular holder as the journey's. */
    .steps--wavy .step__ic,
    /* The hero card's medallions were `.contact-line__ic` until D107 renamed the tile they sit in.
       They keep the gesture rather than gaining one: same component, same arrival, one more
       selector. */
    .hero__contact-ic,
    .decor--heart .band-card > div:first-child::before {
      animation: pop 0.42s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 4% cover 20%;
    }

    /* The sparks land last, after the phrase they hang off has been written and underlined: the
       accent's swoosh finishes at `cover 18%`, so these start at `entry 10%`. Sequencing three marks
       on one heading is the whole reason `s-intro` reads as handwriting rather than as a graphic. */
    .decor--spark h2 .accent-script::before,
    .decor--spark :is(.section__head, .split__head, .cta-band__grid) h2:not(:has(.accent-script))::after {
      animation: pop 0.4s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 10% cover 24%;
    }

    /* --- list items, rather than the list ---
       `.pill-list` and `.feature-list` used to reveal as single blocks and were moved out of that
       group above when their items were given their own reveal: a child fading inside a fading
       parent multiplies to 0.25 opacity at the midpoint, which looks like a rendering fault rather
       than a transition. One or the other, never both.

       Only the pills are staggered. They wrap into rows, so items share a Y and need the offset to
       walk across; `.feature-list` and `.check-list` are vertical and their position on the page
       already staggers them (D77). */
    .pill-list li,
    .feature-list li,
    .factgrid__item,
    .check-list:not(.hero__points) li {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry calc(var(--reveal-i, 0) * 3%) cover calc(16% + var(--reveal-i, 0) * 3%);
    }
    .pill-list li:nth-child(3n + 2) { --reveal-i: 1; }
    .pill-list li:nth-child(3n)     { --reveal-i: 2; }
    /* `.factgrid` is a wrapping grid like the pills, so its items need the walk across a row for
       the same reason (D111, moved into the hero by D115). Its parent `<ul>` is deliberately not in
       the block-reveal group above: a child fading inside a fading parent lands at 0.25 opacity
       mid-range.
       ⚠️ These sit inside the **hero**, which runs the time-based `rise-in` stagger rather than a
       scroll timeline — they are on the first screen, where there is no scroll to drive them. The
       hero rule below already covers them; this pair only applies on the rare narrow viewport where
       the second panel starts below the fold. */
    /* ⚠️ Two per row since D161's tiles, not three — a stagger keyed to the wrong column count
       walks the offset diagonally down the grid instead of across each row. */
    .factgrid__item:nth-child(2n) { --reveal-i: 1; }

    /* --- the footer, which had nothing ---
       Range is measured against `entry` here, not `cover`, and that is a correctness point rather
       than a taste one. The footer is the last thing on the document: once its top reaches the
       window there is only its own height left to scroll, so a range that needs more than that
       strands it part-faded with nowhere left to go. An `entry`-relative range is scaled by the
       element's own height, so 45% of it is always available whatever the viewport. */
    .site-footer__brand,
    .site-footer .footer-col,
    .site-footer__bottom {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry calc(var(--reveal-i, 0) * 5%) entry calc(45% + var(--reveal-i, 0) * 5%);
    }
    .site-footer__top > :nth-child(2) { --reveal-i: 1; }
    .site-footer__top > :nth-child(3) { --reveal-i: 2; }
    .site-footer__top > :nth-child(4) { --reveal-i: 3; }
    .site-footer__top > :nth-child(5) { --reveal-i: 4; }

    /* --- section headings wipe in ---
       The heading is the one element on a section that everything else is subordinate to, and a fade
       treats it like everything else. A wipe from the left is the same gesture as the marks, applied
       to the words.

       Three exclusions, each for a reason. A heading carrying `==wash==` or `~accent~` already has a
       mark animating inside it (D78) — two left-to-right reveals in the same heading race each other
       and neither reads. A heading in a `.decor--spark` section has a sparkle hung off it as a
       pseudo-element, which a clip-path on the heading would cut off mid-flourish. And the hero is
       excluded throughout §8 because it is choreographed on load instead. */
    .section:not(.hero):not(.decor--spark) :is(.section__head, .split__head) > h2:not(:has(.hl, .accent-script)) {
      animation: wipe 0.6s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 16%;
    }

    /* --- photographs sweep in ---
       Excluded: both hero slots. The hero is on its own load-time timeline and a full-bleed overlay
       background sweeping behind a contact card is movement across the whole first screen, which is
       the opposite of what D63 built that layout for. */
    .ph-image:not(.hero__image):not(.hero__media) {
      -webkit-mask: linear-gradient(90deg, #000 0 50%, transparent 50%) 100% 0 / 200% 100% no-repeat;
      mask: linear-gradient(90deg, #000 0 50%, transparent 50%) 100% 0 / 200% 100% no-repeat;
      animation: sweep 0.75s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 18%;
    }
    /* The label rides in after the panel it sits on has arrived. */
    .ph-image__label {
      animation: pop 0.4s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 8% cover 24%;
    }

    /* --- the remaining round things, on the same `pop` as every other round thing ---
       `.journey__ic` was left out of the first pass because its badge already popped and a medallion
       doing two things at once looked fussy. With the rest of the page now moving it reads as the odd
       one out instead, so it joins them — one range earlier than its own numeral, so the disc arrives
       and the number lands on it. */
    .journey__ic {
      animation: pop 0.42s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 0% cover 16%;
    }
    /* Every tick on the site: the check-lists, and the pills' own marks. */
    .check-list:not(.hero__points) .ic,
    .pill-list .ic {
      animation: pop 0.35s var(--motion-ease) both;
      animation-timeline: view();
      animation-range: entry 4% cover 18%;
    }

    /* --- the contact band's lines ---
       Phone, email and address stack, so they stagger by position; the medallion beside each already
       pops with the other icons above. */
    .cta-band__contacts > * {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 16%;
    }

    /* --- FAQ items, rather than the list ---
       Moved out of the container group above for the compounding reason: the list was fading while
       each item faded inside it. Vertical, so no offset is added — position does the staggering. */
    .faq-item {
      animation: reveal linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 16%;
    }
  }

  @keyframes draw-line { from { stroke-dashoffset: 82; } to { stroke-dashoffset: 0; } }

  /* --- the header lifts off the page once you leave the top ---
     `scroll()` is the document's own timeline rather than an element's, so this is the one animation
     here that is not tied to a thing entering the window. Under 4rem of scroll the hairline border
     is exchanged for a shadow, which is the same swap the cards make on hover: the surface has come
     forward. Held to `both` for the same reason as the reveals — deep in a page it must read as
     lifted, not reset. */
  @supports (animation-timeline: scroll()) {
    .site-header {
      animation: header-lift linear both;
      animation-timeline: scroll();
      animation-range: 0 4rem;
    }
    @keyframes header-lift {
      from { box-shadow: 0 0 0 rgba(30, 60, 60, 0); }
      to   { box-shadow: var(--shadow-sm); }
    }

    /* --- reading progress, along the bottom edge of the header ---
       The one piece of motion here that tells the reader something rather than decorating something:
       these are long pages and this says how much of one is left. `scroll()` with no range is the
       whole document, so the bar is literally the scroll position — nothing approximates it and
       nothing has to be kept in sync.

       Green is doing the one job it is allowed to do: it is a 3px rule, not a letter (D33, T32). It
       sits on the header's own border line rather than above it, so it reads as that line filling in
       rather than as a fifth element in the header. `pointer-events: none` because it overlaps the
       top of the page content by a pixel. */
    .site-header::after {
      content: '';
      position: absolute; left: 0; right: 0; bottom: -1px;
      height: 3px; transform-origin: left center;
      background: var(--brand-green);
      pointer-events: none;
      animation: draw linear both;
      animation-timeline: scroll();
    }
  }

  /* --- the FAQ accordion opens instead of jumping ---
     `<details>` has no animatable height, and the answer is display-toggled by the browser, so this
     needs all three of: `::details-content` to have something to size, `interpolate-size` to let
     `auto` be an animation endpoint, and `allow-discrete` to hold the content visible on the way out.
     Any browser missing one of the three keeps today's instant open, which is correct behaviour and
     not a broken accordion.

     `interpolate-size` is inherited and changes what `auto` means to a transition, so it is set on
     `.faq-list` rather than on `:root`. Nothing outside the accordion needs it and nothing outside
     the accordion should be exposed to it. */
  @supports (interpolate-size: allow-keywords) and (selector(::details-content)) {
    .faq-list { interpolate-size: allow-keywords; }
    .faq-item::details-content {
      block-size: 0;
      overflow: hidden;
      transition: block-size 0.3s var(--motion-ease),
                  content-visibility 0.3s allow-discrete;
    }
    .faq-item[open]::details-content { block-size: auto; }

    /* The mobile nav is a `<details>` too (it works with JS off), so it takes the same treatment.
       This is the one place the smooth open matters most — the panel is most of a phone screen, and
       it appearing in a single frame is the jumpiest thing on the site at that width. */
    .nav-toggle { interpolate-size: allow-keywords; }
    .nav-toggle::details-content {
      block-size: 0;
      overflow: hidden;
      transition: block-size 0.28s var(--motion-ease),
                  content-visibility 0.28s allow-discrete;
    }
    .nav-toggle[open]::details-content { block-size: auto; }
  }

  /* --- the small stuff that was missing a curve ---
     These are surfaces that already changed on hover or on state, instantaneously, while the card and
     the button beside them eased. Consistency, not new movement. */
     Deliberately not in this list: `.card--link`, `.gallery__link .ph-image` and `.filter__control`,
     which already carry a `transition` naming `transform` or `border-color`. `transition` is one
     shorthand, not a set of additive declarations, so a second rule further down the file replaces
     the first outright rather than adding to it — listing them here would silently delete the card
     lift and the gallery hover instead of joining them. */
  .main-nav__link,
  .submenu a,
  .site-footer a,
  .faq-item > summary { transition: background-color 0.15s ease, color 0.15s ease; }

  .faq-item__toggle { transition: transform 0.25s var(--motion-ease), background-color 0.2s ease, color 0.2s ease; }
  .nav-toggle__bars span { transition: background-color 0.15s ease; }

  /* The quicklinks medallion already carries the hover for its whole item (§6). It was doing it as a
     hard swap. */
  .quicklinks__ic { transition: background-color 0.2s ease; }

  /* Only the easing for the footer's hover rule. The `transform` that makes it work is in §5, so a
     reduced-motion user still gets the mark — instantly rather than drawn. */
  :is(.footer-col, .footer-contact, .site-footer__bottom) a::after {
    transition: transform 0.25s var(--motion-ease);
  }

  /* The arrow on a button, matched to the one `.link-arrow` has carried since §4. Same 3px, same
     duration, so the two ways this site says "this goes somewhere" behave identically. */
  .btn .ic { transition: transform 0.15s ease; }
  .btn:hover .ic { transform: translateX(3px); }

  /* A button lifts a single pixel under the pointer and puts itself back down when pressed. It is
     the smallest movement on the site and the only one that answers an input rather than a position
     on the page. `:active` returning to 0 is the half that makes it feel like a button rather than a
     card — without it the press has no floor. */
  .btn { transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease,
                     transform 0.15s ease, box-shadow 0.15s ease; }
  .btn:hover  { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
  .btn:active { transform: translateY(0);    box-shadow: none; }

  /* A card's arrow moves with the card, so hovering anywhere on it nudges the arrow the same way
     hovering the link itself would. The link is the whole card; the arrow is just where it is said. */
  .card--link:hover .link-arrow .ic,
  .card--link:hover .card__arrow { transform: translateX(3px); }

  /* --- a photograph leans in when you hover the thing it belongs to ---
     ⚠️ Scoped to `.ph-image--photo`, which is the class `media_image()` adds and `placeholder_image()`
     does not. Every one of these slots is a labelled placeholder today (T06), and a dashed panel with
     "Photography to come" written on it must not drift about — it would read as a broken image rather
     than as a deliberate gap (D35). So this is written for photographs and stays dormant until real
     ones land, at which point it turns on with no further edit. `.ph-image` is already `overflow:
     hidden`, so the frame stays exactly where it is and only its contents move.
     1.04 with the card's own -3px lift is enough; more and the two moves start to fight. */
  .card--link .ph-image--photo img,
  .gallery__link .ph-image--photo img { transition: transform 0.45s var(--motion-ease); }
  .card--link:hover .ph-image--photo img,
  .gallery__link:hover .ph-image--photo img,
  .gallery__link:focus-visible .ph-image--photo img { transform: scale(1.04); }

  /* --- the gallery's open affordance, easing only (D79, D88) ---
     The scrim and the mark go from 0 to 1 in §5, where the hover and focus state belongs, so a
     reduced-motion user still gets the affordance — instantly, rather than not at all. This is the
     curve and nothing else. Same 0.18s as the frame's lift directly above it, so the two halves of
     one hover arrive together instead of the overlay trailing the card.
     Its own rule rather than a line in the blanket list at the top of §8, for the reason stated
     there: `transition` is a shorthand and these selectors would be replaced, not extended. */
  .gallery__zoom { transition: opacity 0.18s ease; }
  /* The label rides the same 0.18s as the scrim it sits on, so the two arrive as one gesture. The
     0.5rem it travels is `pop`'s register rather than a fourth gesture (D80): a small element
     settling into place, not a new kind of move. */
  .gallery__label { transition: opacity 0.18s ease, transform 0.18s var(--motion-ease); }
  .lightbox__thumb { transition: opacity 0.15s ease, border-color 0.15s ease; }
}

/* ---------------------------------------------------- 9. review mode */

.review-tag {
  position: absolute; top: 0; left: 0; z-index: 5;
  padding: 0.15rem 0.5rem;
  background: var(--review-tag-bg); color: var(--white);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.6875rem; letter-spacing: 0.02em;
  border-bottom-right-radius: 6px;
}
.verify-tag {
  position: absolute; top: 0; right: 0; z-index: 5;
  padding: 0.15rem 0.5rem;
  background: var(--verify-tag-bg); color: var(--white);
  font-size: 0.6875rem; font-weight: 700;
  border-bottom-left-radius: 6px;
}
/* A build note attached to a section, only visible under ?review=1 — it explains a behaviour a
   reviewer would otherwise report as a bug (the homes filter waiting on T01). Never client copy. */
.review-note {
  margin-top: 1rem; padding: 0.75rem 1rem;
  border-left: 3px solid var(--review-tag-bg);
  background: var(--white);
  font-size: var(--step-xs); color: var(--muted);
}

/* Marks a gated ITEM inside an otherwise shippable section, only visible under ?verify=1. */
.verify-badge {
  display: inline-block; vertical-align: middle;
  margin-left: 0.5rem; padding: 0.1rem 0.45rem;
  background: var(--verify-tag-bg); color: var(--white);
  border-radius: 4px;
  font-family: var(--font-body); font-size: 0.625rem; font-weight: 700;
  letter-spacing: 0.02em; text-transform: uppercase; white-space: nowrap;
}
.review-missing {
  padding: 1rem var(--pad); margin: 1rem 0;
  border: 2px dashed var(--verify-tag-bg); color: var(--verify-tag-bg); font-weight: 700;
}
body:has(.review-tag) .section { outline: 1px dashed var(--green-light); outline-offset: -1px; }

/* --------------------------------------------------- 10. responsive */

/* The nav collapses to the burger at 70rem, not at the 62rem the rest of the site turns on.

   The client's 2026-08-04 nav is seven items plus the "Make a referral" button, and that row needs
   about 1090px of header before it stops fitting. At the old breakpoint the desktop nav was still on
   at 992px, where it overran the button and pushed a horizontal scrollbar onto every page — measured,
   not predicted. Nothing else in this block is width-dependent, so the whole thing moved together
   rather than the nav being made smaller: shrinking seven labels to fit 992px costs a tap target on
   the audience least able to hit it.

   ⚠️ **The count has moved twice since and is back where this was measured.** D82 removed
   "Referrals" (six items), D87 added "Gallery" (seven). Seven is what 70rem was sized for, so the
   breakpoint is untouched — measured again at 1121px, the first desktop width above it: the row ends
   at 896px with the button starting at 916px, one line, no horizontal scrollbar. "Gallery" is also a
   shorter label than the "Referrals" it effectively replaces, so the row is narrower than the one
   that set this number.

   If nav items are ever dropped back to five, this can go back to 61.999rem/62rem. Measure, don't
   predict — that is how the 992px overrun got here in the first place. */
@media (max-width: 69.999rem) {
  .main-nav { display: none; }
  .nav-toggle { display: block; margin-left: auto; }
  .nav-toggle[open] > summary { border-color: var(--green-dark); }
  /* `z-index` for the desktop dropdown's reason, one line up from §5 — the scroll-progress bar's
     band is y71–74 and this panel opens at the header's own bottom edge (y73), so it clipped a green
     line across the panel's top row. Same one step up, same reason. */
  .nav-toggle__panel {
    position: absolute; left: 0; right: 0; top: 100%; z-index: 1;
    background: var(--white);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-md);
    padding: 0.75rem var(--pad) 1.5rem;
    max-height: calc(100dvh - 4.5rem); overflow-y: auto;
  }
  .nav-toggle__panel ul { display: grid; gap: 0.15rem; }
  .nav-toggle__panel a {
    display: block; padding: 0.75rem 0.5rem;
    border-bottom: 1px solid var(--border);
    font-weight: 600; text-decoration: none;
  }
  /* Same marker, turned on its side: a full-height rule down the left of the row. A centred stub
     under a full-width row would read as a divider, not a position. */
  .nav-toggle__panel a[aria-current='page'] {
    position: relative;
    color: var(--accent-text);
    padding-left: 1rem;
  }
  .nav-toggle__panel a[aria-current='page']::before {
    content: '';
    position: absolute; left: 0; top: 0.55rem; bottom: 0.55rem;
    width: 3px; border-radius: 2px;
    background: var(--brand-green);
  }
  .nav-toggle__panel .submenu-mobile a { padding-left: 1.5rem; font-weight: 500; font-size: var(--step-sm); }
  /* A child row keeps its indent, so the rule sits beside the label rather than out at the panel
     edge where it would line up with nothing. */
  .nav-toggle__panel .submenu-mobile a[aria-current='page'] { padding-left: 1.5rem; }
  .nav-toggle__panel .submenu-mobile a[aria-current='page']::before { left: 0.75rem; }
  .nav-toggle__panel .btn { margin-top: 1rem; width: 100%; justify-content: center; }
  .header-cta { display: none; }
}

@media (min-width: 70rem) { .nav-toggle { display: none; } }

@media print {
  .site-header, .site-footer, .cta-band, .nav-toggle { display: none; }
  /* Print black is ink on paper, not a brand colour, so it is deliberately not a token. */
  body { color: #000; }
}
