/* OptimumPrime Sports Therapy
   Generated by tools/assemble.py from src/css/ , do not edit directly.
   Order matters: tokens, base, components, layout, then sections. */

/* ======================================================================
   00-fonts.css
   ====================================================================== */

/* ============================================================================
   00-fonts.css - self-hosted variable fonts, latin subset.
   Archivo (display) and Inter (body/UI), both SIL Open Font License 1.1.
   Fetched by tools/fetch-fonts.py. No third-party request at runtime.
   ========================================================================= */

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url('../fonts/archivo-variable.woff2') format('woff2-variations'),
       url('../fonts/archivo-variable.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('../fonts/inter-variable.woff2') format('woff2-variations'),
       url('../fonts/inter-variable.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ======================================================================
   00-tokens.css
   ====================================================================== */

/* ============================================================================
   OptimumPrime, MC Sports Therapy
   00-tokens.css, the ONLY file in the build permitted to contain a literal
   colour value. Everything downstream references these custom properties.

   Direction: "The Assessment Plate, Quartered".
   Load order: 00-tokens.css → 01-base.css → 02-components.css → 03-layout.css
   ========================================================================= */

:root {

  /* ---------------------------------------------------------------------
     1. BRAND COLOURS, the six from the badge. Add no seventh hue.
     --------------------------------------------------------------------- */
  --steel: #2E5F7E;   /* primary: buttons, links, rules, icon strokes        */
  --navy:  #17384F;   /* dark bands, footer, hero scrim, nameplates          */
  --ink:   #101418;   /* headings and body copy on light                     */
  --ice:   #8FC9E0;   /* accent. TEXT-SAFE ON NAVY ONLY. Decoration on light */
  /* Darkened from #F6F8F9, which measured 1.065:1 against white - a band you
     had to be told was there. #ECF1F4 is 1.138:1: still quiet, but the step
     between sections is now visible rather than theoretical.

     This is the floor, not a preference. --slate-ink is the tightest pairing
     on this background and it sits at 4.62:1 here; two steps darker takes it
     under the 4.5:1 AA line and the secondary text on every bone band fails
     at once. Do not darken it further without re-measuring that pair. */
  --bone:  #ECF1F4;   /* page background, alternating band                   */
  --slate: #6B7A85;   /* NON-TEXT ONLY, see --slate-ink below               */
  --white: #FFFFFF;   /* white cards and white bands                         */

  /* ---------------------------------------------------------------------
     2. DERIVED TEXT COLOURS, measured, WCAG AA safe.
        Contrast results are stated in CONTRACT.md. Summary:
          white  on --steel .......... 6.80:1  PASS AA normal
          --slate on --bone .......... 3.88:1  FAIL AA normal  -> do not use
          --ice   on --white ......... 1.83:1  FAIL badly      -> decoration
          --slate-ink on --bone ...... 4.62:1  PASS AA normal   (secondary text)
          --ice-ink   on --bone ...... 5.76:1  PASS AA normal   (accent text)
          --ice   on --navy .......... 6.69:1  PASS AA normal
          --ink   on --bone ......... 16.26:1  PASS AAA
          --steel on --bone .......... 6.04:1  PASS AA normal
          --on-navy-muted on --navy .. 8.21:1  PASS AA normal
     --------------------------------------------------------------------- */
  --slate-ink:     #5F6E79;   /* replaces --slate wherever it is TEXT        */
  --ice-ink:       #2A6382;   /* ice-flavoured accent text on light bands    */
  --on-navy:       #F6F8F9;   /* body copy on navy / ink                     */
  --on-navy-muted: #C8D7E0;   /* captions and meta on navy / ink             */

  /* Role aliases, set per band in 03-layout.css. Never hard-set downstream. */
  --fg:         var(--ink);
  --fg-muted:   var(--slate-ink);
  --bg:         var(--bone);
  --accent:     var(--steel);
  --eyebrow-c:  var(--steel);
  --link:       var(--steel);
  --link-hover: var(--navy);
  --node-bg:    var(--bone);   /* knockout colour behind the quadrant node   */
  --focus:      var(--steel);

  /* ---------------------------------------------------------------------
     3. RULES AND TINTS, the hairline system. --rule is re-pointed inside
        dark bands so .quad and .sheet need no dark-mode variants.
     --------------------------------------------------------------------- */
  --rule:               rgba(46, 95, 126, .22);   /* steel 22%, light bands */
  --rule-strong:        rgba(46, 95, 126, .40);   /* steel 40%, emphasis    */
  --rule-dark:          rgba(143, 201, 224, .28); /* ice 28%, dark bands   */
  --rule-dark-strong:   rgba(143, 201, 224, .48); /* ice 48%, dark bands   */
  --tint-steel-06:      rgba(46, 95, 126, .06);
  --tint-steel-10:      rgba(46, 95, 126, .10);
  --tint-steel-20:      rgba(46, 95, 126, .20);   /* focus halo on form fields */

  /* Form field borders. WCAG 1.4.11 wants 3:1 for the boundary of a control
     you are expected to find and click, and --rule-strong at 40% measures
     1.90:1 over white, so empty fields read as blank space. This is the same
     steel at the lowest alpha that actually clears the check with headroom:
     measured 3.43:1. 65% would technically pass at 3.09:1, but that is close
     enough to the line that subpixel compositing can eat the margin. */
  --field-border:       rgba(46, 95, 126, .70);
  --tint-ice-14:        rgba(143, 201, 224, .14);
  --tint-white-08:      rgba(255, 255, 255, .08);

  /* Hero scrim, the ONE gradient permitted in the entire build. */
  --scrim-rgb: 23, 56, 79;
  --scrim: linear-gradient(96deg,
            rgba(23, 56, 79, .92) 0%,
            rgba(23, 56, 79, .86) 32%,
            rgba(23, 56, 79, .52) 66%,
            rgba(23, 56, 79, .32) 100%);
  --scrim-stacked: linear-gradient(180deg,
            rgba(23, 56, 79, .34) 0%,
            rgba(23, 56, 79, .72) 52%,
            rgba(23, 56, 79, .92) 100%);

  /* Placeholder chip (.tbc) */
  --tbc-bg:   var(--tint-steel-06);
  --tbc-line: var(--steel);
  --tbc-fg:   var(--ink);

  /* ---------------------------------------------------------------------
     4. TYPEFACES, self-hosted woff2, font-display: swap.
        Display: Bitter 600 / 700 / 800. Body: Inter 400 / 500 / 600 / 700.
     --------------------------------------------------------------------- */
  /* Archivo: a squared grotesk. Sporty and modern, and it holds up small.
     Replaced Bitter (slab serif) at the client's request. */
  --font-display: "Archivo", "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-sans:    "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;

  /* ---------------------------------------------------------------------
     5. TYPE SCALE, fluid, 375px floor / 1440px ceiling.
     Dialled back from the first pass: the old h1 hit 76px and h2 52px, which
     shouted on desktop and pushed everything else down the page.
     --------------------------------------------------------------------- */
  --fs-h1:      clamp(2.125rem,  1.45rem + 2.90vw, 3.5rem);   /* 34 -> 56 */
  --fs-h2:      clamp(1.625rem,  1.28rem + 1.48vw, 2.375rem); /* 26 -> 38 */
  --fs-h3:      clamp(1.1875rem, 1.09rem + 0.42vw, 1.375rem); /* 19 -> 22 */
  --fs-h4:      clamp(1rem,      0.96rem + 0.17vw, 1.0625rem);/* 16 -> 17 */
  --fs-lead:    clamp(1.0625rem, 1.00rem + 0.25vw, 1.1875rem);/* 17 -> 19 */
  --fs-body:    clamp(1rem,      0.97rem + 0.12vw, 1.0625rem);/* 16 -> 17 */
  --fs-small:   0.9375rem;                                    /* 15       */
  --fs-meta:    0.8125rem;                                    /* 13       */
  --fs-eyebrow: 0.6875rem;                                    /* 11       */
  --fs-price:   clamp(1.25rem,   1.14rem + 0.46vw, 1.5rem);   /* 20 -> 24 */
  --fs-numeral: clamp(2rem,      1.66rem + 1.48vw, 2.75rem);  /* 32 -> 44 */

  --lh-tight:   1.05;
  --lh-heading: 1.14;
  --lh-snug:    1.35;
  --lh-body:    1.6;

  --ls-h1: -0.021em;
  --ls-h2: -0.016em;
  --ls-h3: -0.008em;
  --ls-eyebrow: 0.14em;
  --ls-meta: 0.03em;

  /* ---------------------------------------------------------------------
     6. SPACING, 4px base.
     --------------------------------------------------------------------- */
  --sp-1:  0.25rem;   /*   4 */
  --sp-2:  0.5rem;    /*   8 */
  --sp-3:  0.75rem;   /*  12 */
  --sp-4:  1rem;      /*  16 */
  --sp-5:  1.5rem;    /*  24 */
  --sp-6:  2rem;      /*  32 */
  --sp-7:  3rem;      /*  48 */
  --sp-8:  4rem;      /*  64 */
  --sp-9:  6rem;      /*  96 */
  --sp-10: 8rem;      /* 128 */

  /* Band padding was the single biggest complaint on the first pass: sections
     ran to 136px top and bottom and the page felt empty. Roughly halved. */
  --gutter:    clamp(1.25rem, 0.85rem + 1.30vw, 2rem);      /* 20 -> 32 */
  --band:      clamp(2.75rem, 2.05rem + 2.80vw, 4.5rem);    /* 44 -> 72 */
  --band-dark: clamp(3rem,    2.25rem + 3.00vw, 4.875rem);  /* 48 -> 78 */
  --cell:      clamp(1.125rem, 0.95rem + 0.75vw, 1.625rem); /* 18 -> 26 */
  --stack:     clamp(1.5rem,  1.28rem + 0.90vw, 2.125rem);  /* 24 -> 34 */
  --grid-gap:  clamp(1rem,    0.85rem + 0.60vw, 1.375rem);  /* 16 -> 22 */

  /* ---------------------------------------------------------------------
     7. CONTAINERS
     --------------------------------------------------------------------- */
  /* One page width for everything. Three different container widths meant
     section left edges stepped in and out down the page, which read as the
     site having no alignment at all. --w-wide and --w-narrow are kept as
     names so section CSS keeps working, but they now resolve to the same
     outer edge; narrow blocks cap their MEASURE instead of their box. */
  --w-page:   1200px;
  --w-wide:   1200px;
  --w-narrow: 1200px;
  --w-measure: 62ch;   /* reading cap for narrow prose blocks */
  --w-prose:  68ch;    /* measure cap for running body copy                  */
  --w-hero:   34ch;    /* hero text block measure                            */

  /* ---------------------------------------------------------------------
     8. RADII, SHADOWS, BORDERS
     --------------------------------------------------------------------- */
  /* Two radii, and they mean something. Surfaces you put things ON are 10px;
     things you press, type into or tap are 8px. That is the whole scale.

     It used to be four steps - 3, 6, 8 and 50% - with comments restricting
     --r-lg to "hero photo frame only" and --r-full to "portrait disc only".
     Both were already false: --r-lg was on eight things and --r-full on
     fourteen. Worse, --r at 6px was doing 26 jobs, roughly half of them cards
     and half of them controls, so the distinction did not exist in the CSS at
     all - it just looked like every corner was the same by accident.

     999px is NOT interchangeable with 50%. On anything wider than it is tall,
     50% gives an ellipse and 999px gives a stadium. Pills need the pill. */
  --r-card: 10px;
  --r-ctl:  8px;
  --r-pill: 999px;   /* stadium: brand pills, the "most booked" flag */
  --r-full: 50%;     /* true circles only: icon discs, the portrait */

  /* Legacy names, kept pointing at the new scale so a missed call site lands
     somewhere sensible instead of losing its radius. Prefer --r-card and
     --r-ctl in new work; these can go once nothing references them. */
  --r-sm:   var(--r-ctl);
  --r:      var(--r-ctl);
  --r-lg:   var(--r-card);

  --hair: 1px;
  --hair-dark: 1px;

  /* ---- ELEVATION -------------------------------------------------------
     One scale, three steps, and this really is the only place it is declared.

     It used to be two scales. This file declared --shadow and --shadow-header
     under a comment claiming to be "the ONLY shadow definition in the build",
     while 22-sections.css quietly opened a second :root declaring
     --shadow-rest and --shadow-lift. Because that file concatenates last, the
     second set won, so the token file did not describe the elevation the site
     actually rendered. Both are folded in here now.

     The steps are not decorative. rest is what a card sits at, raised is for
     something floating above the page but at rest (the condensed header, the
     dock tooltip), and lift is the hover and focus response. Reaching for a
     step above lift means inventing a fourth, so do not. */
  --shadow-rest:   0 1px 2px rgba(16, 20, 24, .04), 0 4px 14px rgba(16, 20, 24, .06);
  --shadow-raised: 0 2px 6px rgba(16, 20, 24, .06), 0 10px 26px rgba(16, 20, 24, .09);
  --shadow-lift:   0 3px 8px rgba(16, 20, 24, .08), 0 18px 40px rgba(16, 20, 24, .14);

  /* ---------------------------------------------------------------------
     9. MOTION
     --------------------------------------------------------------------- */
  --dur-fast: 160ms;
  --dur:      240ms;
  --dur-slow: 520ms;

  /* This value had the commas between its first three decimals eaten by a bad
     find-and-replace, and sat that way for a long time. Two arguments where
     four are required, so it was invalid, so every declaration substituting it
     was thrown out at computed-value time and fell back to the initial value.
     Custom properties parse permissively, which is why nothing errored: the
     token stored the broken string happily and 44 transitions across nine
     files simply never ran. The whole motion layer of the site was inert.

     Do not reformat this line. If the commas go, the site stops animating and
     nothing in the console will tell you. */
  --ease: cubic-bezier(.16, .84, .44, 1);

  /* --ease-out was declared here and was malformed in the same way. It had
     zero consumers, so its breakage was never visible. Deleted rather than
     repaired: the approved design uses a single easing curve, and a valid but
     unused second curve is an invitation to drift back to two. */

  --reveal-step: 80ms;   /* multiplied by --i on staggered children */

  /* ---------------------------------------------------------------------
     10. LAYERING
     --------------------------------------------------------------------- */
  --z-base:      0;
  --z-rule:      1;
  --z-raised:    2;
  --z-sticky:  100;
  --z-header:  200;
  --z-bar:     300;   /* mobile bottom bar   */
  --z-overlay: 400;   /* mobile menu overlay */
  --z-modal:   500;   /* lightbox            */

  /* ---------------------------------------------------------------------
     11. CHROME METRICS, the JS author reads these names, do not rename.
     --------------------------------------------------------------------- */
  --header-h:      76px;
  --header-h-cond: 60px;
  --bar-h:         64px;
  --tap:           44px;   /* minimum tap target, non-negotiable */
  --scroll-offset: calc(var(--header-h-cond) + var(--sp-4));
}

/* Condensed header metric swap. The JS adds .is-condensed to #site-header. */
:root:has(#site-header.is-condensed) {
  --scroll-offset: calc(var(--header-h-cond) + var(--sp-4));
}

/* ======================================================================
   01-base.css
   ====================================================================== */

/* ============================================================================
   01-base.css, reset, base typography, focus, skip link.tbc, utilities.
   No literal colours. Mobile-first. Every media query is min-width.
   ========================================================================= */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-padding-top: var(--scroll-offset);
  scroll-behavior: smooth;
  tab-size: 4;
}

body {
  min-height: 100svh;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Locked while the mobile overlay menu or the lightbox is open. JS adds this. */
body.is-locked { overflow: hidden; }

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

img { height: auto; }

/* Photographs are reconciled to one look, the source set is 35 phone photos
   with inconsistent white balance. Applied globally to content imagery only. */
img:not([data-raw]) { filter: saturate(.94) contrast(1.03); }

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

button { background: none; border: 0; }

p, h1, h2, h3, h4, h5, h6, li, dt, dd, figcaption {
  overflow-wrap: break-word;
}

ul[class], ol[class] { list-style: none; padding: 0; }

table { border-collapse: collapse; width: 100%; }

svg { fill: currentColor; }

/* Respect an explicit fill="none" on the root svg. Without this, the rule above
   beats the presentation attribute and every stroke-only icon fills in solid. */
svg[fill="none"] { fill: none; }

fieldset { border: 0; padding: 0; min-inline-size: 0; }

address { font-style: normal; }

hr {
  border: 0;
  border-top: var(--hair) solid var(--rule);
}

/* ---------------------------------------------------------------------------
   BASE TYPOGRAPHY
   Display face is Bitter. Case discipline: nothing above h2 is uppercased;
   h3 down, body, FAQ questions and all case-study fields stay sentence case.
   --------------------------------------------------------------------------- */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--fg);
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-h1);
  font-weight: 800;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h1);
}

h2 {
  font-size: var(--fs-h2);
  font-weight: 700;
  line-height: var(--lh-heading);
  letter-spacing: var(--ls-h2);
}

h3 {
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: var(--ls-h3);
}

h4 {
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
}

p { text-wrap: pretty; }

p + p { margin-top: var(--sp-4); }

/* Running prose gets a measure cap. Apply to the wrapper, not every <p>. */
.prose { max-width: var(--w-prose); }
.prose > * + * { margin-top: var(--sp-4); }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.5;
  color: var(--fg-muted);
  max-width: var(--w-prose);
}

.small     { font-size: var(--fs-small); }
.meta      { font-size: var(--fs-meta); letter-spacing: var(--ls-meta); color: var(--fg-muted); }
.smallprint{ font-size: var(--fs-meta); line-height: 1.5; color: var(--fg-muted); max-width: 62ch; }

strong, b { font-weight: 600; }

/* Every numeral on the site is tabular. This is the clinical-chart signal. */
.num,
td, th,
time,
.price,
data {
  font-variant-numeric: tabular-nums lining-nums;
  font-feature-settings: "tnum" 1, "lnum" 1;
}

/* ---------------------------------------------------------------------------
   LINKS
   --------------------------------------------------------------------------- */
a {
  color: var(--link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--dur-fast) var(--ease);
}

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

/* Links that are structural (nav items, cards, buttons) opt out of underline. */
a.plain,
a.btn,
a.card__link,
a.nav__link { text-decoration: none; }

/* ---------------------------------------------------------------------------
   FOCUS, visible everywhere, never removed. Ice on dark, steel on light.
   --------------------------------------------------------------------------- */
:focus { outline: none; }

:where(a, button, summary, input, select, textarea, [tabindex], [contenteditable]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
  border-radius: 4px;   /* the focus ring traces the outside edge; it is not a control */
}

/* Fallback for engines without :focus-visible support. */
@supports not selector(:focus-visible) {
  :where(a, button, summary, input, select, textarea, [tabindex]):focus {
    outline: 3px solid var(--focus);
    outline-offset: 3px;
  }
}

::selection {
  background: var(--ice);
  color: var(--navy);
}

/* ---------------------------------------------------------------------------
   SKIP LINK
   --------------------------------------------------------------------------- */
.skip-link {
  position: absolute;
  left: var(--sp-4);
  top: var(--sp-4);
  z-index: var(--z-modal);
  transform: translateY(-200%);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-5);
  background: var(--steel);
  color: var(--white);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--r);
  transition: transform var(--dur) var(--ease);
}

.skip-link:focus-visible {
  transform: translateY(0);
  color: var(--white);
}

/* ---------------------------------------------------------------------------
   .tbc, UNCONFIRMED CONTENT MARKER
   Every value still missing from FACTS.md ships wrapped in this. It must be
   obvious to a reviewer and greppable in source, without looking broken.
   Usage: <span class="tbc">[TOWN]</span>
   --------------------------------------------------------------------------- */
.tbc {
  display: inline;
  padding: 0.08em 0.36em;
  background: var(--tbc-bg);
  border-bottom: 1px dashed var(--tbc-line);
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  color: var(--tbc-fg);
  font-weight: 600;
  font-variant-numeric: tabular-nums lining-nums;
  text-decoration: none;
  white-space: nowrap;
}

/* Long placeholders (a sentence, an address block) may wrap. */
.tbc--wrap { white-space: normal; }

/* Block-level placeholder for a whole missing panel (a testimonial, a case
   study field group). Still obviously unfinished, still not broken-looking. */
.tbc-block {
  display: block;
  padding: var(--sp-5);
  background: var(--tbc-bg);
  border: 1px dashed var(--tbc-line);
  border-radius: var(--r);
  color: var(--fg-muted);
  font-size: var(--fs-small);
}

/* ---------------------------------------------------------------------------
   UTILITIES
   --------------------------------------------------------------------------- */
.visually-hidden:not(:focus):not(:active) {
  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;
  border: 0;
}

[hidden] { display: none !important; }

.no-wrap { white-space: nowrap; }
.text-center { text-align: center; }
.flow > * + * { margin-top: var(--sp-4); }
.flow-lg > * + * { margin-top: var(--sp-6); }

/* ---------------------------------------------------------------------------
   REDUCED MOTION, honoured globally, not per-component.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------------------------------------------------------------------------
   PRINT, the price table and contact details should print cleanly.
   --------------------------------------------------------------------------- */
@media print {
  #site-header, #bottom-bar, #nav-overlay, #lightbox.btn { display: none !important; }
  body { background: #fff; color: #000; }
  img:not([data-raw]) { filter: none; }
}

/* ======================================================================
   02-components.css
   ====================================================================== */

/* ============================================================================
   02-components.css, the shared vocabulary. Section authors REUSE these.
   Do not restyle a component from a section stylesheet; extend it with a
   modifier (.card--plate) or set a documented custom property.
   No literal colours. Mobile-first. Every media query is min-width.
   ========================================================================= */

/* ===========================================================================
   1. EYEBROW
   <p class="eyebrow">Where I've Worked</p>
   =========================================================================== */
.eyebrow {
  font-family: var(--font-sans);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--eyebrow-c);
}

/* ===========================================================================
   2. SECTION HEADING BLOCK
   Left-aligned eyebrow, heading and optional lead. The section index numeral
   and the four-segment rule that used to sit beside this were removed at the
   client's request; the crosshair motif still carries through .quad, the
   brands lattice and the process numerals.

   <header class="shead">
     <div class="shead__text">
       <p class="eyebrow">Where I've Worked</p>
       <h2 id="brands-title">From Sunday league to the London Marathon.</h2>
       <p class="lead">Pitch-side and event work across elite and community sport…</p>
     </div>
   </header>
   =========================================================================== */
.shead {
  display: block;
  margin-bottom: var(--stack);
}

.shead__text > * + * { margin-top: var(--sp-3); }
.shead__text > .lead { margin-top: var(--sp-5); }

/* Centred variant is NOT provided. Every section heading is left-aligned. */

/* ===========================================================================
   3. THE QUADRANT, the load-bearing motif.
   Four cells sharing ONE continuous crosshair of hairlines. No outer border,
   no gaps, no card boxes inside. Where the hairlines cross sits the node: a
   22px ring with the band colour knocked out around it, so the two rules run
   INTO the ring and cross at its centre. That is the badge, reduced to
   geometry, repeated at five scales down the page.

   <div class="quad quad--hold">
     <div class="quad__cell">…</div>
     <div class="quad__cell">…</div>
     <div class="quad__cell">…</div>
     <div class="quad__cell">…</div>
   </div>

   Variants:
     .quad--hold  stays 2x2 at 375px (use for compact, text-short blocks)
     .quad--4     goes 4-across at 1024px+ (Qualifications, Aftercare)
     .quad--flush cells have no padding (image cells)
   Dark bands need NO variant: .band--navy re-points --rule and --node-bg.
   =========================================================================== */
.quad {
  display: grid;
  grid-template-columns: 1fr;
  position: relative;
}

.quad > * {
  padding: var(--cell);
  min-width: 0;
  position: relative;
}

.quad > * + * { border-top: var(--hair) solid var(--rule); }

.quad--flush > * { padding: 0; }

/* The node. Hidden until there is a real crossing to mark. */
.quad::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r-full);
  background: transparent;
  box-shadow: 0 0 0 5px var(--node-bg);
  pointer-events: none;
  z-index: var(--z-rule);
  display: none;
}

/* .quad--hold keeps the crosshair at every width, including 375px. */
.quad--hold { grid-template-columns: 1fr 1fr; }
.quad--hold > * + * { border-top: 0; }
.quad--hold > :nth-child(odd)  { border-right: var(--hair) solid var(--rule); }
.quad--hold > :nth-child(-n+2) { border-bottom: var(--hair) solid var(--rule); }
.quad--hold::after { display: block; }
.quad--hold > * { padding: var(--sp-4); }

@media (min-width: 40em) {          /* 640px */
  .quad { grid-template-columns: 1fr 1fr; }
  .quad > * + * { border-top: 0; }
  .quad > :nth-child(odd)  { border-right: var(--hair) solid var(--rule); }
  .quad > :nth-child(-n+2) { border-bottom: var(--hair) solid var(--rule); }
  .quad::after { display: block; }
  .quad--hold > * { padding: var(--cell); }
}

@media (min-width: 64em) {          /* 1024px */
  .quad--4 { grid-template-columns: repeat(4, 1fr); }
  /* :nth-child(n) matches everything and lifts specificity to (0,2,0) so these
     beat the .quad--hold / .quad nth-child border rules above. Source order
     then resolves it. Without this, a .quad--hold.quad--4 keeps a stray
     horizontal rule under cells 1 and 2 at 4-across. */
  .quad--4 > :nth-child(n) {
    border-top: 0;
    border-bottom: 0;
    border-right: var(--hair) solid var(--rule);
  }
  .quad--4 > :nth-child(n):last-child { border-right: 0; }
  .quad--4::after { display: none; }
}

/* ===========================================================================
   4. BUTTONS, 6px radius, 44px minimum, no pills, no gradients.
   <a class="btn btn--primary" href="#contact">Book an Appointment</a>
   =========================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  min-width: var(--tap);
  padding: var(--sp-3) var(--sp-5);
  border: 2px solid transparent;
  border-radius: var(--r);
  font-family: var(--font-sans);
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}

/* Filled buttons sit on the page and lift off it. .btn has always transitioned
   box-shadow and never declared one in any state, so every CTA on the site
   changed colour and moved a single pixel. Elevation is the third channel:
   rest -> raised, alongside the fill swap and the lift from .pressable.

   Outline buttons (--secondary, --ghost) deliberately get none. A shadow under
   a transparent button shades the band behind it rather than lifting anything. */
.btn--primary {
  background: var(--steel);
  border-color: var(--steel);
  color: var(--white);
  box-shadow: var(--shadow-rest);
}
.btn--primary:hover,
.btn--primary:focus-visible {
  background: var(--navy);
  border-color: var(--navy);
  color: var(--white);
  box-shadow: var(--shadow-raised);
}

/* Outline on light bands. */
.btn--secondary {
  background: transparent;
  border-color: var(--steel);
  color: var(--steel);
}
.btn--secondary:hover,
.btn--secondary:focus-visible {
  background: var(--steel);
  color: var(--white);
}

/* Ghost on photography or navy. White border. */
.btn--ghost {
  background: transparent;
  border-color: var(--white);
  color: var(--white);
}
.btn--ghost:hover,
.btn--ghost:focus-visible {
  background: var(--white);
  color: var(--navy);
}

/* Ghost on navy where a softer edge is wanted. Ice border, ice label: 6.69:1. */
.btn--ice {
  background: transparent;
  border-color: var(--ice);
  color: var(--ice);
}
.btn--ice:hover,
.btn--ice:focus-visible {
  background: var(--ice);
  color: var(--navy);
}

.btn--sm { min-height: var(--tap); padding: var(--sp-2) var(--sp-4); font-size: var(--fs-small); }
.btn--block { display: flex; width: 100%; }

/* Icon-only control. ALWAYS needs aria-label. */
.btn--icon {
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  border-radius: var(--r);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
}

/* ===========================================================================
   5. CARDS
   <article class="card">
     <div class="card__media">
       <img src="assets/img/service-cupping.webp" width="900" height="675"
            alt="…" loading="lazy" decoding="async">
     </div>
     <div class="card__body">
       <h3 class="card__title">Cupping Therapy</h3>
       <p class="card__text">…</p>
       <p class="card__meta num">From £30 · 30 min</p>
     </div>
   </article>
   =========================================================================== */
.card {
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-card);
  overflow: hidden;
  transition: box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

.card:hover,
.card:focus-within {
  box-shadow: var(--shadow-lift);
  border-color: var(--rule-strong);
}

/* The steel top rule the brief asks for, drawn without shifting layout. */
.card::before {
  content: "";
  display: block;
  height: 3px;
  background: var(--steel);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur) var(--ease);
}
.card:hover::before,
.card:focus-within::before { transform: scaleX(1); }

.card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--navy);
}
.card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--fp, 50% 50%);
}

.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--cell);
  flex: 1;
}

.card__title { font-size: var(--fs-h3); }
.card__text  { color: var(--fg); }
.card__meta  {
  margin-top: auto;
  padding-top: var(--sp-3);
  border-top: var(--hair) solid var(--rule);
  font-size: var(--fs-meta);
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
  font-variant-numeric: tabular-nums lining-nums;
}

/* Whole-card link. Stretch the anchor, keep the accessible name on the title. */
.card__link { text-decoration: none; color: inherit; }
.card__link::after { content: ""; position: absolute; inset: 0; }
.card { position: relative; }

/* Cards on navy. */
.band--navy .card,
.on-dark .card {
  background: var(--tint-white-08);
  border-color: var(--rule-dark);
  color: var(--on-navy);
}
.band--navy .card__meta,
.on-dark .card__meta { color: var(--on-navy-muted); border-top-color: var(--rule-dark); }

/* --- PLATE CARD: portrait 3:4 phone photo with a flush navy nameplate. ---
   This is the treatment that makes casual phone photography read as evidence.
   <article class="card card--plate">
     <div class="card__media">
       <img src="assets/img/gallery-kabaddi-600.webp" width="600" height="800" …>
       <p class="card__plate">England and Scotland Kabaddi tournament cover</p>
     </div>
   </article>                                                                */
.card--plate { border: 0; background: transparent; border-radius: var(--r); }
.card--plate::before { display: none; }
.card--plate .card__media {
  aspect-ratio: 3 / 4;
  border-radius: var(--r-card);
}
/* The plate is anchored to the BOTTOM of the photograph, so its height is set
   by how many lines the caption runs to and it grows upwards. Across a row of
   four that meant the one-line caption started 18px lower than the two-line
   ones and the whole row read as misaligned - the cards were identical
   heights, it was only the text that stepped.

   A min-height of two lines plus the padding gives every plate the same box,
   and aligning the content to the top of it means the first line of every
   caption starts on the same baseline whether it runs to one line or two.
   Three lines still grow the box, as they should. */
.card__plate {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: flex-start;
  /* Two lines of leading, the padding, and the 2px rule on top - box-sizing is
     border-box, so the border has to be counted or the box lands 2px short and
     the one-line caption still steps below the others. */
  min-height: calc((2 * 1.35em) + (2 * var(--sp-3)) + 2px);
  margin: 0;
  padding: var(--sp-3) var(--sp-4);
  background: var(--navy);
  border-top: 2px solid var(--ice);
  color: var(--on-navy);
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: var(--ls-meta);
}

/* ===========================================================================
   6. PLATE, a photograph mounted with a ruled caption. Not decoration.
   <figure class="plate">
     <img src="assets/img/gallery-gothia-ceremony-1200.webp"
          width="1200" height="1600" loading="lazy" decoding="async" alt="…">
     <figcaption>Gothia Cup opening ceremony, Ullevi Stadium</figcaption>
   </figure>
   =========================================================================== */
.plate { margin: 0; }

.plate img {
  width: 100%;
  border-radius: var(--r-card);
  background: var(--navy);
}

.plate figcaption {
  margin-top: var(--sp-3);
  padding-top: var(--sp-2);
  border-top: var(--hair) solid var(--rule);
  font-size: var(--fs-meta);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
}

.band--navy .plate figcaption,
.on-dark .plate figcaption { color: var(--on-navy-muted); border-top-color: var(--rule-dark); }

/* Circular crop, human subjects only, and only in About Me and avatars.
   Do NOT introduce a fourth disc context. */
.disc {
  aspect-ratio: 1;
  border-radius: var(--r-full);
  overflow: hidden;
  background: var(--navy);
  position: relative;
}
.disc img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--fp, 50% 35%);
}
.disc::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-full);
  box-shadow: inset 0 0 0 1.5px var(--rule-strong);
}

/* ===========================================================================
   7. THE TEAM SHEET, the Brands lattice. Twelve confirmed engagements as a
   numbered record, never as third-party logo files.
   <ol class="sheet">
     <li class="sheet__row">
       <span class="sheet__no num">01</span>
       <span class="sheet__name">Sporting Khalsa FC</span>
       <span class="sheet__role">Pitch-side</span>
     </li> …
   </ol>
   =========================================================================== */
.sheet {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  border-top: var(--hair) solid var(--rule);
}

.sheet__row {
  position: relative;
  display: grid;
  grid-template-columns: 2.75rem 1fr;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-4);
  padding: var(--sp-4) var(--sp-4) var(--sp-4) var(--sp-5);
  border-bottom: var(--hair) solid var(--rule);
  min-height: var(--tap);
}

.sheet__row::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--accent);
  transition: width var(--dur-fast) var(--ease);
}
.sheet__row:hover::before,
.sheet__row:focus-within::before { width: 4px; }

.sheet__no {
  font-family: var(--font-sans);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--accent);
}

.sheet__name {
  font-family: var(--font-display);
  font-size: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
  font-weight: 700;
  line-height: 1.15;
  color: var(--fg);
}

.sheet__role {
  grid-column: 2;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.band--navy .sheet__name,
.on-dark .sheet__name { color: var(--white); }
.band--navy .sheet__no,
.on-dark .sheet__no   { color: var(--ice); }
.band--navy .sheet__role,
.on-dark .sheet__role { color: var(--on-navy-muted); }
.band--navy .sheet__row::before,
.on-dark .sheet__row::before { background: var(--ice); }

/* Two ruled columns of six from 760px, a crosshair made of the content. */
@media (min-width: 47.5em) {         /* 760px */
  .sheet {
    grid-template-columns: 1fr 1fr;
    grid-template-rows: repeat(6, auto);
    grid-auto-flow: column;
  }
  .sheet__row:nth-child(-n+6) { border-right: var(--hair) solid var(--rule); }
  .sheet__role { grid-column: 3; align-self: center; }
  .sheet__row { grid-template-columns: 2.75rem 1fr auto; }
}

/* ===========================================================================
   8. TAGS, Conditions Treated.
   <ul class="tagfield"><li class="tag">Muscle strains</li> …</ul>
   =========================================================================== */
.tagfield {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  list-style: none;
  padding: 0;
  margin: 0;
}

.tag {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--sp-2) var(--sp-4);
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r);
  font-size: var(--fs-small);
  font-weight: 500;
  line-height: 1.3;
  color: var(--fg);
  background: transparent;
}

/* Interactive tags (gallery filters) must hit 44px. */
.tag--action { min-height: var(--tap); cursor: pointer; }
.tag--action[aria-pressed="true"] {
  background: var(--steel);
  border-color: var(--steel);
  color: var(--white);
}

/* ===========================================================================
   9. TABLES, price list and availability. Hairline rows, no zebra fill.
   <table class="table">
     <caption class="visually-hidden">Treatment times and prices</caption>
     <thead><tr><th scope="col">Treatment time</th><th scope="col">Price</th></tr></thead>
     <tbody><tr><th scope="row">30 minutes</th><td class="price">£30</td></tr></tbody>
   </table>
   =========================================================================== */
.table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums lining-nums;
}

.table caption {
  text-align: left;
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--eyebrow-c);
  padding-bottom: var(--sp-3);
}

.table th,
.table td {
  text-align: left;
  padding: var(--sp-4) var(--sp-3);
  border-bottom: var(--hair) solid var(--rule);
  vertical-align: baseline;
}

.table thead th {
  font-family: var(--font-sans);
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
  padding-bottom: var(--sp-2);
}

.table tbody th {
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 500;
}

.table td:last-child,
.table th:last-child { text-align: right; }

.table .price {
  font-family: var(--font-display);
  font-size: var(--fs-price);
  font-weight: 700;
  line-height: 1.1;
  white-space: nowrap;
}

.table--tight th,
.table--tight td { padding: var(--sp-3) var(--sp-2); }

/* Wide tables never make the page scroll sideways. */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* ===========================================================================
   10. FORM CONTROLS
   <div class="field">
     <label class="field__label" for="name">Name</label>
     <input class="field__input" id="name" name="name" type="text"
            autocomplete="name" required aria-describedby="name-error">
     <p class="field__error" id="name-error" hidden>Please enter your name.</p>
   </div>
   =========================================================================== */
.form { display: grid; gap: var(--sp-5); }

/* align-content: start so a field never distributes spare height between its
   label, input and error. Without it, any parent that stretches the field
   (see .s-contact__row) pulls the input away from its label. */
.field { display: grid; gap: var(--sp-2); align-content: start; }

.field__label {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.field__input,
.field__select,
.field__textarea {
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-3) var(--sp-4);
  background: var(--white);
  /* 2px at --field-border, not 1px at --rule-strong. An empty field is a box
     of white on white and its border is the only thing telling you it is
     there, so it has to clear the 3:1 that WCAG 1.4.11 asks of a control
     boundary. The old 1px at 40% measured 1.90:1 and read as blank space.
     This is the form the whole site exists to get filled in. */
  border: 2px solid var(--field-border);
  border-radius: var(--r);
  color: var(--ink);
  font-size: var(--fs-body);
  transition: border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}

.field__textarea { min-height: 8rem; resize: vertical; line-height: var(--lh-body); }

.field__input:hover,
.field__select:hover,
.field__textarea:hover { border-color: var(--steel); }

/* Focus was doing nothing for a mouse user. The global :focus-visible outline
   in 01-base.css only fires for keyboard focus, and 01-base.css clears the
   default :focus outline before it, so clicking into a field changed nothing
   at all: no border shift, no ring, no indication of where you had landed.

   Plain :focus, so it fires however you got there. The border goes solid and
   a soft halo appears outside it - two channels, not one, so it survives both
   a colour-blind reading and a low-contrast screen. Keyboard users keep the
   outline as well; it sits outside the halo and the two read as one state. */
.field__input:focus,
.field__select:focus,
.field__textarea:focus {
  border-color: var(--steel);
  box-shadow: 0 0 0 4px var(--tint-steel-20);
}

.field__input::placeholder,
.field__textarea::placeholder { color: var(--slate-ink); opacity: 1; }

.field__hint { font-size: var(--fs-meta); color: var(--fg-muted); }

.field__error {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--navy);
}
.field__error::before {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  border-radius: var(--r-full);
  background: var(--steel);
}

[aria-invalid="true"] {
  border-color: var(--navy);
  border-width: 2px;
}

/* Honeypot, visually and aurally removed, never display:none. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__status {
  padding: var(--sp-4) var(--sp-5);
  border: var(--hair) solid var(--steel);
  border-left-width: 4px;
  border-radius: var(--r-card);
  background: var(--tint-steel-06);
  font-weight: 500;
}

/* ===========================================================================
   11. ACCORDION, FAQ. Native <details>, progressively enhanced.
   <div class="accordion" id="faq-list" data-accordion="single">
     <details class="accordion__item">
       <summary class="accordion__q">Do I need a GP referral?</summary>
       <div class="accordion__a"></div>
     </details>
   </div>
   =========================================================================== */
.accordion {
  border-top: var(--hair) solid var(--rule);
  background: var(--white);
  border-radius: var(--r-card);
  overflow: hidden;
}

.accordion__item { border-bottom: var(--hair) solid var(--rule); }

.accordion__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: var(--tap);
  padding: var(--sp-4) var(--sp-5);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--fg);
}
.accordion__q::-webkit-details-marker { display: none; }

.accordion__q::after {
  content: "";
  flex: none;
  width: 12px;
  height: 12px;
  border-right: 2px solid var(--steel);
  border-bottom: 2px solid var(--steel);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur) var(--ease);
}
.accordion__item[open] > .accordion__q::after {
  transform: rotate(225deg) translate(-2px, -2px);
}

.accordion__a {
  padding: 0 var(--sp-5) var(--sp-5);
  max-width: var(--w-prose);
  color: var(--fg);
}
.accordion__a > * + * { margin-top: var(--sp-4); }

/* ===========================================================================
   12. LIGHTBOX, markup lives in the page once; JS drives it.
   =========================================================================== */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  grid-template-rows: auto 1fr auto;
  gap: var(--sp-4);
  padding: var(--sp-4);
  background: rgba(16, 20, 24, .94);
  color: var(--on-navy);
}

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

.lightbox__counter {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--on-navy-muted);
}

.lightbox__stage {
  display: grid;
  place-items: center;
  min-height: 0;
}

.lightbox__img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r-card);
}

.lightbox__caption {
  text-align: center;
  font-size: var(--fs-meta);
  letter-spacing: var(--ls-meta);
  color: var(--on-navy-muted);
  max-width: 62ch;
  margin-inline: auto;
}

.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  border: var(--hair) solid var(--rule-dark-strong);
  border-radius: var(--r);
  background: rgba(23, 56, 79, .7);
  color: var(--on-navy);
  cursor: pointer;
}
.lightbox__nav--prev { left: var(--sp-4); }
.lightbox__nav--next { right: var(--sp-4); }

.lightbox :focus-visible { outline-color: var(--ice); }

/* ===========================================================================
   13. COMPARISON SLIDER, DORMANT BY DEFAULT.
   The manifest supplies case-01/02/03 as single pre-composited two-panel
   images, so case studies ship as .plate figures. This component only
   activates if two separate before/after files are ever supplied.
   =========================================================================== */
.compare {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-card);
  --pos: 50%;
}
.compare__after {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 var(--pos));
}
.compare__range {
  position: absolute;
  inset: auto 0 var(--sp-4) 0;
  width: calc(100% - var(--sp-8));
  margin-inline: auto;
  min-height: var(--tap);
}
.compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 2px;
  background: var(--ice);
  pointer-events: none;
}

/* ===========================================================================
   14. SCROLL REVEAL, one class, one state class, one stagger property.
   <div class="reveal" style="--i:2">…</div>
   JS adds .is-revealed. No JS, no animation, content visible: the base state
   is authored so that a failed script never hides content, see 03-layout.css
   .no-js fallback and the html.js gate below.
   =========================================================================== */
.reveal { will-change: opacity, transform; }

html.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-slow) var(--ease) calc(var(--i, 0) * var(--reveal-step)),
              transform var(--dur-slow) var(--ease) calc(var(--i, 0) * var(--reveal-step));
}

html.js .reveal.is-revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  html.js .reveal { opacity: 1; transform: none; transition: none; }
}

/* ===========================================================================
   15. ICONS, 1.5px steel strokes in a bare box. No pastel circles, no fills.
   <svg class="icon" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">…</svg>
   =========================================================================== */
.icon {
  flex: none;
  width: 24px;
  height: 24px;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon--lg { width: 32px; height: 32px; }
.band--navy .icon,
.on-dark .icon { stroke: var(--ice); }

/* ===========================================================================
   16. CALLOUT, e.g. "not sure if you're in range".
   <aside class="callout"><p>…</p></aside>
   =========================================================================== */
.callout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--cell);
  background: var(--tint-steel-06);
  border: var(--hair) solid var(--rule);
  border-left: 4px solid var(--steel);
  border-radius: var(--r-card);
}
.band--navy .callout,
.on-dark .callout {
  background: var(--tint-ice-14);
  border-color: var(--rule-dark);
  border-left-color: var(--ice);
}

/* ======================================================================
   03-layout.css
   ====================================================================== */

/* ============================================================================
   03-layout.css, containers, bands, grids, dividers, site chrome.
   No literal colours. Mobile-first. Every media query is min-width.

   Breakpoints used across the whole build (do not invent others):
     30em   480px   small phone -> large phone
     40em   640px   quadrant becomes a real 2x2
     47.5em 760px   team sheet becomes two ruled columns of six
     48em   768px   mobile bottom bar disappears; 2-up card grids
     56.25em 900px  hamburger -> horizontal nav (brief-specified)
     64em   1024px  4-across grids.quad--4
     80em   1280px  full desktop rhythm
     90em   1440px  wide container ceiling
   ========================================================================= */

/* ===========================================================================
   1. CONTAINER
   =========================================================================== */
.container {
  width: 100%;
  max-width: var(--w-page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* All three resolve to the same outer edge so every section lines up on one
   vertical. --narrow now constrains the reading measure, not the box. */
.container--wide   { max-width: var(--w-wide); }
.container--narrow { max-width: var(--w-narrow); }
.container--narrow > * > p,
.container--narrow > p { max-width: var(--w-measure); }

/* Full-bleed child inside a container (rules, strips, photography). */
.bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* ===========================================================================
   2. BANDS
   Every <section> is a band. The band sets its own colour role tokens, so
   components (quad, sheet, card, plate, callout) adapt with no dark variant.

   <section class="band band--bone" id="qualifications"
            aria-labelledby="qualifications-title">
     <div class="container"> … </div>
   </section>

   Rhythm rule enforced in review: no two dark bands ever touch, and there are
   never more than two consecutive bands of the same colour.
   =========================================================================== */
.band {
  position: relative;
  padding-block: var(--band);
  scroll-margin-top: var(--scroll-offset);
}

.band--bone {
  background: var(--bone);
  --node-bg: var(--bone);
}

.band--white {
  background: var(--white);
  --node-bg: var(--white);
}

.band--navy {
  background: var(--navy);
  padding-block: var(--band-dark);
  color: var(--on-navy);
  --fg: var(--on-navy);
  --fg-muted: var(--on-navy-muted);
  --bg: var(--navy);
  --accent: var(--ice);
  --eyebrow-c: var(--ice);
  --link: var(--ice);
  --link-hover: var(--white);
  --node-bg: var(--navy);
  --focus: var(--ice);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--on-navy);
}

/* Darkest note on the page. Use ONCE, and never adjacent to a navy band. */
.band--ink {
  background: var(--ink);
  padding-block: var(--band-dark);
  color: var(--on-navy);
  --fg: var(--on-navy);
  --fg-muted: var(--on-navy-muted);
  --bg: var(--ink);
  --accent: var(--ice);
  --eyebrow-c: var(--ice);
  --link: var(--ice);
  --link-hover: var(--white);
  --node-bg: var(--ink);
  --focus: var(--ice);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--on-navy);
}

/* Utility for a dark island inside a light band (e.g. a navy card cluster). */
.on-dark {
  color: var(--on-navy);
  --fg: var(--on-navy);
  --fg-muted: var(--on-navy-muted);
  --accent: var(--ice);
  --eyebrow-c: var(--ice);
  --link: var(--ice);
  --link-hover: var(--white);
  --node-bg: var(--navy);
  --focus: var(--ice);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--on-navy);
}

.band--tight { padding-block: calc(var(--band) * 0.6); }

/* ===========================================================================
   3. DIVIDERS, the joins between bands read as ruled, not faded.
   <hr class="rule" aria-hidden="true"> placed as the last child of a band.
   =========================================================================== */
.rule {
  border: 0;
  border-top: var(--hair) solid var(--rule);
  margin: 0;
}

.rule--bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

/* A rule carrying the crosshair node at the container's centre axis.
   Use at MOST five times on the page, restraint is what keeps it structural. */
.rule--node { position: relative; }
.rule--node::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r-full);
  box-shadow: 0 0 0 5px var(--node-bg);
}

/* ===========================================================================
   4. GRID HELPERS
   <div class="grid grid--3"> … </div>
   Card grids use real gaps. Hairline lattices (.quad.sheet) use gap: 0, the rule IS the gap. Never mix the two in one block.
   =========================================================================== */
.grid {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: 1fr;
}

@media (min-width: 30em)   { .grid--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 48em)   { .grid--3 { grid-template-columns: repeat(2, 1fr); }
                             .grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64em)   { .grid--3 { grid-template-columns: repeat(3, 1fr); }
                             .grid--4 { grid-template-columns: repeat(4, 1fr); } }

/* Portrait 3:4 plates need to be bigger than a quarter-column at tablet.
   2-up at 480, 3-up at 768, 4-up at 1024, never 4-up at 768. */
.grid--plates { grid-template-columns: 1fr; }
@media (min-width: 30em) { .grid--plates { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 48em) { .grid--plates { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 64em) { .grid--plates { grid-template-columns: repeat(4, 1fr); } }

/* Two-column media/text split, About, Areas, Contact, Case Studies. */
.split {
  display: grid;
  gap: var(--stack);
  grid-template-columns: 1fr;
  align-items: start;
}
@media (min-width: 56.25em) {
  .split { grid-template-columns: 1fr 1fr; gap: clamp(2rem, 1rem + 4vw, 5rem); }
  .split--media-first { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .split--text-first  { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
}

/* Opt-in equal height. .split is align-items: start by default and should
   stay that way: About, Areas and Case Studies pair a block of text with a
   photograph, and stretching text to match an image just strands it. Contact
   is the exception - two cards side by side, where mismatched bottom edges
   read as a mistake. */
.split--stretch { align-items: stretch; }

.stack     { display: grid; gap: var(--sp-5); }
.stack-lg  { display: grid; gap: var(--stack); }

/* ===========================================================================
   5. STICKY HEADER
   <header class="site-header" id="site-header"> … </header>
   JS adds .is-condensed past 120px of scroll.
   =========================================================================== */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  height: var(--header-h);
  background: var(--white);
  border-bottom: var(--hair) solid var(--rule);
  transition: height var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

.site-header.is-condensed {
  height: var(--header-h-cond);
  box-shadow: var(--shadow-raised);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  height: 100%;
}

.site-header__brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  text-decoration: none;
  color: var(--ink);
  min-height: var(--tap);
}

.site-header__logo {
  width: 44px;
  height: 44px;
  transition: width var(--dur) var(--ease), height var(--dur) var(--ease);
}
/* The logo does NOT shrink when the header condenses.

   It used to go 44px to 36px here, and this was the rule still doing it after
   the --hp version was fixed: the class is what actually drives the condense,
   and it out-specifies the calc. Those 8px came out of the brand block, and
   because .site-header__inner is justify-content: space-between every one of
   them was handed to the nav - the wordmark stepped 8px left, the whole nav
   and all eight links stepped 4px left, 23 elements sliding sideways every
   time you scrolled past 120px. The header is supposed to shorten, not
   shuffle.

   44px sits inside the 60px condensed header with room to spare, so nothing
   has to give. Height is the only thing that changes now. */

.site-header__wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  line-height: 1.05;
}
.site-header__descriptor {
  display: block;
  font-family: var(--font-sans);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--slate-ink);
}

.nav { display: none; }

.nav__list {
  display: flex;
  align-items: center;
  gap: clamp(0.75rem, 0.2rem + 1.2vw, 1.5rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: var(--sp-1);
  font-size: var(--fs-small);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}

.nav__link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 8px;
  height: 2px;
  background: var(--steel);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur-fast) var(--ease);
}
.nav__link:hover::after,
.nav__link.is-current::after { transform: scaleX(1); }
.nav__link.is-current { color: var(--steel); }

.site-header__actions { display: flex; align-items: center; gap: var(--sp-3); }

/* Hamburger, hidden from 900px per the brief. */
.nav-toggle {
  display: inline-grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r);
  cursor: pointer;
}
.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  display: block;
  width: 20px;
  height: 2px;
  background: var(--ink);
  transition: transform var(--dur) var(--ease), opacity var(--dur-fast) var(--ease);
}
.nav-toggle__bars { position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after { content: ""; position: absolute; left: 0; }
.nav-toggle__bars::before { top: -6px; }
.nav-toggle__bars::after  { top: 6px; }

.nav-toggle[aria-expanded="true"] .nav-toggle__bars { background: transparent; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (min-width: 56.25em) {        /* 900px */
  .nav { display: block; }
  .nav-toggle { display: none; }
}

/* ===========================================================================
   6. MOBILE OVERLAY MENU
   <div class="nav-overlay" id="nav-overlay" hidden> … </div>
   JS toggles the [hidden] attribute AND the .is-open class, locks the body
   with .is-locked, and traps focus.
   =========================================================================== */
.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: grid;
  grid-template-rows: auto 1fr auto;
  background: var(--navy);
  color: var(--on-navy);
  padding: var(--gutter);
  overflow-y: auto;
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}
.nav-overlay.is-open { opacity: 1; }

.nav-overlay__list {
  display: grid;
  align-content: center;
  gap: var(--sp-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav-overlay__link {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 56px;
  padding-block: var(--sp-2);
  border-bottom: var(--hair) solid var(--rule-dark);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 700;
  color: var(--on-navy);
  text-decoration: none;
}
.nav-overlay__link .num { color: var(--ice); font-size: var(--fs-meta); font-weight: 700; }
.nav-overlay__link:hover,
.nav-overlay__link:focus-visible { color: var(--ice); }
.nav-overlay :focus-visible { outline-color: var(--ice); }

@media (min-width: 56.25em) { .nav-overlay { display: none !important; } }

/* ===========================================================================
   7. MOBILE BOTTOM BAR, below 768px only.
   <div class="bottom-bar" id="bottom-bar"> … </div>
   JS adds .is-visible once the hero has scrolled out of view.
   =========================================================================== */
.bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-bar);
  display: grid;
  grid-template-columns: 1fr 1px 1fr;
  min-height: var(--bar-h);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--navy);
  border-top: var(--hair) solid var(--rule-dark-strong);
  transform: translateY(110%);
  transition: transform var(--dur) var(--ease);
}
.bottom-bar.is-visible { transform: translateY(0); }
.bottom-bar__divider { background: var(--rule-dark-strong); }

.bottom-bar__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: var(--sp-3);
  color: var(--on-navy);
  font-size: var(--fs-small);
  font-weight: 600;
  text-decoration: none;
}
.bottom-bar__btn--primary { color: var(--ice); }
.bottom-bar :focus-visible { outline-color: var(--ice); outline-offset: -3px; }

@media (min-width: 48em) { .bottom-bar { display: none !important; } }

/* Keep the last band clear of the bar. */
@media (max-width: 47.99em) {
  body { padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom, 0px)); }
}

/* ===========================================================================
   8. HERO, the LCP element. One photograph, one scrim, one strip.
   <section class="hero" id="hero"> …img… <div class="hero__scrim"></div>
     <div class="container hero__inner"> … </div> </section>
   =========================================================================== */
.hero {
  position: relative;
  isolation: isolate;
  display: grid;
  align-items: center;
  min-height: clamp(520px, 78svh, 760px);
  background: var(--navy);
  overflow: hidden;
}

.hero__media,
.hero__scrim { position: absolute; inset: 0; }

.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 62% 50%;   /* keeps the assessment right of the text block */
}

.hero__scrim { background: var(--scrim-stacked); z-index: var(--z-rule); }

.hero__inner {
  position: relative;
  z-index: var(--z-raised);
  padding-block: var(--sp-8);
  color: var(--white);
}

.hero__text { max-width: 40ch; }
.hero__text > * + * { margin-top: var(--sp-4); }
.hero h1 { color: var(--white); }
.hero .eyebrow { color: var(--ice); }
.hero__sub { font-size: var(--fs-lead); color: var(--on-navy); max-width: 44ch; }
.hero :focus-visible { outline-color: var(--ice); }
.hero .tbc {
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--white);
}

@media (min-width: 56.25em) {
  .hero__scrim { background: var(--scrim); }
  .hero__text { max-width: var(--w-hero); }
}

/* --- Availability strip: the quadrant flattened into a row. ---------------
   Full-bleed, flush to the hero's bottom edge. 2x2 at mobile, 4-across from
   768px. Its fourth cell is what becomes the sticky bottom bar on scroll. */
.strip {
  background: var(--navy);
  color: var(--on-navy);
  border-top: var(--hair) solid var(--rule-dark-strong);
  --fg: var(--on-navy);
  --fg-muted: var(--on-navy-muted);
  --eyebrow-c: var(--ice);
  --focus: var(--ice);
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--on-navy);
}

/* Subgrid keeps every label on one row and every value on the next, so a cell
   whose value wraps cannot shunt its neighbours out of alignment. The icon
   spans both rows and centres against them. */
/* The navy BAND stays full-bleed; the content inside it does not.

   The grid used to run the whole viewport. On a 1900px screen that put the
   first cell's icon hard against x=0 while every other section on the page
   started at x=223, so the strip read as belonging to a different site - four
   labels stranded at opposite edges under a hero that was properly contained.
   Capping it to the page width lines its four cells up with the container
   edge above and below, which is the only thing that was wrong with it. */
.strip__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: auto auto;
  max-width: var(--w-page);
  margin-inline: auto;
}

.strip__cell {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  grid-template-columns: auto minmax(0, 1fr);
  align-content: start;
  column-gap: var(--sp-3);
  row-gap: var(--sp-1);
  padding: var(--sp-5) var(--gutter);
  border-bottom: var(--hair) solid var(--rule-dark);
  border-right: var(--hair) solid var(--rule-dark);
  transition: background-color var(--dur-fast) var(--ease);
}

.strip__icon {
  grid-row: 1 / 3;
  grid-column: 1;
  align-self: center;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border-radius: var(--r-full);
  background: var(--tint-ice-14);
  border: var(--hair) solid var(--rule-dark-strong);
  color: var(--ice);
}

.strip__label,
.strip__value { grid-column: 2; }

/* The phone cell is the only interactive one, so it gets a hover surface. */
.strip__cell--action:hover { background: var(--tint-white-08); }
.strip__link { color: inherit; text-decoration: none; }
.strip__link:hover { color: var(--ice); }
.strip__cell:nth-child(2n) { border-right: 0; }
.strip__cell:nth-last-child(-n+2) { border-bottom: 0; }

.strip__label {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ice);
  /* Never orphan a letter. A global overflow-wrap: break-word applies here,
     and these labels are single uppercase words in a narrow column, so the
     moment one is a few pixels too wide it does not wrap - it amputates.
     APPOINTMENTS was rendering as APPOINTMENT over a lone S on the first
     screen of every phone. */
  overflow-wrap: normal;
}

/* And make sure it fits, rather than relying on it not overflowing.
   APPOINTMENTS wanted 107px in a 101px column: twelve characters of 0.14em
   tracking is 18px of pure air. 0.09em gives back the 6px it was short by and
   is still unmistakably an eyebrow. Only at the width where the column is
   tight; the desktop tracking is untouched. */
@media (max-width: 47.99em) {
  .strip__label {
    letter-spacing: 0.09em;
    /* letter-spacing adds its gap AFTER every character, including the last,
       so a tracked word is always one gap wider than it looks. At 0.09em that
       put APPOINTMENTS at exactly 101px in a 101px column - fitting on paper
       and wrapping in practice. The negative end margin cancels the trailing
       gap, which is what it is for. */
    margin-inline-end: -0.09em;
  }
}

.strip__value {
  font-family: var(--font-display);
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  font-weight: 700;
  line-height: 1.2;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--white);
}
.strip__value a { color: inherit; text-decoration: none; }
.strip__value a:hover { color: var(--ice); }

@media (min-width: 48em) {
  .strip__grid { grid-template-columns: repeat(4, 1fr); }
  .strip__cell { border-bottom: 0; border-right: var(--hair) solid var(--rule-dark); }
  .strip__cell:nth-child(2n) { border-right: var(--hair) solid var(--rule-dark); }
  .strip__cell:last-child { border-right: 0; }
  .strip__cell { padding-inline: clamp(1rem, 0.4rem + 2vw, 2.5rem); }
}

/* ===========================================================================
   9. FOOTER
   =========================================================================== */
.site-footer {
  background: var(--navy);
  color: var(--on-navy);
  padding-block: var(--band-dark) var(--sp-7);
  --fg: var(--on-navy);
  --fg-muted: var(--on-navy-muted);
  --accent: var(--ice);
  --eyebrow-c: var(--ice);
  --link: var(--on-navy);
  --link-hover: var(--ice);
  --focus: var(--ice);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --tbc-bg: var(--tint-ice-14);
  --tbc-line: var(--ice);
  --tbc-fg: var(--on-navy);
}

.site-footer__mark { display: grid; justify-items: center; gap: var(--sp-4); margin-bottom: var(--stack); }

.site-footer__cols {
  display: grid;
  gap: var(--stack) var(--grid-gap);
  grid-template-columns: 1fr;
}
@media (min-width: 30em) { .site-footer__cols { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64em) { .site-footer__cols { grid-template-columns: repeat(4, 1fr); } }

.site-footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-1); }
.site-footer__list a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-size: var(--fs-small);
  text-decoration: none;
}
.site-footer__list a:hover { text-decoration: underline; }

.site-footer__base {
  margin-top: var(--stack);
  padding-top: var(--sp-5);
  border-top: var(--hair) solid var(--rule-dark);
  display: grid;
  gap: var(--sp-3);
  font-size: var(--fs-meta);
  color: var(--on-navy-muted);
}

/* ===========================================================================
   10. GALLERY GRID (teaser + full page)
   =========================================================================== */
.gallery-grid {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 48em) { .gallery-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 80em) { .gallery-grid--full { grid-template-columns: repeat(4, 1fr); } }

.gallery-item { margin: 0; }
.gallery-item button,
.gallery-item .gallery-item__trigger {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: var(--r);
  overflow: hidden;
  cursor: pointer;
  background: var(--navy);
}
.gallery-item img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }

/* ===========================================================================
   11. NO-JS / JS GATE
   The document must ship with class="no-js" on <html>; the JS replaces it
   with class="js" as its first statement. Anything that could hide content
   is gated behind html.js so a script failure never blanks the page.
   =========================================================================== */
html.no-js .nav-toggle { display: none; }
html.no-js .nav { display: block; }
html.no-js .bottom-bar { display: none !important; }
html.no-js .reveal { opacity: 1; transform: none; }

/* ======================================================================
   10-header-hero.css
   ====================================================================== */

/* ============================================================================
   10-header-hero.css, bespoke rules for 01-header.html, 02-hero.html and
   90-mobile-bar.html.

   Almost everything these partials need already exists: .site-header.nav.nav-toggle.nav-overlay.hero.hero__axis.strip and .bottom-bar are
   all defined in 03-layout.css, and .btn / .icon / .tbc in 02-components.css
   and 01-base.css. Nothing shared is restyled here. This file only adds the
   handful of elements those files do not describe.

   No literal colours. Mobile-first. Every media query is min-width.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   HEADER, brand name block
   Below 480px the badge alone carries the brand. At 375px the container is
   335px wide and the Book Now button plus the hamburger already claim 155px
   of it; the wordmark and its descriptor cannot fit on the same 44px row
   without the header overflowing. The brand link keeps its accessible name
   from its own aria-label, so nothing is lost to assistive technology.
   --------------------------------------------------------------------------- */
.s-header__name { display: none; }

@media (min-width: 30em) {          /* 480px */
  .s-header__name { display: block; }
}

/* ---------------------------------------------------------------------------
   HEADER, no-JS nav fallback
   With JavaScript off, 03-layout.css shows the horizontal nav at every width
   and hides the hamburger, because the overlay cannot open. Seven links do
   not fit a 375px header row, so let that one fallback scroll inside itself
   rather than pushing the page sideways.
   --------------------------------------------------------------------------- */
html.no-js .s-header__nav {
  min-width: 0;
  overflow-x: auto;
}

/* ---------------------------------------------------------------------------
   OVERLAY MENU, head and foot rows
   .nav-overlay is grid-template-rows: auto 1fr auto. The middle row is the
   link list, already styled. These are the two outer rows.
   --------------------------------------------------------------------------- */
.s-header__overlay-head {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  min-height: var(--tap);
  margin-bottom: var(--sp-5);
}

.s-header__overlay-foot {
  display: grid;
  gap: var(--sp-4);
  margin-top: var(--sp-5);
  padding-top: var(--sp-5);
  border-top: var(--hair) solid var(--rule-dark);
}

.s-header__overlay-contact {
  display: grid;
  gap: var(--sp-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 44px targets. Colour comes from .on-dark, which re-points --link to --ice
   (6.69:1 on navy). Never --steel here: that would be 1.8:1 and unreadable. */
.s-header__overlay-contact a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-size: var(--fs-small);
  font-weight: 500;
  text-decoration: none;
}

.s-header__overlay-contact a:hover,
.s-header__overlay-contact a:focus-visible { text-decoration: underline; }

/* ---------------------------------------------------------------------------
   HERO, availability strip
   The phone number in the fourth cell is a control, not running copy, so it
   has to clear the 44px minimum on its own rather than relying on the cell.
   --------------------------------------------------------------------------- */
.s-hero__tel {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

/* ======================================================================
   11-about.css
   ====================================================================== */

/* ============================================================================
   11-about.css: section-scoped rules for the two sections owned by A2.
     01  #about           About Me                  (band--white, tight)
     02  #qualifications  Qualifications & Training (band--bone,  tight)

   Only .s-about__* and .s-qualifications__* selectors live here. No shared
   component is restyled, no literal colour is written, every value is a token.
   Mobile-first; the shared breakpoints are the only ones used.

   NO MOTION IS AUTHORED HERE. Every hover, lift, zoom and reveal comes from
   18-motion.css, so the single prefers-reduced-motion block in that file stays
   the only place motion is switched off.
   ========================================================================= */

/* ===========================================================================
   01 · ABOUT ME

   The height problem this solves: a full-width circular portrait is as tall as
   the container is wide, which cost roughly 340px on a 375px phone for one
   photograph. Below 900px the figure is therefore a byline row: a 180px disc
   (the floor the contract allows for a circular crop) with the name ruled off
   beside it. From 900px the .split goes two-column and the figure returns to
   the stacked portrait-over-caption arrangement, capped so it cannot swallow
   half the band.
   =========================================================================== */
.s-about__figure {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}

.s-about__portrait {
  flex: 0 0 auto;
  width: 180px;
}

.s-about__caption {
  min-width: 0;
  padding-inline-start: var(--sp-4);
  border-inline-start: var(--hair) solid var(--rule);
  font-size: var(--fs-meta);
  line-height: 1.45;
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
}

.s-about__name,
.s-about__role {
  display: block;
}

.s-about__name {
  font-weight: 600;
  color: var(--fg);
}

/* .btn-row carries its own top margin, so the body needs no gap of its own. */
.s-about__body {
  min-width: 0;
}

@media (min-width: 56.25em) {
  /* Two-column split: back to the stacked plate reading, but capped well under
     the 380px ceiling the contract sets for circular crops. */
  .s-about__figure {
    display: block;
    max-width: 300px;
  }

  .s-about__portrait { width: 100%; }

  .s-about__caption {
    margin-top: var(--sp-4);
    padding-inline-start: 0;
    padding-top: var(--sp-2);
    border-inline-start: 0;
    border-top: var(--hair) solid var(--rule);
  }
}

/* ===========================================================================
   02 · QUALIFICATIONS & TRAINING
   Four confirmed credentials in the quadrant motif: icon, title, one line.
   A real 2x2 from 375px comes from .quad--hold in 02-components.css; the only
   job here is to keep each cell to about four lines so the 2x2 does not become
   two tall stacked columns of wrapped text.
   =========================================================================== */
.s-qualifications__item {
  display: grid;
  align-content: start;
  gap: var(--sp-2);
}

@media (min-width: 40em) {
  .s-qualifications__item { gap: var(--sp-3); }
}

/* Stepped down from the display scale so a credential name fits a half-column
   at 375px in two lines. The heading level itself is unchanged. */
.s-qualifications__title {
  font-size: var(--fs-h4);
  line-height: var(--lh-snug);
  text-wrap: balance;
}

/* The disclosure host. 18-motion.css owns the summary layout and the marker
   rotation; padding and the rule that separates it from the quad belong here. */
.s-qualifications__notes {
  margin-top: var(--sp-4);
  border-top: var(--hair) solid var(--rule);
}

.s-qualifications__summary {
  padding-block: var(--sp-1);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--fg);
}

.s-qualifications__panel {
  display: grid;
  gap: var(--sp-2);
  padding-bottom: var(--sp-3);
}

/* ======================================================================
   12-services.css
   ====================================================================== */

/* ============================================================================
   12-services.css: section-scoped rules for TWO sections only:
     03  #treatments  (.band--white,  .container)
     04  #pricing     (.band--bone,   .container--narrow)

   Only .s-treatments__* and .s-pricing__* selectors live here. No shared
   component is restyled, no literal colour is written, every media query is
   min-width and every value comes from 00-tokens.css.

   No motion is authored here. Lift, zoom, stagger, press and the disclosure
   fade all come from 18-motion.css, which carries the single
   prefers-reduced-motion escape for the whole build.
   ========================================================================= */

/* ===========================================================================
   TREATMENTS. Six photo cards, then six compact rows, then one disclosure.

   The grid is 2-up from 375px and 3-up from 768px. Twelve full photo cards
   stacked one per row is what made this the tallest section on the phone.
   Card padding is tightened by re-pointing --cell (a documented token) on the
   grid rather than restyling .card__body from a section file.
   =========================================================================== */
.s-treatments__grid {
  --cell: var(--sp-4);
  grid-template-columns: repeat(2, 1fr);
  /* Inherited, so only .card__text picks it up: at a 157px card width, 17px
     body copy runs to six lines and the photograph stops carrying the card. */
  font-size: var(--fs-small);
}

@media (min-width: 48em) {          /* 768px */
  .s-treatments__grid {
    --cell: var(--sp-5);
    grid-template-columns: repeat(3, 1fr);
    font-size: var(--fs-body);
  }
}

/* The six treatments with no photograph of their own. A ruled list, one line
   each: the information is identical, the height is a fifth of a card grid. */
.s-treatments__also {
  margin-top: var(--stack);
}

.s-treatments__subhead {
  font-size: var(--fs-h4);
  font-weight: 600;
  margin-bottom: var(--sp-4);
}

.s-treatments__list {
  display: grid;
  gap: 0;
  margin: 0;
  border-top: var(--hair) solid var(--rule);
}

.s-treatments__row {
  padding: var(--sp-3) 0;
  border-bottom: var(--hair) solid var(--rule);
}

@media (min-width: 30em) {          /* 480px */
  .s-treatments__list {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--grid-gap);
  }
}

@media (min-width: 64em) {          /* 1024px */
  .s-treatments__list { grid-template-columns: repeat(3, 1fr); }
}

.s-treatments__name {
  font-weight: 600;
  color: var(--fg);
}

.s-treatments__desc {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-small);
  line-height: var(--lh-snug);
  color: var(--fg-muted);
}

/* Kinesiology taping is an add-on, not a thirteenth treatment, so it is
   collapsed by default. The summary's own layout comes from 18-motion.css;
   only the frame around it is set here. */
.s-treatments__addon {
  margin-top: var(--stack);
  padding: 0 var(--sp-4);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
}

.s-treatments__tape {
  padding-bottom: var(--sp-5);
}

.s-treatments__note {
  align-self: center;
  max-width: var(--w-prose);
}

/* The Treatments/Pricing band join, one of the five permitted .rule--node
   placements. The rule itself is a layout helper; this only spaces it. */
.s-treatments__join {
  margin-top: var(--stack);
}

/* ===========================================================================
   PRICING. The real price list. Time-based, never per treatment.
   All three tables share one grid: two columns at 375px, three from 640px.
   Stacked one under another they were half the section's height.
   =========================================================================== */
.s-pricing__stack {
  display: grid;
  gap: var(--sp-4);
}

/* Two columns at 375px: treatment times on the left, blocks and plans stacked
   on the right. Three tables one under another was half the section's height. */
.s-pricing__tables {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-5) var(--grid-gap);
  align-items: start;
}

.s-pricing__group {
  display: grid;
  gap: var(--sp-5);
}

@media (min-width: 40em) {          /* 640px */
  .s-pricing__tables { grid-template-columns: repeat(3, 1fr); }
  /* The group stops being a box and hands its two tables straight to the
     three-column grid above. A plain <div> carries no semantics to lose. */
  .s-pricing__group { display: contents; }
}

/* Event and pitch-side cover has no figure to publish, so it is priced on
   enquiry. Set as a definition pair ruled to match the table rows above it
   rather than faked as a one-row table. */
.s-pricing__enquiry {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) 0;
  border-top: var(--hair) solid var(--rule);
  border-bottom: var(--hair) solid var(--rule);
}

.s-pricing__term {
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--fg);
}

.s-pricing__value {
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
  text-align: right;
}

/* ===========================================================================
   PRICING, rebuilt as cards.
   Three stacked tables read like an invoice. The price is what people scan
   for, so it leads: four time cards across the top, then two compact panels
   for blocks and plans. Still a <dl>, so term and price stay paired.
   =========================================================================== */
.pcards {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--grid-gap);
  margin: 0 0 var(--stack);
}

@media (min-width: 48em) {
  .pcards { grid-template-columns: repeat(4, 1fr); }
}

.pcard {
  position: relative;
  display: grid;
  align-content: start;
  gap: var(--sp-1);
  padding: var(--sp-5) var(--sp-4);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  text-align: center;
  transition: transform var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}

.pcard:hover {
  transform: translateY(-3px);
  border-color: var(--steel);
  box-shadow: var(--shadow-lift);
}

.pcard__term {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--slate-ink);
}

.pcard__price {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}

/* The hour is the session most people book, so it is marked rather than
   left for the visitor to work out. */
.pcard--featured {
  border-color: var(--steel);
  background: var(--tint-steel-06);
}

.pcard__flag {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  padding: 2px var(--sp-3);
  border-radius: var(--r-pill);
  background: var(--steel);
  color: var(--white);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  white-space: nowrap;
}

.ppanels {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--grid-gap);
  margin-bottom: var(--stack);
}

@media (min-width: 40em) {
  .ppanels { grid-template-columns: repeat(2, 1fr); }
}

.ppanel {
  padding: var(--sp-5);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
}

.ppanel__title {
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--steel);
}

.ppanel__list { margin: 0; }

.ppanel__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-4);
  padding-block: var(--sp-3);
  border-top: var(--hair) solid var(--rule);
}
.ppanel__row:first-child { border-top: 0; padding-top: 0; }
.ppanel__row dt { color: var(--fg); }
.ppanel__row dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-price);
  font-weight: 700;
  font-variant-numeric: tabular-nums lining-nums;
  color: var(--ink);
}

.ppanel__note {
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-meta);
  color: var(--slate-ink);
}

/* ======================================================================
   13-brands.css
   ====================================================================== */

/* ============================================================================
   13-brands.css: section-scoped rules for the two sections in this author's
   scope:

     05  Brands & Clubs                 .s-brands__*   (partials/07-brands.html)
     06  Areas Covered & Availability   .s-areas__*    (partials/08-areas.html)

   Section-scoped classes only. No shared component is restyled from here, no
   literal colour value appears, every length is a token or a ratio, and every
   media query is min-width.

   MOTION: none is declared in this file. Every hover, lift, zoom, pop, nudge
   and reveal comes from 18-motion.css, which carries the single
   prefers-reduced-motion block that switches all of it off. The only
   transition-adjacent rules below are focus offsets, which are state, not
   decoration.
   ========================================================================= */

/* ===========================================================================
   05. BRANDS & CLUBS

   Rebuilt for height. The first pass ran to roughly 4,100px on a 375px phone:
   four portrait plates stacked one per row, and a twelve-row full-width team
   sheet. Both are now grids that hold two columns at 375px, which is where
   most of the saving comes from.
   =========================================================================== */

.s-brands__body {
  display: grid;
  gap: var(--sp-5);
}

@media (min-width: 48em) {          /* 768px */
  .s-brands__body { gap: var(--sp-6); }
}

@media (min-width: 64em) {          /* 1024px */
  .s-brands__body { gap: var(--sp-7); }
}

/* --- Intro: the lead and the one landscape evidence photograph ------------
   Stacked on a phone. From 900px the text sits left at 5fr and the 16:9
   photograph right at 7fr, which lands the photograph at the same height as
   the plate row below it instead of a 640px full-width slab. */
.s-brands__intro {
  display: grid;
  gap: var(--sp-5);
  align-items: center;
}

@media (min-width: 56.25em) {       /* 900px */
  .s-brands__intro {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--stack);
  }
}

/* The stretched hit area on the two photographic card types. The nameplate is
   itself absolutely positioned, so a .card__link inside it would only cover
   the nameplate; this covers the whole card and carries the accessible name.
   Negative focus offset because .card clips its overflow. */
.s-brands__cover {
  position: absolute;
  inset: 0;
  z-index: var(--z-raised);
  border-radius: var(--r-card);
}

.s-brands__cover:focus-visible { outline-offset: -3px; }

/* --- Four portrait plates ------------------------------------------------
   2-up from 375px (the shared .grid stacks to one column, which cost roughly
   900px of phone height), then 4-across at 768px. Four items never go into a
   three-column grid: it leaves an orphan and 2x2 restates the quadrant. */
.s-brands__plates { grid-template-columns: repeat(2, minmax(0, 1fr)); }

@media (min-width: 48em) {          /* 768px */
  .s-brands__plates { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* --- The record ----------------------------------------------------------
   Twelve engagements as a hairline lattice: the crosshair motif at list
   scale, in roughly a third of the height of the full-width team sheet. */
.s-brands__record {
  display: grid;
  gap: var(--sp-4);
}

.s-brands__subhead { font-size: var(--fs-h4); }

.s-brands__sheet {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  border-top: var(--hair) solid var(--rule);
}

.s-brands__tile {
  display: grid;
  gap: var(--sp-1);
  align-content: start;
  min-width: 0;
  /* Tight on a phone on purpose: at 375px a 12px side pad is the difference
     between most names setting on one line and most names setting on two. */
  padding: var(--sp-2);
  border-bottom: var(--hair) solid var(--rule);
}

.s-brands__tile:nth-child(odd) { border-right: var(--hair) solid var(--rule); }

.s-brands__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1) var(--sp-2);
  font-size: var(--fs-eyebrow);
  line-height: 1.2;
}

.s-brands__no {
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent);
}

.s-brands__role {
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.s-brands__name {
  display: block;
  font-family: var(--font-display);
  font-size: clamp(0.9375rem, 0.90rem + 0.20vw, 1.0625rem);
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: var(--ls-h3);
  color: var(--fg);
}

/* Three columns at 768, four at 1024. Twelve divides evenly by 2, 3 and 4, so
   the lattice never leaves a half-ruled cell. The :nth-child(n) reset lifts
   specificity to match the :not() rule that follows it, so source order
   decides; without it the 2-column rule leaks into the wider grids. */
@media (min-width: 48em) {          /* 768px */
  .s-brands__sheet { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .s-brands__tile { padding: var(--sp-3) var(--sp-4); }
  .s-brands__tile:nth-child(n) { border-right: 0; }
  .s-brands__tile:not(:nth-child(3n)) { border-right: var(--hair) solid var(--rule); }
}

@media (min-width: 64em) {          /* 1024px */
  .s-brands__sheet { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .s-brands__tile:nth-child(n) { border-right: 0; }
  .s-brands__tile:not(:nth-child(4n)) { border-right: var(--hair) solid var(--rule); }
}

/* Capacity note and the gallery link. */
.s-brands__recordfoot {
  display: grid;
  gap: var(--sp-2);
}

@media (min-width: 47.5em) {        /* 760px */
  .s-brands__recordfoot {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-5);
  }
}

/* Standalone "more" link. .underline-slide is not used here: its rule sits on
   the bottom edge of the box, and the 44px minimum this control needs would
   push that rule well clear of the text. The arrow still nudges. */
.s-brands__more {
  min-height: var(--tap);
  font-size: var(--fs-small);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
}

/* --- Closing note and the secondary CTA ---------------------------------- */
.s-brands__foot {
  display: grid;
  gap: var(--sp-5);
}

.s-brands__footnote { max-width: 62ch; }

/* Same reason as .s-areas__cta: keeps the button at its own width. */
.s-brands__footcta {
  display: flex;
  flex-wrap: wrap;
}

@media (min-width: 56.25em) {       /* 900px */
  .s-brands__foot {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--sp-7);
  }
}

/* ===========================================================================
   06. AREAS COVERED & AVAILABILITY

   One ruled record and one compact table. Two columns from 640px rather than
   the shared .split's 900px, because both blocks are narrow by nature and
   waiting until 900px wasted the whole tablet range.
   =========================================================================== */

.s-areas__grid {
  display: grid;
  gap: var(--sp-5);
  align-items: start;
}

@media (min-width: 40em) {          /* 640px */
  .s-areas__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--sp-6) var(--stack);
  }
}

.s-areas__col {
  display: grid;
  gap: var(--sp-3);
  align-content: start;
}

@media (min-width: 40em) {          /* 640px */
  .s-areas__col { gap: var(--sp-4); }
}

/* The subhead's rule doubles as the top rule of the record beneath it, so the
   record itself carries no border-top. One hairline, not two. */
.s-areas__subhead {
  padding-bottom: var(--sp-2);
  border-bottom: var(--hair) solid var(--rule-strong);
}

.s-areas__facts { display: grid; }

.s-areas__fact {
  display: grid;
  grid-template-columns: 6rem minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-4);
  align-items: baseline;
  padding-block: var(--sp-3);
  border-bottom: var(--hair) solid var(--rule);
}

/* The town chips need the full width of the column, not the value gutter. */
.s-areas__fact--wide { grid-template-columns: minmax(0, 1fr); }

/* 11px so "Appointments" and "Towns covered" set on one line inside the 6rem
   label column at 375px. Field labels are one of the four things the case
   discipline permits in uppercase. --fg-muted on bone is 4.94:1. */
.s-areas__fact dt {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  letter-spacing: var(--ls-meta);
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--fg-muted);
}

.s-areas__towns {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
}

/* Compact, non-interactive placeholder chip. See the CONTRACT-GAP note in the
   partial: this wants to be a .tag--sm modifier on the shared component. The
   44px minimum does not apply, since nothing here is a control. */
.s-areas__town {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding: var(--sp-1) var(--sp-2);
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r);
  font-size: var(--fs-meta);
  font-weight: 500;
  line-height: 1.3;
  color: var(--fg);
}

/* Keeps the CTA at its natural width inside the .s-areas__col grid: a lone
   inline-flex .btn would be blockified by the grid and stretch edge to edge. */
.s-areas__cta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
}

/* ======================================================================
   14-process.css
   ====================================================================== */

/* ============================================================================
   14-process.css, Conditions Treated (08) and Case Studies (09).

   No transitions, no transforms, no @keyframes live in this file. Every moving
   part in both sections is composed from 18-motion.css (.reveal.pop.stagger.zoomable.pressable.disclose.underline-slide), which loads
   after this file and is switched off wholesale by its single
   prefers-reduced-motion block. Adding motion here would break that guarantee.
   ========================================================================= */

/* ===========================================================================
   08 · CONDITIONS TREATED

   The tag grid is the shared .tagfield / .tag component. .s-conditions__tags is
   a section-scoped DENSITY modifier only: it changes size, never colour and
   never state. Fifteen full-size tags wrapped two-to-a-row at 375px and ran to
   roughly 400px of chips on their own; at this size the field wraps three to a
   row and the section reads as one block rather than a list.

   These tags are labels, not controls, so the 44px target minimum does not
   apply to them. The one interactive thing in the section is the inline link in
   the closing note, which is exempt as inline text.
   =========================================================================== */
.s-conditions__tags { gap: var(--sp-1) var(--sp-2); }

.s-conditions__tags .tag {
  min-height: 28px;
  padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-meta);
  line-height: 1.25;
}

/* Room to breathe once the row is no longer the constraint. */
@media (min-width: 48em) {           /* 768px */
  .s-conditions__tags .tag {
    min-height: 36px;
    padding: var(--sp-2) var(--sp-4);
    font-size: var(--fs-small);
  }
}

/* margin-inline: auto is what actually centres it. 22-sections.css sets
   text-align: center on this note, but a max-width box with no auto margins
   sits hard against the left edge of the container, so the text was being
   centred inside a box that was itself off to one side - which reads as
   nearly-but-not-quite centred, and looks like a mistake rather than a
   choice. Centre the box, then the text. */
.s-conditions__note {
  margin-top: var(--sp-5);
  margin-inline: auto;
  max-width: var(--w-prose);
  font-size: var(--fs-small);
  color: var(--fg-muted);
}

/* ===========================================================================
   09 · CASE STUDIES

   Three .plate figures on a grid: 1-up, 2-up at 640px, 3-up at 1024px. Each is
   a 3:2 photograph over a ruled caption row, and that caption row is the
   disclosure control for the four-field record. One row, one heading, one 44px
   target, instead of a heading, a caption and a stacked four-row list.
   =========================================================================== */
.s-cases__list {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--stack) var(--grid-gap);
}

@media (min-width: 40em) {           /* 640px */
  .s-cases__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 64em) {           /* 1024px */
  .s-cases__list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Column, so the caption row can be pushed to the foot of a stretched cell and
   the three record rows line up across a grid row. */
.s-cases__case {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The crop frame. .ratio-3x2 sets the box.zoomable clips it and drives the
   push-in; the radius moves from the image to the frame so the clip is round.
   Cases 01 and 02 are natively 3:2, so this crops nothing at all. */
.s-cases__frame {
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--navy);
  box-shadow: var(--shadow-rest);
  transition: box-shadow var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

/* These are the photographs a visitor actually leans in at, so they get the
   full card response: lift, deepen, and a steel edge drawn round the frame.
   Focus-within is paired with it so the disclosure control below each frame
   lights its own photograph when tabbed to. */
.s-cases__frame { border: var(--hair) solid transparent; }

.s-cases__case:hover .s-cases__frame,
.s-cases__case:focus-within .s-cases__frame {
  transform: translateY(-4px);
  border-color: var(--steel);
  box-shadow: var(--shadow-lift);
}
.s-cases__frame > img { border-radius: 0; }

/* Caption sits directly under its photograph, NOT pushed to the foot of the
   cell.

   margin-top: auto used to bottom-align it, which meant the caption's TOP was
   set by how many lines its description ran to. The three frames are a fixed
   3:2 and always identical heights, so whenever one description wrapped to an
   extra line - and at several widths one of them does - that card's heading
   sat a full line above the other two and the row looked broken.

   Anchored to the top, the three headings share a baseline at every width,
   because they all start at the same fixed frame bottom. The ragged edge moves
   to the end of the paragraphs, which is where a ragged edge belongs.

   (0,2,0) so it still beats .plate figcaption's own margin. */
.s-cases__case > .s-cases__cap {
  margin-top: 0;
  padding-top: var(--sp-3);
}

/* The record row. .disclose__summary already supplies flex, space-between,
   44px and the pointer; only the type is set here. */
.s-cases__rec > .disclose__summary { padding-block: var(--sp-2); }

.s-cases__h3 {
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--fs-small);
  font-weight: 600;
  line-height: var(--lh-snug);
  letter-spacing: 0;
  color: var(--fg);
}

.s-cases__no {
  flex: none;
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--accent);
}

.s-cases__capt { flex: 1; min-width: 0; }

.s-cases__h3 .disclose__marker { flex: none; }

/* The record itself: 2x2 from 375px, never a stacked list of four. */
.s-cases__fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-4);
  margin: 0;
  padding-block: var(--sp-3) var(--sp-2);
}

.s-cases__field {
  display: grid;
  align-content: start;
  gap: var(--sp-1);
  min-width: 0;
}

.s-cases__field dt {
  font-size: var(--fs-eyebrow);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.s-cases__field dd {
  margin: 0;
  font-size: var(--fs-meta);
  line-height: var(--lh-snug);
  color: var(--fg);
}

.s-cases__note { margin-top: var(--stack); }

/* ======================================================================
   15-aftercare.css
   ====================================================================== */

/* ============================================================================
   15-aftercare.css: section-scoped rules for #aftercare ONLY.
   No literal colours. Mobile-first, min-width queries only.

   The shared components do the heavy lifting: .band--navy re-points the colour
   roles, .quad .quad--hold .quad--4 draws the crosshair and the node, .btn--ice
   is the navy-band button, .smallprint and .tbc adapt on their own. This file
   only handles the rhythm INSIDE a quadrant cell and the block below it.

   SECOND PASS, LENGTH: the cell was a vertical stack of icon, heading, then
   copy, which cost a whole 24px icon line plus a 12px gap in every cell, twice
   over in the 2x2. It is now a two-column grid: the icon sits beside the
   heading on row 1, the sentence runs full width underneath. Same information,
   roughly 70px shorter on a 375px phone.

   No motion lives here. Every transform and transition in this section comes
   from 18-motion.css (.stagger, .reveal--fade, .pressable), so the single
   prefers-reduced-motion block at the bottom of that file switches all of it
   off. Adding a transition here would break that guarantee.

   #faq needs no section CSS of its own (.accordion, .stack and .btn-row cover
   it), so no 15-faq.css is shipped: contract §3 says a section that needs no
   rules of its own does not get a file.
   ========================================================================= */

/* One quadrant cell: icon and heading share row 1, the guidance line spans
   both columns on row 2 so it keeps the full cell measure rather than being
   indented behind the icon (which would cost it an extra line at 375px).
   .quad already supplies the cell padding and the hairlines. */
/* Centred at the client's request, and the icon has to travel with its
   heading.

   The first attempt centred the grid tracks and left the columns as
   "auto 1fr". The 1fr still swallowed all the spare width, so the heading
   centred inside a wide column while the icon stayed pinned to the far left
   of the cell - the two ended up at opposite ends of the row with a gap
   between them.

   Flex instead of grid solves it in one move: the icon and the heading are
   two items on one centred line, so they sit together and the pair centres as
   a unit, and the guidance line takes a full-width basis to drop onto its own
   row underneath. Nothing needs to know the cell's width. */
.s-aftercare__cell {
  display: flex !important;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  align-content: flex-start;
  gap: var(--sp-2);
  text-align: center;
}

/* The icon never shrinks; the heading always may. flex: none on the heading
   stopped it wrapping, so on a 180px cell it ran straight out of the box
   instead of breaking - which is how the icon and its words ended up looking
   unrelated. min-width: 0 is the other half: a flex item will not shrink
   below its longest word without it.

   Where the pair fits on one line they sit side by side; where it does not,
   the heading drops to its own centred line under the icon. Both read as
   deliberate, which is the point. */
.s-aftercare__cell > .icon { flex: 0 0 auto; }
.s-aftercare__cell > h3   { flex: 0 1 auto; min-width: 0; }
.s-aftercare__cell > p    { flex-basis: 100%; margin-inline: auto; }

.s-aftercare__cell {
  /* CONTRACT-GAP: .quad--hold sets 16px cell padding at 375px. In a 2x2 that
     leaves a 135px measure and every guidance line broke to five or six rows.
     12px widens the measure enough to take two rows off the whole block. This
     overrides a component value from a section file, which §5 discourages, so
     it is scoped to this section and handed straight back at 640px, where
     .quad--hold's own --cell padding is the right call again. The clean fix is
     a --quad-cell custom property on .quad in 02-components.css. */
  padding: var(--sp-3);
  display: grid;
  grid-template-columns: auto 1fr;
  /* align-items centres the icon against a heading that has wrapped to two
     lines. align-content pins the rows to the top of the cell, so a card with
     a shorter sentence does not float its heading down the cell and break the
     line the four headings are meant to sit on. */
  align-items: center;
  align-content: start;
  column-gap: var(--sp-2);
  row-gap: var(--sp-2);
}

.s-aftercare__cell > .icon {
  grid-column: 1;
  grid-row: 1;
}

.s-aftercare__cell > h3 {
  grid-column: 2;
  grid-row: 1;
  font-size: var(--fs-h4);
  font-weight: 700;
}

/* Full width, and measure-capped for the two-column state (640 to 1023px),
   where a cell is wide enough for the line to run long. */
.s-aftercare__cell > p {
  grid-column: 1 / -1;
  grid-row: 2;
  max-width: 34ch;
}

/* The aftercare guide button sits below the lattice, on the same left axis.
   From 640px the note pulls up alongside the button instead of sitting under
   it, which removes another line of height on tablet and desktop. */
.s-aftercare__pdf {
  margin-top: var(--stack);
}

.s-aftercare__note {
  margin-top: var(--sp-3);
}

@media (min-width: 40em) {          /* 640px */
  /* Hand the cell padding back to the component. */
  .s-aftercare__cell { padding: var(--cell); }

  .s-aftercare__pdf {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-5);
  }
  .s-aftercare__note {
    margin-top: 0;
    max-width: 40ch;
  }
}

/* ======================================================================
   16-contact.css
   ====================================================================== */

/* ============================================================================
   16-contact.css

   One file, three owned blocks (this author owns the three partials that use
   them, and the build has no 14-reviews.css or 17-footer.css slot):

     .s-reviews__*     section 13, #reviews
     .s-contact__*     section 14, #contact
     .site-footer__*   density only. The base footer rules stay in 03-layout.css
                       and are not duplicated here.

   No literal colours, tokens only. Mobile-first, every query is min-width.
   No transition, no transform, no @keyframes: motion lives in 18-motion.css.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   REVIEWS
   Three placeholder testimonials. The cards deliberately do NOT take .hoverable:
   nothing in them is clickable, and lifting a card that cannot be pressed is the
   dead decoration the brief is trying to remove. The card's own hover rule (a
   steel top rule plus the shadow) is enough of a response.
   --------------------------------------------------------------------------- */

/* --cell is the documented card-padding property, so the body tightens without
   .card being restyled from a section file. 40px of padding around one line of
   placeholder text was most of this section's height.
   Two columns from 375px: right now each card holds one marked placeholder and a
   name, which does not need the full width of a phone. WHEN THE REAL QUOTES LAND,
   delete the grid-template-columns line below so a full sentence is not read in a
   150px column; the 1024px rule and .grid--3 then do the rest. */
.s-reviews__grid {
  --cell: var(--sp-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* .grid--3 goes 3-up at 1024px in 03-layout.css, which loads first. The base
   rule above would otherwise win at every width, so the 3-up is re-asserted. */
@media (min-width: 64em) {
  .s-reviews__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Compact .tbc-block. Same dashed panel, a third of the padding. */
.s-reviews__quote {
  padding: var(--sp-3) var(--sp-4);
  line-height: var(--lh-snug);
}

.s-reviews__note { margin-top: var(--sp-4); }

/* ---------------------------------------------------------------------------
   CONTACT
   The two halves of the .split. Tighter than .stack (24px) between a heading
   and the block it introduces, which is all either column contains.
   --------------------------------------------------------------------------- */
/* Both halves are cards now, not bare regions, and they stretch to the same
   height via .split--stretch on the parent. They used to end wherever their
   content happened to stop, which on a wide screen left the details column
   finishing well above the form and reading as unfinished.

   Flex rather than grid, so the details list inside can take flex: 1 and share
   the surplus height out across its rows instead of leaving it dead at the
   foot of the card. */
.s-contact__col {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-width: 0;
  padding: var(--sp-5);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-rest);
}

@media (min-width: 30em) { .s-contact__col { padding: var(--cell); } }

/* ---------------------------------------------------------------------------
   CONTACT: DETAILS LIST
   Rows of an assessment chart: a 24px icon gutter, a hairline under every row,
   a field label above the value. It rhymes with the .field__label stack in the
   form opposite. Two columns from 375px, with the long values (email, service
   area) spanning both so nothing has to break mid-word.
   --------------------------------------------------------------------------- */
.s-contact__details {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--sp-5);
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: var(--hair) solid var(--rule);
}

.s-contact__item {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
  padding-block: var(--sp-3);
  border-bottom: var(--hair) solid var(--rule);
}

.s-contact__item--wide { grid-column: 1 / -1; }

.s-contact__label {
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

.s-contact__value {
  margin-top: var(--sp-1);
  font-size: var(--fs-body);
  font-weight: 600;
  line-height: var(--lh-snug);
  overflow-wrap: anywhere;
}

/* Email and phone are the primary action of this column, so they carry a full
   44px target rather than sitting as bare inline text. */
.s-contact__value a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

.s-contact__aside {
  margin-top: var(--sp-1);
  font-size: var(--fs-meta);
  line-height: 1.45;
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
  max-width: 46ch;
}

/* ---------------------------------------------------------------------------
   CONTACT: "BOOKING AND SOCIAL" DISCLOSURE
   Secondary detail collapsed rather than deleted. 18-motion.css owns the
   summary's flex layout and marker rotation; padding and borders are left to
   the section, which is what this is.
   --------------------------------------------------------------------------- */
.s-contact__more { border-bottom: var(--hair) solid var(--rule); }

.s-contact__more .disclose__summary {
  padding-block: var(--sp-3);
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* The panel is plain flow: .s-contact__value and .s-contact__aside carry their
   own 4px top margin, and the second label picks up the base p + p 16px, which
   is exactly the rhythm wanted. Nothing else to set. */
.s-contact__more .disclose__panel { padding-bottom: var(--sp-4); }

/* ---------------------------------------------------------------------------
   CONTACT: FORM
   The single longest block on the page. Nothing is removed: the field count,
   the labels, the error paragraphs and the honeypot are all still here. Only
   the spacing and the textarea are tightened.
   --------------------------------------------------------------------------- */

/* .form ships a --sp-5 gap. Set on the element itself, not as a descendant
   override of the component, so the shared rule is untouched everywhere else. */
.s-contact__form { gap: var(--sp-4); }

/* Name and Phone share a row from 375px. "(optional)" drops the uppercase
   tracking so the label still fits on one line in a half-width column at 375px;
   without that it wraps and the two inputs stop lining up, which reads as a bug. */
/* align-items: start is the whole fix for the jump. Both are grid: the row
   stretched its two cells to match, and .field distributed that extra height
   across its own rows - so the moment Name showed an error, Phone's input grew
   taller and slid down to sit level with nothing. The fields were never
   misaligned; the empty one was being inflated by its neighbour's error. */
.s-contact__row {
  display: grid;
  gap: var(--sp-4);
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
}

.s-contact__optional {
  font-weight: 400;
  letter-spacing: normal;
  text-transform: lowercase;
}

/* CONTRACT-GAP: .field__textarea hard-sets min-height: 8rem and there is no
   custom property for it, so rows="4" alone could not shrink the box. Applied
   as a second class ON the textarea rather than as a descendant override. */
.s-contact__textarea { min-height: 6rem; }

/* ---------------------------------------------------------------------------
   FOOTER DENSITY
   Two columns from 375px instead of one, four from 1024px as before, and the
   badge laid on its side. The 192px badge stacked above centred text was ~250px
   of the footer on a phone on its own.
   --------------------------------------------------------------------------- */
.site-footer__mark {
  grid-auto-flow: column;
  justify-content: center;
  align-items: center;
  gap: var(--sp-4);
  margin-bottom: var(--sp-7);
}

.site-footer__mark img { width: 56px; }

/* minmax(0, 1fr), not 1fr: the email address is one unbreakable 27-character
   token, and a plain 1fr lets it push its own column to 210px and squeeze the
   neighbour to 109px. Paired with the wrap below, the columns stay equal. */
.site-footer__cols {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-6) var(--grid-gap);
}

@media (min-width: 64em) {
  .site-footer__cols { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.site-footer__list a { overflow-wrap: anywhere; }

/* Tighter than .stack (24px) between a column heading and its list. */
.site-footer__col {
  display: grid;
  gap: var(--sp-2);
  align-content: start;
}

.site-footer__base { margin-top: var(--sp-6); }

/* ======================================================================
   17-gallery.css
   ====================================================================== */

/* ============================================================================
   17-gallery.css
   #gallery on index.html (the four-photograph teaser) and the full filterable
   gallery on gallery.html. Section-scoped rules only. No literal colours.
   Mobile-first. Every media query is min-width.

   SHORTENED PASS. Two changes carry almost all of the height saving:
     1. The teaser tiles are cropped 3:2 (.ratio-3x2 from 18-motion.css)
        instead of 3:4, and there are four of them instead of six.
     2. The gallery page masonry starts at TWO columns at 375px instead of
        one. One column at 375px was roughly 11,000px of scroll on its own.

   Motion is composed from 18-motion.css wherever a class exists there:
   .hoverable, .zoomable, .pressable, .accent-bar, .stagger, .ratio-3x2.
   Two effects had no equivalent and are authored here. Both are flagged
   CONTRACT-GAP below and both are switched off by the local
   prefers-reduced-motion block at the foot of this file.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   1. TILE TRIGGER, both pages.
   Shared .gallery-item button / .gallery-item__trigger (03-layout.css) already
   supply the box: block, full width, 6px radius, navy backplate, overflow
   hidden. The lift and the image push-in now come from .hoverable on the
   <figure> and .zoomable on the trigger, so the hand-rolled box-shadow
   transition and the scaleX accent rule that used to live here are gone.
   All this needs is a positioning context for the teaser flag.
   --------------------------------------------------------------------------- */
.s-gallery__trigger {
  position: relative;
}

/* ---------------------------------------------------------------------------
   2. CAPTION, both pages.
   A ruled caption under every photograph, matching .plate. These carry the
   event names and do the SEO work on the gallery page, so they are never
   decoration, never truncated and never hidden. --fg-muted is --slate-ink on
   light and --on-navy-muted on dark, so this is AA in both places.

   Tightened from the first pass: margin and padding drop a step each, and the
   line height comes in from 1.45 to 1.4. Across 23 tiles that alone is a few
   hundred pixels.
   --------------------------------------------------------------------------- */
.s-gallery__caption {
  margin-top: var(--sp-2);
  padding-top: var(--sp-2);
  border-top: var(--hair) solid var(--rule);
  font-size: var(--fs-meta);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: var(--ls-meta);
  color: var(--fg-muted);
}

/* ---------------------------------------------------------------------------
   3. TEASER GRID AND TILE, index.html only.

   The shared .gallery-grid helper goes 2-up then 3-up at 768px. The teaser
   carries four tiles, one per gallery category, and 3-up would strand the
   fourth on a row of its own. So the teaser owns its grid: 2-up from 375px,
   4-up from 900px, one clean row on a desktop. This is a new section-scoped
   class rather than an override, per section 5 of the contract: .gallery-grid
   is simply not applied here, so nothing shared is restyled. Everything else
   the tile needs still comes from .gallery-item in 03-layout.css.

   Between 768px and 900px the tiles stay 2-up and get generous, which suits a
   tablet better than four cramped 166px frames would.
   --------------------------------------------------------------------------- */
.s-gallery__grid {
  display: grid;
  gap: var(--grid-gap);
  grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 56.25em) {                                      /*  900 */
  .s-gallery__grid { grid-template-columns: repeat(4, 1fr); }
}

/* The crop is forced locally as well as by .ratio-3x2, because 03-layout.css
   sets .gallery-item img { aspect-ratio: 3 / 4 } for the default gallery
   helper and this section deliberately overrides that. */
.s-gallery__card .s-gallery__trigger img {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  object-fit: cover;
  object-position: var(--fp, 50% 50%);
}

/* CONTRACT-GAP (motion): a caption plate that slides up out of the bottom edge
   of the frame on hover and on focus. 18-motion.css has no equivalent, and the
   nearest thing it does have, .card__plate, is a resting nameplate rather than
   a reveal. Authored here, named .s-gallery__flag, and neutralised by the
   reduced-motion block at the foot of this file.

   It is aria-hidden and purely a second read of the category: the real caption
   is the <figcaption> below the photograph, which is always visible, and the
   destination is already in the anchor's aria-label. Nothing is hidden behind
   the hover. Navy plate with --on-navy text measures 12.24:1. */
.s-gallery__flag {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: var(--sp-2) var(--sp-3);
  background: var(--navy);
  color: var(--on-navy);
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: var(--ls-meta);
  transform: translateY(100%);
  transition: transform var(--dur) var(--ease);
}

/* Hover is gated to real pointers, matching 18-motion.css, so the plate never
   sticks after a tap on a phone. Focus is ungated: focus is not a pointer
   capability, and a keyboard user must see exactly what a mouse user sees. */
@media (hover: hover) and (pointer: fine) {
  .s-gallery__card:hover .s-gallery__flag { transform: translateY(0); }
}

.s-gallery__card:focus-within .s-gallery__flag { transform: translateY(0); }

/* ---------------------------------------------------------------------------
   4. FILTER BAR, gallery.html only.
   Buttons are shared .tag.tag--action, 44px, with aria-pressed driven by
   gallery.js. They now also carry .pressable and .accent-bar from
   18-motion.css: the accent rule grows from the left of whichever button is
   pressed, reading [aria-pressed="true"] directly, so there is no second
   source of truth for the active filter.

   This rule is layout only, and it is tighter than the first pass: the
   bottom padding drops from --sp-5 to --sp-3 and the margin from --stack
   to --sp-5.
   --------------------------------------------------------------------------- */
.s-gallery__filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  padding-bottom: var(--sp-3);
  margin-bottom: var(--sp-5);
  border-bottom: var(--hair) solid var(--rule);
}

/* ---------------------------------------------------------------------------
   5. MASONRY, gallery.html only.

   CONTRACT-GAP (unchanged from the first pass): the brief specifies a masonry
   grid. The shared .gallery-grid helper crops every tile to 3:4. Four frames
   in the manifest are landscape (946x598, 946x609, 1200x900, 1200x1151) and a
   3:4 crop discards about half of each, so the full page uses CSS multi-column
   masonry over section-scoped classes and lets each photograph keep its native
   ratio, which is also what the contract asks for in section 8.7. No shared
   selector is redefined: the released aspect-ratio below is set on
   .s-gallery__tile img, this file's own class, which simply lands later in the
   cascade than 03-layout.css.

   No JS layout library and no JS at all: columns reflow natively when the
   filter toggles [hidden] on a tile.

   COLUMN COUNTS ARE THE HEIGHT FIX. Was 1 / 2 / 3 / 4 at 0 / 30em / 48em /
   80em. Now 2 / 3 / 4 at 0 / 48em / 64em. Two columns at 375px roughly halves
   the page.
   --------------------------------------------------------------------------- */
.s-gallery__masonry {
  columns: 2;
  column-gap: var(--grid-gap);
}

@media (min-width: 48em) { .s-gallery__masonry { columns: 3; } }   /*  768 */
@media (min-width: 64em) { .s-gallery__masonry { columns: 4; } }   /* 1024 */

.s-gallery__tile {
  display: block;
  break-inside: avoid;
  page-break-inside: avoid;      /* older engines */
  margin: 0 0 var(--grid-gap);
}

/* Native ratio, set by the width and height attributes on the element itself,
   so the column heights are known before the bytes arrive and nothing
   reflows. */
.s-gallery__tile img {
  width: 100%;
  height: auto;
  aspect-ratio: auto;
  object-fit: fill;
}

/* CONTRACT-GAP (motion): the grid fades as it reflows when a filter changes.
   18-motion.css has no equivalent, and .reveal cannot do this job: a tile
   hidden by the filter never intersects the viewport, so a revealed-once
   observer would leave it at opacity 0 when the filter brought it back.

   No JavaScript is involved and none is asked for. gallery.js toggles the
   [hidden] attribute, which takes a tile to display:none; when the attribute
   comes off, the element is displayed again and its CSS animation restarts
   from the beginning. So the tiles that reappear fade in and the ones that
   were already on screen simply stay put.

   OPACITY ONLY, deliberately. An animation with fill "both" holds its final
   value at a higher cascade origin than a normal declaration, so animating
   transform here would permanently defeat the .hoverable lift on the same
   element. Nothing sets opacity on a tile, so opacity is safe to hold.

   The 25ms step reads --i, which .stagger on #gallery-grid sets for the first
   twelve children and then caps, so the wave tops out at 275ms rather than
   compounding across 23 tiles. */
@keyframes s-gallery-tile-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

html.js .s-gallery__tile {
  animation: s-gallery-tile-in var(--dur) var(--ease) calc(var(--i, 0) * 25ms) both;
}

/* ---------------------------------------------------------------------------
   6. CLOSING CTA, gallery.html only. Shared .callout, pushed clear of the last
   row of photographs.
   --------------------------------------------------------------------------- */
.s-gallery__cta {
  margin-top: var(--stack);
}

/* ---------------------------------------------------------------------------
   7. REDUCED MOTION.
   18-motion.css carries one block that neutralises everything it owns, but it
   cannot know about selectors written here, so the two section-authored
   effects above get their own escape. This block is last in the file, so it
   wins over both.

   The flag settles to its resting position and stays there: it becomes a
   static nameplate rather than disappearing, because taking it away would
   remove information rather than remove motion.
   --------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {

  .s-gallery__flag,
  .s-gallery__card:hover .s-gallery__flag,
  .s-gallery__card:focus-within .s-gallery__flag {
    transform: none;
    transition: none;
  }

  html.js .s-gallery__tile { animation: none; }
}

/* ======================================================================
   18-motion.css
   ====================================================================== */

/* ============================================================================
   18-motion.css: THE SHARED MOTION LAYER.

   Loads after 02-components.css and every section file, so it is allowed to
   win. Nothing in here sets a colour, a font or a page-level dimension: it is
   transform, opacity and timing only.

   Everything animates transform and opacity. Never width, height, top, left,
   margin or padding. Those trigger layout on every frame and are the reason a
   hover effect feels cheap on a mid-range phone.

   THE ONE RULE: every effect in this file is switched off by the single
   prefers-reduced-motion block at the bottom. Do not write a motion effect in
   a section file. Compose these classes instead, or ask for a new one here.

   Timings come from 00-tokens.css:
     --dur-fast 160ms   pressed states, small nudges
     --dur      240ms   the standard card lift
     --dur-slow 520ms   scroll reveal only
     --ease            cubic-bezier(.16,.84,.44,1)
   ========================================================================= */

/* ===========================================================================
   1. SCROLL REVEAL: extensions to the .reveal defined in 02-components.css §14

   02-components.css already does the work: html.js .reveal starts at
   opacity 0 / translateY(14px), and main.js adds .is-revealed when the element
   enters the viewport. This section adds the stagger ergonomics and the
   direction variants, and drops will-change once the element has landed.

   <ul class="stagger">
     <li class="reveal">…</li>   <- --i 0
     <li class="reveal">…</li>   <- --i 1, fires 80ms later
     <li class="reveal">…</li>   <- --i 2
   </ul>
   =========================================================================== */

/* Hand-authored stagger still works: style="--i:3" on any .reveal.
   .stagger does it for you for the first twelve children, which covers every
   grid on the site. From the thirteenth the delay CAPS rather than compounding,
   on purpose: on a forty-item tag list a compounding step means the last one
   arrives three seconds late, which reads as broken, not as choreography. */
.stagger > *               { --i: 0; }
.stagger > *:nth-child(2)  { --i: 1; }
.stagger > *:nth-child(3)  { --i: 2; }
.stagger > *:nth-child(4)  { --i: 3; }
.stagger > *:nth-child(5)  { --i: 4; }
.stagger > *:nth-child(6)  { --i: 5; }
.stagger > *:nth-child(7)  { --i: 6; }
.stagger > *:nth-child(8)  { --i: 7; }
.stagger > *:nth-child(9)  { --i: 8; }
.stagger > *:nth-child(10) { --i: 9; }
.stagger > *:nth-child(11) { --i: 10; }
.stagger > *:nth-child(n + 12) { --i: 11; }

/* Stagger pace. Default --reveal-step is 80ms (00-tokens.css).
   .stagger--tight on long lists of small things (tags, price rows). */
.stagger--tight { --reveal-step: 40ms; }
.stagger--loose { --reveal-step: 110ms; }

/* Direction variants. Apply alongside .reveal. Default is up. */
html.js .reveal--right { transform: translateX(-16px); }
html.js .reveal--left  { transform: translateX(16px); }
html.js .reveal--fade  { transform: none; }
html.js :is(.reveal--right, .reveal--left, .reveal--fade).is-revealed { transform: none; }

/* Compositor hint is only useful before the element lands. Holding
   will-change on forty elements for the life of the page is a memory tax. */
html.js .reveal.is-revealed { will-change: auto; }

/* ===========================================================================
   2. .hoverable: THE STANDARD CARD LIFT

   The one card interaction on the site. Add it to anything card-shaped that is
   also a link target. :hover and :focus-within are always paired, so a keyboard
   user tabbing into the card sees exactly what a mouse user sees.

   <article class="card hoverable">
     <div class="card__media zoomable">
       <img src="assets/img/…" width="600" height="400" alt="…">
     </div>
     <div class="card__body">
       <h3 class="card__title"><a class="card__link" href="#…">Sports massage</a></h3>
     </div>
   </article>

   Tune per section with --lift: <article class="card hoverable" style="--lift:-6px">
   =========================================================================== */
.hoverable {
  --lift: -3px;
  --lift-press: -1px;
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

/* Hover is gated to real pointers. On a touch screen :hover sticks after a tap
   and the card stays lifted until you tap elsewhere, which looks like a bug. */
@media (hover: hover) and (pointer: fine) {
  .hoverable:hover {
    transform: translateY(var(--lift));
    box-shadow: var(--shadow-lift);
  }
}

/* Keyboard parity. Ungated: focus is not a pointer capability. */
.hoverable:focus-within {
  transform: translateY(var(--lift));
  box-shadow: var(--shadow-lift);
}

/* Pressed. The card settles back towards the page so the tap feels physical. */
.hoverable:active {
  transform: translateY(var(--lift-press));
  transition-duration: var(--dur-fast);
}

/* Same pressed state when the thing actually being pressed is the stretched
   .card__link inside, rather than the card box itself. Kept in its own rule:
   an engine that cannot parse :has must not throw away the rule above. */
.hoverable:has(:active) {
  transform: translateY(var(--lift-press));
  transition-duration: var(--dur-fast);
}

/* ===========================================================================
   3. .zoomable: SLOW IMAGE PUSH-IN INSIDE A FIXED FRAME

   Wraps an image so the image scales inside a clipped box. The box never
   changes size, so nothing around it moves. Works on its own, and also
   responds when an ancestor .hoverable is hovered or focused, which is the
   normal case: the whole card is the hit area, not the photo.

   <a class="zoomable" href="gallery.html">
     <img src="assets/img/gallery-netball-600.webp" width="600" height="400" alt="…">
   </a>

   .card__media already sets overflow: hidden and aspect-ratio, so on a card you
   only need to add the class: <div class="card__media zoomable">
   =========================================================================== */
.zoomable {
  --zoom: 1.04;
  position: relative;
  overflow: hidden;
}

/* img only. A <picture> is an inline box and would either ignore the transform
   or, worse, compound with the img inside it. */
.zoomable img {
  transition: transform var(--dur-slow) var(--ease);
  transform-origin: 50% 50%;
}

@media (hover: hover) and (pointer: fine) {
  .zoomable:hover img,
  .hoverable:hover .zoomable img { transform: scale(var(--zoom)); }
}

.zoomable:focus-within img,
.hoverable:focus-within .zoomable img { transform: scale(var(--zoom)); }

/* Portrait plate cards carry a nameplate across the bottom of the photograph.
   A 1.04 push crawls out from behind it, so those get a smaller move.
   Equivalent to writing style="--zoom:1.02" and named so it is greppable. */
.zoomable--subtle { --zoom: 1.02; }

/* ===========================================================================
   4. MEDIA RATIO HELPERS: companion to .zoomable, and the single biggest
   lever on page height.

   Square and 4:3 photographs are what made the phone layout 26,000px tall. Use
   a landscape crop unless the subject genuinely needs the height. Named here
   rather than reinvented in seven section files.

   <div class="card__media ratio-16x9 zoomable"> … </div>
   =========================================================================== */
.ratio-16x9 { aspect-ratio: 16 / 9; }
.ratio-3x2  { aspect-ratio: 3 / 2; }
.ratio-4x3  { aspect-ratio: 4 / 3; }
.ratio-1x1  { aspect-ratio: 1 / 1; }
.ratio-3x4  { aspect-ratio: 3 / 4; }

/* The image still has to fill and crop. .card__media does this already; these
   two rules cover the same crop used outside a card, including the
   <picture><img></picture> form. Focal point is tunable per image with --fp,
   the same custom property .card__media img already reads:
   <img style="--fp:50% 25%" …>  keeps a head in frame on a wide crop. */
:is(.ratio-16x9, .ratio-3x2, .ratio-4x3, .ratio-1x1, .ratio-3x4) > img,
:is(.ratio-16x9, .ratio-3x2, .ratio-4x3, .ratio-1x1, .ratio-3x4) > picture > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--fp, 50% 50%);
}

:is(.ratio-16x9, .ratio-3x2, .ratio-4x3, .ratio-1x1, .ratio-3x4) > picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* ===========================================================================
   5. .underline-slide: LINK UNDERLINE THAT WIPES IN FROM THE LEFT

   For inline text links that carry weight: "See the full gallery",
   "Read the aftercare guide". Not for nav (the header has its own), not for
   buttons, not for every link in a paragraph.

   <a class="underline-slide" href="gallery.html">See all 35 photographs</a>

   The rule is currentColor, so it inherits correctly on navy bands.
   =========================================================================== */
.underline-slide {
  position: relative;
  text-decoration: none;
  /* The rule sits below the text, so the box needs a little room for it. */
  padding-bottom: 0.12em;
}

.underline-slide::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur) var(--ease);
}

.underline-slide:hover::after,
.underline-slide:focus-visible::after { transform: scaleX(1); }

/* Wipes back out to the right rather than snapping. */
.underline-slide:not(:hover):not(:focus-visible)::after { transform-origin: 100% 50%; }

/* Permanently underlined variant: the rule is there, and it thickens on hover.
   Use where the link must read as a link before you touch it. */
.underline-slide--static::after { transform: scaleX(1); transform-origin: 0 50%; }
.underline-slide--static:hover::after,
.underline-slide--static:focus-visible::after { transform: scaleX(1) scaleY(1.5); }

/* ===========================================================================
   6. .pop: SMALL SCALE ENTRANCE FOR CHIPS, TAGS AND PILLS

   A modifier on .reveal, not a standalone class. It needs .reveal because
   main.js only observes .reveal, and because that is what guarantees the
   content is visible if the script never runs.

   <ul class="tagfield stagger stagger--tight">
     <li class="tag reveal pop">Runner's knee</li>
     <li class="tag reveal pop">Achilles tendinopathy</li>
   </ul>

   Written on its own it does nothing at all, which is the safe failure.
   =========================================================================== */
html.js .reveal.pop {
  transform: scale(.94);
  transition-duration: var(--dur);
}

html.js .reveal.pop.is-revealed { transform: none; }

/* ===========================================================================
   7. .pressable: TACTILE RESPONSE FOR BUTTONS AND CONTROLS

   Buttons, filter tags, accordion headers, the form submit. Anything the user
   pushes should move under the push. Depth only, no colour change: the colour
   states already live in 02-components.css.

   <button class="btn btn--primary pressable" type="submit">Send enquiry</button>
   <button class="tag tag--action pressable" aria-pressed="false">Pitch-side</button>
   =========================================================================== */
.pressable {
  transition: transform var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}

@media (hover: hover) and (pointer: fine) {
  .pressable:hover { transform: translateY(-2px); }
}

/* The press returns the control to the page rather than pushing it below.
   It used to go to translateY(1px), so a pressed button sat lower than an
   untouched one and the whole gesture read as a wobble instead of a press.

   The scale stays, and it is not decoration. On a touch screen there is no
   hover to fall back from, so without it a tap would produce no visible
   change at all. */
.pressable:active {
  transform: translateY(0) scale(.97);
  transition-duration: 90ms;
}

/* ===========================================================================
   8. .accent-bar: THE MOVING ACCENT ON AN ACTIVE CONTROL

   A 2px rule that grows from the left edge of a control when it becomes the
   active one. Gallery filters, and any tab-like row. Drives off the real
   accessibility state, [aria-pressed="true"] or [aria-current], so there is no
   second source of truth for which item is on.

   <button class="tag tag--action pressable accent-bar" aria-pressed="true">All</button>

   The host needs position: relative and room at the bottom; .tag has both.
   =========================================================================== */
.accent-bar { position: relative; }

.accent-bar::after {
  content: "";
  position: absolute;
  left: var(--sp-3);
  right: var(--sp-3);
  bottom: 4px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  opacity: .85;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--dur) var(--ease);
}

.accent-bar[aria-pressed="true"]::after,
.accent-bar[aria-current]:not([aria-current="false"])::after,
.accent-bar.is-active::after { transform: scaleX(1); }

/* ===========================================================================
   9. .icon-nudge: ARROW SLIDES ON HOVER

   For "more" links that end in a chevron. The glyph moves, the text does not,
   so the line never reflows.

   <a class="underline-slide icon-nudge" href="gallery.html">
     See the gallery
     <svg class="icon icon-nudge__glyph" width="24" height="24" viewBox="0 0 24 24"
          aria-hidden="true" fill="none" stroke="currentColor">
       <path d="M5 12h13M13 7l5 5-5 5"/>
     </svg>
   </a>
   =========================================================================== */
.icon-nudge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

.icon-nudge__glyph { transition: transform var(--dur) var(--ease); }

@media (hover: hover) and (pointer: fine) {
  .icon-nudge:hover .icon-nudge__glyph { transform: translateX(4px); }
}
.icon-nudge:focus-visible .icon-nudge__glyph { transform: translateX(4px); }

/* Downward variant, for a disclosure or a scroll cue. */
.icon-nudge--down:hover .icon-nudge__glyph,
.icon-nudge--down:focus-visible .icon-nudge__glyph { transform: translateY(3px); }

/* ===========================================================================
   10. <details> DISCLOSURE: SECONDARY CONTENT, COLLAPSED

   Brief instruction 4: collapse secondary content rather than deleting it. The
   marker rotates; the panel itself is not height-animated, because animating
   height on a variable-length panel is either a hard-coded max-height lie or a
   JS measurement, and both are worse than an honest instant open.

   <details class="disclose">
     <summary class="disclose__summary">
       What to expect in a first appointment
       <svg class="icon disclose__marker" width="24" height="24" viewBox="0 0 24 24"
            aria-hidden="true" fill="none" stroke="currentColor">
         <path d="M6 9.5l6 6 6-6"/>
       </svg>
     </summary>
     <div class="disclose__panel"> … </div>
   </details>
   =========================================================================== */
.disclose__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  min-height: var(--tap);
  cursor: pointer;
  list-style: none;
}
.disclose__summary::-webkit-details-marker { display: none; }

.disclose__marker { transition: transform var(--dur) var(--ease); }
.disclose[open] > .disclose__summary .disclose__marker { transform: rotate(180deg); }

/* A short fade on the panel is safe: it is opacity only, and the panel is
   already laid out by the time it runs. */
html.js .disclose[open] > .disclose__panel {
  animation: op-fade-in var(--dur) var(--ease) both;
}

@keyframes op-fade-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ===========================================================================
   11. REDUCED MOTION: ONE BLOCK, EVERYTHING OFF.

   01-base.css already crushes every transition and animation duration to
   0.01ms. That is not enough on its own: a hover rule that sets
   translateY(-3px) still jumps the card three pixels instantly. So the states
   themselves are neutralised here, not just their timing.

   What deliberately survives: the underline on .underline-slide and the rule
   on .accent-bar. Those are state indicators, not decoration. Removing them
   would take information away from the user rather than take motion away.
   They simply appear instead of wiping.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {

  /* Reveal: everything is present from the start. */
  html.js .reveal,
  html.js .reveal--right,
  html.js .reveal--left,
  html.js .reveal--fade,
  html.js .reveal.pop {
    opacity: 1;
    transform: none;
    transition: none;
    will-change: auto;
  }

  /* Card lift: keep the shadow and the border as the hover signal, drop the move. */
  .hoverable,
  .hoverable:hover,
  .hoverable:focus-within,
  .hoverable:active,
  .hoverable:has(:active) { transform: none; }

  /* Image push-in: off. */
  .zoomable img,
  .zoomable:hover img,
  .zoomable:focus-within img,
  .hoverable:hover .zoomable img,
  .hoverable:focus-within .zoomable img {
    transform: none;
    transition: none;
  }

  /* Underline and accent: appear, do not wipe. */
  .underline-slide::after,
  .accent-bar::after { transition: none; }

  /* Press and nudge: off. */
  .pressable,
  .pressable:hover,
  .pressable:active { transform: none; }

  .icon-nudge__glyph,
  .icon-nudge:hover .icon-nudge__glyph,
  .icon-nudge:focus-visible .icon-nudge__glyph,
  .icon-nudge--down:hover .icon-nudge__glyph,
  .icon-nudge--down:focus-visible .icon-nudge__glyph { transform: none; }

  /* Disclosure: the marker still rotates, because it states open or closed.
     The panel fade is removed. */
  .disclose__marker { transition: none; }
  html.js .disclose[open] > .disclose__panel { animation: none; }
}

/* ======================================================================
   19-social-dock.css
   ====================================================================== */

/* ============================================================================
   19-social-dock.css: THE FLOATING WHATSAPP AND INSTAGRAM DOCK.

   Markup: src/partials/91-social-dock.html
   Root:   <div class="dock" id="social-dock" data-dock data-dock-after="#hero">
   State:  main.js adds .is-visible to #social-dock once #hero has scrolled out
           of view, exactly as it already does for #bottom-bar, and sets
           .inert while hidden so the two links are not focusable off-screen.

   Two things this file has to get right and both are easy to get wrong:

   1. IT MUST NOT SIT ON TOP OF THE MOBILE BOTTOM BAR. That bar is full-width,
      --bar-h (64px) tall, fixed to the bottom, and present below 768px only.
      The dock clears it by --bar-h plus the safe-area inset below 48em, and
      drops to a normal offset from 48em up where the bar is display:none.

   2. NO LITERAL COLOURS. 00-tokens.css is the only file in the build allowed
      one, which rules out WhatsApp green and the Instagram gradient. The
      buttons therefore use the brand palette: steel with a white glyph, navy
      with an ice glyph. Both are measured AA (6.80:1 and 6.69:1). The shapes
      still read instantly, which is what actually does the recognising. If the
      client would rather have the official brand colours, that is two new
      tokens in 00-tokens.css and two lines here, not a rebuild.
   ========================================================================= */

/* ===========================================================================
   1. THE DOCK ITSELF
   =========================================================================== */
.dock {
  position: fixed;
  right: var(--gutter);

  /* Below 768px the mobile bottom bar owns the bottom of the screen.
     Clear it, and clear the home indicator underneath it as well. */
  bottom: calc(var(--bar-h) + var(--sp-3) + env(safe-area-inset-bottom, 0px));

  z-index: var(--z-bar);
  display: grid;
  gap: var(--sp-3);
  justify-items: end;

  /* Resting state: off, out of the way, and not clickable. Fading a fixed
     element out is not enough on its own; visibility and pointer-events have
     to go too, or it keeps swallowing taps in the corner of the page. */
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(10px);
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              visibility 0s linear var(--dur);
}

.dock.is-visible {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
  transition: opacity var(--dur) var(--ease),
              transform var(--dur) var(--ease),
              visibility 0s linear 0s;
}

/* From 48em the bottom bar is gone, so the dock takes the corner back. */
@media (min-width: 48em) {
  .dock { bottom: calc(var(--gutter) + env(safe-area-inset-bottom, 0px)); }
}

/* No script, no scroll observer, so nothing would ever reveal it. The dock is
   two plain links with no state of their own, so the honest fallback is simply
   to show it. This is why it differs from .bottom-bar, which 03-layout.css
   hides outright under no-js. */
html.no-js .dock {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
}

/* The mobile menu overlay and the lightbox both lock the body. Neither wants a
   pair of floating buttons punched through them. */
body.is-locked .dock {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

@media print { .dock { display: none !important; } }

/* ===========================================================================
   2. THE BUTTONS
   Real <a> elements. 48px circles, above the 44px minimum. Each carries its
   own aria-label, because the visible label is decorative and hidden from
   assistive technology.
   =========================================================================== */
.dock__btn {
  position: relative;
  display: block;
  width: 48px;
  height: 48px;
  border-radius: var(--r-full);
  text-decoration: none;

  /* Staggered entrance, --i set per button in the markup. */
  opacity: 0;
  transform: translateY(8px) scale(.9);
  transition: opacity var(--dur) var(--ease) calc(var(--i, 0) * 70ms),
              transform var(--dur) var(--ease) calc(var(--i, 0) * 70ms);
}

.dock.is-visible .dock__btn {
  opacity: 1;
  transform: none;
}

/* Round the focus ring. 01-base.css gives every focusable a 3px steel outline
   at --r-sm, which on a circle reads as a rounded square around it. */
.dock__btn:focus-visible {
  border-radius: var(--r-full);
  outline-offset: 4px;
}

/* ===========================================================================
   3. THE DISC: the coloured circle and the glyph inside it.
   Kept as a separate element from the anchor so the hover scale and the
   entrance transform never fight over the same transform property, and so
   scaling the disc does not scale the text label beside it.
   =========================================================================== */
.dock__disc {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  border-radius: var(--r-full);
  box-shadow: var(--shadow-lift);
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

/* Both discs carry a white ring. The dock floats over whatever section happens
   to be behind it, and the navy-on-navy version of the Instagram button
   disappeared completely against the footer band.

   The ring is what does the separating, so the shadow that used to be declared
   here as well is gone. At .28 and .22 alpha it was roughly three times the
   weight of every other elevation in the build, which is what made the dock
   read as bolted on rather than floating. The base .dock__disc rule above
   supplies --shadow-lift, and that is enough behind a 2px white ring. */
.dock__btn .dock__disc {
  border: 2px solid var(--white);
}

.dock__btn--whatsapp .dock__disc {
  background: var(--steel);
  color: var(--white);
}

.dock__btn--instagram .dock__disc {
  background: var(--white);
  color: var(--navy);
}

@media (hover: hover) and (pointer: fine) {
  .dock__btn:hover .dock__disc { transform: scale(1.08); }
}

.dock__btn:focus-visible .dock__disc { transform: scale(1.08); }

.dock__btn:active .dock__disc {
  transform: scale(.94);
  transition-duration: var(--dur-fast);
}

.dock__icon {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ===========================================================================
   4. THE EXPANDING LABEL

   Pointer devices only. On a touch screen there is no hover, the label would
   either never appear or stick after a tap, and the aria-label already names
   the button. So it is not rendered at all below the pointer:fine gate.

   It is absolutely positioned to the left of the circle and moves on transform
   and opacity, so nothing reflows and the dock never changes size.
   =========================================================================== */
.dock__label { display: none; }

@media (hover: hover) and (pointer: fine) {
  .dock__label {
    display: block;
    position: absolute;
    right: calc(100% + var(--sp-2));
    top: 50%;
    padding: var(--sp-2) var(--sp-4);
    border-radius: var(--r);
    /* The dock floats over every band on the page, and two of them are navy.
       A navy label on a navy band is an invisible label with a shadow under
       it, which is what this was. The ice ring is the same trick the discs
       use: it reads on the light bands as a soft edge and on the dark ones it
       is the only thing separating the label from the background. Ice on navy
       measures 6.69:1, so the outline is legible in its own right. */
    border: 2px solid var(--ice);
    background: var(--navy);
    color: var(--on-navy);
    font-size: var(--fs-meta);
    font-weight: 600;
    letter-spacing: var(--ls-meta);
    line-height: 1.2;
    white-space: nowrap;
    box-shadow: var(--shadow-raised);
    pointer-events: none;

    opacity: 0;
    transform: translate(8px, -50%);
    transition: opacity var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease);
  }

  .dock__btn:hover .dock__label,
  .dock__btn:focus-visible .dock__label {
    opacity: 1;
    transform: translate(0, -50%);
  }
}

/* ===========================================================================
   5. REDUCED MOTION: the dock still appears and disappears, it just does not
   travel to get there. The entrance becomes a plain state change, and the
   hover scale, the press and the label slide are removed.
   =========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .dock,
  .dock.is-visible { transform: none; transition: none; }

  .dock__btn,
  .dock.is-visible .dock__btn { transform: none; transition: none; }

  .dock__disc,
  .dock__btn:hover .dock__disc,
  .dock__btn:focus-visible .dock__disc,
  .dock__btn:active .dock__disc { transform: none; transition: none; }

  .dock__label,
  .dock__btn:hover .dock__label,
  .dock__btn:focus-visible .dock__label { transform: translate(0, -50%); transition: none; }
}

/* ======================================================================
   20-failsafe.css
   ====================================================================== */

/* ============================================================================
   20-failsafe.css - loaded last on purpose.

   The scroll-reveal system hides ~120 elements at opacity 0 behind html.js and
   relies on main.js to reveal them. That is fine when everything works, and a
   blank page when it does not: a 404 on main.js, a syntax error, a blocked
   script, or an IntersectionObserver that never fires would all leave the
   content permanently invisible.

   The inline head script arms a timer. If main.js has not revealed anything a
   few seconds after load, html gets .reveal-failsafe and everything is shown
   outright. Content beats animation, always.

   Sorted last so it wins on source order against 18-motion.css.
   ========================================================================= */

html.reveal-failsafe .reveal,
html.reveal-failsafe .reveal--right,
html.reveal-failsafe .reveal--left,
html.reveal-failsafe .reveal--fade,
html.reveal-failsafe .stagger > * {
  opacity: 1 !important;
  transform: none !important;
  visibility: visible !important;
  clip-path: none !important;
  transition: none !important;
}

/* ======================================================================
   21-brand-wall.css
   ====================================================================== */

/* ============================================================================
   21-brand-wall.css - the "who I've worked alongside" wall, and the Google
   review orbit. Both replace flat grids the client asked to be presented
   better, and both are motion-optional: everything degrades to a readable
   static layout under prefers-reduced-motion.
   ========================================================================= */

/* ===========================================================================
   1. BRAND WALL
   Top row drifts left, bottom row drifts right, both at the same rate and
   neither ever stopping. Each row holds the same <ul> twice, the second
   aria-hidden, so translating one full track width loops with no seam. Sits on --navy, so rules and text use the
   dark-band tokens.
   =========================================================================== */
.bwall {
  --bwall-speed: 46s;
  display: grid;
  gap: var(--sp-3);
  margin-block: var(--sp-5);
  /* Each row is ~3000px wide. mask-image only affects painting, so without
     this the page itself scrolls sideways on desktop. overflow clips at the
     PADDING edge, so the padding is what stops the pills being shaved off
     top and bottom. */
  overflow: hidden;
  padding-block: var(--sp-2);
  /* Out through the container's gutters, so the names run off the edge of the
     screen instead of stopping short of it and revealing that the wall is a
     box. Exactly one gutter each side, so on a phone - where the container is
     the viewport minus its gutters - this lands flush with the screen edge and
     never past it. */
  margin-inline: calc(var(--gutter) * -1);
  /* Fade the ends so names slide out rather than being chopped by the edge. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}

/* gap: 0 is load-bearing, not tidiness. The loop is only invisible if the row
   travels exactly one track width per cycle, and -50% of the row is only that
   distance when NOTHING sits between the two copies. With the 12px gap that
   used to be here, every cycle landed 6px short and snapped back - the visible
   reset. The spacing between the last pill of one copy and the first of the
   next is carried by a trailing padding on the track instead. */
.bwall__row {
  display: flex;
  align-items: center;
  gap: 0;
  width: max-content;
  animation: bwall-drift var(--bwall-speed) linear infinite;
}

/* Top row travels left, bottom row travels right, both forever.

   The bottom row is the SHORTER of the two, so it needs a shorter period to
   cover its track at the same speed: 34s against the top row's 46s puts them
   within 0.1px/s of each other. Matching the rate matters more than matching
   the number - a shorter track on a longer period visibly crawls next to its
   neighbour.

   Nothing pauses either of them. No hover pause, no reduced speed, no end.
   Both play the same keyframes and the row holds its list twice with nothing
   between the copies, so the last frame is pixel-identical to the first: a
   real infinite loop, not a long animation that snaps back. */
.bwall__row--reverse {
  animation-direction: reverse;
  --bwall-speed: 34s;
}

/* The wall does NOT pause on hover: the client wants it always moving. Nothing
   inside a pill is focusable or clickable, so pausing buys no accessibility
   here either. Reduced-motion users get the static field below. */

.bwall__track {
  display: flex;
  gap: var(--sp-3);
  margin: 0;
  /* The gap the row is no longer allowed to have. Keeps the two copies apart
     by exactly one pill gap while leaving the row's total width an exact
     multiple of the track. */
  padding: 0 var(--sp-3) 0 0;
  list-style: none;
}

/* Travels one full track width leftwards and lands on a frame pixel-identical
   to the one it started on, so there is no seam and no restart to see. With
   gap: 0 on the row, -50% IS exactly one track width - which is what the old
   comment here claimed while the 12px gap quietly made it false.

   The lower row plays this same animation in reverse, which sends it right. */
@keyframes bwall-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

.bwall__item {
  flex: 0 0 auto;
  padding: var(--sp-3) var(--sp-5);
  border: var(--hair) solid var(--rule-dark-strong);
  border-radius: var(--r-pill);
  background: var(--tint-white-08);
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.005em;
  color: var(--on-navy);
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.bwall__item:hover {
  background: var(--ice);
  border-color: var(--ice);
  color: var(--navy);
}

/* Only reduced-motion now. The width clause that used to sit here switched
   the marquee off below 768px, so phone visitors - most of them - never saw
   the thing at all and got a static block of fifteen names instead. It is
   masked and clipped, so it cannot scroll the page sideways on a phone any
   more than it can on a desktop. */
@media (prefers-reduced-motion: reduce) {
  .bwall {
    -webkit-mask-image: none;
            mask-image: none;
  }
  .bwall__row {
    width: auto;
    flex-wrap: wrap;
    animation: none;
  }
  .bwall__track { flex-wrap: wrap; }
  .bwall__track[aria-hidden="true"] { display: none; }
  .bwall__item { font-size: var(--fs-small); padding: var(--sp-2) var(--sp-4); }
}

/* ===========================================================================
   2. GOOGLE REVIEW ORBIT
   One card at the front, the others behind it, offset and scaled back.
   JS sets data-pos on each slot: 0 front, 1 and 2 behind. Under reduced
   motion the deck becomes a plain stack and the JS stops auto-advancing.
   =========================================================================== */
.gorbit {
  --gorbit-h: 340px;
  /* How far the two resting cards sit out to the sides. Small on a phone so
     they only peek, wide on desktop so all three read as a set. */
  --gorbit-off: 26%;
  position: relative;
  margin-block: var(--sp-5) var(--sp-6);
  /* The side cards deliberately overhang. Clip here or the page scrolls
     sideways on narrow screens. Padding keeps their shadows off the edge. */
  overflow: hidden;
  padding-inline: var(--sp-2);
}

@media (min-width: 48em)  { .gorbit { --gorbit-off: 42%; } }
@media (min-width: 80em)  { .gorbit { --gorbit-off: 46%; } }

.gorbit__deck {
  position: relative;
  height: var(--gorbit-h);
  margin: 0;
  padding: 0;
  list-style: none;
  perspective: 1200px;
}

.gorbit__slot {
  position: absolute;
  inset: 0;
  margin-inline: auto;
  max-width: 560px;
  transition: transform var(--dur-slow) var(--ease),
              opacity var(--dur-slow) var(--ease);
  will-change: transform, opacity;
}

/* Front. */
.gorbit__slot[data-pos="0"] {
  transform: translate3d(0, 0, 0) scale(1);
  opacity: 1;
  z-index: 3;
}

/* The two resting cards sit out to the right and the left, scaled back and
   dimmed. pointer-events off so a card behind can never swallow a click meant
   for the front one. */
.gorbit__slot[data-pos="1"] {
  transform: translate3d(var(--gorbit-off), 14px, 0) scale(.86);
  opacity: .5;
  z-index: 2;
  pointer-events: none;
}

.gorbit__slot[data-pos="2"] {
  transform: translate3d(calc(var(--gorbit-off) * -1), 14px, 0) scale(.86);
  opacity: .5;
  z-index: 1;
  pointer-events: none;
}

.gorbit__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-4);
  margin-top: var(--sp-4);
}

.gorbit__arrow {
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  border: var(--hair) solid var(--rule-strong);
  border-radius: var(--r);
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.gorbit__arrow:hover,
.gorbit__arrow:focus-visible { background: var(--steel); color: var(--white); }




/* ---------------------------------------------------------------------------
   The card itself. Deliberately close to a real Google review, because these
   ARE real reviews from her Google profile: avatar, name, star row, body, and
   the Google mark identifying where they came from.
   --------------------------------------------------------------------------- */
.greview {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-rest);
}

.greview__head {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--sp-3);
}

.greview__avatar {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--r-full);
  background: var(--steel);
  color: var(--white);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
}

.greview__who { display: grid; }
.greview__name { font-weight: 600; color: var(--ink); }
.greview__meta { font-size: var(--fs-meta); color: var(--slate-ink); }

.greview__stars {
  margin: 0;
  font-size: 1.05rem;
  letter-spacing: .12em;
  /* Was Google's own #F4B400, on the reasoning that it identified the source
     rather than claiming to be brand colour. It measures 1.73:1 on this band.
     The stars are the ONLY thing carrying the rating - the number five appears
     nowhere in the visible text, only in the aria-label - so at that contrast
     the rating is effectively invisible to anyone who needs contrast, and the
     Google mark in the card header already identifies the source. Steel
     measures 6.45:1 on bone. */
  color: var(--steel);
}

.greview__text {
  margin: 0;
  flex: 1;
  color: var(--ink);
}
.greview__text p { margin: 0; }

.greview__foot {
  margin: 0;
  font-size: var(--fs-meta);
  color: var(--slate-ink);
}

@media (min-width: 48em) {
  .gorbit { --gorbit-h: 300px; }
}

@media (prefers-reduced-motion: reduce) {
  .gorbit__deck {
    height: auto;
    display: grid;
    gap: var(--grid-gap);
    perspective: none;
  }
  .gorbit__slot {
    position: static;
    inset: auto;
    transform: none !important;
    opacity: 1 !important;
    pointer-events: auto !important;
    max-width: none;
  }
  .gorbit__controls { display: none; }
}

/* The wall's own heading sits centred above the two moving rows. */
.s-brands__record .s-brands__subhead {
  text-align: center;
  margin-bottom: var(--sp-4);
}

/* ======================================================================
   22-sections.css
   ====================================================================== */

/* ============================================================================
   22-sections.css - presentation upgrades asked for after the first review.

   0. Image zoom turned off site-wide
   1. Treatment picture tiles that enlarge on hover
   2. The coverage map and the town cards that drive it
   3. Conditions grouped by body region
   ========================================================================= */

/* ===========================================================================
   0. NO IMAGE ZOOM.
   18-motion.css scales the photograph inside .zoomable on hover. The client
   wants the CARD to react, not the picture, so the inner scale is switched
   off everywhere and the lift on the card does the work. .zoomable is left in
   the markup because it still supplies the overflow clip and the radius.
   =========================================================================== */
.zoomable img,
.zoomable > img,
.card__media img,
.s-gallery__trigger img {
  transform: none !important;
}

/* ===========================================================================
   1. TREATMENT TILES
   Was a two-column text list. Now picture tiles: hovering or focusing one
   lifts it, scales it and pushes it above its neighbours, so the photograph
   is the thing you notice.
   =========================================================================== */
.ttiles {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--grid-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 48em) { .ttiles { grid-template-columns: repeat(3, 1fr); } }

.ttile__inner {
  height: 100%;
  display: grid;
  align-content: start;
  gap: var(--sp-2);
  padding: var(--sp-3);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  transition: transform var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

/* The lift. z-index on the <li> so the growing tile sits over its neighbours
   rather than being overlapped by the next one in source order. */
.ttile { position: relative; }
.ttile:hover,
.ttile:focus-within { z-index: 2; }

.ttile:hover .ttile__inner,
.ttile:focus-within .ttile__inner {
  transform: translateY(-4px) scale(1.035);
  border-color: var(--steel);
  box-shadow: var(--shadow-lift);
}

/* The :focus-within half of that rule was dead for as long as it existed: the
   tiles held an <img>, an <h3> and a <p> and nothing tabbable, so a keyboard
   user could not reach a treatment card at all, let alone see it respond.

   Each name is now a stretched link to the enquiry form, using the
   .card__link primitive that 02-components.css has defined since the first
   build and that nothing on the site used. Twelve treatments become twelve
   routes to the form, the lift finally answers the keyboard, and the
   accessible name stays on the heading where a screen reader expects it. */
.ttile__name .card__link { color: inherit; text-decoration: none; }

.ttile:hover .ttile__name,
.ttile:focus-within .ttile__name { color: var(--steel); }

/* The stretched pseudo-element is the hit area, so the anchor's own focus ring
   would draw a box round the text only. Suppress it and let the tile's
   focus-within lift be the indicator, with a real outline on the whole tile. */
.ttile__name .card__link:focus-visible { outline: none; }

.ttile:focus-within .ttile__inner {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}

.ttile__media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--r);
  background: var(--bone);
}

.ttile__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No photograph yet. An honest panel rather than a borrowed image: on a
   clinical page the wrong photo misrepresents the treatment. */
.ttile__media--empty {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--sp-2);
  background: var(--tint-steel-06);
  border: var(--hair) dashed var(--rule-strong);
  color: var(--steel);
}
.ttile__media--empty svg { width: 30px; height: 30px; }

.ttile__pending {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--slate-ink);
}

.ttile__name {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-h4);
  line-height: 1.25;
  color: var(--ink);
}

.ttile__desc {
  margin: 0;
  font-size: var(--fs-small);
  color: var(--slate-ink);
}

/* ===========================================================================
   2. COVERAGE MAP
   Hand-drawn SVG rather than an embedded provider map: no API key, no
   third-party request, no cookie banner, and it takes the brand palette.
   Dots are plotted from real coordinates projected flat around Halesowen.
   =========================================================================== */
.cmap {
  margin: var(--sp-5) 0 var(--sp-5);
  padding: var(--sp-4);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
}

.cmap__svg {
  display: block;
  width: 100%;
  max-width: 420px;
  height: auto;
  margin-inline: auto;
  overflow: visible;
}

.cmap__ring { fill: none; stroke: var(--rule); }
.cmap__ring--outer {
  stroke: var(--steel);
  stroke-width: 1.5;
  fill: var(--tint-steel-06);
  stroke-dasharray: 5 5;
}
.cmap__ring--mid,
.cmap__ring--inner { stroke-width: 1; opacity: .6; }

.cmap__cross { stroke: var(--rule); stroke-width: 1; opacity: .5; }

.cmap__dot {
  fill: var(--white);
  stroke: var(--steel);
  stroke-width: 2;
  transition: r var(--dur-fast) var(--ease),
              fill var(--dur-fast) var(--ease);
}

/* Driven from the town cards by main.js, and mirrored on direct hover. */
.cmap__dot:hover,
.cmap__dot.is-active {
  fill: var(--steel);
  r: 8;
}

.cmap__home-halo {
  fill: var(--tint-steel-10);
  stroke: var(--steel);
  stroke-width: 1;
}
.cmap__home-pin { fill: var(--steel); }

.cmap__home-label {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: 700;
  fill: var(--ink);
}

.cmap__caption {
  margin-top: var(--sp-3);
  font-size: var(--fs-meta);
  text-align: center;
  color: var(--slate-ink);
}

/* --- Town cards ----------------------------------------------------------- */
.s-areas__townhead {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--steel);
}

.tcards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-5);
  padding: 0;
  list-style: none;
}

.tcard {
  padding: var(--sp-2) var(--sp-4);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r);
  font-size: var(--fs-small);
  font-weight: 500;
  color: var(--fg);
  cursor: default;
  transition: transform var(--dur-fast) var(--ease),
              background-color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.tcard:hover,
.tcard.is-active {
  transform: translateY(-2px);
  background: var(--steel);
  border-color: var(--steel);
  color: var(--white);
}

/* ===========================================================================
   3. CONDITION GROUPS
   Fifteen chips in one field read as random. Three labelled groups let a
   visitor find their own complaint at a glance. Same fifteen conditions.
   =========================================================================== */
.cgroups {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--grid-gap);
}

@media (min-width: 48em) { .cgroups { grid-template-columns: repeat(3, 1fr); } }

.cgroup {
  padding: var(--sp-5) var(--sp-4);
  background: var(--white);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  border-top: 3px solid var(--steel);
}

.cgroup__title {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-4);
  font-size: var(--fs-h4);
  line-height: 1.2;
  color: var(--ink);
}

.cgroup__icon {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--r-full);
  background: var(--tint-steel-10);
  color: var(--steel);
}
.cgroup__icon svg { width: 20px; height: 20px; }

.cgroup__tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ===========================================================================
   4. CENTRED SECTION HEADINGS
   The original contract left-aligned every section heading. The client prefers
   them centred, so the eyebrow, heading and lead centre as a block and the
   lead gets auto margins so its measure stays readable rather than stretching
   the full container width.

   The hero is deliberately excluded: its text block is positioned over the
   open grass on the left of the photograph, which is why the photo was
   cropped the way it was.

   Running body copy inside cards stays left-aligned. Centred paragraphs are
   harder to read once they run past a couple of lines.
   =========================================================================== */
.band .shead { text-align: center; }

.band .shead__text > .lead {
  max-width: var(--w-measure);
  margin-inline: auto;
}

/* Row-level things that sat hard left now centre under the centred heading. */
.band .btn-row,
.tcards,
.tagfield,
.gorbit__controls { justify-content: center; }

.band .btn-row { display: flex; flex-wrap: wrap; gap: var(--sp-3); }

.s-areas__townhead,
.s-brands__subhead { text-align: center; }

/* Section-level notes and small print read as captions, so they centre too. */
.s-conditions__note,
.cmap__caption,
.s-reviews__note,
.s-cases__smallprint { text-align: center; }

/* The callout is a single short line; centring it stops it looking orphaned. */
.callout { text-align: center; }

/* ===========================================================================
   5. CONTACT DETAILS
   Icon, label, value. Two across from 480px, with the clinic address spanning
   both because it is the longest value.
   =========================================================================== */
/* One per row, not two across. Phone, WhatsApp, email and Instagram are the
   four things this page exists to get used, and two-across made each one half
   a tap target wide on the very screens where someone is most likely to just
   ring her.

   flex: 1 on the rows is what levels the two columns: the card stretches to
   match the form opposite, and the surplus height is shared out across the
   rows rather than pooling as dead space under the last one. */
.cdetails {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
  text-align: left;
}

.cdetail {
  flex: 1;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-3);
  align-items: center;
  padding: var(--sp-3) var(--sp-4);
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-lg);
  transition: border-color var(--dur) var(--ease),
              background-color var(--dur) var(--ease),
              box-shadow var(--dur) var(--ease);
}

/* Only the rows that actually go somewhere respond. The clinic address is an
   <address>, not a link, so giving it a hover would promise a tap that does
   nothing. */
.cdetail:has(a):hover,
.cdetail:has(a):focus-within {
  border-color: var(--steel);
  background: var(--tint-steel-06);
  box-shadow: var(--shadow-rest);
}

.cdetail:has(a):hover .cdetail__icon,
.cdetail:has(a):focus-within .cdetail__icon {
  background: var(--steel);
  color: var(--white);
}

.cdetail__icon {
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  border-radius: var(--r-full);
  background: var(--tint-steel-10);
  color: var(--steel);
  transition: background-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}
.cdetail__icon svg { width: 19px; height: 19px; }

.cdetail__body { display: grid; gap: 2px; min-width: 0; }

.cdetail__label {
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--slate-ink);
}

.cdetail__value {
  font-family: var(--font-display);
  font-size: var(--fs-h4);
  font-weight: 600;
  font-style: normal;
  line-height: 1.25;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: anywhere;
}
.cdetail__value--sm { font-size: var(--fs-small); font-weight: 500; }

a.cdetail__value:hover,
a.cdetail__value:focus-visible { color: var(--steel); text-decoration: underline; }

.cdetail__note {
  font-size: var(--fs-meta);
  color: var(--slate-ink);
}

/* The six headline treatments: one size up from the "also offered" row. */
.ttiles--lead { grid-template-columns: 1fr; margin-bottom: var(--stack); }

@media (min-width: 30em) { .ttiles--lead { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 64em) { .ttiles--lead { grid-template-columns: repeat(3, 1fr); } }

.ttiles--lead .ttile__inner { padding: var(--sp-4); gap: var(--sp-3); }
.ttiles--lead .ttile__name  { font-size: var(--fs-h3); }
.ttiles--lead .ttile__desc  { font-size: var(--fs-body); }

.s-treatments__subhead {
  margin: 0 0 var(--sp-4);
  text-align: center;
  font-size: var(--fs-meta);
  font-weight: 600;
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--steel);
}

/* ===========================================================================
   6. COVERAGE MAP OVERLAY
   The map is a real OSM raster. The ring, the dimming outside it and the
   markers are an SVG stretched over the top in the same pixel space.
   =========================================================================== */
.cmap__stage {
  position: relative;
  max-width: 460px;
  margin-inline: auto;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: var(--hair) solid var(--rule);
}

.cmap__img { display: block; width: 100%; height: auto; }

.cmap__overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Everything beyond the travel limit is knocked back so the ring reads. */
.cmap__outside { fill: rgba(23, 56, 79, .55); }

.cmap__ring {
  fill: none;
  stroke: var(--ice);
  stroke-width: 4;
  stroke-dasharray: 10 8;
}

.cmap__dot {
  fill: var(--white);
  stroke: var(--navy);
  stroke-width: 3;
  transition: r var(--dur-fast) var(--ease), fill var(--dur-fast) var(--ease);
}
.cmap__dot:hover,
.cmap__dot.is-active { fill: var(--ice); r: 11; }

.cmap__home-halo { fill: rgba(143, 201, 224, .45); stroke: var(--white); stroke-width: 2; }
.cmap__home-pin  { fill: var(--steel); stroke: var(--white); stroke-width: 2.5; }

/* The email is the longest value in the contact column and was breaking
   mid-word. Smaller, and allowed to break only where it sensibly can. */
.cdetail__value--sm { font-size: var(--fs-meta); line-height: 1.45; }
.cdetail__value[href^="mailto:"] { overflow-wrap: break-word; word-break: break-word; }

/* ===========================================================================
   7. HEADER GOES TRANSLUCENT ONCE SCROLLED
   =========================================================================== */
.site-header.is-condensed {
  background: rgba(255, 255, 255, .78);
  -webkit-backdrop-filter: saturate(1.6) blur(12px);
          backdrop-filter: saturate(1.6) blur(12px);
  border-bottom-color: rgba(46, 95, 126, .16);
}

/* Backdrop-filter is widely supported now, but where it is not the header
   would be see-through with unreadable text behind it. Fall back to solid. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header.is-condensed { background: var(--white); }
}

/* ===========================================================================
   8. FOOTER
   =========================================================================== */
.site-footer__base { text-align: center; display: grid; gap: var(--sp-3); justify-items: center; }

/* The build credit was inheriting the footer colour at 60% and disappearing.
   The markup stays verbatim; the colour is lifted from here. */
/* The credit ships with inline styles from the supplier's own snippet, which
   is why everything here needs !important to reach it.

   Two of those inline values do not survive contact with this footer:
   opacity .6 made it disappear against the band, and font-size .8em of the
   13px base rendered at 10.4px - small enough to read as a disclaimer rather
   than a credit. Nudged up to 12px, which is a step below the footer's own
   type and still clearly secondary, and the icon grows with it since the SVG
   is sized in em. */
.site-footer__base a[href*="thesitemechanic"] {
  opacity: 1 !important;
  color: var(--ice) !important;
  font-size: 12px !important;
}

.site-footer__base a[href*="thesitemechanic"] svg {
  width: 1.15em;
  height: 1.15em;
}
.site-footer__base a[href*="thesitemechanic"]:hover { color: var(--white) !important; }

/* Bordered chips sitting inside a bordered card is a box inside a box, and it
   read as cluttered. Inside a .cgroup the conditions become a plain ruled
   list: one per row, hairline between, small steel marker. .tag keeps its
   own styling everywhere else on the site. */
.cgroup__tags {
  display: block;
  border-top: var(--hair) solid var(--rule);
}

.cgroup__tags .tag {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  min-height: 0;
  padding: var(--sp-3) 0;
  border: 0;
  border-bottom: var(--hair) solid var(--rule);
  border-radius: 0;
  background: none;
  font-size: var(--fs-small);
  color: var(--fg);
  text-align: left;
}

.cgroup__tags .tag::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--steel);
}

.cgroup__tags .tag:last-child { border-bottom: 0; padding-bottom: 0; }

/* ===========================================================================
   9. ORBIT: faster, with more depth, and ready for a longer review list.
   Only three cards are on stage at once. Everything else parks at
   data-pos="x" so the deck scales to any number of reviews.
   =========================================================================== */
/* html.js gates the invisible default, and that gate is not cosmetic.

   The slots ship with no data-pos; JS writes it. So the off-stage opacity: 0
   below was the state of ALL THREE cards until main.js ran. A 404, a syntax
   error, a blocked script or a corporate proxy and the three real Google
   reviews - the only third-party word on the whole page that Mia is any good -
   were a 340px hole. The site-wide failsafe did not cover it either: that
   rescues .reveal and .stagger children, and .gorbit__slot is neither.

   With the gate, no JS means no hidden default, and the deck falls back to
   three plainly stacked cards. Content beats animation, always - which is
   what 20-failsafe.css says at the top and what this rule was quietly
   ignoring. */
/* :where() and not a plain descendant selector, and this matters.

   The off-stage default has to LOSE to the three [data-pos] rules that put a
   card on stage, and it did, because .gorbit__slot is (0,1,0) against their
   (0,2,0). Writing the gate as `html.js .gorbit__slot` lifts it to (0,2,0),
   which ties - and this file loads after 21-brand-wall.css, so the tie broke
   the wrong way and every review went invisible even with the JS working
   perfectly. :where() contributes nothing to specificity, so the gate is free:
   the rule stays exactly as weak as it was and the positions still win. */
:where(html.js) .gorbit__slot {
  /* Off-stage default. The three on-stage positions override it below. */
  opacity: 0;
  transform: scale(.8);
  pointer-events: none;
}

.gorbit__slot {
  transition: transform 620ms cubic-bezier(.22, 1, .36, 1),
              opacity 420ms var(--ease),
              filter 420ms var(--ease);
}

/* No script: the absolute stacking would pile all three on top of each other,
   so the deck becomes a plain column. Same shape as the reduced-motion
   fallback further down. */
html.no-js .gorbit__deck {
  height: auto;
  display: grid;
  gap: var(--grid-gap);
  perspective: none;
}
html.no-js .gorbit__slot { position: static; inset: auto; max-width: none; }
html.no-js .gorbit__controls { display: none; }

.gorbit__slot[data-pos="x"] {
  opacity: 0;
  transform: scale(.8);
  filter: blur(3px);
}

.gorbit__slot[data-pos="0"] {
  transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
  opacity: 1;
  filter: none;
  z-index: 3;
  pointer-events: auto;
}

.gorbit__slot[data-pos="1"] {
  transform: translate3d(var(--gorbit-off), 16px, 0) scale(.84) rotate(1.6deg);
  opacity: .55;
  filter: blur(1px);
  z-index: 2;
}

.gorbit__slot[data-pos="2"] {
  transform: translate3d(calc(var(--gorbit-off) * -1), 16px, 0) scale(.84) rotate(-1.6deg);
  opacity: .55;
  filter: blur(1px);
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .gorbit__slot,
  .gorbit__slot[data-pos="x"] {
    opacity: 1 !important;
    filter: none !important;
    transform: none !important;
    pointer-events: auto !important;
  }
}

/* ===========================================================================
   10. HEADER: continuous, scroll-driven shrink.
   --hp runs 0 to 1 over the first 160px of scroll (set by main.js). Every
   value below interpolates from it, so the header tracks the scroll exactly
   instead of snapping at a threshold. No transitions here on purpose: a
   transition would lag behind and fight the per-frame updates.

   position:fixed, not sticky. A sticky header that changes height alters the
   flow height at the top of the document, which nudges the scroll position
   while you are scrolling. Taking it out of flow and paying for it with
   padding on the body removes that feedback entirely.
   =========================================================================== */
.site-header {
  --hp: 0;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: calc(var(--header-h) - (var(--header-h) - var(--header-h-cond)) * var(--hp));
  background: rgba(255, 255, 255, calc(1 - 0.18 * var(--hp)));
  border-bottom-color: rgba(46, 95, 126, calc(0.10 + 0.12 * var(--hp)));
  /* Cannot be a plain token: the alpha is interpolated by --hp, the 0-to-1
     scroll progress JS writes on this element. So the geometry is restated by
     hand to match --shadow-raised exactly, and both alphas are scaled by --hp.
     At --hp 1 this resolves to raised, which is what .is-condensed sets, so
     the header settles into the scale rather than approaching a fourth step
     nothing else uses. */
  box-shadow: 0 2px 6px  rgba(16, 20, 24, calc(0.06 * var(--hp))),
              0 10px 26px rgba(16, 20, 24, calc(0.09 * var(--hp)));
  transition: none;
}

/* The fixed header no longer occupies space, so the page pays for it here. */
body { padding-top: var(--header-h); }

/* The logo keeps a constant box, and that is what stops the header moving
   sideways as you scroll.

   It used to interpolate 44px down to 36px along with the header height. The
   header is justify-content: space-between, so those 8px came out of the
   brand's width and every one of them was handed to the nav, which slid 4px
   left and back as you scrolled. The header was supposed to condense; instead
   it also drifted horizontally, which is the part you notice.

   Only the height changes now. 44px sits comfortably inside the 60px condensed
   header, so nothing needs to shrink to fit. */
.site-header__logo {
  width: 44px;
  height: 44px;
  transition: none;
}

/* Blur only once there is something behind it worth blurring. */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .site-header {
    -webkit-backdrop-filter: saturate(calc(1 + 0.6 * var(--hp))) blur(calc(14px * var(--hp)));
            backdrop-filter: saturate(calc(1 + 0.6 * var(--hp))) blur(calc(14px * var(--hp)));
  }
}

/* Overrides the earlier flip-based rule so nothing snaps at the class change. */
.site-header.is-condensed {
  background: rgba(255, 255, 255, calc(1 - 0.18 * var(--hp)));
}

/* No JS: --hp stays 0, so the header simply renders at full height. */

/* ===========================================================================
   11. LOCATION MAP
   Replaces the zoomed-out radius map, which was unreadable at render size.
   One static OSM image, one pin, landscape.
   =========================================================================== */
.lmap { margin: var(--sp-5) 0; }

.lmap__stage {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: var(--hair) solid var(--rule);
}

.lmap__img { display: block; width: 100%; height: auto; }

.lmap__pin {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.lmap__marker      { fill: var(--steel); stroke: var(--white); stroke-width: 3; }
.lmap__marker-dot  { fill: var(--white); }
.lmap__pulse       { fill: rgba(46, 95, 126, .18); stroke: rgba(46, 95, 126, .5); stroke-width: 2; }

.lmap__label {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 800;
  fill: var(--ink);
  paint-order: stroke;
  stroke: rgba(255, 255, 255, .9);
  stroke-width: 5;
  stroke-linejoin: round;
}

.lmap__caption {
  margin-top: var(--sp-3);
  font-size: var(--fs-meta);
  text-align: center;
  color: var(--slate-ink);
}

/* Towns: a real grid so the 15 names land in two tidy rows on desktop
   instead of ragging across four. Flex-wrap could not guarantee the count. */
.tcards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-2);
  justify-content: initial;
}

@media (min-width: 30em) { .tcards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 48em) { .tcards { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
@media (min-width: 64em) { .tcards { grid-template-columns: repeat(8, minmax(0, 1fr)); } }

.tcard {
  display: grid;
  place-items: center;
  text-align: center;
  padding: var(--sp-2) var(--sp-2);
  font-size: var(--fs-meta);
  line-height: 1.25;
}

/* The lift went when the interactive coverage map did, and the fill was
   dialled back to a 6% wash at the same time - so faint that hovering a town
   looked like nothing had happened. They are still inert labels, so the lift
   stays off (nothing moves that you cannot click), but the fill comes back to
   solid steel: enough to confirm which of fifteen towns is under the cursor,
   which is the only job it has. cursor stays default so it never claims to be
   a link. */
.tcard:hover {
  transform: none;
  background: var(--steel);
  border-color: var(--steel);
  color: var(--white);
}

/* Before and after: one factual line per pair, no clinical field grid. */
.s-cases__line {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-small);
  color: var(--slate-ink);
}

/* ===========================================================================
   12. GOOGLE MAPS EMBED + full-width town block
   =========================================================================== */
.lmap__stage { aspect-ratio: 16 / 10; }

.lmap__frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Towns sit below both columns now, so they get the full container width. */
.s-areas__towns-block { margin-top: var(--stack); }

/* auto tracks, not 1fr: every chip sizes to its own name, so nothing gets
   squeezed and truncated. Eight across puts the fifteen towns in two rows. */
.tcards {
  display: grid;
  grid-template-columns: repeat(2, auto);
  justify-content: center;
  gap: var(--sp-2);
}

@media (min-width: 30em) { .tcards { grid-template-columns: repeat(3, auto); } }
@media (min-width: 48em) { .tcards { grid-template-columns: repeat(5, auto); } }
@media (min-width: 64em) { .tcards { grid-template-columns: repeat(8, auto); } }

.tcard {
  white-space: nowrap;
  padding: var(--sp-2) var(--sp-3);
  font-size: var(--fs-meta);
}

/* With the timetable gone the right-hand column is short, so both columns
   align to the top rather than stretching. */
.s-areas__grid { align-items: start; }

/* Fixed 8-column tracks left the second row (7 of 15) hanging to the left.
   Flex-wrap centres each row independently, so a short final row sits even
   either side. Still lands as two rows at desktop width. */
.tcards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
}

@media (min-width: 30em) { .tcards { grid-template-columns: none; } }
@media (min-width: 48em) { .tcards { grid-template-columns: none; } }
@media (min-width: 64em) { .tcards { grid-template-columns: none; } }

/* ===========================================================================
   13. COLLAPSIBLE BLOCK
   grid-template-rows 1fr -> 0fr animates height without a measured pixel
   value, so it cannot be thrown off by a reflow or a font swap mid-transition.
   =========================================================================== */
.collapse {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 460ms var(--ease);
}

.collapse.is-collapsed { grid-template-rows: 0fr; }

/* min-height:0 is what lets a grid row actually shrink to nothing. */
.collapse__inner { min-height: 0; overflow: hidden; }

.collapse.is-collapsed .collapse__inner { visibility: hidden; }

.collapse__chevron { transition: transform var(--dur) var(--ease); }
[aria-expanded="true"] .collapse__chevron { transform: rotate(180deg); }

.s-treatments__toggle { margin-top: var(--sp-5); }

@media (prefers-reduced-motion: reduce) {
  .collapse { transition: none; }
  .collapse__chevron { transition: none; }
}

/* ===========================================================================
   14. CARD DEPTH
   The flat hairline-only cards read as dull. A soft resting shadow gives them
   a surface to sit on, and the hover state lifts further so the interaction
   still registers.

   The :root block that used to sit here declaring --shadow-rest and
   --shadow-lift has moved to 00-tokens.css, which is where elevation belongs
   and which had been claiming to own it while this file quietly overrode it.
   This file sorts last in the build, so the block here was the one actually
   winning. Consuming the tokens is fine; declaring them here is not.
   =========================================================================== */
.ttile__inner,
.cgroup,
.pcard,
.ppanel,
.greview,
.lmap__stage,
.card,
.tcard {
  box-shadow: var(--shadow-rest);
}

.ttile:hover .ttile__inner,
.ttile:focus-within .ttile__inner,
.pcard:hover,
.card:hover,
.card:focus-within { box-shadow: var(--shadow-lift); }

/* Cards on the navy band get no shadow at all.

   They used to carry 0 2px 10px rgba(0,0,0,.22) - the only pure black in a
   build that uses ink rgba(16,20,24) everywhere else, and a shadow dark enough
   to muddy the band rather than lift the card off it. The elevation scale is
   built for light bands: a dark shadow on a dark surface has nothing to cast
   onto. Separation on dark comes from a light hairline instead, which is what
   the approved design does. --rule-strong already re-points to the ice-based
   dark rule inside these bands, so this needs no dark-mode colour of its own. */
.band--navy .card,
.on-dark .card {
  box-shadow: none;
  border-color: var(--rule-strong);
}

/* ...except any card holding a photograph, which gets no border at all.

   A photo card fills its own box edge to edge, so it already has a hard edge
   on all four sides. An outline round that is not separation - it is a second
   edge a few pixels outside the first, and below the caption bar it reads as
   a stray line floating under the card rather than as part of it.

   Scoped with :has(.card__media) rather than to .card--plate, because the
   plate modifier is not the only photo card on the navy band: the wide
   Sporting Khalsa card is a plain .card and kept its border when this rule
   named the modifier. A text-only card on navy, if one is ever added, still
   gets its hairline. */
.band--navy .card:has(.card__media),
.on-dark .card:has(.card__media) {
  border: 0;
}

.cgroup:hover { box-shadow: var(--shadow-lift); }
.cgroup { transition: box-shadow var(--dur) var(--ease); }

/* Section-level notes read as captions and centre with the headings. */
.s-conditions__note { text-align: center; }

/* ===========================================================================
   15. AREAS: map gets its own column
   The map used to sit under the coverage facts in the left column, which
   squashed it into a strip while the right column sat half empty. Coverage
   and availability now share the text column and the map takes the other.
   =========================================================================== */
@media (min-width: 56.25em) {
  .s-areas__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

.s-areas__subhead--second { margin-top: var(--stack); }

.lmap { margin: 0; height: 100%; display: flex; flex-direction: column; }
.lmap__stage { flex: 1; min-height: 320px; aspect-ratio: auto; }

@media (min-width: 56.25em) {
  .lmap__stage { min-height: 420px; }
}

/* ===========================================================================
   16. WIDE SCREENS
   The container was pinned at 1200px at every size above 1200. On a large or
   zoomed-out display that left the whole design marooned in the middle with
   big empty margins, and the hero text block (34ch) looked tiny against a
   full-bleed photograph. Both now step up past 1440px.

   The reading measure for running prose is deliberately NOT widened: long
   lines are harder to read, so only the layout container grows.
   =========================================================================== */
@media (min-width: 90em) {          /* 1440px */
  :root {
    --w-page: 1340px;
    --w-wide: 1340px;
    --w-narrow: 1340px;
    --w-hero: 40ch;
  }
}

@media (min-width: 110em) {         /* 1760px */
  :root {
    --w-page: 1440px;
    --w-wide: 1440px;
    --w-narrow: 1440px;
    --w-hero: 44ch;
  }
  .hero { min-height: 78vh; }
}

/* The six headline tiles were stacking one-up below 480px, which made
   Treatments 2,648px tall on a phone even with the second half collapsed.
   Two-up from the smallest width, with the copy sized down to suit.

   SCOPED to the width it was written for. It used to sit outside this media
   query, and because it is later in the file than the 64em three-up rule
   above, it won at EVERY width - so the six headline treatments were locked
   two-up on desktop as well. At 1440px that made each card 627px wide with a
   3:2 photograph 593x395px inside it: a picture roughly the size of a hero,
   six times over. Fixing the scope restores three-up above 1024px and takes
   the photographs back to a sensible size on their own. */
@media (max-width: 29.99em) {
  .ttiles--lead { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ttiles--lead .ttile__inner { padding: var(--sp-3); gap: var(--sp-2); }
  .ttiles--lead .ttile__name  { font-size: var(--fs-h4); }
  .ttiles--lead .ttile__desc  { font-size: var(--fs-meta); }
}
