:root{
  --navy:#284570; --navy-deep:#1d3552; --navy-ink:#14263f;
  --pink:#d97195; --pink-deep:#c45f80;
  --charcoal:#26292e; --charcoal-2:#31353b;
  --paper:#fbf7f7; --white:#ffffff; --black:#000000;
  --ink:#2b2d31; --muted:#5b6672; --line:#e7dee1;
  --serif:"Aleo",Georgia,serif; --body:"Red Hat Text","Helvetica Neue",Arial,sans-serif;
  --wrap:1280px;
}
*{box-sizing:border-box;margin:0;padding:0;}
/* ─── NOTHING ON THE SITE IS SELECTABLE, AND NOTHING SHOWS AN I-BEAM ───
 *
 * Dylan, 19 September 2026: "a visitor to the website can't highlight any of
 * the text ... I don't want an I-beam cursor to appear anywhere, and I don't
 * want you to be able to highlight any of the text either."
 *
 * Two properties, because they are two different things and neither one fixes
 * the other. user-select stops the drag; cursor is what the pointer LOOKS like
 * over a run of text, and it would still turn into an I-beam over copy that
 * cannot be selected.
 *
 * ⚠ DECLARED ON THE ROOT, NOT ON *. Both properties are inherited, so one
 * declaration on the root element reaches every element that does not declare
 * its own — and a declaration on the element itself always beats an inherited
 * value, INCLUDING the browser's own a{cursor:pointer}. That is the whole
 * reason this is not written as *{cursor:default}: the universal selector
 * would have flattened the twenty-seven cursor:pointer rules further down
 * this file plus the UA's, and every link would have lost its hand. Written
 * this way, links, buttons, the slider arrows and the disabled-arrow
 * cursor:default all keep exactly the cursor they had.
 *
 * ⚠ AND THE COMMENT SAYS "root element", NOT the tag in angle brackets, on
 * purpose. These comments ship: BASE_CSS goes out verbatim inside the page's
 * style block, and compile.test.ts counts the opening html tags in the
 * document to prove it is one self-contained page. A prose mention of the
 * tag reads as a second one and fails that guard — which it duly did, on the
 * first draft of this block.
 *
 * -webkit-touch-callout is the phone half of the same request: without it a
 * long press on iOS still raises the copy/share sheet over a paragraph, which
 * is what highlighting looks like on a touch screen.
 *
 * ⚠ WHAT THIS DELIBERATELY DOES NOT COVER — THE ENQUIRY FORM. Its text inputs,
 * its textarea and its country <select> are handed back both properties below.
 * A visitor who cannot select the phone number they have just mistyped cannot
 * correct it, and the enquiry form is the one place on the site where the text
 * belongs to the visitor rather than to us. Those fields keep their I-beam for
 * the same reason: it is the affordance that says "you may type here", and the
 * form is the site's only conversion point.
 *
 * ⚠ AND IT IS NOT A CONTENT LOCK. The words are still in the HTML: view-source,
 * reader mode, print, or switching this rule off in dev tools all still get
 * them. This is a presentation choice and must never be cited as protection.
 */
html{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;cursor:default;}
/* Select-all is not a drag. ⌘A/Ctrl+A, and a caret-browsing or find-in-page
   selection, may still land a range across an unselectable region — the spec
   only forbids the user STARTING a selection there — and Firefox paints one.
   Painting the highlight transparent closes that last gap, and is handed back
   to the form fields on the line after so a visitor editing their message can
   still see what they have selected. */
::selection{background:transparent;}
input,textarea,select{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;-webkit-touch-callout:default;}
input,textarea{cursor:text;}
select{cursor:pointer;}
/* A tick-box is not text. The two consent boxes and the honeypot sit inside
   <label>s, so an accidental I-beam or a selectable caption is exactly what a
   visitor would drag over on the way to clicking one. */
input[type=checkbox],input[type=radio],input[type=submit],input[type=button]{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}
input::selection,textarea::selection{background:var(--pink);color:var(--white);}
/* ─── WHERE THE EYEBROW SITS ───
 *
 * Above the heading is the default and every published site keeps it. Set
 * --eb-flow:flex and the nine places an eyebrow appears become flex columns
 * in which the heading is ordered BEFORE it, so the small tracked capitals
 * read as a line of detail under the headline rather than a kicker over it.
 *
 * THE DOCUMENT DOES NOT MOVE. The eyebrow is a <span> that stays exactly
 * where it is in the markup, and it was never a heading tag, so the outline
 * a crawler builds and the order a screen reader speaks are identical either
 * way. Only the paint order changes.
 *
 * Both rules are inert until the variable is set: an order is ignored in
 * normal flow, and a flex direction is ignored on a block box.
 */
.hero-lead,.notfound .wrap,.whatdo .lead,.ins-head-l,.slider .shead,.trust-card,.cta-band .inner,.enqf-side,.voice .fv-body{display:var(--eb-flow,block);flex-direction:column;}
/* Four ranks: the roundel crest leads, then the heading, then the eyebrow,
   then everything else. Ranking the rest explicitly is what keeps the
   eyebrow directly beneath its heading rather than at the foot of the
   section, below the body copy and the buttons. */
.hero-lead>*,.notfound .wrap>*,.whatdo .lead>*,.ins-head-l>*,.slider .shead>*,.trust-card>*,.cta-band .inner>*,.enqf-side>*,.voice .fv-body>*{order:4;}
.hero-lead>.eyecrest,.notfound .wrap>.eyecrest,.whatdo .lead>.eyecrest,.ins-head-l>.eyecrest,.slider .shead>.eyecrest,.trust-card>.eyecrest,.cta-band .inner>.eyecrest,.enqf-side>.eyecrest,.voice .fv-body>.eyecrest{order:1;}
.hero-lead>h1,.notfound .wrap>h1,.whatdo .lead>h1,.ins-head-l>h1,.slider .shead>h1,.trust-card>h1,.cta-band .inner>h1,.enqf-side>h1,.voice .fv-body>h1,.hero-lead>h2,.notfound .wrap>h2,.whatdo .lead>h2,.ins-head-l>h2,.slider .shead>h2,.trust-card>h2,.cta-band .inner>h2,.enqf-side>h2,.voice .fv-body>h2,.voice .fv-body>.fv-q{order:2;}
.hero-lead>.eyebrow,.notfound .wrap>.eyebrow,.whatdo .lead>.eyebrow,.ins-head-l>.eyebrow,.slider .shead>.eyebrow,.trust-card>.eyebrow,.cta-band .inner>.eyebrow,.enqf-side>.eyebrow,.voice .fv-body>.eyebrow{order:3;}
/* A BUTTON IS INLINE, AND FLEX BLOCKIFIES IT.
   .btn is display:inline-flex, so in normal flow it shrink-wraps and the
   section's text-align places it. The moment its parent becomes a flex column
   it is blockified and stretched edge to edge — which is exactly what happened
   to the call-to-action button the first time this shipped: a full-width pink
   bar with its label against the left edge.
   align-self restores the shrink-wrap, at the position the section's text
   alignment used to give it. Inert in normal flow, so it costs nothing while
   the eyebrow sits above. */
.notfound .wrap>.btn,.cta-band .inner>.btn{align-self:center;}
.hero-lead>.btn,.whatdo .lead>.btn,.ins-head-l>.btn,.slider .shead>.btn,.trust-card>.btn,.enqf-side>.btn,.voice .fv-body>.btn{align-self:start;}

/* Instant-tap feel on touch: kill the translucent grey tap-highlight box (reads
   as lag on iOS/Android) and set touch-action:manipulation on interactive targets
   so there's no wait for a possible double-tap-zoom. manipulation still allows
   scroll/pan — it only drops double-tap-zoom on those elements.

   text-size-adjust (device review, 18 October 2026): Safari on an iPhone turned
   sideways enlarges running text on its own ("text inflation"), so the same page
   read in landscape got bigger body copy than the design sets and lines broke
   in new places. 100% keeps every size exactly what the stylesheet says, which
   is what portrait, Android and desktop already show. Not "none": that would
   also stop a reader's own zoom setting on some Android browsers. */
html{overscroll-behavior:none;-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;text-size-adjust:100%;}
a,button,summary,label,[role=button],.btn,.navpill,.wh-enq,.wh-burger,.arwbtn,.flagbtn,.whdd-btn,.whdd-opt{touch-action:manipulation;}
html[data-wh-intro],html[data-wh-hold],html[data-wh-arriving],html.wh-cover{overflow:hidden;touch-action:none;}
/* ⚠ AND NOTHING BEHIND IT MAY BE CLICKED — 1 October 2026. Dylan: "when you
   click on the overlay that appears as a page transition or when you first
   arrive on the site, you can click on things that are on the page behind. This
   should not be the case."

   The line above locks SCROLLING and GESTURES. It does not touch clicks, and
   .wh-curtain is pointer-events:none in every state, so the screen was fully
   opaque and every link under it was live — measured at 1,746ms on a repeat load
   and 2,017ms on a same-page transition, with a real dispatched click landing on
   the masthead logo's anchor. A touch TAP got through too: touch-action:none
   suppresses gestures, not taps.

   ⚠ ON THE ROOT, NOT ON THE CURTAIN, AND THAT IS THE WHOLE DESIGN. Eight of the
   ten exit styles finish with the top curtain still at translateY(0) and
   opacity:0 — fade, zoom and shimmer directly; blinds, split and dissolve via
   #wh-fx — and opacity does not affect hit-testing. pointer-events:auto on
   .wh-curtain would therefore leave every site using one of those PERMANENTLY
   unclickable. Only curtainUp/curtainDown translate off-screen, and wipe/iris
   clip, which does clip hit-testing.

   Keyed here, the shield cannot outlive the cover it belongs to, because it is
   keyed on the same four roots the stylesheet already calls "a curtain is over
   the page" — the set reveal.ts is explicit is the single source of truth for
   that state. All four are removed by doReveal/dropHold/cov(false), by
   CURTAIN_JS's catch, by its two six-second failsafes, and by RUNTIME_MISSING_JS
   if the bundle never loads at all. With JavaScript off none of them is ever
   set. Its lifetime is exactly the scroll lock's, so it adds no way for the page
   to end up permanently dead that the scroll lock does not already have.

   z-index 9995: above the curtains (9992) and the hold cover (9989), below #wh-fx
   (9996), the roundel layer (9997) and #wh-intro-overlay (10000) — so the intro
   overlay, which already blocks correctly at full opacity and mid-fade, keeps
   receiving its own hits and that path is unchanged. */
html[data-wh-intro]::after,html[data-wh-hold]::after,html[data-wh-arriving]::after,html.wh-cover::after{
  content:"";position:fixed;top:0;right:0;bottom:0;left:0;inset:0;z-index:9995;background:transparent;pointer-events:auto;}
main{overflow:clip;}
/* BODY LEADING REACHES THE PROSE, not just this rule.
   --leading-body was consumed in exactly one place — here — so every block
   that restated its own line-height silently opted out of the setting. The
   services list was the visible case: its descriptions sat at a fixed 1.55
   however the body leading was set.
   Each prose block now scales PROPORTIONALLY rather than adopting the global
   value outright: calc(var(--leading-body,1.7) / 1.7 * 1.55) is exactly 1.55
   at today's default, so nothing moves, and it keeps a compact list row
   tighter than running prose when the setting is raised. Writing the local
   value out in the calc keeps the designed number visible in the source.
   Buttons, lockups and the intro slogan are deliberately NOT in this set:
   line-height:1 on a button is geometry, not typography. */
body{font-family:var(--body);color:var(--ink);background:var(--paper);overflow-x:hidden;overflow-x:clip;overscroll-behavior:none;font-weight:var(--wt-body,400);line-height:var(--leading-body,1.7);letter-spacing:var(--tracking-body,0em);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;}
/* ── THE UNDERLINE LEAVES BY FADING, NOT BY REVERSING ───────────────────────
   Dylan, 26 September 2026: *"across the site, we have an underline effect on
   links when you hover. Great. The way it appears is really nice. However, at
   present, its exit effect is just the same as the entrance effect but in
   reverse. When you stop hovering over a link or when an actively selected menu
   item is no longer selected, I'd like it instead just to fade out, not to
   reverse back out the way it came."*

   THE ENTRANCE IS UNCHANGED, DELIBERATELY AND EXACTLY. Every sweep still runs
   its own original duration and easing on background-size; nothing about the
   way the bar arrives has been retuned. Only the leaving is new.

   HOW, because the obvious way does not work. background-size cannot be faded:
   opacity would take the words with it, and a background-image is not an
   interpolable property, so the gradient cannot be animated to transparent
   directly. What CAN be animated is a registered custom property, so the
   gradient's colour becomes one — every underlined surface paints
   linear-gradient(var(--ul-c),var(--ul-c)) and --ul-c is what moves.

   The two halves are separated by which rule is in effect at the END of the
   change, which is how CSS already decides whose transition governs:

     the resting rule   carries the EXIT   (transition:var(--ul-out))
     the hover/active   carries the ENTER  (transition:var(--ul-in))

   --ul-out fades the colour out over --ul-fade while holding the bar at full
   width: background-size has a ZERO duration and a --ul-fade DELAY, so it snaps
   back to 0% only once the bar is already invisible and ready for the next
   sweep. --ul-in sets the colour in the same frame (0s) and sweeps the bar.

   ⚠ RE-HOVERING DURING THE FADE DOES NOT RE-SWEEP. The bar is still at 100%,
   so --ul-in only restores the colour and the line is simply there again. That
   is the honest reading of "it was still on its way out", and it is better than
   a bar that jumps to 0 and grows again under a pointer that never left.

   ⚠ WHERE @property IS NOT SUPPORTED the colour is still substituted, just not
   animated, so the bar disappears in one frame instead of fading. That is
   Firefox before 128 and Safari before 16.4. It is a smaller loss than the
   defect, and it cannot strand a bar on screen, because background-size still
   resets on the same clock.

   ⚠ transparent IS NOT A BLACK FADE. Colour interpolation is premultiplied by
   alpha, so pink to transparent takes the pink down in opacity rather than
   travelling through grey. The alternative — a second variable holding an alpha
   — would need every surface to know its own colour twice.

   ⚠ AND THE ACTIVE STATES NEEDED THE COLOUR TOO, WHICH IS THE ONE WAY THIS
   CHANGE COULD HAVE GONE SILENTLY WRONG. A rule that sets only
   background-size:100% 2px — the audience chip while its callout is open, a
   full-screen menu category while its submenu shows — would have drawn a
   full-width bar in the resting colour, which is now transparent: the state
   underline would have vanished with nothing failing. Each of those rules sets
   --ul-c as well, and each is listed in
   src/lib/website/__tests__/theUnderlineFadesOut.test.ts. */
@property --ul-c{syntax:"<color>";inherits:false;initial-value:transparent;}
:root{
  --ul-fade:.26s;
  --ul-in:background-size .35s cubic-bezier(.4,0,.2,1),--ul-c 0s;
  --ul-out:--ul-c var(--ul-fade) linear,background-size 0s linear var(--ul-fade);
}
a{color:inherit;text-decoration:none;}
/* Links inside body text (textLinks.ts): the words keep the paragraph's own
   colour and font, so a link reads on navy and white alike; only the underline
   says it is one. On hover the thin line steps aside for the site's own pink
   sweep (the a:not(…) rule below), so a text link behaves like every other. */
a.tl{text-decoration:underline;text-decoration-color:currentColor;text-decoration-thickness:1px;text-underline-offset:3px;}
a.tl:hover{text-decoration-color:transparent;}
/* ⚠ THE EXCLUSION LIST IS LOAD-BEARING IN BOTH DIRECTIONS, and on 1 October 2026
   it was wrong in both. What is IN it gets no sweep; what is OUT of it gets one
   on the ANCHOR'S OWN BOX, which for a block-level anchor is the full column.

   ⚠ IN, AND THIS REVERSES A CHANGE MADE EARLIER THE SAME DAY — all three logos:
   .brand, .brandcrest and .fbrand. The reversal is Dylan's and the history is
   worth keeping, because the two instructions read as opposites and the second
   is the live one.

   On the morning of 1 October he said: "in the footer, there is an underline
   effect on the logo itself when you hover over it. I like this. Can you also
   apply this to the logo in the masthead and in the sticky header". .brand and
   .brandcrest came OUT of this list, which gave all three the sweep.

   That afternoon: "if you hover over the logo in the top left of the sticky
   header, or in the masthead, or in the footer, it has no underline effect but
   it does grow by 2%." So all three go back IN — .fbrand too, which had never
   been excluded and is the only reason the footer had the effect in the first
   place. The growth is in LOGO_CSS, on the independent 'scale' property rather
   than on 'transform', because .brand's transform is already carrying the sticky
   header's entrance and exit.

   IN, since the same day: .svcrow and .svcall, the services mega-menu rows
   and their chip. Both are <a> elements wrapping a .ul span, and the span is
   what is meant to underline — the text. The anchor was ALSO matching here, so a
   hovered service drew a second 2px line across the FULL WIDTH OF ITS COLUMN,
   under the roundel and the signature word as well as the name. Measured in
   Chromium: the row box is 255px and .ul is 118px, and the computed
   background-image on the anchor was the gradient. That is what Dylan saw:
   "the underline extends far beyond the service name text itself."

   ⚠ AND .svccat JOINS THEM, 1 October 2026 — the submenu's category HEADING,
   which became an <a> when its chip was removed and it took over as the way
   through to the category page (see 'title' in compile.ts). It wraps a .ul for
   exactly the same reason the rows do, so it needs exactly the same exclusion:
   without it the heading would draw a second line the full width of the column,
   which is the defect above in a larger font.

   ⚠ .svcall IS KEPT IN THE LIST THOUGH NOTHING EMITS IT ANY MORE. The chip went
   the same day. Removing the name as well would be tidier and is not free: this
   selector is a blocklist, so a class that reappears is instantly styled, and the
   next person to reintroduce an "All of …" chip here — the control has now had
   three shapes in two days — would get the full-width line back with no clue
   where it came from. The name is cheap; re-learning that is not.              */
a:not(.btn):not(.wd-item):not(.card):not(.hero-feature):not(.ncard):not(.scard):not(.feature):not(.item):not(.more):not(.view):not(.link):not(.explore):not(.viewall):not(.go):not(.trow):not(.navpill):not(.nrow):not(.svcrow):not(.svcall):not(.svccat):not(.brand):not(.brandcrest):not(.fbrand){
  --ul-c:transparent;background-image:linear-gradient(var(--ul-c),var(--ul-c));background-repeat:no-repeat;background-position:0 100%;background-size:0% 2px;
  transition:var(--ul-out);-webkit-box-decoration-break:slice;box-decoration-break:slice;padding-bottom:2px;}
a:not(.btn):not(.wd-item):not(.card):not(.hero-feature):not(.ncard):not(.scard):not(.feature):not(.item):not(.more):not(.view):not(.link):not(.explore):not(.viewall):not(.go):not(.trow):not(.navpill):not(.nrow):not(.svcrow):not(.svcall):not(.svccat):not(.brand):not(.brandcrest):not(.fbrand):hover{background-size:100% 2px;--ul-c:#d97195;transition:var(--ul-in);}
/* ⚠⚠ THE UNDERLINE IS SLICED, NOT CLONED, AND THAT IS WHAT MAKES IT SWEEP.
   Dylan, 2 October 2026, of a submenu heading that wraps: *"when, in a submenu,
   the title is over two lines … the underline effect on hover doesn't seem to be
   staggered. The underline on both lines (possibly more if there were more) both
   happens at the same time."*

   With box-decoration-break:clone, EACH line fragment is a complete box with its
   own background, so a background-size of 50% draws half an underline under every
   line at once — two pens moving in parallel. With slice, the fragments share one
   background positioning area the width of the unbroken inline, so 50% fills the
   first line entirely and has not reached the second — one pen, finishing a line
   before starting the next, which is what an underline being drawn looks like.

   MEASURED IN CHROMIUM at a fixed 50%, side by side: clone half-underlines both
   lines; slice completes line one and leaves line two bare. The screenshot is the
   reason this is a one-word change rather than an argument.

   ⚠ CHANGED GLOBALLY ON PURPOSE. The two differ only where an anchor wraps, and
   almost none of them do — but '.ul' is ONE effect, and an effect that behaves two
   ways depending on which element wears it is how this comes back on the next
   multi-line heading somebody adds. '.hero-feature h3 > .ul' already set slice
   explicitly, which is the same conclusion reached once before and not followed
   through.

   ⚠ THE PADDING IS UNAFFECTED. Slicing applies to the horizontal start and end
   decorations; a bottom padding is vertical and is still applied to every
   fragment, so the 2px gap under each line is unchanged. */
.ul{--ul-c:transparent;background:linear-gradient(var(--ul-c),var(--ul-c)) no-repeat 0 100% / 0% 2px;-webkit-box-decoration-break:slice;box-decoration-break:slice;transition:var(--ul-out);padding-bottom:1px;}
/* Hover-only sweep, gated so a touch device's sticky :hover can never strand it
   on. Anchors only: a card with no destination renders as a <div> and must not
   pretend to be clickable. */
@media (hover:hover){a.feature:hover .ul,a.item:hover .ul,a.ncard:hover .ul,a.card:hover .ul,
  a.svp-card:hover .ul,a.svp-insight:hover .ul{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}}
/* ⚠ THE SERVICE-PAGE CARDS JOIN THE EXISTING RULE RATHER THAN GETTING THEIR OWN.
   Both '.svp-card' and '.svp-insight' kill the site-wide anchor sweep with
   'background-image:none!important' (they are photographic tiles; a 2px line
   across the whole tile is not the effect). That left the words inside them
   with no hover response at all, which is what Dylan reported on 20 September
   2026. Adding them here rather than writing a second sweep means one
   definition of what the effect IS — same colour, same 2px, same easing, same
   'hover:hover' gate that stops a touch device's sticky :hover stranding it on.
   '.svp-card-t' is the block; the '.ul' span inside it is the inline run that
   clones per line. */

.eyecrest{display:block;width:var(--crest-w,42px);height:var(--crest-w,42px);margin:0 0 var(--crest-gap,12px);filter:drop-shadow(0 3px 7px rgba(8,16,34,.2));}
.whatdo .eyecrest,.cta-band .eyecrest{margin-left:auto;margin-right:auto;}
/* The roundel (eyecrest) above each SECTION heading can be sized as a share of
   that heading's own text — a --crest-w of 0.8em means 80% of the heading — and
   scales correctly across the five section blocks, whose headings differ in
   size. Each eyecrest's font-size is matched to its heading below for that em to
   resolve against. Unset, --crest-w stays the fixed 42px it has always been. */
.whatdo .lead .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(27px,3.1vw,41px));}
.ins-head-l .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(30px,4.4vw,52px));}
.slider .shead .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(26px,3.4vw,44px));}
.trust-card .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(27px,2.7vw,40px));}
.cta-band .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(30px,4.4vw,56px));}
.wrap{max-width:var(--wrap);margin:0 auto;padding:0 clamp(22px,4vw,48px);}
.eyebrow{font-family:var(--eb-face,var(--body));font-size:calc(var(--eb-scale,1) * 12px);font-weight:var(--eb-weight,700);letter-spacing:var(--tracking-eyebrow,.24em);text-transform:uppercase;color:var(--eyebrow);}
/* Body paragraphs. The default resolves to inherit, not to left, so this
   rule is inert until justification is switched on. */
/* -webkit-hyphens (device review, 18 October 2026): Safari before 17 (iOS 16
   and older iPads) ignores the unprefixed property, so justified body text there
   was never hyphenated and opened wide gaps between words. */
p{text-align:var(--align-body,inherit);-webkit-hyphens:var(--hyphens-body,manual);hyphens:var(--hyphens-body,manual);}
/* The sections that centre their copy on purpose. Justification is for
   running prose, and a centred call to action is not running prose — without
   this they would jump to justified the moment the setting was used.

   ⚠ THREE SECTIONS WERE MISSING FROM THIS LIST UNTIL 25 SEPTEMBER 2026, and
   the omission is invisible until somebody sets --align-body. Dylan reported
   one of them: "with the pull quote block, the attribution is also horizontally
   centred; at the moment it is left-aligned, which doesn't work." The section
   really does say text-align:center — it is this rule, on the paragraph inside it,
   that was overriding it.

   Swept rather than patched (AGENTS.md §2.5): every rule in this stylesheet
   that centres a block, checked for whether the block can contain a paragraph. Three
   could and were absent — .svp-pull (the attribution), .svp-cover--centre (the
   lede) and .svp-ins-mast--centre (the lede, the figure and its label).

   ⚠ WHAT IS DELIBERATELY NOT HERE: .svp-car-stage and .wh-lg-t centre their
   contents and contain no paragraph at all, so adding them would be dead weight that
   reads as coverage; .wh-fsv-in is covered by .wh-fsv-lockup p, which is its
   only layout that draws a paragraph. If a future block adds a paragraph to any
   of those, it belongs here — pullQuoteAttributionCentres.test.ts sweeps for
   exactly that and will say so. */
.notfound p,.cta-band p,.feat-photo p,.masthead .brandtop p,.wh-fsv-lockup p,.voice .founder p,.svp-pull p,.svp-cover--centre p,.svp-ins-mast--centre p{text-align:inherit;-webkit-hyphens:manual;hyphens:manual;}
h1,h2,h3,h4{font-family:var(--serif);color:var(--heading);font-weight:var(--wt-heading,600);line-height:1.05;letter-spacing:var(--tracking-heading,-.01em);}
/* THE THREE HEADING TIERS. Each variable falls back to what the rule above
   already computes, so a site with no tier settings renders identically —
   these rules are inert until something is actually chosen. Size, colour and
   leading stay where they were: a tier owns the FACE, the WEIGHT, the SLANT
   and the TRACKING, and nothing else. */
h1{font-family:var(--h-title-face,var(--serif));color:var(--h-title-colour,var(--heading));font-weight:var(--h-title-weight,var(--wt-heading,600));font-style:var(--h-title-style,normal);letter-spacing:var(--h-title-track,var(--tracking-heading,-.01em));text-transform:var(--h-title-case,none);}
h2{font-family:var(--h-section-face,var(--serif));color:var(--h-section-colour,var(--heading));font-weight:var(--h-section-weight,var(--wt-heading,600));font-style:var(--h-section-style,normal);letter-spacing:var(--h-section-track,var(--tracking-heading,-.01em));text-transform:var(--h-section-case,none);}
h3,h4{font-family:var(--h-sub-face,var(--serif));color:var(--h-sub-colour,var(--heading));font-weight:var(--h-sub-weight,var(--wt-heading,600));font-style:var(--h-sub-style,normal);letter-spacing:var(--h-sub-track,var(--tracking-heading,-.01em));text-transform:var(--h-sub-case,none);}
.tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--eyebrow);}
.purpose{color:var(--muted);font-size:16px;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.6);margin-top:var(--h-section-gap,14px);}
.btn{display:inline-flex;align-items:center;gap:9px;font-family:var(--body);font-weight:600;font-size:14px;padding:12px 24px;border:1.5px solid transparent;border-radius:2px;cursor:pointer;transition:.18s;}
.btn-pink{background:var(--pink);color:#fff;}.btn-pink:hover{background:var(--pink-deep);}
.btn-white{background:#fff;color:var(--navy);}.btn-white:hover{background:var(--pink);color:#fff;}
.arwbtn{flex:0 0 auto;width:46px;height:46px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:18px;transition:.18s;border:1.5px solid;}
.crest{flex:0 0 auto;filter:drop-shadow(0 6px 16px rgba(8,16,34,.42));}
/* THE RING, EVERYWHERE AT ONCE.
   The roundel SVGs draw their ring with stroke="currentColor" — the disc and
   the P are painted explicitly, so the ring is the only stroke left that can
   inherit. Setting the CSS color property on the crest classes therefore
   moves the ring and
   nothing else, in the masthead, the footer, the intro, the callout and the
   15px signature marker, from one declaration. The favicon is the exception
   and is baked at compile time; see faviconSvg(). */
.crest,.eyecrest,.wd-crest,.wh-cal-round,#wh-intro-roundel{color:var(--ring,#d97195);}
.ph{background-position:center;background-size:cover;background-repeat:no-repeat;}
/* longhands before the inset shorthand (Safari 14.1+) so the scrim still covers
   on old iOS instead of collapsing to zero size and leaving the caption unreadable. */
.tintb::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;inset:0;background:linear-gradient(0deg,rgba(15,25,42,.72),rgba(15,25,42,.25) 55%,rgba(15,25,42,.05));}
.tintn::after{content:"";position:absolute;top:0;left:0;right:0;bottom:0;inset:0;background:linear-gradient(120deg,rgba(0,0,0,.82),rgba(0,0,0,.4));}
.topstrip{background:var(--chrome);color:#c9d3e4;position:relative;z-index:61;}
.topstrip .row{display:flex;align-items:center;gap:10px;height:44px;flex-wrap:wrap;padding:0 clamp(12px,3vw,30px);}
.topstrip .sel{display:flex;align-items:center;gap:8px;}
.topstrip label{color:#8ea3c4;letter-spacing:.09em;text-transform:uppercase;font-size:10px;font-weight:700;}
.topstrip select{background:transparent;color:#fff;border:none;border-bottom:1px solid rgba(255,255,255,.32);font-family:inherit;font-size:12.5px;font-weight:600;padding:3px 4px 3px 2px;cursor:pointer;}
.topstrip select:focus{outline:none;border-color:var(--pink);}
.topstrip select option{color:#111;}
.topstrip .right{margin-left:auto;font-size:12px;letter-spacing:.04em;}
.topstrip .right a{color:#c9d3e4;}
.topstrip .sel select[data-wh-nav]{max-width:22ch;}
.wh-mm{background:#fbf7f7;color:#1d3552;}
.wh-mm-in{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px clamp(12px,3vw,30px);font-size:12.5px;line-height:1.35;letter-spacing:.01em;}
.wh-mm b{font-weight:700;}
.wh-mm-go{color:var(--pink);font-weight:700;text-decoration:none;}
@media (hover:hover){.wh-mm-go:hover .ul{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}}
.wh-mm-x{margin-left:auto;background:none;border:0;padding:0 2px;font-size:16px;line-height:1;color:#8a7f80;cursor:pointer;}
.wh-mm-x:hover{color:#1d3552;}
.whdd{position:relative;display:inline-block}
/* A display value on the element itself beats the UA's [hidden] rule, so say it
   again. The region chip is hidden for an audience the region axis skips. */
.whdd[hidden]{display:none}
.whdd-btn{display:inline-flex;align-items:center;gap:6px;background:transparent;border:1px solid rgba(255,255,255,.30);border-radius:999px;padding:4px 10px;font:inherit;font-size:12px;color:inherit;cursor:pointer;line-height:1.4}
.whdd-btn:hover{border-color:rgba(255,255,255,.6)}
.whdd--label .whdd-btn{background:var(--pink);border-color:var(--pink);color:#fff;
  /* THE LABEL SITS IN THE MIDDLE OF THE PILL, NOT THE EM BOX IN THE MIDDLE.
     Dylan: "the text isn't vertically centred within the audience chip."
     He is right, and the reason is a thing flexbox cannot see. align-items
     centres the LINE BOX, and the line box is the font's full em box —
     ascent plus descent. Red Hat Text reserves 0.917em of ascent (room for
     accents and tall letters) against a cap height of only 0.75em, so a word
     of plain capitals and lower case leaves a quarter of an em of empty air
     above it that the browser counts as part of the text.
     Measured in Chromium at the chip's real size, with the real face: the ink
     of "Family" ran from 9.40px below the top of a 26.80px pill to 5.40px
     above the bottom — four pixels more space above the F than below the tail
     of the y. The caret was 1.50px low by the same reading.
     So the padding is rebalanced: taken off the top and given to the bottom, in
     the same breath, so the pill's height does not move by a hundredth of a
     pixel. In em, not px, because the correction IS a fraction of the font:
     change the chip's size and it stays right. Scoped to the audience chip
     because the flag chip beside it carries a flag glyph with entirely
     different metrics, and the number here is not its number.

     ⚠⚠ 29 SEPTEMBER 2026 — THE NUMBER WAS 0.146em AND IT IS NOW 0, BECAUSE IT
     WAS MEASURED AGAINST A TYPEFACE THIS CHIP DOES NOT USE. Dylan, with a
     screenshot: "the text doesn't look vertically centred within the audience
     chip ... I think the text and downward icon should really be slightly lower."
     He is right again, and this time the cause is the previous fix.

     Read the paragraph above: every metric in it is Red Hat Text's — 0.917em of
     ascent against a 0.75em cap height. But this chip sets no family at all.
     .whdd-btn is font:inherit, .topstrip sets only a colour, so the chip inherits
     body{font-family:var(--body)} — the BODY face, which on the live site is
     Ysabeau Office and on any other tenant is whatever they chose. A correction
     derived from one font's ascent and applied to a different font is not a
     correction; it is a nudge in a direction nobody measured.

     That also means the ORIGINAL diagnosis is suspect in the same way. "Four
     pixels more space above the F than below the tail of the y" was measured on
     the wrong face too, so the fault it describes may never have applied to what
     visitors actually see — which would explain why pushing the label up by
     ~1.75px (0.146em at 12px) has left it looking about that much too high.

     ⚠ SO IT IS ZERO, NOT A SMALLER GUESS. Zero means flexbox centres the line box
     and nothing second-guesses it. A number here has to come from a measurement of
     the face actually in use, and there is no browser in this sandbox to take one
     (AGENTS.md §3) — inventing a smaller constant would repeat the original
     mistake with a different digit. The mechanism is kept and named so that a
     measured value is a one-token change: set --wh-aud-nudge, per tenant if their
     body face needs it. Dylan is the instrument; if it now reads LOW, the number
     that fixes it is his measurement and not mine. */
  padding:calc(4px - var(--wh-aud-nudge,0em)) 10px calc(4px + var(--wh-aud-nudge,0em))}
.whdd--flag .whdd-cur{font-size:15px;line-height:1}
.whdd-caret{font-size:9px;opacity:.7}
/* A VISIBLE scrollbar: there are twenty countries in this list and, with the
   platform's overlay scrollbars, nothing said so until you were already
   scrolling. overscroll-behavior stops the page moving underneath once the list
   reaches its end. */
.whdd-list{position:absolute;top:calc(100% + 6px);left:0;z-index:300;min-width:190px;max-height:300px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:rgba(20,38,63,.35) transparent;margin:0;padding:6px;list-style:none;background:#fff;color:#14263f;border:1px solid rgba(20,38,63,.14);border-radius:2px;box-shadow:0 18px 44px rgba(20,38,63,.20)}
.whdd-list::-webkit-scrollbar{width:10px}
.whdd-list::-webkit-scrollbar-track{background:transparent}
.whdd-list::-webkit-scrollbar-thumb{background:rgba(20,38,63,.28);border-radius:999px;border:3px solid #fff}
.whdd-list::-webkit-scrollbar-thumb:hover{background:rgba(20,38,63,.45)}
.whdd-list[hidden]{display:none}
.whdd-opt{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:2px;font-size:13px;cursor:pointer;white-space:nowrap}
.whdd-opt:hover,.whdd-opt.is-active,.whdd-opt[aria-selected="true"]{background:var(--paper,#fbf7f7)}
.whdd-opt.is-active{outline:2px solid var(--pink);outline-offset:-2px}
.whdd-list:focus,.whdd-opt:focus{outline:none}
.whdd-flag{font-size:15px;line-height:1}
.whpop .whdd-btn{border-color:rgba(20,38,63,.24);color:#14263f}
.whpop .whdd-btn:hover{border-color:rgba(20,38,63,.5)}
.topstrip .flagbtn{display:inline-flex;align-items:center;gap:7px;background:none;border:0;padding:5px 2px;cursor:pointer;line-height:0;}
.topstrip .flagbtn .usflag{display:block;border-radius:2.6px;box-shadow:0 0 0 1px rgba(255,255,255,.3);}
.topstrip .flagbtn .fcar{font-size:9px;line-height:1;color:#8ea3c4;transition:color .18s;}
.topstrip .flagbtn:hover .fcar{color:#fff;}
.topstrip .flagbtn:hover .usflag{box-shadow:0 0 0 1px rgba(255,255,255,.62);}
/* Touch: give the small top-strip controls a 44px hit TARGET (Apple/WCAG min)
   WITHOUT growing their visible boxes — the chips must stay compact pills inside
   the 44px strip, with air above and below (min-height:44px made them fill the
   strip and read as oversized). A transparent ::after extends each button's hit
   area to the full 44px; pseudo-elements hit-test as part of their element, so
   taps on the padding land on the button. The dropdown OPTIONS keep the real
   44px row height — they're inside the open list, where tall rows help.
   (Native selects can't carry pseudo-elements; the strip's select keeps its
   compact box — its whole 44px row cell is already tappable.) */
@media (pointer:coarse){
  .topstrip .row{height:auto;min-height:44px;}
  .whdd-opt{min-height:44px;}
  .whdd-btn,.topstrip .flagbtn{position:relative;}
  .whdd-btn::after,.topstrip .flagbtn::after{content:"";position:absolute;left:-5px;right:-5px;top:50%;height:44px;transform:translateY(-50%);}
}
.whpop{position:fixed;left:clamp(12px,3vw,30px);top:54px;z-index:200;max-width:340px;background:#fff;border:1px solid rgba(20,38,63,.14);border-radius:12px;box-shadow:0 18px 44px rgba(20,38,63,.20);padding:14px 16px;font-size:13px;color:#14263f;}
.whpop::before{content:"";position:absolute;top:-7px;left:26px;width:13px;height:13px;background:#fff;border-left:1px solid rgba(20,38,63,.14);border-top:1px solid rgba(20,38,63,.14);transform:rotate(45deg);}
.wh-curtain{position:fixed;top:0;right:0;bottom:0;left:0;inset:0;transform:translateY(-101%);pointer-events:none;-webkit-backface-visibility:hidden;backface-visibility:hidden}
html[data-wh-arriving] .wh-curtain{transform:translateY(0)}
#wh-fx{position:fixed;inset:0;z-index:9996;display:none;pointer-events:none;overflow:hidden}
.wh-roundel-layer{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:9997;pointer-events:none}
#wh-roundel{width:61px;height:61px;opacity:0;transform:scale(.4)}
html[data-wh-arriving] #wh-roundel{opacity:1;transform:scale(1)}
#wh-roundel svg{width:100%;height:100%;display:block}
#wh-intro-overlay{position:fixed;top:0;right:0;bottom:0;left:0;inset:0;z-index:10000;display:none;align-items:center;justify-content:center;overflow:hidden;background:#fbf7f7;pointer-events:auto}
html[data-wh-intro] #wh-intro-overlay{display:flex}
.wh-intro-white{position:absolute;inset:0;background:#fff;z-index:3;pointer-events:none}

.wh-intro-stack{display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;z-index:10010}
#wh-intro-roundel{opacity:0;transform:scale(.4)}
#wh-intro-roundel svg{width:100%;height:100%;display:block}
/* ⚠ THE SERIF, NOT THE BODY FACE — CHANGED 28 SEPTEMBER 2026, AT DYLAN'S REQUEST:
   "make it instead the serif font that is used for the h3 headings and quotations,
   whatever that is, without changing the size or weight."

   --serif IS that face: it is written from theme.fonts.heading (see the palette
   variables above), the h3/h4 tier takes its font from the same value unless
   overridden, and resolveQuote returns an EMPTY family for quotations precisely so
   they fall back to it too. So pointing at the variable follows the heading face
   wherever the author moves it, where storing "Aleo" here would have been a snapshot
   that stopped following it the first time they changed their headings.

   Size, weight, tracking and colour are NOT touched by this rule: the runtime writes
   all four from intro.text (12px / 700 / .10em / #1d3552 by default) and they were
   already editable. Only the family was pinned, which is what this changes.

   --wh-intro-face is the author's override, written by the Global tab's font card
   into theme.fonts.intro and emitted beside --serif ONLY when it is set. Unset,
   the serif wins, so an untouched site stores nothing and still gets the face Dylan
   asked for.

   ⚠ A CSS VARIABLE RATHER THAN THE RUNTIME, AND THE FIRST DRAFT HAD IT THE OTHER
   WAY. I first stored this as chrome.intro.text.face and had the overlay's script
   assign line.style.fontFamily, beside the size and weight it already writes from
   the same object. That was the wrong home: the control Dylan asked for is in the
   GLOBAL tab, whose panel writes the theme column under a compare-and-swap, so a
   value in chrome would have made one card write two whole blobs behind a single
   version check. Moving it to theme.fonts.intro also puts it with every other
   family the site chooses, and means the default needs no JavaScript at all. */
/* ⚠⚠ CORRECTED 29 SEPTEMBER 2026 — THIS POINTED AT --serif AND --serif IS NOT THE
   FACE DYLAN NAMED. Keep this note; the mistake is the instructive part.

   He asked for "the serif font that is used for the h3 headings and quotations,
   whatever that is". I reasoned that --serif IS that face, because it is written
   from theme.fonts.heading and the h3/h4 tier takes its font from the same value
   "unless overridden". ON THIS SITE IT IS OVERRIDDEN: theme.headings.sub.font is
   Literata and theme.quote.font is Literata, while theme.fonts.heading is Red Hat
   Display — a SANS. So --serif resolved to a sans-serif and the slogan was set in
   the wrong face, in a variable literally named --serif.

   ⚠ I WROTE THE ESCAPE HATCH INTO MY OWN REASONING AND THEN IGNORED IT. The words
   "unless overridden" were in the note I wrote beside the old rule. What was missing
   was one look at the live theme, which would have answered it in seconds. Reasoning
   from a variable's NAME and a default, about a value whose whole purpose is to be
   overridden, is how this went wrong.

   --h-sub-face is the h3/h4 tier's own face, emitted unconditionally for every site
   from resolveHeadings(), so it always exists and it always IS what h3 paints. The
   quote face is the same value here, which is why one variable satisfies both halves
   of what he asked for. --serif survives only as a last-ditch fallback.

   --wh-intro-face is still the author's override from the Global tab's font card, and
   still emitted only when set, so an untouched site follows h3 wherever it moves. */
.wh-intro-slogan{font-family:var(--wh-intro-face,var(--mn-row-face,var(--body)));font-weight:700;text-transform:uppercase;white-space:nowrap;line-height:1;color:#1d3552}
.wh-iw{display:inline-block;opacity:0}
.wh-iw.wh-icomma{letter-spacing:0}
.wh-isp{display:inline-block;width:.5em}
.wh-intro-curtain{position:fixed;top:0;right:0;bottom:0;left:0;inset:0;z-index:10001;transform:translateY(0);pointer-events:none;will-change:transform}
/* HOW FAR BELOW A MENU ENTRY ANYTHING IT OPENS BEGINS.
   Declared here, in the stylesheet, and READ by positionCallout() rather than
   written down again in the script — because two things now hang off it: the
   audience callout, which is placed in JS, and the classic header's category
   band, which is placed in CSS (see .masthead .navgrp[data-wh-services] > .mm).
   They are the same gesture — a panel dropping from the row — so they must
   start at the same height, and the only way to guarantee that is for both to
   read one number. If the script never runs there is no callout to disagree
   with, and the band keeps the same value from the fallback. */
:root{--wh-anchor-gap:16px;}
.wh-cal{position:fixed;left:clamp(12px,3vw,30px);top:54px;z-index:60;max-width:min(420px,calc(100vw - 24px));background:var(--paper);border-radius:2px;box-shadow:0 16px 40px rgba(15,25,42,.24);font-size:13px;color:var(--ink);transform-origin:20% 0%;transition:transform .24s ease,opacity .24s ease;}
.wh-cal.wh-cal-leaving{transform:scale(1.04);opacity:0;pointer-events:none;}
/* Menu-hover (anchored) mode: sits above the sticky header stack so nothing
   clips its beak, fades in while rising, and has no close button (it closes on
   hover-out). The ::before is a transparent hover bridge across the gap between
   the menu entry and the panel, so the pointer never crosses dead space. */
/* 65: above .topstrip (61) so the panel is never clipped by the strip it hangs
   from, BELOW .drophdr (70) so it can never paint over the sticky header, and
   below .mm (80) so the services megamenu keeps winning. */
.wh-cal.wh-cal-anchored{z-index:65;transform-origin:50% 0%;transition:transform .2s ease-out,opacity .2s ease-out;}
.wh-cal.wh-cal-anchored::before{content:'';position:absolute;left:0;right:0;top:-20px;height:20px;}
.wh-cal.wh-cal-anchored.wh-cal-enter{transform:translateY(8px);opacity:0;}
.wh-cal.wh-cal-anchored.wh-cal-leaving{transform:translateY(8px);opacity:0;pointer-events:none;transition:transform .17s ease-in,opacity .17s ease-in;}
.wh-cal.wh-cal-anchored .wh-cal-x{display:none;}
.wh-cal[hidden]{display:none}
/* THE GATE THE CODE ALREADY CLAIMED WAS HERE. The script gates its
   first-arrival WAAPI pop on prefers-reduced-motion and says it does so
   "because a WAAPI animate() ignores the CSS prefers-reduced-motion rules" —
   but there were no such rules for this panel, so every neighbouring surface
   in this stylesheet honoured the setting and the callout did not. It still
   appears and disappears; it simply stops sliding and scaling to do it. */
@media (prefers-reduced-motion:reduce){
  .wh-cal,.wh-cal.wh-cal-anchored,.wh-cal.wh-cal-anchored.wh-cal-leaving,.wh-cal-beak{transition:none;}
  .wh-cal.wh-cal-leaving,.wh-cal.wh-cal-anchored.wh-cal-enter,.wh-cal.wh-cal-anchored.wh-cal-leaving{transform:none;}
  .wh-cal.wh-cal-scrolled .wh-cal-beak{transform:none;}
}

.wh-cal-beak{position:absolute;top:-8px;left:30px;width:0;height:0;border-left:9px solid transparent;border-right:9px solid transparent;border-bottom:9px solid var(--pink);transition:transform .2s ease,opacity .2s ease;transform-origin:bottom center;}
.wh-cal.wh-cal-scrolled .wh-cal-beak{transform:scaleY(0);opacity:0;}
.wh-cal-hd{display:flex;align-items:center;gap:12px;padding:18px 16px;background:var(--pink);border-radius:2px 2px 0 0;}
.wh-cal-round{width:38px;height:38px;flex:none;line-height:0;display:block;}
.wh-cal-round svg{width:100%;height:100%;display:block;}
.wh-cal-title{font-family:var(--body);font-size:17px;font-weight:400;color:#fff;line-height:1.2;}
.wh-cal-title b{font-weight:700;}
.wh-cal-x{margin-left:auto;background:none;border:0;color:rgba(255,255,255,.9);font-size:20px;line-height:1;cursor:pointer;padding:0 2px;}
.wh-cal-bd{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:12px 16px;}
/* Keep the audience chips together on one row — if the label + chips don't fit,
   the whole chip group drops to the next line rather than one chip peeling off. */
.wh-cal-chips{display:inline-flex;align-items:center;gap:10px;flex-wrap:nowrap;}
.wh-cal-nu{color:var(--muted);font-size:13px;font-weight:500;}
.wh-cal-chip{font-family:inherit;font-size:13px;font-weight:600;border-radius:999px;padding:7px 14px;cursor:pointer;line-height:1;border:1.5px solid;white-space:nowrap;transition:.18s;}
.wh-cal-chip--fill{background:var(--navy-deep);border-color:var(--navy-deep);color:#fff;}
.wh-cal-chip--fill:hover{background:var(--navy-ink);border-color:var(--navy-ink);}
.wh-cal-chip--out{background:transparent;border-color:rgba(29,53,82,.5);color:var(--navy-deep);}
.wh-cal-chip--out:hover{border-color:var(--navy-deep);background:rgba(29,53,82,.06);}

.wh-pulse{animation:wh-pulse 1.1s ease-out 3}
@keyframes wh-pulse{0%{box-shadow:0 0 0 0 rgba(217,113,149,.75)}70%{box-shadow:0 0 0 12px rgba(217,113,149,0)}100%{box-shadow:0 0 0 0 rgba(217,113,149,0)}}

.whpop[hidden]{display:none;}
.whpop p{margin:0 0 10px;}
.whpop .whpop-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.whpop select{font-family:inherit;font-size:12.5px;padding:4px 6px;border:1px solid rgba(20,38,63,.2);border-radius:7px;background:#fff;color:#14263f;}
.whpop button{font-family:inherit;font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;background:var(--navy-deep);color:#fff;border:0;border-radius:999px;padding:7px 14px;cursor:pointer;}
/* THE COOKIE NOTICE — Floating note, approved by Dylan, 23 October 2026.
   A soft white card with no coloured edge. The native markup, action order,
   consent runtime and entrance/dismissal behavior are unchanged. Both actions
   keep the same hit area; phones stack them in reading order. Safe edge insets
   and an independent scroll area keep longer wording usable on short screens. */
.whcc{position:fixed;left:max(28px,env(safe-area-inset-left,0px));right:auto;bottom:max(28px,env(safe-area-inset-bottom,0px));z-index:210;width:min(520px,calc(100vw - 56px));box-sizing:border-box;background:#fff;color:var(--navy-ink);border:0;border-radius:24px;box-shadow:0 16px 55px rgba(20,40,63,.095),0 2px 12px rgba(20,40,63,.03);padding:29px 32px 27px;font-family:var(--body);font-size:16px;line-height:1.55;letter-spacing:0;max-height:calc(100vh - 56px);max-height:calc(100dvh - max(28px,env(safe-area-inset-top,0px)) - max(28px,env(safe-area-inset-bottom,0px)));overflow-y:auto;overscroll-behavior:contain;transition:transform .28s cubic-bezier(.4,0,.2,1),opacity .28s ease;animation:whcc-in .42s cubic-bezier(.2,.7,.2,1);}
@keyframes whcc-in{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:none;}}
/* No entrance animation while leaving: its end state would otherwise outrank
   the dismissal transform. Keep the existing runtime's animation contract. */
.whcc.whcc-leaving{animation:none;transform:translateY(8px);opacity:0;pointer-events:none;}
.whcc[hidden]{display:none;}
.whcc .whcc-t{margin:0 0 13px;font-family:var(--serif);font-weight:400;font-size:24px;line-height:1.2;letter-spacing:-.025em;color:var(--navy-ink);font-style:normal;text-align:left;}
.whcc p{margin:0 0 22px;color:#415063;font-size:16px;line-height:1.55;font-style:normal;text-align:left;letter-spacing:0;-webkit-hyphens:manual;hyphens:manual;overflow-wrap:anywhere;}
.whcc a{color:var(--navy-ink);text-decoration:underline;text-decoration-color:var(--navy-ink);text-decoration-thickness:1px;text-underline-offset:3px;background-image:none!important;}
.whcc a:hover{text-decoration-thickness:2px;}
.whcc .whcc-row{display:flex;align-items:stretch;gap:12px;flex-wrap:nowrap;justify-content:flex-end;}
.whcc button{flex:1 1 0;min-width:0;min-height:46px;font-family:var(--body);font-size:15px;font-weight:600;line-height:1.2;letter-spacing:0;text-transform:none;background:#fff;color:var(--navy-ink);border:1px solid #c5cdd6;border-radius:28px;padding:12px 18px;white-space:normal;overflow-wrap:anywhere;cursor:pointer;transition:background .18s,color .18s,border-color .18s;}
.whcc button.primary{background:var(--navy-ink);border-color:var(--navy-ink);color:#fff;}
.whcc button:hover{background:#e9edf2;color:var(--navy-ink);border-color:#b6c0cd;}
.whcc button.primary:hover{background:var(--navy);border-color:var(--navy);color:#fff;}
.whcc button:focus-visible,.whcc a:focus-visible{outline:2px solid var(--navy-ink);outline-offset:4px;}
@media (max-width:600px){
  .whcc{left:max(16px,env(safe-area-inset-left,0px));right:max(16px,env(safe-area-inset-right,0px));bottom:max(16px,env(safe-area-inset-bottom,0px));width:auto;padding:25px 24px 23px;font-size:15.5px;border-radius:22px;max-height:calc(100vh - 32px);max-height:calc(100dvh - max(16px,env(safe-area-inset-top,0px)) - max(16px,env(safe-area-inset-bottom,0px)));}
  .whcc .whcc-t{font-size:23px;}
  .whcc p{font-size:15.5px;line-height:1.5;}
  .whcc .whcc-row{flex-direction:column;gap:10px;}
  .whcc button{flex:0 0 auto;width:100%;font-size:14.5px;padding:12px 10px;}
}
@media (prefers-reduced-motion:reduce){.whcc{animation:none;transition:opacity .2s;}.whcc.whcc-leaving{transform:none;}}
/* ─────────────── POLICY PAGES ───────────────
 *
 * A policy page reads as: the name of the policy, its title, when it last
 * changed, an optional row of plain reassurances, an image that acts as the
 * threshold into the detail, then the body with each section collapsed.
 *
 * TWO GROUNDS, NO RULES. The summary sits on --paper and the policy body on
 * --white, and that change of ground is the only thing separating them. There
 * is deliberately no hairline anywhere on the page: a document that people
 * already find forbidding does not need more lines drawn across it.
 *
 * EVERY TYPE DECISION COMES FROM THE HUB. The old rules here named --serif and
 * --body directly, which quietly overrode the per-level faces set in Global
 * Styles > Heading styles — so a site with Literata on its sub-headings lost it
 * on exactly the page where sub-headings carry the most weight. These use the
 * same --h-title-*, --h-section-* and --h-sub-* variables the rest of the site
 * does, so a policy page can never drift from the settings again.
 */
.legalpage{padding:0;background:var(--paper);}
.legalpage .wrap{max-width:760px;}
.legalpage h1{font-family:var(--h-title-face,var(--serif));font-size:calc(var(--h-title-scale,1) * 46px);line-height:1.06;margin:0;color:var(--h-title-colour,var(--heading-deep));font-weight:var(--h-title-weight,var(--wt-heading,600));font-style:var(--h-title-style,normal);letter-spacing:var(--h-title-track,var(--tracking-heading,-.01em));text-transform:var(--h-title-case,none);text-wrap:balance;}
.legalpage h2{font-family:var(--h-section-face,var(--serif));font-size:calc(var(--h-section-scale,1) * 20px);line-height:1.3;margin:0;color:var(--h-section-colour,var(--heading-deep));font-weight:var(--h-section-weight,var(--wt-heading,600));font-style:var(--h-section-style,normal);letter-spacing:var(--h-section-track,var(--tracking-heading,-.01em));text-transform:var(--h-section-case,none);}
.legalpage h3,.legalpage h4{font-family:var(--h-sub-face,var(--serif));font-size:calc(var(--h-sub-scale,1) * 16px);font-weight:var(--h-sub-weight,600);font-style:var(--h-sub-style,normal);letter-spacing:var(--h-sub-track,var(--tracking-heading,-.01em));text-transform:var(--h-sub-case,none);margin:24px 0 var(--h-sub-gap,8px);color:var(--h-sub-colour,var(--heading-deep));}
.legalpage p{margin-bottom:14px;color:var(--ink);}
.legalpage p:last-child{margin-bottom:0;}
.legalpage ul,.legalpage ol{margin:0 0 14px 20px;color:var(--ink);}
.legalpage li{margin-bottom:6px;}
.legalpage .legal-updated{color:var(--muted);font-size:13px;margin:18px 0 0;}

/* ── THE PRIVACY NOTICE LAYOUT DYLAN CHOSE, 18 OCTOBER 2026 ──────────────
   "Version 1, revised": the photograph fixed in a left-hand column as on the
   client agreement page, a white page, the title with the opening words as an
   introduction under it, then numbered rows divided by fine lines, each with a
   thin plus that becomes a minus when the row is open. He asked for it because
   the old page was "not elegant at all": headings too big, and pink chevrons
   that did not line up with them.

   ⚠ A ROW HEADING TAKES THE SUB TIER'S SCALE, NOT THE SECTION TIER'S. The hub
   has the section tier at 147%, which is right for a marketing section and
   made every row of a seventeen-section notice a 29px headline. A row is read
   as a list entry, one tier down (as the portal's page already did), so its
   size multiplies the sub tier's scale: about 23px at the hub's 90%. The face
   and weight are still the section tier's, so Red Hat Display stays Red Hat
   Display.
   ⚠ AND EVERY WEIGHT ON A TIER FACE IS THAT TIER'S OWN VARIABLE. The font
   loader fetches only the weights each tier is set to (fontLoading.test.ts), so
   a hard-coded 300 or 400 here would be drawn from a file nobody fetched. The
   numbers are in the body face for the same reason.
   Mockup: /mnt/project-files/mockups/privacy-notice/version-1-revised.html */
.policypage{background:var(--white);}
.policypage .pp-main{min-width:0;}
.policypage .pp-head{padding:clamp(56px,9vw,120px) 0 clamp(40px,5vw,64px);}
.policypage .pp-head h1{font-size:calc(var(--h-title-scale,1) * clamp(46px,5.6vw,80px));line-height:1.03;letter-spacing:-.025em;}
/* The opening words, set as an introduction in the title's face and weight
   (Red Hat Display at 300 on this site). Any later opening
   paragraph (the date line, for instance) steps back to small and muted. */
.pp-intro{margin-top:clamp(26px,3vw,36px);max-width:640px;}
.pp-intro>p:first-child{font-family:var(--h-title-face,var(--serif));font-weight:var(--h-title-weight,var(--wt-heading,600));font-size:clamp(18.5px,1.6vw,21px);line-height:1.55;letter-spacing:-.003em;color:var(--heading-deep);margin:0;}
.pp-intro>*+*{margin-top:18px;font-size:14px;line-height:1.6;color:var(--muted);}

/* The reassurance row. A fourth column would wrap, which is why the authoring
   side caps it at three. */
.pp-promises{padding:0 0 clamp(34px,5vw,52px);}
.pp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(22px,4vw,46px);}
.pp-grid h3{margin:0 0 8px;}
.pp-grid p{font-size:15px;line-height:1.5;color:var(--muted);margin:0;}
@media(max-width:700px){.pp-grid{grid-template-columns:1fr;gap:24px;}}

/* The photograph column. 5:7 like the agreement page; sticky so it stays put
   while the notice scrolls past. On a tablet or phone it becomes a band above
   the title. A page with no photograph set is one column, as before. */
.pp-aside{line-height:0;background:var(--navy-deep,#14283f);}
.pp-aside img{display:block;width:100%;height:clamp(220px,40vw,320px);object-fit:cover;}
@media(min-width:1024px){
  .pp-split{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);align-items:start;}
  .pp-split .pp-aside{position:sticky;top:0;height:100vh;}
  .pp-split .pp-aside img{height:100%;}
  /* The site's 760px column plus its own side padding, so the words keep the
     measure they have on a one-column page rather than losing the padding. */
  .pp-split .wrap{max-width:952px;margin:0;padding:0 clamp(28px,5vw,96px);}
}

.pp-body{padding-bottom:clamp(64px,9vw,140px);}
.pp-list{border-top:1px solid rgba(20,40,63,.12);}

.pp-sec{border-bottom:1px solid rgba(20,40,63,.12);}
/* ⚠ ONE LINE BOX FOR ALL THREE. The number, the heading and the plus each take
   the same 34px line height and sit at the top of the row, so the number and
   the plus line up with the heading's FIRST line whatever its face or length;
   a two-line heading keeps them beside its first line rather than centring
   them between two. */
.pp-sec>summary{cursor:pointer;list-style:none;display:grid;grid-template-columns:52px minmax(0,1fr) 34px;column-gap:14px;align-items:start;padding:26px 0;}
.pp-sec>summary::-webkit-details-marker{display:none;}
.pp-sec>summary::marker{content:"";}
.pp-sec>summary:focus-visible{outline:2px solid var(--pink);outline-offset:4px;}
.pp-num,.pp-sec>summary h2,.pp-tg{line-height:34px;}
.pp-num{font-family:var(--body);font-weight:var(--wt-body,400);font-size:15px;letter-spacing:.04em;color:var(--pink);font-variant-numeric:lining-nums tabular-nums;}
.legalpage .pp-sec>summary h2{font-size:calc(var(--h-sub-scale,1) * 26px);letter-spacing:-.01em;color:var(--heading-deep);text-transform:none;}
/* The plus: two hairlines drawn as backgrounds, the upright one shrinking to
   nothing when the row opens, which leaves the minus. */
.pp-tg{height:34px;background:linear-gradient(var(--heading-deep),var(--heading-deep)) right 8px center/18px 1.5px no-repeat,linear-gradient(var(--heading-deep),var(--heading-deep)) right 16.25px center/1.5px 18px no-repeat;transition:background-size .3s ease;}
.pp-sec[open] .pp-tg{background-size:18px 1.5px,1.5px 0;}
.pp-sec>summary:hover .pp-tg{background-image:linear-gradient(var(--pink),var(--pink)),linear-gradient(var(--pink),var(--pink));}
@media(prefers-reduced-motion:reduce){.pp-tg{transition:none;}}
.pp-secbody{padding:0 48px 40px 66px;font-size:17px;line-height:1.68;}
.pp-secbody p,.pp-secbody li{line-height:1.68;}
.pp-secbody p{margin:0 0 1.05em;}
.pp-secbody>*:last-child{margin-bottom:0;}
.policypage .pp-secbody ul{list-style:none;margin:0 0 1.1em;padding:0;}
.policypage .pp-secbody li{position:relative;padding-left:26px;margin:0 0 .6em;}
.policypage .pp-secbody ul>li::before{content:"";position:absolute;left:2px;top:.85em;width:12px;height:1px;background:var(--pink);}
.policypage .pp-secbody strong,.policypage .pp-secbody b{font-weight:600;color:var(--heading-deep);}
.policypage .pp-secbody h3,.policypage .pp-secbody h4{font-family:var(--h-section-face,var(--serif));font-size:calc(var(--h-sub-scale,1) * 22px);font-weight:var(--h-section-weight,var(--wt-heading,600));line-height:1.3;letter-spacing:-.005em;margin:1.8em 0 .6em;}
.pp-plain{font-size:17px;line-height:1.68;}
@media(max-width:560px){
  .pp-sec>summary{grid-template-columns:36px minmax(0,1fr) 28px;column-gap:10px;padding:20px 0;}
  .pp-num,.pp-sec>summary h2,.pp-tg{line-height:28px;}
  .pp-tg{height:28px;background-position:right 4px center,right 11.25px center;}
  .legalpage .pp-sec>summary h2{font-size:calc(var(--h-sub-scale,1) * 22px);}
  .pp-secbody{padding:0 0 32px 46px;font-size:16px;}
}
.notfound{background:var(--paper);padding:clamp(70px,12vw,150px) 0;text-align:center;}
.notfound .wrap{max-width:640px;}
.notfound .eyebrow{display:block;margin-top:var(--eb-mt-title,0px);margin-bottom:var(--eb-mb-title,16px);}
.notfound h1{font-size:calc(var(--h-title-scale,1) * clamp(34px,5vw,60px));color:var(--h-title-colour,var(--heading-deep));margin-bottom:var(--h-title-gap,20px);}
.notfound .lead{color:var(--muted);font-size:17px;margin:0 auto 28px;max-width:46ch;}
.notfound .nf-actions{display:flex;justify-content:center;}
/* THE WAY IN, FROM THE SITE ROOT.
   These are the only links on an otherwise empty root, and they were inheriting
   nothing: a bare flex row with no gap, so three choices read as one word —
   "FamilySchoolGlobal Mobility". They are the most important control on the
   page and they should look like something you press. */
.wh-routes .wh-router-links{gap:12px;flex-wrap:wrap;margin-top:6px;}
.wh-router-links a{display:inline-block;font-family:var(--body);font-size:14px;letter-spacing:.02em;line-height:1;padding:13px 24px;border:1px solid rgba(20,38,63,.26);border-radius:999px;color:var(--ink);text-decoration:none;background:transparent;transition:background .18s ease,border-color .18s ease,color .18s ease;}
.wh-router-links a:hover,.wh-router-links a:focus-visible{background:var(--navy-deep);border-color:var(--navy-deep);color:#fff;}
@media (prefers-reduced-motion:reduce){.wh-router-links a{transition:none;}}
.legalpage ul,.legalpage ol{margin:0 0 16px 22px;}
.legalpage li{margin-bottom:6px;}
.legalpage blockquote{margin:18px 0;padding-left:16px;border-left:3px solid var(--pink);color:var(--muted);}
.footer-legal{display:flex;gap:14px;flex-wrap:wrap;}
/* ⚠ THE MASTHEAD IS ITS OWN STACKING CONTEXT, AND THAT IS WHAT KEEPS THE STICKY
   BAR ON TOP. Dylan, 1 October 2026: open a masthead submenu at the top of the
   page, scroll down, and the sticky bar slid in UNDERNEATH the panel that was
   already open.

   The arithmetic was doing exactly what it was told. '.mm' is z-index 80 and
   '.drophdr' is 70, and with the masthead positioned statically its panel was
   not confined to it — the two numbers met in the ROOT stacking order, where 80
   beats 70 and the open panel won.

   Giving the masthead a context of its own means every number inside it, 80
   included, is resolved WITHIN the masthead and only the masthead's own 60 is
   compared against the bar's 70. So the bar is above the panel whatever the
   panel's z-index later becomes — which is the half that matters, because the
   next person to raise '.mm' for an unrelated reason should not reopen this.

   ⚠ 60 MATCHES '.masthead.wh-hdr-overlay', WHICH ALREADY SET EXACTLY THIS. The
   overlay variant has carried 'position:absolute;z-index:60' all along; the
   plain masthead simply never had the pair, so the two variants behaved
   differently and only one of them was ever noticed. 'position:relative' is the
   weakest thing that creates the context, and the overlay rule still overrides
   it to absolute. */
.masthead{background:var(--navy-ink);color:#fff;position:relative;z-index:60;}
.masthead .inner{display:flex;flex-direction:column;align-items:center;padding:26px 0 30px;}
/* The brand cluster is one home link: a centred column, no underline sweep. */
.masthead .brandtop{display:flex;flex-direction:column;align-items:center;text-align:center;color:inherit;background-image:none!important;padding-bottom:0;}
.masthead .crest{width:58px;height:58px;}
.masthead .cm{font-size:10.5px;font-weight:700;letter-spacing:.34em;text-transform:uppercase;color:rgba(255,255,255,.72);margin-top:12px;}
.masthead .pe{font-family:var(--serif);font-size:clamp(28px,3.4vw,40px);font-weight:600;color:#fff;letter-spacing:.01em;line-height:1;margin-top:6px;}
/* THE GAP IS A TOKEN, and the items' hit zones close it (see .navgrp-btn::after).
   Visually identical: the same clamp, still applied as gap. What changed is that the
   number is now readable by the rule that has to cover half of it on each side,
   rather than guessed at as a flat -10px — which under-reached at any
   viewport wide enough for the clamp to pass 20px, leaving a dead strip between
   two category items. Crossing it closed one panel before the next opened,
   which is the flicker. */
.masthead{--wh-navgap:clamp(16px,2.2vw,32px);}
.masthead .nav{display:flex;gap:var(--wh-navgap);margin-top:16px;flex-wrap:wrap;justify-content:center;align-items:center;
  /* Up into the 16px above the row. The DOWNWARD reach is measured at runtime
     (__whNavZone) rather than written here: it has to land exactly on the
     panel, which opens at the header's bottom edge, and that distance depends
     on the header's padding, the row's font size and whether the row wrapped —
     none of which a constant can track. This is the fallback for no-JS. */
  --wh-navzone-b:30px; --wh-navzone-t:16px;}
/* ── THE TYPE IN THE MENUS ───────────────────────────────────────────────────
   Six roles, each a --mn-<role>-* set that menuTypeCss emits ONLY when the
   setting has been touched. Every fallback below is the value the rule had
   before these variables existed, so an untouched site is byte-identical —
   menuType.test.ts reads each number back out of this stylesheet and fails if
   the editor's size table has drifted from it.

   The same variable is read by BOTH menu designs wherever they draw the same
   thing, which is why there is one control for a service name and not two. */
.masthead .nav a{font-size:calc(var(--mn-row-scale,1) * 13.5px);font-weight:var(--mn-row-weight,500);color:rgba(255,255,255,.85);letter-spacing:var(--mn-row-track,.02em);}
/* Pink-button menu entries. Must beat every normal nav-link treatment in all
   three headers, including any animated ::after underline. */
/* ⚠ A CHIP, NOT A ROUNDED RECTANGLE, SINCE 28 SEPTEMBER 2026, and the point is
   that there were TWO Enquire buttons on the page disagreeing with each other.
   Dylan, with a screenshot of the masthead row: "the enquire button is currently
   a rectangle with rounded corners... Instead make it a chip shape, same colour
   and everything else being the same otherwise."

   The corner's Enquire (.wh-enq, further down this sheet) has been a 999px chip
   since it stopped being a ring. This one — the SAME word, the SAME pink, six
   inches away in the same header — was 2px, because the two were written months
   apart and nothing compares them. So this is not a restyle; it is the row
   catching up with the corner. Colour, padding, face, weight, size, tracking and
   the hover are all untouched, which is exactly what he asked for.

   ⚠ 999px rather than a computed half-height ON PURPOSE: the pill's height comes
   from its padding and its line box, so any number that tries to be "half of it"
   goes stale the moment the row's type scale is touched. An over-large radius is
   clamped by the browser to the true half-height and cannot be wrong. */
.masthead .nav a.navpill,.drophdr .nav a.navpill{background:#D97195;color:#fff;border-radius:999px;padding:9px 18px;text-decoration:none;border-bottom:0;transition:background-color .15s ease;
  /* A BUTTON, not menu text — but a button that sits IN the row, so its FACE
     follows the row's typeface (one voice across the header) while everything
     that deformed it when the whole role leaked in — size, weight, slant,
     tracking, capitals — stays stated at its own values. Same rule for the
     corner's Enquire pill and audience switch. Its look is the Buttons card's. */
  font-family:var(--mn-row-face,var(--body));font-weight:500;font-style:normal;font-size:13.5px;letter-spacing:.02em;text-transform:none;}
/* Pink nav buttons: hover just fades the fill to a slightly darker pink. */
.masthead .nav a.navpill:hover,.drophdr .nav a.navpill:hover{background:var(--pink-deep);color:#fff;text-decoration:none;border-bottom:0;}

.masthead .nav a.navpill::after,.drophdr .nav a.navpill::after,.masthead .nav a.navpill:hover::after,.drophdr .nav a.navpill:hover::after{content:none;display:none;transform:none;background:none;}
/* ── ⚠ "NOT A {AUDIENCE}?" IS GONE FROM THE CORNER, 27 SEPTEMBER 2026 ───────
   Dylan: *"In the top right of the rendered website for schools, it still displays
   'not a school?' Please remove this bit from the menu of ALL of the different
   audience/region cells."* Asked whether one cell should be the model: *"I am happy
   to remove the masthead's audience switcher... Use the UK families set up as a
   model - I didn't want that to be the exception"*, and then: *"I didn't want it to
   be a special case - I was using UK families as an example, but I want it to apply
   across the board."*

   ⚠ AND THE PREMISE THAT UK FAMILIES WAS A SPECIAL CASE IN THE CODE IS FALSE,
   which is worth recording because it is the thing that would have sent the next
   reader looking for a per-cell rule to generalise. There is none. cornerAud in
   compile.ts drew this button for EVERY cell, gated only on the first-visit popup
   being enabled and there being more than one audience — its own comment said so
   in as many words ("It is NOT gated on the menu layout"). What differed between
   two cells was only which of the HIDING conditions happened to be true when
   somebody looked: html.wh-aud-unknown (the reader had not chosen an audience yet),
   html.wh-nav-tight, or the 560px media query. So the fix is to stop emitting it,
   not to copy a configuration that was never doing the work.

   Every rule that styled it went with it: the active-underline pair, the [hidden]
   entry, the wh-aud-unknown hide, the caret, the two wh-past-navy colour flips, the
   base rule and its hover and focus ring, the wh-fs-open fade-out, the 560px and
   wh-nav-tight hides, the is-dark pair, and the reduced-motion restatement. They are
   named here rather than left as dead selectors because a dead selector that looks
   like a fix is what the note further down this file was written about.

   ⚠ WHAT STILL CHANGES AUDIENCE, checked rather than assumed: the mega menu's
   "Viewing as" chip (a real switcher — .wh-fs-audopt carries data-wh-audience and
   calls window.__whSetAudience), reachable from the burger on every page; the
   first-arrival callout #wh-modal; and the utility strip's audience chip
   (.whdd[data-wh-nav="audience"]) — which, note, exists only where the operator has
   switched the strip's selectors on. */
/* THE LANDMARKS THE MENU LAYOUT OWNS, HIDDEN WHEN IT DOES NOT ASK FOR THEM.
   Every one of these sets its own display value, which beats the user agent's
   [hidden]{display:none} on specificity alone — so setting the attribute from
   the runtime moved nothing at all, silently. Stated here once, where the rule
   that causes the problem is, rather than left for the next control to
   rediscover. */
.wh-enq[hidden],
.navcta[hidden],.wh-fs-cta[hidden]{display:none!important;}
/* A HEADER ROW A REGION HIDES, on a page published once for everyone (Our
   Story, /privacy). Both rows set display:flex, so the attribute alone would
   leave it on screen. Drawn so the swap can reveal it for a region that shows
   it (applyRows); Dylan, 18 October 2026. */
.masthead .nav[hidden],.drophdr .nav[hidden]{display:none!important;}
/* ── NOTHING PRESUMES AN AUDIENCE THE READER HAS NOT CHOSEN ─────────────────
   "Not a family?" is a question with no subject until you are one. A page is
   baked for a cell, and a visitor who has never chosen is only DEFAULTED into
   that cell — so on an ordinary /uk/family page the prompt used to say "Not a
   family?" to someone who had said nothing, while the same page's full-screen
   menu said "Choose audience" and its callout asked "Who are you?". One page,
   two answers to whether the reader had answered.

   A SECOND MECHANISM ON PURPOSE, not a second [hidden] writer. The attribute
   answers "does this menu have this entry?" — the layout's question, written
   by applyLandmarks. This class answers "has the reader chosen?" — a different
   question with a different owner (syncAudPrompt, which already decides the
   wording of every other audience control from it). Two questions, two
   mechanisms, so neither can un-hide what the other hid.

   Assumed at first paint and removed once the cookie has been read, so the
   presumptuous label is never shown and then taken back. */
/* ⚠ THE RULE THAT READ html.wh-aud-unknown IS GONE WITH THE PROMPT and the class
   is NOT. Nothing presentational reads it now, but it is still a true statement
   about the reader, written before first paint and kept in step by syncAudPrompt,
   and the chip's "Viewing as" prefix answers to the same fact (chosenAud) inside
   the script. Kept so the next control that needs to know does not have to invent
   the measurement again; said out loud so nobody spends an afternoon looking for
   what it draws. */

/* --wh-drop-ms is the retract, named once: the dropdown panels inside this
   header time their own exit against it (see .drophdr:not(.show) .mm) and must
   not be able to drift from it. */
/* ⚠ THE SHADOW IS A TOKEN BECAUSE TWO THINGS WEAR IT. The services band that
   opens under this bar takes --wh-bar-shadow as its own, so that the moment the
   band appears — zero pixels tall, its shadow sitting exactly where the bar's
   is — there is no visible hand-over at all. See the OPENING AND CLOSING block
   in the services panel's CSS for why that matters: it is what makes 'never two
   shadows, and never none' a property of the two being the SAME shadow rather
   than of two timings agreeing. */
.drophdr{--wh-bar-shadow:0 8px 24px -16px rgba(20,38,63,.4);
  --wh-drop-ms:.38s;position:fixed;top:0;left:0;right:0;z-index:70;background:#fff;border-bottom:1px solid var(--line);box-shadow:var(--wh-bar-shadow);transform:translateY(-100%) translateZ(0);transition:transform var(--wh-drop-ms) cubic-bezier(.4,0,.2,1);}
.drophdr.show{transform:translateY(0) translateZ(0);}
/* ⚠ EVERY HEIGHT BELOW READS --drop-h, AND THAT IS THE WHOLE POINT.
   Dylan asked to adjust the sticky bar's height and said, correctly, that
   other things depend on it — he guessed the full-screen video and he was
   right. Seven rules across this sheet were written against a literal 64px,
   and one of them (.wh-pc) already carried a comment saying "keep the two in
   step", which is a note asking somebody to remember rather than a mechanism.
   This is the mechanism: change --drop-h and all seven follow. */
.drophdr .inner{display:flex;align-items:center;gap:20px;height:var(--drop-h,64px);}
/* THE ROUNDEL KEEPS ITS SHADOW WHEN A SUBMENU IS OPEN.
   Dylan: "the shadow beneath the logo roundel is still getting cut off by the
   LHS of the submenu... please make the z index of the roundel and its shadow
   above the LHS of the submenu."

   The brand and the nav are siblings inside .inner, and both sat at
   z-index:auto — so the only thing separating them was source order, and the
   nav comes second. The submenu hangs below the header, the roundel's shadow
   falls into the same few pixels, and the panel painted over it. The shadow was
   never clipped; it was covered.

   z-index:2 is the whole fix, and it is the design this header was already
   reaching for: the mega menu slides UNDER the roundel and the name, and over
   the strip beside them. Both headers carry the same roundel and the same
   panel, so both get it. */
/* ⚠ 90, NOT 2, IN THE STICKY BAR — 1 October 2026. Dylan: *"the submenu is
   appearing on top of the LHS logo and its shadow, which it should not. The logo
   should appear on top."*

   The mechanism is a single stacking context. '.drophdr .inner' is
   position:relative with z-index:1 (it has to be, or the paper band above wipes
   across the wordmark), so it is a stacking context and everything inside it is
   ordered against its own siblings. The brand sat at 2 and the panel at 80, both
   inside it, and the panel is a later sibling's descendant — so an overhanging
   logo was painted over by its own menu. Measured: elementFromPoint six pixels
   above the brand's bottom edge answered '.svccat', inside '.mm'.

   ⚠ WHY A SEPARATE RULE RATHER THAN CHANGING THE 2. The masthead's brand has
   nothing to sit above — its panel opens below a centred row, nowhere near the
   logo — and Dylan has asked twice that the masthead keep the layout it has. So
   the masthead keeps 2 and only the sticky bar is raised.

   ⚠ AND 90 RATHER THAN 81. .mm is 80; the burger/corner is fixed at root level
   and outside this context entirely. 90 leaves room for something to be put
   between them deliberately, and nothing in this file sits in 81-89. */
.masthead .brand,.drophdr .brand{position:relative;z-index:2;}
.drophdr .brand{z-index:90;}
.drophdr .brand{display:flex;align-items:center;gap:12px;}
.drophdr .crest{width:44px;height:44px;}
/* THE WORDMARK IS ONE LINE, ALWAYS — in all three headers.
   Left free to wrap, a narrow window broke "Price Education" across two lines
   and started the menu immediately beside the second one. Held to one line it
   instead takes its true width, which is what NAVFIT_JS measures against: the
   row folds into the burger rather than the name of the company breaking in
   half. The masthead's two spans are a deliberate STACK — strapline over
   name — so this holds each of them to a line rather than the pair to one. */
.masthead .pe,.masthead .cm,.drophdr .pe,.wh-fs-hdr .pe{white-space:nowrap;}
.drophdr .pe{font-family:var(--serif);font-size:19px;font-weight:600;color:var(--navy);letter-spacing:-.01em;}
/* THE NARROWEST PHONES KEEP THE ROUNDEL AND DROP THE WORDS (device review,
   18 October 2026). Below 360px (the first iPhone SE, a folded Galaxy Fold,
   small Androids) the 19px wordmark ran 36px under the Enquire button in the
   sticky bar and in the burger sheet's header: the two overlapped. There is no
   size at which both fit, so both show the roundel alone there. From 360px up
   nothing changes. ⚠ Measured with the text wordmark; a picture logo
   is drawn by .wh-lg-img and is not touched here. */
@media(max-width:359px){.drophdr .brand .pe,.wh-fs-hdr .pe{display:none;}}
.drophdr{--wh-navgap:clamp(15px,2vw,28px);}
.drophdr .nav{margin-left:auto;display:flex;align-items:center;gap:var(--wh-navgap);
  /* Fallbacks for no-JS; __whNavZone measures the real downward reach. The two
     differ because the row is not centred on the gap it has to cross. */
  --wh-navzone-b:20px; --wh-navzone-t:18px;}
/* A LABEL NEVER SPLITS. "Our Story" broken over two lines in a 64px sticky row
   is the failure this prevents; the row as a whole may still wrap in the
   masthead, where it is a centred stack under the brand. */
/* ⚠ THE 2px UNDERLINE RESERVE IS ON THE BOTTOM ONLY, AND A FLEX ROW CENTRES THE
   BOX RATHER THAN THE TEXT. The site-wide anchor sweep gives every non-excluded
   <a> padding-bottom:2px to hold its underline; .navpill is excluded, so in
   a row with align-items:center the links carry 2px of bottom padding the
   Enquire button does not, and their text therefore sits 1px ABOVE the button's.
   Dylan, 1 October 2026: "the text of the menu items vertically lines up with
   the text of the 'enquire' button. At the moment, though I may be slightly
   wrong, it looks like the former is ever so slightly higher than the latter."
   He was right, and it was 1px. Balancing the reserve rather than removing it
   keeps the underline exactly where it was. */
/* ⚠ DIRECT CHILDREN ONLY. Written without the > first, this reached every <a>
   in the services panel as well - including the "All of ..." chip, whose
   padding-top went from 9px to 2px and tipped its text out of its own box.
   Caught by measuring the chip in Chromium rather than by looking at it. */
.masthead .nav > a:not(.navpill),.drophdr .nav > a:not(.navpill){padding-top:2px;}
/* ⚠ AND THE SERVICES BUTTON'S LABEL CARRIES THE SAME 2px RESERVE, SO IT GETS THE
   SAME BALANCE (Dylan, 15 October 2026: "The services sticky menu item appears to
   be slightly above the others. Not sure if this is true of the masthead menu
   too?"). It was, in both: .navgrp-lbl holds its underline in padding-bottom:2px
   inside a button that centres its box, and the 1 October balance above reached
   only the plain links, so "Services" sat 1px above Home, Story and the rest.
   Measured in Chromium in both headers: 1px before, 0 after. Scoped to the two
   rows, so the full-screen sheet's own rows are untouched. */
.masthead .nav .navgrp-lbl,.drophdr .nav .navgrp-lbl{padding-top:2px;}
.masthead .nav a,.drophdr .nav a,.navgrp-btn{white-space:nowrap;
  /* Face, slant and capitals for the row, said ONCE for all three elements
     that make it up. Their size, weight and tracking are set beside their own
     colours below, because the two headers stand on opposite grounds. */
  font-family:var(--mn-row-face,var(--body));font-style:var(--mn-row-style,normal);text-transform:var(--mn-row-case,none);}
/* A real gap between the brand and the row it sits beside. */
.drophdr.wh-hdr-nav .inner{gap:clamp(24px,3vw,44px);}
/* TOO TIGHT → the row goes, and the sticky header becomes exactly the
   mega-menu-only design: brand on the left, Enquire + burger in the corner,
   every link still one tap away inside the menu. Set by NAVFIT_JS, which
   measures rather than guesses a breakpoint: whether it fits depends on how many
   categories this cell has and how long their names are, not on the viewport.

   ⚠ IT USED TO GO FROM THE TOP HEADER TOO, AND THAT WAS THE DEFECT DYLAN
   REPORTED ON 28 SEPTEMBER 2026 — N1730. The paragraph that stood here said so
   in as many words, headed "THIS IS DELIBERATE — PLEASE DO NOT FIX IT", and it
   is kept below because its reasoning is half right and the half that is wrong
   is the instructive half.

   What it said: the row is one menu that both headers draw, so hiding it in the
   sticky bar and leaving it in the masthead means the same page offers a set of
   links until you scroll and then takes them away, which reads as the menu
   breaking rather than as a design. One question, one answer, both headers.

   What it cost: the OTHER way round is worse, and it is what a reader actually
   meets. He opened /our-story at a laptop width and there was no menu at all —
   navy, a logo, and then the page. The row was in the markup, every group
   included, hidden by this rule. And the reason it was hidden had nothing to do
   with the masthead: the STICKY bar draws its row beside the brand and the
   category band, so at 1440px it needed 770px and had 594px, failed, and took
   the top row down with it. The masthead itself had room to spare — the same
   paragraph says why, and that is exactly the argument against this rule:
   A HEADER THAT CANNOT FAIL THE MEASUREMENT SHOULD NOT BE HIDDEN BY ANOTHER
   HEADER FAILING IT.

   Nor was the page missing a way in: the fixed corner keeps Enquire and the
   burger at every scroll offset, in both designs, so "the burger is the menu
   everywhere" is still true of the bar once its own row folds.

   So there are TWO facts now, and each hides only what it measured:
     · html.wh-nav-tight      — NO ROW IS ON SCREEN ANYWHERE. Published when the
       document draws no row at all, when the row is empty, when the stylesheet
       is already hiding it (the 900px burger design), and when a LEFT-ALIGNED
       masthead overflows beside its own brand — the one arrangement in which the
       top row really can fail on its own.
     · html.wh-dropnav-tight  — the STICKY bar's row does not fit beside the
       sticky bar's brand. Folds that row and nothing else.
     · html.wh-mastnav-tight  — a dropdown in the MASTHEAD's row cannot lay its
       columns in one row at this width (Dylan, 19 October 2026). Folds the
       masthead's row and nothing else; the sticky bar's own dropdown publishes
       wh-dropnav-tight for the same reason. See crowded() in NAVFIT_JS.
   All three live on the ROOT element rather than on a header. (This said "Both"
   until the third arrived on 19 October 2026.) */
html.wh-nav-tight .drophdr .nav,
html.wh-nav-tight .masthead .nav,
html.wh-dropnav-tight .drophdr .nav,
html.wh-mastnav-tight .masthead .nav{display:none;}
.drophdr .nav a{font-size:calc(var(--mn-row-scale,1) * 13.5px);font-weight:var(--mn-row-weight,500);color:#3c4757;letter-spacing:var(--mn-row-track,.02em);}
/* ── WHY THERE ARE NO ".nav .wh-corneraud" RULES HERE ANY MORE ──────────────
   ⚠ AND SINCE 27 SEPTEMBER 2026 THERE IS NO .wh-corneraud EITHER — Dylan asked for
   the control itself to go (see the block above). This note is kept because the
   MISTAKE in it is general and will be made again by somebody writing a rule for a
   control in the fixed corner: the corner is not in either header's nav, so a
   selector that walks through .nav to reach it matches nothing, silently.

   There were, and they were the reason this bug outlived two attempts at it.

   An earlier fix diagnosed the audience switch's three differences from a menu
   label correctly — asymmetric padding, a tighter line box, side padding eating
   the gap — and then wrote the corrections as .masthead .nav .wh-corneraud and
   .drophdr .nav .wh-corneraud. The button is not in the nav. It is emitted
   exactly once, into the root-level .wh-corner (see cornerAud in compile.ts,
   whose own comment records that the row copy was removed), so every one of
   those rules matched nothing and the switch went on looking exactly as wrong
   as before.

   The corrections now live on .wh-corneraud itself, where they apply: it takes
   the row's line-height and the same padding-bottom:2px the anchor sweep gives
   every nav link, and the space before the corner is the row's own
   --wh-navgap. Measured after: the glyphs land on 22.53, which is the links'
   own, and the gap is 28px, which is theirs.

   Left here as a note rather than deleted quietly, because a dead selector that
   looks like a fix is worse than no fix at all — it stops the next person
   looking. */
/* A header CATEGORY dropdown — a wide "megamenu callout": a light panel with a
   pink pointer + top accent, the category's numbered service list on the left
   and a feature-photo panel on the right. Its own light ground guarantees the
   rows read on BOTH the navy masthead and the white drop header. */
/* SIGNATURE services carry the roundel and the word, in the menus exactly as in
   the Services list block. One rule for both designs: the classic dropdown's
   rows and the full-screen menu's rows are different markup, same mark. */
/* The roundel and the word are one piece that wraps as a whole, never apart
   (Dylan, 12 October 2026). Every menu that shows the mark wraps it in .wh-sigm. */
.wh-sigm{white-space:nowrap;}
.nrow .wd-crest,.wh-fs-row .wd-crest{display:inline-block;width:13px;height:13px;margin-left:8px;vertical-align:-2px;filter:drop-shadow(0 2px 4px rgba(8,16,34,.2));}
.nrow .wh-sig,.wh-fs-row .wh-sig{font-family:var(--body);font-size:9px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--pink);margin-left:4px;white-space:nowrap;}
.navgrp{position:relative;display:inline-flex;align-items:center;}
/* ── THE GAP BETWEEN MENU ITEMS BELONGED TO NOBODY, AND THAT WAS TWO BUGS ──
   Dylan, 20 September 2026, reported these as separate complaints:

     (a) "if you then switch between menu items which have submenus associated
          with them and a new submenu opens ... the submenu paper effect
          animates in fresh each time (which shouldn't happen)";
     (b) "there is a small gap between menu items that, when you move the
          cursor between them, means the submenu disappears entirely ...
          ensure that the 'active' area around each of the menu buttons
          extends up to edge of the next menu item's active area."

   THEY ARE ONE FAULT. .nav is a flex row with gap:var(--wh-navgap) — 15 to
   28px of space that is inside .nav and inside NO .navgrp. Crossing it makes
   :hover false on the group you left before it is true on the group you are
   going to, so the menu CLOSES. The script's hand-over (mm-hold / mm-warm /
   mm-out) never runs, because from its point of view you left the menu and
   came back — and a cold open plays the full opening by design: the paper
   wipes, the services curtain, the rows rise. That is (a), and it is caused by
   (b). Fixing the dead space fixes both, which is why there is one change here
   and not two.

   ⚠ A PSEUDO-ELEMENT, NOT PADDING, AND THE REASON IS THE UNDERLINE. The
   obvious fix is gap:0 plus padding-inline on each item. But the nav's plain
   links (Home, Insights) carry the site-wide hover underline, which is a
   background gradient sized 0% to 100% of the PADDING BOX — so horizontal
   padding would draw the pink rule wider than the word it underlines, on every
   ordinary link, to fix a submenu. This extends the hit area without touching
   layout, spacing or any box the underline measures.

   Each group reaches half the gap either side, so two neighbours MEET exactly
   in the middle and never overlap: there is no dead space left and no ambiguity
   about which item owns a pixel.

   ⚠ AND THE FIRST ATTEMPT AT IT WAS WRONG, IN A WAY THAT SHIPPED.
   On 20 September I added this:

     .navgrp::before{position:absolute;top:0;bottom:0;
       left:calc(var(--wh-navgap)/-2);right:calc(var(--wh-navgap)/-2);}
     .navgrp > *{position:relative;z-index:1;}

   and Dylan came back with "when I open the mega menu in the published
   website, there appears to be a slightly bit of flicker".

   The rule reads as "extend each group half a gap either side". It is not,
   because of a line 170 further down this file: .navgrp[data-wh-services]
   {position:static;} — and EVERY .navgrp the compiler emits carries
   data-wh-services (compile.ts:1785; there is no other .navgrp in the site).
   So the group is never a containing block, and the pseudo-element resolved
   against .drophdr .inner instead: a transparent box over the ENTIRE header
   row, one per category, each making its own .navgrp:hover true wherever the
   pointer was in the header. Every panel's open condition true at once, with
   the hover-intent delay and the hand-over markers arbitrating — which is
   exactly what a flicker is.

   The comment on .navgrp-btn::after below already knew this trap ("it hangs
   off the BUTTON, not the .navgrp: a services .navgrp is position:static"),
   and that rule ALREADY extends the hit area by half the gap either side. So
   the correct fix for the dead gap was the rule that was already there, and
   what I added was redundant as well as harmful. Removed rather than scoped:
   a second mechanism doing the same job is how the two drift apart later.

   Kept in full because the next person to meet a dead strip in this row will
   reach for the same three lines. */
/* ── THE DEAD STRIP UNDER A MENU ITEM ────────────────────────────────────────
   A category's panel opens below the HEADER, not below the label, so between
   the two sat the header's own padding: a band that is neither the item nor the
   panel, and crossing it — the one path a pointer actually takes — closed the
   dropdown. The item's hit area is extended to meet the panel instead, with a
   pseudo-element rather than real padding, so nothing about the layout moves
   (real padding would push .mm's top:100% down with it).

   It hangs off the BUTTON, not the .navgrp: a services .navgrp is
   position:static so its full-width panel can size to the header, which would
   make an absolutely positioned child resolve against the header and stretch
   across every other nav item, swallowing their clicks. Sized off a token each
   header sets, so it reaches the panel and stops there. */
.navgrp-btn{position:relative;}
.navgrp-btn::after{content:"";position:absolute;
  /* HALF THE GAP EACH SIDE, so two adjacent items' zones meet exactly in the
     middle with nothing between them. */
  left:calc(-0.5 * var(--wh-navgap,20px));right:calc(-0.5 * var(--wh-navgap,20px));
  top:calc(-1 * var(--wh-navzone-t,0px));bottom:calc(-1 * var(--wh-navzone-b,0px));}
/* ── EVERY ITEM IN THE ROW OWNS THE STRIP ABOVE AND BELOW IT, NOT ONLY SERVICES ──
   Dylan, 18 October 2026: with no submenu open, hovering anywhere directly
   under a menu item, or in the header immediately above it, is "probably going
   for the menu item immediately above", and the space between two items splits
   at its midpoint, each half going to the nearer item. The submenu button
   already had that reach (above); a plain link had only its own box, so the
   strip under "Our Story" was dead and the reader had to find the text.
   The same reach, from the same measured tokens (__whNavZone): half the gap
   each side, so neighbouring zones meet in the middle, and down to the header's
   bottom edge. The link's own measured reach (--wh-navzone-lb/-lt) is used where
   the runtime has set it, because a link's box does not sit exactly where the
   button's does and a zone that overhung the header would eat clicks on the page.

   ⚠ NOT WHILE A SUBMENU IS OPEN. Then the strip under the other items is the
   lane the reader crosses to reach the panel (see watchLane() in MEGAMENU_JS),
   and only another item's text, plus a few pixels, may claim the pointer.
   .wh-mm-open is set on the row by the runtime for exactly as long as one of
   its submenus is open, and takes these zones away. The Enquire pill is left
   as it is: it is a button with a box of its own. */
.masthead .nav > a:not(.navpill),.drophdr .nav > a:not(.navpill){position:relative;}
.masthead .nav > a:not(.navpill)::after,.drophdr .nav > a:not(.navpill)::after{content:"";position:absolute;
  left:calc(-0.5 * var(--wh-navgap,20px));right:calc(-0.5 * var(--wh-navgap,20px));
  top:calc(-1 * var(--wh-navzone-lt,var(--wh-navzone-t,0px)));
  bottom:calc(-1 * var(--wh-navzone-lb,var(--wh-navzone-b,0px)));}
.masthead .nav.wh-mm-open > a::after,.drophdr .nav.wh-mm-open > a::after{content:none;}
.navgrp-btn{background:none;border:0;padding:0;margin:0;font:inherit;
  /* AFTER the font shorthand above, which resets family and slant to inherit and
     would otherwise undo the two the shared rule above sets — same specificity,
     later in the sheet, so it wins. The result would have been a row whose
     plain links moved to a chosen face while the category buttons beside them
     did not. (Size and weight were always restated here for the same reason;
     text-transform survives, because the shorthand does not reach it.) */
  font-family:var(--mn-row-face,var(--body));font-style:var(--mn-row-style,normal);
  font-size:calc(var(--mn-row-scale,1) * 13.5px);font-weight:var(--mn-row-weight,500);letter-spacing:var(--mn-row-track,.02em);line-height:inherit;cursor:pointer;display:inline-flex;align-items:center;gap:5px;color:inherit;}
.masthead .navgrp-btn{color:rgba(255,255,255,.85);}
.drophdr .navgrp-btn{color:#3c4757;}
/* The same pink hover-underline sweep the other nav links have, under the label. */
.navgrp-lbl{--ul-c:transparent;background-image:linear-gradient(var(--ul-c),var(--ul-c));background-repeat:no-repeat;background-position:0 100%;background-size:0% 2px;transition:var(--ul-out);padding-bottom:2px;}
.navgrp:hover .navgrp-lbl,.navgrp:focus-within .navgrp-lbl{background-size:100% 2px;--ul-c:#d97195;transition:var(--ul-in);}
.navgrp-caret{font-size:9px;opacity:.7;transition:transform .2s ease;}
.navgrp:hover .navgrp-caret,.navgrp:focus-within .navgrp-caret{transform:rotate(180deg);}
/* ── megamenu panel (product-owner spec — geometry is load-bearing) ── */
/* The panel's shadow is a FILTER, not a box-shadow, so it follows the pointer's
   triangle as well as the panel. The cost: a 40px blur offset only 16px down
   reaches ~24px ABOVE the panel — over the header — where it tinted the white
   behind the hovered label a shade darker than the white beside it. Clipping the
   filter's output at the pointer's tip (-15px, its exact top) keeps every bit of
   shadow the panel and pointer need and lets the header keep its own white. The
   generous side/bottom allowance leaves the rest of the blur untouched. */
.mm{position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:14px;z-index:80;filter:drop-shadow(0 16px 40px rgba(15,25,42,.24));clip-path:inset(-15px -70px -70px -70px)}
.ptr{position:absolute;top:-15px;left:50%;transform:translateX(-50%);width:40px;height:17px;display:block;z-index:3;overflow:visible}
.mm-body{background:var(--paper);border-radius:2px;position:relative;z-index:1;overflow:hidden}
.mm a{text-decoration:none}
/* LHS column sizes to its widest service row (max-content) so names never wrap;
   the nlist min-width below is the floor and the panel grows rightward. */
.split-r{display:grid;grid-template-columns:max-content 246px}
.nlist{padding:14px 12px;min-width:300px}
.nrow{display:grid;grid-template-columns:52px 1fr;align-items:baseline;gap:0 10px;padding:10px 12px;border-radius:2px;transition:background-color .15s}
.nrow:hover{background:#fff}
.nno{font-family:var(--serif);font-weight:700;font-size:12px;color:var(--pink);white-space:nowrap}
.nbody{min-width:0}
.ntl{display:block;font-size:13.5px;font-weight:500;color:var(--navy);line-height:1.3}
.feat{padding:20px 18px;display:flex;flex-direction:column;gap:9px}
.feat-photo{background:var(--navy-deep);color:#fff;align-items:center;text-align:center}
.fcirc{display:block;width:118px;height:118px;border-radius:50%;background:var(--nav-photo) center/cover no-repeat;box-shadow:0 0 0 2px var(--pink)}
.ft{font-family:var(--serif);font-size:16px;font-weight:600;line-height:1.22}
.mm .btn{display:inline-flex;align-items:center;gap:8px;font-weight:600;font-size:12.5px;padding:9px 16px;border-radius:2px;margin-top:3px;white-space:nowrap;flex:0 0 auto}
.mm .btn-pink{background:var(--pink);color:#fff}
.mm a:hover .ul{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in)}
@media (max-width:900px){.split-r{grid-template-columns:1fr}.nlist{min-width:min(300px,78vw)}}
/* ── integration: hover/focus visibility, hover bridge, clamp + solo ── */
/* The panel is hidden until the group is hovered or holds keyboard focus; it
   fades in. The transform (translateX(-50%)) is kept for centring; the runtime
   overrides it inline only when it must be clamped to the viewport. */
/* WHY THERE IS NO pointer-events:none HERE, AND WHY THAT IS THE WHOLE FIX.
   The panel already stayed on screen for the length of its fade-out — but
   pointer-events flipped to none the instant the pointer left the trigger,
   so for those milliseconds it was visible and untouchable. Moving down into
   it could not re-acquire :hover, and it closed in your face. Every attempt to
   solve that with an invisible bridge either failed to cover the gap or
   covered the other nav items with a strip that swallowed their clicks.
   Nothing needs to be laid over anything: let the panel itself stay hittable
   while it fades, and crossing the gap lands on the panel. visibility:hidden
   already makes it untouchable once the fade has finished. */
.mm{opacity:0;visibility:hidden;
  transition:opacity var(--wh-mm-exit,160ms) ease,visibility 0s var(--wh-mm-exit,160ms)}
/* ── THE CLOSE IS A FADE, AND NOTHING ELSE ─────────────────────────────────
   ⚠ 28 September 2026. Dylan: "when you close a submenu completely … the
   submenu doesn't seem to fade out gracefully. The LHS paper-coloured column
   also seems to get smaller/in reverse of how it entered. Please remove this.
   That expand effect should only be on entry, never on close."

   He is describing four things happening at once, and MEASURED in Chromium
   (1920x1000, a settled panel, pointer moved off the row) they are:

     t      panel opacity   paper column clip       services curtain   first row
     23ms       1.000       inset(0 0% 0 0)         inset(0 0 0%)      1.00, y 0
     55ms       0.685       inset(0 14.9% 0 0)      inset(0 0 17.6%)   0.65, y +12
     86ms       0.295       inset(0 64.5% 0 0)      inset(0 0 69.4%)   0.26, y +25
    120ms       0.099       inset(0 89.4% 0 0)      inset(0 0 92.1%)   0.08, y +31
    189ms       0 hidden    inset(0 100% 0 0)       inset(0 0 100%)    0.00, y +34

   So the paper column's right edge races leftward, the services column's
   bottom edge races upward, and every row slides 34px back down — all of it
   while the panel is still between a third and two thirds opaque, so all of it
   is seen. Worse, the closing curve is front-loaded (cubic-bezier(.4,0,.2,1)):
   at 86ms the paper is already 64% gone while the panel is still 30% there, so
   what you actually see is the coloured column snapping away and a ghost fading
   after it. That is the whole of his complaint, and "in reverse of how it
   entered" is literally true — the closed state carried the transition, so
   removing :hover played the opening backwards.

   THE FIX IS A DELAY, NOT A SHORTER DURATION.
   A transition is chosen from the AFTER-CHANGE style, so a declaration written
   on the closed state governs the way OUT and one written on the open state
   governs the way in. The closed state therefore keeps a transition — but a
   zero-duration one, delayed by exactly the panel's own fade. Each property
   holds its OPEN value for the whole fade and then jumps back while the panel
   is already invisible. Entry is untouched: the open rules declare their own
   .42s/.38s/.78s journeys and those are what the way in reads.

   --wh-mm-exit IS THE PANEL'S FADE, SAID ONCE. The hold and the fade must be
   the same number or either a reversal shows through or the reset lands on a
   panel still on screen; two literals agreeing is how they drift. It is
   declared on .wh-hdr-nav — a class the compiler puts on a header exactly when
   that header draws a row — so the panel, the bands, the rows and the drop-in
   header's paper band all read the one value.

   RE-OPENING INSIDE THE HOLD IS BETTER THAN IT WAS, not worse: the pending jump
   is abandoned (the after-change value becomes the open one again) and the
   panel fades back in with its paper already full, where before it re-wiped
   from wherever the reversal had got to. */
.wh-hdr-nav{--wh-mm-exit:160ms;}
/* HOVER INTENT. A pointer crossing the row on its way somewhere else is not a
   request to open anything, so the FIRST panel waits a moment before it appears.
   Once one is open the reader has plainly decided, and moving along the row must
   feel immediate — so the runtime arms the row (.mm-armed) and the delay goes to
   zero until they leave it. Expressed as a token so the timing lives here in the
   stylesheet and the script only has to say when, not how long. */
/* ⚠ 120ms, AND THIS NUMBER HAS NOW BEEN SET THREE TIMES BY THE SAME PERSON. The
   history matters more than the value, because the two instructions read as
   opposites and only the dates separate them.

   It was 170ms. On 27 September 2026 Dylan said: "there appears to be a slight
   delay between hovering over any of the top menu items and the submenu beneath
   appearing … I think I may have previously added that delay purposefully, but
   I'd now like to remove it." It went to 0ms.

   On 1 October 2026 he asked for it back, having met what it was for: "at
   present, it's easy if you accidentally move your cursor over the 'our
   services' item in the menu within the masthead for the submenu to appear
   briefly as a flicker, when you weren't intending to open it. Make it so that
   this can't happen — there should be a small but deliberate minimum amount of
   time you have to hover over it for it to open."

   ⚠ 120 RATHER THAN THE OLD 170, deliberately, and the difference is the whole
   of what he is balancing. 170ms was what he called "a slight delay" and asked
   to remove; the thing he is asking for now is a floor under an ACCIDENT, and an
   accidental pass across a 110px label at any ordinary pointer speed is well
   under 120ms. So this is the smallest number that does the job he describes
   rather than a return to the one he rejected.

   ⚠ AND IT IS THE STYLESHEET THAT OPENS THE PANEL, which is why this is a token
   and not a timer in the script. '.navgrp:hover .mm' is what makes it visible,
   delayed by this; the runtime's open() only sets aria-expanded and arms the
   row. A JavaScript wait here would hold the panel's state back while CSS showed
   it anyway. Keyboard focus is exempt in the stylesheet, two rules below, and
   stays instant: a caret does not travel across a row by accident.

   ⚠ THE SWAP DELAY BELOW IS A DIFFERENT THING AND STAYS. That one governs moving
   from an ALREADY-OPEN panel to another, and it exists because crossing a
   neighbouring label on the way to the open panel is not a request to change
   panels. It is still the longer of the two, which is the relation its own
   comment claims and which 0ms had quietly broken. */
.nav{--wh-mm-delay:120ms;
  /* AND HOW LONG A SWAP TAKES TO BE MEANT. Dylan: "if a submenu is open, you
     should have to be hovering over another submenu for ever so slightly longer
     than you do usually to open that other submenu." Crossing a neighbouring
     label on the way to the open panel is not a request to change panels, and
     it used to change them instantly — the row arms after the first opening and
     the delay went to nought, so every pass flashed a panel nobody asked for.
     Slightly longer than the cold delay above, deliberately: a deliberate move
     to a different category still feels immediate, and a pass through does
     nothing at all. The panel being LEFT is held for the same window by the
     runtime (.mm-hold), so the reader never crosses a gap either. */
  /* 180ms since 28 Oct 2026 (Dylan: the wait was "slightly too long", cut by
     25% from 240ms). */
  --wh-mm-swap:180ms;
  /* HOW LONG THE BAND'S LOWER EDGE TAKES to travel from one category's height
     to the next. The runtime reads this value rather than carrying its own
     copy — height cannot be transitioned from auto, so the two heights have
     to be measured in script, but how long the journey takes is a matter of
     design and belongs here with the rest of the timing. */
  --wh-mm-hms:260ms;
  /* And how fast the OUTGOING category's name clears, which is also how long
     the arriving one waits before it starts. Quick, because nothing can arrive
     in its place until it has gone. */
  --wh-mm-fade:120ms;}
.nav.mm-armed{--wh-mm-delay:var(--wh-mm-swap,180ms);}
/* The sticky header waits a little longer, and for every kind of item (Dylan,
   26 October 2026, "approx 0.3 seconds"; 28 October, cut by 25% to 225ms):
   the runtime's aimAt() reads this same token for items without a submenu. */
.drophdr .nav{--wh-mm-swap:225ms;}
/* While a submenu is open there, a plain item does not look chosen until the
   wait is served and the submenu has closed (the row loses .wh-mm-open). */
.drophdr .nav.wh-mm-open > a:not(.navpill):hover{background-size:0% 2px!important;--ul-c:transparent!important;}
.navgrp:hover .mm{opacity:1;visibility:visible;
  transition:opacity .14s ease var(--wh-mm-delay,0ms),visibility 0s var(--wh-mm-delay,0ms)}
/* Keyboard focus is never accidental — it opens at once, whatever the row's
   state, and this rule sits after the hover one so it wins on a tie. */
.navgrp:focus-within .mm{opacity:1;visibility:visible;transition:opacity .14s ease,visibility 0s}
/* THE PANEL BELONGS TO THE HEADER, SO IT LEAVES WITH THE HEADER.
   Scrolling back to the top retracts .drophdr by translateY(-100%) — its own
   height, 64px. The panel hangs 100% BELOW that, is four hundred pixels tall,
   and rides the same transform: 64px up is nowhere near off-screen, so it was
   left sitting in the middle of the page attached to a header that had gone.
   Hover could not end it either, because the pointer was on the panel.
   pointer-events go at once, so nothing can be clicked on the way out and the
   pointer stops counting as over it — which is also what ends the hover, and so
   closes the panel for real: the trigger's aria-expanded goes false and the row
   disarms, rather than the panel merely being made invisible.

   THE FADE STARTS WITH THE RETRACT, NOT AFTER IT.
   It used to wait out the header's full .38s, on the reading that the panel
   should leave once the header had gone. But the header is 64px tall and the
   panel is several hundred: translateY(-100%) moves BOTH up by 64px, which
   takes the header off the top edge almost at once and leaves the panel sitting
   in plain view, at full opacity, for the rest of the retract. What that looked
   like is the sticky bar vanishing and its submenu staying behind — the thing
   the delay was meant to prevent.
   So they leave together. The panel's normal fade is well finished before the
   header's .38s slide is, which is the right way round: the submenu closes as
   the bar goes, and nothing is ever left over it.

   ⚠ 28 September 2026 — AND IT IS NOW THE SAME NUMBER AS THAT NORMAL FADE, said
   once. This rule carried its own literal .16s, which happened to equal the .mm
   closed state's .16s and was held there by nothing at all. The whole point of
   the sentence above is that this exit is measured against the panel's ordinary
   fade, so it reads --wh-mm-exit like everything else that has to keep step with
   it. The relation to the header's own .38s retract is unchanged: a 160ms fade
   still finishes well inside it, and the token would have to be raised past
   380ms before that stopped being true — which is what the sentence above is
   for. The fallback covers a drop-in header that draws no row, and such a header
   has no mega menu to close. */
.drophdr:not(.show) .mm{opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity var(--wh-mm-exit,160ms) ease,visibility 0s var(--wh-mm-exit,160ms)}
/* Escape force-closes even while hovered/focused, until the pointer/focus leaves. */
.navgrp.mm-shut .mm{opacity:0!important;visibility:hidden!important;pointer-events:none!important}
/* .mm-pass: the pointer is only passing UNDER this item on its way to another
   item's open panel (Dylan, 18 October 2026: only landing on a menu item's text
   counts). Script-owned, and it lives only while the pointer is in this group
   and off its label, so it is shut the same way and its label stays unlit. */
.navgrp.mm-pass .mm{opacity:0!important;visibility:hidden!important;pointer-events:none!important}
.navgrp.mm-pass .navgrp-lbl{background-size:0% 2px!important;--ul-c:transparent!important;}
.navgrp.mm-pass .navgrp-caret{transform:none!important;}
/* A HELD sticky panel keeps its label lit and its caret up (Dylan, 26 October
   2026): brushing past other items on the way to it used to fade the underline
   and flip the caret back the moment the pointer left the label, so the menu
   looked as if it were closing while the panel itself stayed. The hold is what
   says it is still open; it ends when another item takes over or the reader
   leaves for the page. .mm-pass still wins (!important). */
.drophdr .navgrp.mm-hold .navgrp-lbl{background-size:100% 2px;--ul-c:#d97195;transition:none;}
.drophdr .navgrp.mm-hold .navgrp-caret{transform:rotate(180deg);}
/* ...and the item being brushed does not light up straight away either: while
   another panel is open (.mm-armed) its underline and caret wait the same swap
   time as its panel, so a quick pass shows nothing and leaves nothing behind. */
.drophdr .nav.mm-armed .navgrp:hover .navgrp-lbl{transition:background-size .35s cubic-bezier(.4,0,.2,1) var(--wh-mm-swap,225ms),--ul-c 0s var(--wh-mm-swap,225ms);}
.drophdr .nav.mm-armed .navgrp:hover .navgrp-caret{transition:transform .2s ease var(--wh-mm-swap,225ms);}
/* Bridge the 14px margin gap so moving from the trigger into the panel keeps
   :hover (the gap is transparent and outside the button's box). */
.mm::before{content:"";position:absolute;left:0;right:0;top:-16px;height:16px}
/* Clamp support: the runtime drives --ptr-left so the pointer stays over the
   trigger when the panel is shifted to fit the viewport. */
.mm .ptr{left:var(--ptr-left,50%)}
/* Kill the site-wide anchor underline sweep on the row (the .ul span keeps its own). */
.mm .nrow{background-image:none;padding-bottom:0}
/* ONE service: drop the wide min-widths so the single row is not marooned. */
.split-r.mm-solo .nlist{min-width:0}
.split-r.mm-solo{grid-template-columns:auto 246px}
@media (prefers-reduced-motion: reduce){.mm{transition:opacity .01s,visibility 0s .01s}.navgrp:hover .mm,.navgrp:focus-within .mm{transition:opacity .01s,visibility 0s}.split-g .mm-cat,.split-g .mm-svcs{transition-duration:.01ms!important;transition-delay:0ms!important;}}
/* The service rows are deliberately NOT in the list above: at this point in
   the stylesheet the override cannot win. See the reduced-motion block beside
   the open-state rule, further down. */
/* In the mobile panel the group is a static, indented list — no floating panel,
   no pointer, no feature photo (a plain services list, as on desktop's old submenu). */
.masthead .inner,.drophdr .inner{max-width:none;}
/* ── THE CATEGORY PANEL (classic header) ──────────────────────────────────
   A band across the whole window rather than a floating card: the category
   name alone on paper to the left, its services as numbered rows on white to
   the right. Everything is scoped to .split-g or to
   .navgrp[data-wh-services], so no other dropdown is touched. */
/* The panel spans the window, so its containing block has to be the header —
   not the .navgrp, which is only as wide as its own label. */
.navgrp[data-wh-services]{position:static;}
.navgrp[data-wh-services] > .mm{left:0;right:0;width:100%;transform:none;margin-top:0;
  clip-path:none;filter:none;box-shadow:0 18px 40px -24px rgb(15 25 42 / .4);}
/* THE HAIRLINE BETWEEN THE BAR AND THE BAND.
   .drophdr carries a border-bottom, which is right against the page and wrong
   against this panel: the band is flush at top:100%, which is the header's
   BORDER-box edge, so the rule drew a line across the seam.
   The panel covers it rather than the header hiding it. One declaration, no
   :has(), no state to keep in step — and the band is a child of the header, so
   it paints over the header's own border whatever the stacking order says.
   Scoped to the drop-in bar because the masthead has no such border and
   positions its band by arithmetic that a stray pixel would falsify. */
.drophdr .navgrp[data-wh-services] > .mm{margin-top:-1px;}
/* AND THE BAND'S OWN TOP BORDER, which sat in exactly the place the header's
   border-bottom did — so covering the header's line (above) only let the band
   draw its own across the same seam, which is the hairline that kept coming
   back. Flush under the bar the band needs no top edge of its own; dropped here
   and nowhere else, so the masthead's FLOATING card keeps its border, where the
   line is the card's own top rather than a seam against a header. */
.drophdr .navgrp[data-wh-services] > .mm .mm-body.split-g{border-top:0;}
/* WHERE THE BAND STARTS IN THE TOP HEADER: level with the audience callout.
   top:100% is the header's own bottom edge, which in the masthead is a good
   way below the row — the header's padding sits between the label and the
   panel, so the band opened noticeably lower than the "Not a family?" callout
   that drops from the same row. Two panels, one gesture, two heights.

   --wh-navzone-b is published per-.nav by zone() in the megamenu script and is
   exactly (header bottom - last label's bottom), so subtracting it lands on the
   row and adding the shared anchor gap lands where positionCallout() puts the
   callout: entry.bottom + var(--wh-anchor-gap). Neither number is written twice.

   NOT IN THE STICKY HEADER. There the row is centred in a 64px bar, so the same
   arithmetic would tuck the band UNDER the header; the drop-in bar is only as
   tall as its row and the band is meant to be flush beneath it. */
/* ⚠ THE TOP MENU'S PANEL IS INSET AND ROUNDED; THE STICKY BAR'S IS NOT.
   Dylan, 27 September 2026: "at present, it extends across the full width of the
   page. The full-width thing is great when it opens from the sticky menu, but not
   so much when it opens from the top menu … make it smaller by the same amount on
   both the left and the right … 2px rounded corners on all sides."

   Why the two differ, so the asymmetry reads as deliberate rather than missed: the
   sticky bar is itself a full-width strip pinned to the viewport, so a panel flush
   to both edges continues the bar. The masthead sits on the page, over content that
   has margins, and a panel running edge to edge under it cuts across that rhythm.

   ⚠ THE INSET IS A TOKEN, not a number buried in a selector, so it can be tuned
   without reading this rule — and it is the SAME clamp the page body uses for its
   own padding (--pad, 22-48px by viewport), which is what makes the panel line up
   with the text above and below it rather than merely being "a bit narrower".

   ⚠ overflow:hidden IS LOAD-BEARING, not tidiness. border-radius alone rounds the
   panel's own box while its children — the split ground, the left column's paper —
   keep their square corners and paint over the curve. The beak is already
   display:none on this panel (see below), so there is nothing that needs to escape
   the box, which is the usual reason not to clip. */
.masthead .navgrp[data-wh-services] > .mm{
  top:calc(100% - var(--wh-navzone-b,30px) + var(--wh-anchor-gap,16px));
  left:var(--wh-mm-inset,clamp(22px,4vw,48px));
  right:var(--wh-mm-inset,clamp(22px,4vw,48px));
  width:auto;
  border-radius:var(--wh-mm-radius,2px);
  overflow:hidden;}
/* THE BEAK, over the trigger — the same shape and colour as the audience
   callout's, so the two panels read as one family. It hangs 9px above the
   band's own top edge, wherever that edge has been placed, so it spans the air
   between the row and the panel in the masthead and sits on the header in the
   sticky bar. The runtime centres it on the hovered label via --ptr-left (see
   clamp()). The floating cards keep their own SVG pointer. */
/* NO BEAK ON THIS PANEL. It used to draw the audience callout's little pink
   triangle over the hovered label, to say "this belongs to that". The panel is
   full-bleed and flush under the header, so it never needed pointing at: it
   fills the width, arrives under the label you are on, and the only thing the
   triangle added was a pink notch interrupting the header's own edge. The
   floating cards keep theirs — they hover clear of the header and do need it. */
.navgrp[data-wh-services] > .mm > .ptr{display:none;}
/* The 16px hover bridge exists to cross the gap under a FLOATING card. This
   panel is flush under the header, so there is no gap to cross — and at full
   width the bridge becomes an invisible strip across the entire header that
   swallows clicks on every other nav item. */
.navgrp[data-wh-services] > .mm::before{display:none;}

.mm-body.split-g{display:block;background:#fff;border-radius:0;border-top:1px solid var(--line);}
/* WHICHEVER SIDE IS TALLER SETS THE PANEL'S HEIGHT. align-items is left at its
   stretch default and neither column carries a height, so the grid row is as
   tall as its tallest item: a category with two services no longer clips a
   roundel and a long title, and a category with twelve still sets the height
   itself. Said out loud because a min-height on either column, added later to
   "fix" a short panel, would quietly take this away. */
.split-g .mm-in{display:grid;grid-template-columns:40% 1fr;align-items:stretch;}

/* Left: anchored to the divider, so the composition stays centred however wide
   the window gets. Longhand padding on purpose — see .mm-svcs below.

   TOP-ALIGNED, NOT CENTRED. The title used to float in the middle of whatever
   height the service list happened to give it, so the same category sat at a
   different height on every menu and the roundel above it would have drifted
   with it. Anchored to the top, the roundel and the title are a fixed piece of
   composition and only the space beneath them changes. */
.split-g .mm-cat{background:var(--paper);display:flex;flex-direction:column;
  justify-content:flex-start;align-items:flex-end;
  padding:30px 56px 32px clamp(22px,4vw,48px);}
/* The roundel sits immediately above the title, on the same right edge. Its
   size follows the title's own scale, so a category set larger takes a larger
   crest with it rather than a fixed disc that stops matching. */
.split-g .mm-cat-rd{display:block;line-height:0;margin-bottom:14px;}
.split-g .mm-cat-rd .crest{display:block;
  width:calc(var(--mn-category-scale,1) * 46px);
  height:calc(var(--mn-category-scale,1) * 46px);}
.split-g .mm-cat-t{font-family:var(--mn-category-face,var(--serif));font-weight:var(--mn-category-weight,600);font-style:var(--mn-category-style,normal);letter-spacing:var(--mn-category-track,-.01em);text-transform:var(--mn-category-case,none);font-size:calc(var(--mn-category-scale,1) * 38px);line-height:1.1;
  /* The second letter-spacing and the colour that used to sit here came AFTER
     the variables above and therefore beat them: the category role's ink and
     spacing moved the sheet's links and left this title behind — the one thing
     a role governing two surfaces must never do. Folded into the variables, at
     the values they already had. */
  color:var(--mn-category-colour,var(--navy));text-align:right;}

/* Right: the list is capped, so rows never stretch across a 2560px screen. */
.split-g .mm-svcs{padding:20px clamp(22px,4vw,48px) 22px 44px;}
.split-g .nlist{padding:0;min-width:0;max-width:676px;}

/* The first track is the numeral. Every row here emits .nno, so 56px is safe;
   a variant without one would drop its title into that track and wrap. */
.split-g .nrow{display:grid;grid-template-columns:56px 1fr auto;align-items:center;
  gap:0 14px;padding:var(--mn-svc-gap,12px) 0;border-bottom:1px solid var(--line);border-radius:0;
  background-image:none!important;padding-bottom:var(--mn-svc-gap,12px)!important;}
/* ── THE "ABOUT" ROW ──
   The way in to the category itself, sitting BENEATH its services: they are
   what the submenu was opened to read, and this is where you go afterwards. It
   is a row like the others — same list, same divider, same arrow — and
   deliberately not a heading: it is somewhere to GO. What marks it out is that
   it is not the Nth of anything, so it has no numeral column at all, and its
   label is set in the body font, uppercase and letterspaced, where a service
   name is serif. */
.split-g .nrow--about{grid-template-columns:1fr auto;}
.split-g .nrow--about .ntl{font-family:var(--body);font-size:10.5px;font-weight:700;
  font-style:normal;
  letter-spacing:.2em;text-transform:uppercase;color:var(--pink-deep,var(--pink));}
/* THE SAME CHIP AS THE SHEET'S, in this band's own vocabulary.
   Chosen once in Global settings and honoured in both menus: an outlined pill
   naming the category, rather than the small uppercase word. It is still a
   .nrow — it keeps its place in the stagger and in the hand-over between
   categories — so what is needed here is only to undo the row's full-width
   grid, its divider and its bottom padding, all of which are set with weight
   the pill has to match. The arrow is a plain glyph, as in the sheet: the
   circled one belongs to a row that spans the panel, not to a chip.
   The 14px ABOVE is what stops it reading as the list's last item. */
/* ⚠ THE CLASSIC BAND'S PILL FOLLOWS THE SHEET'S, AND THAT IS A DECISION RATHER
   THAN A SWEEP. Dylan's item 6 says "in the mega menu". This is the same link, in
   the same position, doing the same job in the other header design, and the
   comment above aboutRow in compile.ts states the rule out loud: "THE CHOICE IS
   THE CATEGORY'S, NOT THE PANEL'S … a reader who has chosen the pill has chosen
   how that link looks — WHEREVER IT APPEARS." Styling only the sheet's copy would
   have created exactly the divergence that rule exists to prevent, and a cell on
   the classic design would show a grey-bordered pill beside a navy one.
   Reversible in one declaration if he wants them to differ; it is written up as a
   numbered decision so he can say so. */
/* ⚠ LIGHTER OUTLINE SINCE 18 OCTOBER 2026, following the sheet's pill (see
   .wh-fs-about--pill) for the reason above; nothing else about it changed. */
.split-g .nrow--pill{display:inline-flex;align-items:center;gap:10px;width:auto;
  grid-template-columns:none;border:1.5px solid rgb(40 69 112 / .35);border-color:color-mix(in srgb,var(--navy) 35%,transparent);border-radius:999px;
  padding:var(--btn-chip-pady,9px) var(--btn-chip-padx,16px)!important;margin-top:14px;
  transition-property:opacity,transform,border-color,background-color,color!important;
  transition-duration:var(--btn-ms,.18s)!important;}
/* THE STANDARD HOVER: the whole thing fills. It used to move its outline
   colour and nothing else, which on a pale ground is a change you have to be
   looking for. This is the same gesture the Enquire button already made, now
   said once and read by every pill — see --btn-fill below. */
.split-g .nrow--pill:hover{border-color:var(--btn-fill,var(--pink));background:var(--btn-fill,var(--pink));}
.split-g .nrow--pill:hover .ntl,.split-g .nrow--pill:hover .npa{color:var(--btn-ink,#fff);}
.split-g .nrow--pill .nbody{display:inline-flex;align-items:center;}
.split-g .nrow--pill .ntl{font-family:var(--body);font-size:var(--btn-chip-size,12.5px);font-weight:600;
  font-style:normal;
  letter-spacing:0;text-transform:none;color:var(--navy-deep,var(--navy));
  background-image:none;padding-bottom:0;}
.split-g .nrow--pill .npa{color:var(--pink);font-size:14px;line-height:1;
  transition:transform var(--btn-ms,.18s) ease;}
.split-g .nrow--pill:hover .npa{transform:rotate(45deg);}
/* NO RULE AT THE FOOT OF THE LIST. :not(.nrow--pill) is load-bearing now that
   About comes last: the pill draws a border on all four sides, and this rule
   both outranks it on specificity and sits after it, so unqualified it would
   quietly rub the bottom off the chip. The :has() clause moves the same
   "no rule at the foot" onto the last SERVICE when the pill follows it — the
   pill is not part of the list, so the list should still end cleanly. Where
   :has() is unsupported the only difference is a hairline above the chip. */
.split-g .nrow:last-child:not(.nrow--pill){border-bottom:0;}
.split-g .nrow:has(+ .nrow--pill){border-bottom:0;}
/* ⚠ :not(.nrow--pill) HERE FOR EXACTLY THE REASON THE COMMENT ABOVE GIVES FOR THE
   BORDER, AND IT WAS MISSING FOR MONTHS ONE LINE BELOW THAT COMMENT.
   Dylan, 27 September 2026: "the same chips in the submenu that opens from the top
   menu and the sticky menu should also have the same design, becoming #D97195
   background and white text/icon on hover."

   They already had the rule for it — .split-g .nrow--pill:hover sets
   background:var(--btn-fill,var(--pink)) forty lines above. It never applied,
   because this rule ties it on specificity at (0,3,0) and sits AFTER it, so a
   later equal wins. Asked which rule was painting the chip, Chromium answered
   .split-g .nrow:hover{background:transparent} and computed rgba(0,0,0,0) with the
   pointer held on the chip.

   ⚠ AND THE SYMPTOM WAS WORSE THAN "NOT PINK". The sibling declaration in the same
   pill block — :hover .ntl,:hover .npa{color:var(--btn-ink,#fff)} — has no
   competitor, so it DID apply: measured going to rgb(255,255,255) on hover. White
   text and an arrow on a transparent chip over paper. Hovering the chip did not
   fail to colour it; it made its label disappear.

   ⚠ WHY THE MEGA MENU'S CHIP WAS FINE ALL ALONG, which is what made this look like
   a missing feature rather than a broken one: .wh-fs-about--pill:hover carries the
   identical declaration and the sheet has no .wh-fs-row:hover{background:...} to
   beat it. Same intent, same colours, one design working and the other not — which
   is exactly what Dylan described.

   Fixed by excluding the chip rather than by moving a rule or adding a class,
   because "a service row's hover is not a fill" is what this rule means and the
   pill is genuinely not a service row. That also makes it match its neighbour one
   line up, which had this treatment and this reasoning already written out. */
.split-g .nrow:not(.nrow--pill):hover{background:transparent;}
.split-g .nno{font-family:var(--mn-number-face,var(--serif));font-weight:var(--mn-number-weight,700);font-style:var(--mn-number-style,normal);letter-spacing:var(--mn-number-track,0em);text-transform:var(--mn-number-case,none);font-size:calc(var(--mn-number-scale,1) * 12.5px);color:var(--mn-number-colour,var(--pink));white-space:nowrap;}
.split-g .ntl{font-family:var(--mn-service-face,var(--serif));font-weight:var(--mn-service-weight,600);font-style:var(--mn-service-style,normal);letter-spacing:var(--mn-service-track,0em);text-transform:var(--mn-service-case,none);font-size:calc(var(--mn-service-scale,1) * 20px);line-height:1.2;color:var(--mn-service-colour,var(--navy));}

/* .arwbtn already gives the circle, the 1.5px border, the centring and the
   transition; only size, colour and whether it shows belong here. The size and
   display are variables so Global menu settings can grow the circle or take it
   off the rows without touching the markup; the glyph tracks the circle at the
   same 15/32 ratio it has today. --mn-arw-d defaults to the .arwbtn display, so
   an untouched theme is unchanged; the value none hides it and the auto grid
   track collapses. */
.split-g .nbtn{display:var(--mn-arw-d,inline-flex);width:var(--mn-arw-size,32px);height:var(--mn-arw-size,32px);background:var(--navy);border-color:var(--navy);color:#fff;}
.split-g .nbtn .gwarr{width:calc(var(--mn-arw-size,32px) * .469);height:calc(var(--mn-arw-size,32px) * .469);display:block;}
.split-g .nrow:hover .nbtn,.split-g .nrow:focus-visible .nbtn{background:var(--pink);border-color:var(--pink);}

/* ── opening: the paper wipes, the services curtain, the rows rise ──────── */
/* Closed — AND THE WAY OUT IS NOT THE WAY IN PLAYED BACKWARDS.
   ⚠ These two lines used to read .16s / .15s cubic-bezier(.4,0,.2,1) with the
   comment "closing is the opening played backwards and faster". That is exactly
   what Dylan asked to have removed on 28 September 2026 — see the measurement
   beside .mm above. Zero duration, held for the panel's own fade: the paper
   stays full and the services stay down while the panel goes, and both reset
   after it is invisible. The easing is gone with the duration because there is
   no longer a journey for it to shape. */
.split-g .mm-cat{clip-path:inset(0 100% 0 0);transition:clip-path 0s linear var(--wh-mm-exit,160ms);}
.split-g .mm-svcs{clip-path:inset(0 0 100% 0);transition:clip-path 0s linear var(--wh-mm-exit,160ms);}
/* !important on transition-property is LOAD-BEARING. .nrow is an anchor, so the
   site-wide anchor sweep — a:not(.btn):not(.wd-item)… nineteen :not()s deep,
   specificity ~(0,19,1) — matches it and sets a background-size transition.
   That outranks anything reasonable written here, leaving opacity and transform
   with no transition at all, and the rows snap in instead of rising. The
   full-screen menu's own rows carry !important for exactly this reason. */
/* AND THE ROWS DO NOT FALL BACK DOWN. Same reasoning as the two bands above:
   they used to slide 34px back and fade over 150ms while the panel was still
   two thirds opaque, which is the third of the four reversals Dylan was
   watching. They hold where they are for the fade and reset unseen. */
.split-g .nrow{opacity:0;transform:translateY(34px);
  transition-property:opacity,transform!important;
  transition-duration:0s!important;
  transition-timing-function:ease!important;
  transition-delay:var(--wh-mm-exit,160ms)!important;}

/* THE GROUND ARRIVES FIRST. The white band is the panel's own background, not
   part of anything that wipes — so it was painted the instant the panel became
   visible, and the wipe, the curtain and the rows all began over a band that
   was already there. It read as the whole white shape appearing at once, with
   decoration afterwards.
   The panel's 140ms fade now lands BEFORE any of it: white fades up, and only
   then does the paper wipe, the services curtain down and the rows rise.
   Closing keeps no delay at all — see the closed state above, where the short
   durations are the closing speed. */
.navgrp:hover .split-g .mm-cat,.navgrp:focus-within .split-g .mm-cat,
.navgrp.mm-hold .split-g .mm-cat{
  clip-path:inset(0 0 0 0);transition-duration:.42s;transition-delay:.14s;
  transition-timing-function:cubic-bezier(.22,1,.36,1);}
.navgrp:hover .split-g .mm-svcs,.navgrp:focus-within .split-g .mm-svcs,
.navgrp.mm-hold .split-g .mm-svcs{
  clip-path:inset(0 0 0 0);transition-duration:.38s;transition-delay:.14s;
  transition-timing-function:cubic-bezier(.22,1,.36,1);}

/* ── AND THE PAPER RUNS UP BEHIND THE BRAND ─────────────────────────────────
   The wipe above fills the panel's left column. It used to stop dead at the
   sticky header's bottom edge, so an open submenu was a coloured block with a
   white strip sitting on top of it: two shapes, one gesture. The same paper now
   continues up through the sticky bar, behind the roundel and the wordmark, so
   the header and the panel read as one surface opening.

   SAME WIDTH, SAME COLOUR, SAME CURVE, BY CONSTRUCTION rather than by two
   numbers agreeing: 40% is the grid column above, var(--paper) is the column's
   own background, and the timings are copied from the rule directly above so
   the two edges travel together. Change one and change both.

   GUARDED ON :has(), and the whole thing lives inside the guard — not just the
   trigger. A browser without :has() gets no band at all, which is exactly the
   header it has today, rather than a band that arrives and never leaves. */
@supports selector(:has(*)){
  .drophdr::before{content:"";position:absolute;top:0;bottom:0;left:0;width:40%;
    background:var(--paper);pointer-events:none;z-index:0;
    /* THE FOURTH REVERSAL, and the one Dylan is most likely to have meant by
       "the LHS paper-coloured column": this is the SAME paper, continuing up
       through the sticky bar behind the roundel and the wordmark, and it was
       un-wiping on close along with the column below it. Held for the panel's
       fade and reset unseen, exactly as .mm-cat is — the two edges travel
       together on the way in and must now stand still together on the way out.
       ⚠ It reads --wh-mm-exit from .wh-hdr-nav, which is on this very element
       when it draws a row; the fallback covers a header that does not. */
    clip-path:inset(0 100% 0 0);transition:clip-path 0s linear var(--wh-mm-exit,160ms);}
  /* The row rides above it. Without this the brand is painted UNDER the band
     and the wipe erases the wordmark on its way past. */
  .drophdr .inner{position:relative;z-index:1;}
  .drophdr:has(.navgrp:hover)::before,
  .drophdr:has(.navgrp:focus-within)::before{
    clip-path:inset(0 0 0 0);transition-duration:.42s;transition-delay:.14s;
    transition-timing-function:cubic-bezier(.22,1,.36,1);}
  /* Moving between submenus, and closing: the panel holds its fill without
     re-animating, so the band must hold too or it wipes out and back on every
     hop along the row. */
  .drophdr:has(.navgrp.mm-warm)::before,
  .drophdr:has(.navgrp.mm-hold)::before,
  .drophdr:has(.navgrp.mm-out)::before{clip-path:inset(0 0 0 0);transition:none;}
  @media (prefers-reduced-motion: reduce){
    .drophdr::before{transition-duration:.01ms!important;transition-delay:0ms!important;}
  }
}
.navgrp:hover .split-g .nrow,.navgrp:focus-within .split-g .nrow,
.navgrp.mm-hold .split-g .nrow{
  opacity:1;transform:none;
  transition-duration:.78s!important;
  transition-timing-function:cubic-bezier(.22,1,.36,1)!important;
  /* THE STAGGER IS THE RHYTHM; the travel is the manner. Lengthening the
     duration and the distance together makes a row arrive more gently without
     changing the beat between one row and the next — so the list still reads
     top-to-bottom at the same pace, it simply stops snapping into place. The
     easing is heavily front-loaded, so a row is legible long before it settles.
     THE BEAT ITSELF is --wh-mm-stag, said once and read by the hand-over rule
     below as well, so opening a panel and moving between panels can never fall
     out of step with each other. 38ms: the list still arrives top-to-bottom
     rather than all at once, but a reader looking at the fourth row is no
     longer waiting a third of a second for it. Each row's own travel — the
     .78s rise, the distance, the easing — is untouched. */
  transition-delay:calc(230ms + var(--i) * var(--wh-mm-stag,38ms))!important;}

/* ── HANDING OVER FROM ONE CATEGORY TO THE NEXT ────────────────────────────
   Every category owns its own panel, so moving along the row is one panel
   leaving and another arriving — and each played its full opening: the paper
   wiped in from the left again, the services curtained down again, over a band
   that had not actually gone anywhere. The two bands are IDENTICAL apart from
   their content and their height, so re-drawing them read as the menu
   rebuilding itself under the pointer.

   What actually differs between the outgoing panel and the incoming one is the
   words and the position of the lower edge. So that is all that moves:

     .mm-out   the panel being left. Its bands stop un-wiping, so it simply
               fades — the fade is the one it already had (the .mm opacity
               transition, 160ms). It is lifted one step in z so it always
               fades ON TOP of the arriving panel, whichever way along the row
               the pointer went; without that, the direction of travel decided
               whether the fade was visible at all.
     .mm-warm  the panel arriving. Its bands are simply there, full extent, no
               wipe and no curtain; its rows wait for the lower edge to settle
               and then rise exactly as they always do.
     .mm-h     on the arriving band while its height is interpolated from the
               outgoing one's to its own. overflow:hidden is what makes that
               read as an edge travelling rather than the content jumping.

   A cold open — nothing was showing — carries none of these classes and is
   untouched: the wipe, the curtain and the stagger are exactly as before. That
   is the whole rule, and it is why these are separate classes rather than a
   tweak to the open state. */
.navgrp.mm-warm .split-g .mm-cat,.navgrp.mm-warm .split-g .mm-svcs,
.navgrp.mm-out .split-g .mm-cat,.navgrp.mm-out .split-g .mm-svcs{
  clip-path:inset(0 0 0 0);transition:none;}
.navgrp.mm-out > .mm{z-index:81;
  /* AND IT CASTS NO SHADOW WHILE IT LEAVES.
     Dylan: moving between menu items, "the contents of the submenus seem to
     flicker slightly, as does the submenu container/the bottom edge of it."
     The bottom edge is the provable half, and it is this rule.
     handOver() puts BOTH bands on the same height for the length of the swap —
     deliberately, so the cover stays whole — and both panels are full-bleed, so
     for those 160ms there are two identical boxes in exactly the same place,
     each carrying .mm's drop-shadow. Shadows do not swap, they ADD: 0.24 over
     0.24 composites to about 0.42 at the peak and eases back to 0.24 as the
     leaving panel fades. That is a dark pulse travelling along the bottom edge
     on every hand-over — the thing Dylan is watching flicker.
     The leaving panel is the one to drop it from: it is on top and on its way
     out, and the panel underneath is the same box in the same place, already
     casting the identical shadow. Nothing changes in what you see except that
     the shadow stops being drawn twice. A cold close is untouched, because
     .mm-out is only ever set by handOver() — a panel closing on its own keeps
     its shadow all the way out. */
  filter:none;}
/* THE TWO BANDS MUST NEVER CROSS-FADE.
   Both are opaque, and both were fading — one out over 160ms, one in over
   140ms — so for the length of the overlap the combined cover was
   a + b(1-a), which peaks BELOW 1 in the middle: about four fifths at the
   worst of it, and the page showed through the seam. That is the flicker.
   Two opaque things swapping places do not need a cross-fade at all. The
   arriving band is simply opaque from the first frame, the leaving one fades
   ON TOP of it, and the cover is 1 throughout by construction.

   DRIVEN BY .mm-warm ALONE, not by :hover. On a mouse the arriving group is
   always hovered at the moment of the hand-over, so :hover was true and the
   panel opaque from frame one. On a touch device (iPad) it is not: tapping the
   next category does not put :hover on that group in the same frame the runtime
   adds .mm-warm and starts the leaving band fading — iOS latches the synthetic
   hover a beat later, and for that one frame .mm-warm:hover matched nothing, so
   the arriving panel fell back to opacity:0 while the leaving one faded on top
   of it. That gap is the flash the reader saw when switching submenus by tap.
   .mm-warm is a script-owned marker that lives ONLY while the pointer or touch
   is within the freshly-opened group — relax() strips it on the way out, a cold
   re-open clears it, shutIn() and .mm-shut force it closed with the header — so
   gating on it alone shows exactly the same panels a :hover gate did on desktop,
   while staying solid from the first frame under touch. */
.navgrp.mm-warm > .mm,
/* .mm-hold is the same idea pointing the other way: the panel being LEFT, held
   in place for the swap window so that crossing a neighbour on the way to it
   does not make it disappear and come back. Like .mm-warm it is script-owned
   and lives only for that window; .mm-shut and the header's retract both carry
   !important, so neither can be defeated by a hold. */
.navgrp.mm-hold > .mm{
  opacity:1;visibility:visible;transition:none;}
/* THE CATEGORY'S NAME CHANGES OVER, rather than being there already.
   With the band no longer fading in, the name inside it would otherwise appear
   in one frame while the old one was still fading out on top — two names in
   one place. The old one clears first and fast; the new one follows it up,
   over the same journey its lower edge is making, so the two halves of the
   panel settle together.
   An animation rather than a transition, deliberately: the class and the hover
   arrive in the same frame, so there is no earlier value for a transition to
   start from — an animation has its own. */
.navgrp.mm-out .split-g .mm-cat-t{opacity:0;transition:opacity var(--wh-mm-fade,120ms) ease;}
.navgrp.mm-warm:hover .split-g .mm-cat-t,.navgrp.mm-warm:focus-within .split-g .mm-cat-t{
  animation:whMmTitleIn var(--wh-mm-hms,260ms) ease var(--wh-mm-fade,120ms) both;}
@keyframes whMmTitleIn{from{opacity:0}to{opacity:1}}
.split-g.mm-h{overflow:hidden;
  transition:height var(--wh-mm-hms,260ms) cubic-bezier(.22,1,.36,1);}
/* The rows are the only thing that still animates on a hand-over, and they wait
   their turn. Same stagger, same manner — just after the edge has arrived. */
.nav .navgrp.mm-warm:hover .split-g .nrow,
.nav .navgrp.mm-warm:focus-within .split-g .nrow{
  transition-delay:calc(var(--wh-mm-hms,260ms) + var(--i) * var(--wh-mm-stag,38ms))!important;}

/* THE SILENCE HAS TO OUTWEIGH THE MOTION, and here that is a matter of source
   order rather than of intent. The open-state rule above is forced, because the
   site-wide anchor underline sweep otherwise wins the transition-property from
   it — and a forced declaration is only beaten by one of higher specificity or,
   at equal specificity, one written later. The reduced-motion block up beside
   the dropdown fade is both earlier AND weaker at (0,2,0) against this rule's
   (0,4,0), so it silenced nothing at all: a visitor who had asked their browser
   to calm things down still got the full staggered rise, and would have got a
   longer one when these durations grew. Restated here, at matching weight and
   after the rule it answers, so that it actually applies. */
@media (prefers-reduced-motion: reduce){
  .split-g .nrow,
  .navgrp:hover .split-g .nrow,.navgrp:focus-within .split-g .nrow,
  .nav .navgrp.mm-warm:hover .split-g .nrow,
  .nav .navgrp.mm-warm:focus-within .split-g .nrow{
    transition-duration:.01ms!important;
    transition-delay:0ms!important;}
  /* The lower edge arrives rather than travels. The runtime still sets the two
     heights — it has no business reading a motion preference — and this is what
     makes them a jump. */
  .split-g.mm-h{transition-duration:.01ms!important;}
  /* The name changes over without the change being animated. */
  .navgrp.mm-out .split-g .mm-cat-t{transition-duration:.01ms!important;}
  .navgrp.mm-warm:hover .split-g .mm-cat-t,
  .navgrp.mm-warm:focus-within .split-g .mm-cat-t{animation:none!important;}
}

/* Full-screen video block: an edge-to-edge looping background video.
   object-fit:cover fills the frame without distortion; navy-ink ground shows
   while the video loads.

   lvh, NOT dvh, AND THAT IS THE SECOND HALF OF A BUG FIXED ONCE BEFORE.
   This was calc(100dvh - 64px). dvh is the DYNAMIC viewport: on a phone it
   grows the moment the browser's URL bar retracts, part-way down the first
   scroll. The media layer was pinned to lvh for exactly that reason (see the
   long note below it — a growing box re-cropped the footage and read as the
   video zooming). But the SECTION was deliberately left on dvh, and the
   overlay — .wh-fsv-in, inset:0, its content centred — is centred in the
   SECTION. So the section grew mid-scroll and everything inside it slid down
   by half that growth, while the video behind it did not move. Dylan, from a
   phone: the roundel and title "seem to shift when I scroll", measured
   against the top of the video.

   Measured in Chromium at 412x915 with a 72px URL bar, the real compiled
   block, roundel offset from the section's top edge:

     dvh   URL bar shown  section 779  roundel 266
           URL bar hidden section 851  roundel 302     ← 36px of drift
     lvh   URL bar shown  section 851  roundel 302
           URL bar hidden section 851  roundel 302     ← none

   Half the URL-bar height, every time, because the content is centred.

   lvh rather than svh: svh is stable too, but it is the viewport with the
   chrome SHOWN, so once the bar retracts the section would be shorter than
   the screen and a strip of the next section would appear under a block whose
   whole job is to fill the view. lvh makes the section exactly as tall as the
   media it already agrees with; the only cost is that the block runs ~72px
   past the fold while the bar is showing, which is invisible on a full-bleed
   video. Plain vh first as the fallback: on iOS that is already the
   large-viewport height, so old WebKit gets the fix too. */
.wh-fsvideo{position:relative;isolation:isolate;width:100%;height:calc(100vh - var(--drop-h,64px));height:calc(100lvh - var(--drop-h,64px));overflow:hidden;background:var(--navy-ink);}
/* Hub preview only: cap the full-screen block to a sensible 16:9 banner so it
   doesn't fill (and overflow) the small preview iframe. The live page keeps the
   100vh rule above — this class is emitted only when compiled for the editor. */
/* ⚠ THREE QUARTERS OF WHAT THE SECTION ALREADY IS, not 75vh. Dylan,
   25 September 2026: "I'd also like to be able to make the full-screen video
   75% of the height rather than 100%". The full-height rule is
   calc(100lvh - var(--drop-h,64px)) - the window MINUS the drop-in header,
   whatever height that bar has been set to - so 75lvh would
   not be three quarters of it, and the gap between the two readings grows as
   the window shrinks. Multiplying the existing expression keeps the header
   subtraction inside the proportion, which is what "75% rather than 100%"
   means when the 100% was never the whole window.
   ⚠ BOTH declarations, in the same order as the base rule: the lvh one
   overwrites the vh one on a browser that understands it, and a browser that
   does not keeps the vh fallback. Writing only the lvh line would leave older
   browsers on the FULL height with no warning. */
.wh-fsvideo.wh-fsv-34{height:calc((100vh - var(--drop-h,64px)) * .75);height:calc((100lvh - var(--drop-h,64px)) * .75);}
.wh-fsvideo.wh-fsv-preview{height:auto;min-height:320px;aspect-ratio:16/9;}
/* inset:-1px, not 0: the section's height is a fractional value
   (calc(100lvh - var(--drop-h,64px)) on a device with a non-integer pixel
   ratio), so the
   media layer's rounded box can fall a fraction of a pixel short of it and the
   navy ground shows through as a hairline — most visibly along the top edge on
   iPad. Overscanning by 1px on every side costs nothing (object-fit:cover is
   already cropping) and guarantees full coverage at any rounding. */
/* THE PICTURE MUST NOT RESIZE WHEN THE SECTION DOES — THE iPAD ZOOM.
   The section USED to be calc(100dvh - 64px), and dvh is the DYNAMIC
   viewport: on iPad it grows the moment the browser's toolbar retracts,
   part-way down the first scroll. (The section is lvh now too — see the note
   on the rule itself — but this reasoning is why the media must never size
   off the section, whatever the section is sized in.) object-fit:cover scales the footage to whatever box it is
   given, so a box that grows by the height of a toolbar re-crops the video
   larger — read on the device as the block quietly zooming in at one point of
   the page, "as if it goes into a focus mode". Nothing was animating it; the
   viewport unit was moving underneath it.

   The MEDIA does not size off the section. It is pinned to the LARGE
   viewport (lvh: the height with the toolbar retracted, which never changes
   mid-scroll) and centred, so it is always at least as tall as the section and
   always at the same scale. As the toolbar comes and goes the section reveals
   a little more or less of a still picture, which is invisible, instead of
   re-scaling all of it, which is not. Plain vh first as the fallback: on iOS that
   is already the large-viewport height, so old WebKit gets the fix too. */
.wh-fsvideo-video,.wh-fsvideo-poster,.wh-fsvideo-fallback{position:absolute;left:-1px;width:calc(100% + 2px);top:50%;transform:translateY(-50%);height:calc(100vh - 62px);height:calc(100lvh - 62px);object-fit:cover;display:block;}
/* The hub preview is a small iframe, where the viewport height means nothing:
   there the media follows its 16:9 box, exactly as it did before. */
.wh-fsvideo.wh-fsv-preview .wh-fsvideo-video,.wh-fsvideo.wh-fsv-preview .wh-fsvideo-poster,.wh-fsvideo.wh-fsv-preview .wh-fsvideo-fallback{top:-1px;transform:none;height:calc(100% + 2px);}
.wh-fsvideo-poster,.wh-fsvideo-fallback{background-position:center;background-size:cover;background-repeat:no-repeat;}
/* The video fades in over the first-frame poster once it is playing, so the
   hand-off from static still to motion is seamless. */
.wh-fsvideo-video{opacity:0;transition:opacity .6s ease;}
.wh-fsvideo-video.is-playing{opacity:1;}
/* ── ZOOM IN ON SCROLL (switched on per block) ────────────────────────────
   Dylan, 15 and 16 October 2026: the video starts smaller with a border of
   colour at the sides and below, and as the visitor scrolls it zooms to full
   width, "the video and all its contents (including the text on top)". The
   top edge stays put; it grows down, left and right. The text follows a
   moment behind the video, at the same speed and to the same size, and
   scrolling back up reverses it. Settings are site-wide (Global → Block
   settings → Full-screen video); the switch is per block.

   ⚠ NOTHING HERE MATCHES A BLOCK WITHOUT THE SWITCH. Every rule hangs off
   .wh-fsv-zoom or the two wrappers, which the block emits only when its
   switch is on. A video without it compiles and paints exactly as before.

   TWO LAYERS, BOTH PINNED TO THE TOP CENTRE, MOVED WITH TRANSFORM ONLY. The
   frame carries the words at the words' size; the media (poster, video and
   darkening) sits inside it with a small extra scale, so the video can run
   ahead of the words while both use one clock. At rest the extra scale is 1.
   ⚠ THE LEAD IS ON THE MEDIA, NOT THE WORDS, ON PURPOSE. The knock-out button
   blends with the footage underneath it; a transform of its own on the words
   would make them a separate group and the pill would turn solid white for
   the length of the zoom.

   The resting small state is set here, behind html.wh-js, so the first paint
   is already small (no flash of a full-width video that then shrinks) and a
   visitor without JavaScript keeps today's full-width video. FSVZOOM_JS takes
   over with inline styles. Reduced motion: full width, nothing moves. */
.wh-fsvideo.wh-fsv-zoom{background:var(--fsvz-pad,var(--paper));}
/* Per block, over the site-wide colour (Dylan, 16 October 2026). Each is
   only on a block whose editor picked it. */
.wh-fsvideo.wh-fsv-zoom.wh-fsvz-bg-white{background:#ffffff;}
.wh-fsvideo.wh-fsv-zoom.wh-fsvz-bg-paper{background:var(--paper);}
.wh-fsvideo.wh-fsv-zoom.wh-fsvz-bg-navy{background:var(--navy-ink);}
.wh-fsvz-frame,.wh-fsvz-media{position:absolute;top:0;left:0;right:0;bottom:0;transform-origin:50% 0;}
.wh-fsvz-media{overflow:hidden;background:var(--navy-ink);}
html.wh-js .wh-fsv-zoom .wh-fsvz-frame{transform:scale(var(--fsvz-start,.6));will-change:transform;}
html.wh-js .wh-fsv-zoom .wh-fsvz-media{border-radius:calc(var(--fsvz-radius,12) * 1px / var(--fsvz-start,.6));will-change:transform;}
@media(prefers-reduced-motion:reduce){html.wh-js .wh-fsv-zoom .wh-fsvz-frame{transform:none;will-change:auto;}html.wh-js .wh-fsv-zoom .wh-fsvz-media{border-radius:0;will-change:auto;}}
/* PHONES: NO ZOOM. Dylan, 17 October 2026: "on phone screen sizes, the zoom
   in animation should not occur; it should just appear on screen at full
   size." 640px is the site's phone breakpoint; tablets keep the zoom.
   ⚠ THE WIDTH IS READ ONCE, AT LOAD, BY BOTH HALVES. The runtime marks each
   section wh-fsvz-flat (phone) or wh-fsvz-live (zooming), and the rules below
   follow the mark, not the live width. Keyed on the width alone, a phone
   turned to landscape (wider than 640px) dropped back to the small resting
   size with nothing left to grow it (Codex on #524). The media query is only
   for the first paint, before the runtime has marked anything. */
@media(max-width:640px){html.wh-js .wh-fsv-zoom:not(.wh-fsvz-live) .wh-fsvz-frame{transform:none;will-change:auto;}html.wh-js .wh-fsv-zoom:not(.wh-fsvz-live) .wh-fsvz-media{border-radius:0;will-change:auto;}}
html.wh-js .wh-fsv-zoom.wh-fsvz-flat .wh-fsvz-frame{transform:none;will-change:auto;}html.wh-js .wh-fsv-zoom.wh-fsvz-flat .wh-fsvz-media{border-radius:0;will-change:auto;}
/* NO PLAY BUTTON, EVER. iOS Safari paints its own grey start-playback badge
   over any <video> it does not consider to have started — in a layer of its
   own that ignores the element's opacity — so on an iPad a play circle sat in
   the middle of the picture until autoplay caught up, then vanished. Nothing
   here is a player: no controls, muted, looping, and a tap has nothing to do.
   The badge was purely a promise the page does not keep.

   ONE SELECTOR PER RULE, deliberately. A browser that does not recognise a
   pseudo-element discards the ENTIRE rule it appears in, so listing these
   together would mean one unsupported name silently taking the others with it.
   These are the WebKit and Blink spellings; each stands or falls alone. */
.wh-fsvideo-video::-webkit-media-controls-start-playback-button{display:none!important;-webkit-appearance:none;}
.wh-fsvideo-video::-webkit-media-controls-play-button{display:none!important;-webkit-appearance:none;}
.wh-fsvideo-video::-webkit-media-controls-overlay-play-button{display:none!important;-webkit-appearance:none;}
.wh-fsvideo-video::-webkit-media-controls-panel{display:none!important;}
.wh-fsvideo-video::-webkit-media-controls-enclosure{display:none!important;}
.wh-fsvideo-video::-webkit-media-controls{display:none!important;-webkit-appearance:none;}
/* The hero card's background video is the same kind of thing and shows the
   same badge — it is decoration behind a heading, not something to press. */
.ph video::-webkit-media-controls-start-playback-button{display:none!important;-webkit-appearance:none;}
.ph video::-webkit-media-controls-play-button{display:none!important;-webkit-appearance:none;}
.ph video::-webkit-media-controls-overlay-play-button{display:none!important;-webkit-appearance:none;}
.ph video::-webkit-media-controls-panel{display:none!important;}
.ph video::-webkit-media-controls-enclosure{display:none!important;}
.ph video::-webkit-media-controls{display:none!important;-webkit-appearance:none;}
/* Optional title overlay. Nothing below is emitted unless a title is set. */
.wh-fsv-scrim{position:absolute;inset:0;pointer-events:none;}
.wh-fsv-scrim.is-bottom{background:linear-gradient(0deg,rgb(15 25 42 / calc(var(--wh-scrim,1) * .72)),rgb(15 25 42 / calc(var(--wh-scrim,1) * .25)) 55%,rgb(15 25 42 / calc(var(--wh-scrim,1) * .05)));}
.wh-fsv-scrim.is-diagonal{background:linear-gradient(120deg,rgb(0 0 0 / calc(var(--wh-scrim,1) * .82)),rgb(0 0 0 / calc(var(--wh-scrim,1) * .4)));}
/* ⚠ A FLAT TINT, WHICH IS THE POINT — the other two are gradients and fade
   away from one edge, so a clip that is bright at the TOP defeats both. Dylan,
   25 September 2026: "a translucent black overlay just to make the text on top
   slightly more visible". .45 is the ceiling at the slider's 100%, below the
   bottom gradient's own .72 foot, so the loudest even wash is still quieter
   than the loudest gradient. The same --wh-scrim multiplier drives it, so the
   existing intensity slider and the existing None both already work on it and
   nothing new had to be taught either control. */
.wh-fsv-scrim.is-even{background:rgb(0 0 0 / calc(var(--wh-scrim,1) * .45));}
.wh-fsv-in{position:absolute;inset:0;display:flex;color:#fff;}
.wh-fsv-in h2{color:#fff;}
.wh-fsv-pad{width:100%;display:flex;padding:clamp(28px,5vw,64px);}
.wh-fsv-corners .wh-fsv-in{align-items:flex-end;}
.wh-fsv-corners .wh-fsv-pad{align-items:flex-end;gap:clamp(34px,5vw,68px);}
.wh-fsv-corners .wh-fsv-rail{display:flex;flex-direction:column;align-items:center;gap:16px;flex:0 0 auto;}
.wh-fsv-corners .wh-fsv-rail i{display:block;width:1px;height:74px;background:var(--pink);}
.wh-fsv-corners .wh-fsv-rail span{writing-mode:vertical-rl;transform:rotate(180deg);font-size:11px;font-weight:700;letter-spacing:.26em;text-transform:uppercase;color:#fff;opacity:.85;}
.wh-fsv-corners .wh-fsv-title{font-size:clamp(32px,4.7vw,60px);max-width:14ch;}
.wh-fsv-lockup .wh-fsv-in{align-items:center;justify-content:center;text-align:center;}
.wh-fsv-lockup .wh-fsv-pad{justify-content:center;}
/* ── THE LOCKUP'S TWO LINES, AS TOKENS ────────────────────────────────────
   Dylan, 25 September 2026: "within the full-screen video block, I'd like to be
   able to change the font of both the top line of text and the bottom
   (including selecting from the fonts we have installed and choosing italics or
   not, sizing, etc) within the global block settings for that block."

   EVERY FALLBACK BELOW IS THE VALUE OF TODAY, so a site that opens no control
   compiles byte-for-byte as it did.

   Size is a MULTIPLIER on the clamp rather than a number of pixels. The design
   is responsive — 23px on a phone, 44px on a desktop — and a flat px value
   would throw that away at one end or the other; a scale moves all three terms
   together, which is what "bigger" actually means here.

   Tracking is carried in 1/1000 em, the same unit the logo studio's --lg-t*
   uses, so a reader moving between the two controls is reading one scale. */
.wh-fsv-lockup .wh-fsv-l1{display:block;font-family:var(--fsv-l1-face,var(--serif));font-style:var(--fsv-l1-style,italic);font-weight:var(--fsv-l1-weight,500);font-size:calc(var(--fsv-l1-scale,1) * clamp(23px,3.4vw,44px));line-height:1.05;letter-spacing:calc(var(--fsv-l1-track,0)*.001em);text-transform:var(--fsv-l1-case,none);}
/* text-indent matches the tracking on purpose: an uppercase line carries a
   trailing letter-space after its last glyph, so a centred line sits half a
   space left of centre without it. Reading the SAME token keeps the two in step
   whatever the setting. */
.wh-fsv-lockup .wh-fsv-l2{display:block;font-family:var(--fsv-l2-face,var(--body));font-weight:var(--fsv-l2-weight,700);font-style:var(--fsv-l2-style,normal);font-size:calc(var(--fsv-l2-scale,1) * clamp(28px,4.5vw,58px));line-height:1.02;letter-spacing:calc(var(--fsv-l2-track,140)*.001em);text-transform:var(--fsv-l2-case,uppercase);margin-top:12px;text-indent:calc(var(--fsv-l2-track,140)*.001em);}
.wh-fsv-lockup .wh-fsv-rule{width:min(520px,74%);height:1px;background:rgba(255,255,255,.32);margin:clamp(20px,2.4vw,30px) auto clamp(18px,2.2vw,28px);}
/* Knocked-out Enquire-style pill: white fill, label blended out with
   mix-blend-mode:screen so the footage shows through the letterforms. The
   blend root is .wh-fsvideo (isolation:isolate) — neither the scrim nor the
   overlay may create a stacking context, so paint order is DOM order.
   The !important pair keeps it out of the site-wide pink underline sweep. */
.wh-fsv-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;height:44px;padding:0 22px;border-radius:999px;background:#fff;color:var(--navy);border:0;font-family:var(--body);font-size:13px;font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;transition:background-color .18s ease,color .18s ease;background-image:none!important;padding-bottom:0!important;}
.wh-fsv-btn:hover{background:var(--pink);color:#fff;}
/* Knock-out: the white pill screens over the footage and the black letters
   knock through it. No ring to recolour any more — the button is the shape. */
.wh-fsv-btn--knock{color:#000;mix-blend-mode:screen;}
.wh-fsv-btn--knock:hover{mix-blend-mode:normal;background:#fff;color:var(--navy);}
@supports not (mix-blend-mode:screen){.wh-fsv-btn--knock{mix-blend-mode:normal;color:var(--navy);}}
/* Pink chip, chosen per block (Dylan, 18 October 2026: "#D97195 pink with
   white text"). The hover deepens it, as the hero's pink chip does, because
   the white button's hover is already this pink. */
.wh-fsv-btn--pink{background:#d97195;color:#fff;}
.wh-fsv-btn--pink:hover{background:var(--pink-deep);color:#fff;}
.wh-fsv-corners .wh-fsv-btn{margin-top:clamp(20px,2.4vw,28px);}
/* THE SHADOW HANGS OFF THE WRAPPER, NOT OFF THE SVG. A drop-shadow on an
   <svg> root is the one place engines disagree about how far outside the
   element's own box the filter may paint: Chromium lets it spread (measured —
   it decays smoothly for 26px past the edge), WebKit clips it to the SVG's
   filter region. The crest is drawn at 88% of its viewBox, so the shadow needs
   about 13px to the side and 19px below more than the box has, and on iPad
   those are exactly the two edges that came out cut. Moving the filter one
   level out, onto a plain div, puts it on an ordinary CSS box where every
   engine inflates the region — and costs nothing anywhere else.
   ⚠ AND THEN NO FILTER AT ALL (17 October 2026). Dylan's screenshot from an
   iPhone showed the wrapper's shadow cut off too, as a darker square around
   the roundel: when the video zooms, the words' layer is scaled on its own
   compositing layer, and WebKit sizes a filter's paint region before the
   scale, so the blur still runs out of room. A box-shadow is not a filter:
   it is painted with the box, and every engine counts it in the layer's
   bounds. So the shadow is now a box-shadow on a round box behind the
   roundel, the size of its disc (88% of the viewBox, r=176 of 200).
   Same offset and colour as the drop-shadow it replaces, and the same blur:
   drop-shadow's length is the blur's standard deviation, box-shadow's is
   twice it, hence 36px for 18px. ⚠ The first try put the box-shadow on the
   <svg> with a negative spread: a box-shadow is never drawn inside its own
   box, and the svg's box is wider than the disc, so it drew a hard ring
   (compared side by side in Chromium). The wrapper is now the roundel's
   width, so the box behind can be placed by percentage.
   ⚠ It assumes a round, opaque disc filling 88% of the box, which both crest
   cuts are. A crest drawn another shape would need this changed. */
.wh-fsv-crest{position:relative;width:var(--wh-crest,88px);margin:0 0 clamp(16px,2vw,26px);}
.wh-fsv-crest::before{content:"";position:absolute;left:6%;top:6%;width:88%;height:88%;border-radius:50%;box-shadow:0 6px 36px rgba(8,16,34,.45);pointer-events:none;}
/* filter:none, or the base .eyecrest shadow stacks under this one; relative,
   so the roundel paints over the disc behind it. */
.wh-fsv-crest .eyecrest{width:var(--wh-crest,88px);height:var(--wh-crest,88px);position:relative;margin:0;filter:none;overflow:visible;}
.wh-fsv-lockup .wh-fsv-crest{margin-left:auto;margin-right:auto;}
@media(max-width:900px){.wh-fsv-btn{height:40px;padding:0 16px;font-size:12px;}}
@media(max-width:600px){
.wh-fsv-corners .wh-fsv-pad{flex-direction:column;align-items:flex-start;gap:14px;}
.wh-fsv-corners .wh-fsv-rail{flex-direction:row;gap:12px;}
.wh-fsv-corners .wh-fsv-rail i{width:34px;height:1px;}
.wh-fsv-corners .wh-fsv-rail span{writing-mode:horizontal-tb;transform:none;letter-spacing:.2em;}
/* ⚠ FOUR SEVENTHS OF THE WIDE TRACKING, not a second flat number. On a phone
   the line is much narrower and .14em pushes it to two lines; .08em was the
   measured answer for the DEFAULT, and .08/.14 is the relationship — so it is
   written as that fraction of whatever --fsv-l2-track says. A setting made in
   the block settings therefore still means something here instead of being
   silently thrown away below 680px, which is what a literal .08em would do.
   140 x .0005714 = .0799996em, which is the old value to four decimals. */
.wh-fsv-lockup .wh-fsv-l2{letter-spacing:calc(var(--fsv-l2-track,140)*.0005714em);text-indent:calc(var(--fsv-l2-track,140)*.0005714em);}
}
.hero{background:var(--navy-ink);color:#fff;padding:clamp(40px,5vw,72px) 0 clamp(60px,7vw,90px);}
.hero-grid{display:grid;grid-template-columns:1.02fr 1fr;grid-template-rows:auto auto;gap:24px 30px;}
.hero .hero-grid{max-width:none;}
.hero-lead{grid-column:1;grid-row:1;align-self:end;}
.hero-lead .eyebrow{color:#fff;opacity:.85;display:block;margin-top:var(--eb-mt-title,0px);margin-bottom:var(--eb-mb-title,20px);}
.hero-lead h1{color:#fff;font-family:var(--h-hero-face,var(--h-title-face,var(--serif)));font-size:calc(var(--h-title-scale,1) * clamp(38px,4.9vw,66px));}
.hero-lead h1 em{font-style:normal;color:var(--pink);}
.hero-lead h1 .hero-h1b{display:block;padding-left:0.9em;}
.hero-lead p{color:rgba(255,255,255,.82);font-size:16px;}
.hero-prow{width:75%;min-width:min(100%,32ch);display:flex;align-items:center;gap:22px;margin:var(--h-title-gap,20px) 0 24px;}
.hero-prow p{flex:1;margin:0;}
.hero-prow .go{flex:none;margin:0;}
.gwarr{width:22px;height:22px;display:block;}
.hero-lead .eyebrow,.hero-lead h1{width:75%;min-width:min(100%,32ch);}
.hero-lead .go{display:inline-flex;align-items:center;gap:14px;color:#fff;font-weight:600;font-size:14px;}
.hero-lead .go .arwbtn{border-color:var(--pink);color:#fff;background:var(--pink);}
.hero-lead .go:hover .arwbtn{background:var(--pink-deep);}
.hero-mini{grid-column:1;grid-row:2;display:grid;grid-template-columns:1fr 1fr;gap:20px;}
}
.hero-mini .card{min-height:clamp(240px,46vh,520px);}
.hero-feature{grid-column:2;grid-row:1 / span 2;}
.card{position:relative;overflow:hidden;border-radius:2px;display:flex;flex-direction:column;justify-content:flex-end;color:#fff;min-height:220px;}
/* top/left/right/bottom longhands first, inset shorthand after: the shorthand
   is Safari 14.1+, so an old iPad (iOS 12-14.0) that ignores it still gets the
   four offsets and the hero image renders instead of collapsing to zero size. */
.card .ph{position:absolute;top:-1px;left:-1px;right:-1px;bottom:-1px;inset:-1px;}
/* Hover zooms the IMAGE only: the card is the frame (overflow:hidden above), so
   its box is untouched — no layout shift. Gated on (hover:hover) so a touch
   device's sticky :hover cannot strand the zoom on; focus-visible is not gated. */
.hero .card .ph{transition:transform var(--wh-hero-zoom-dur,.5s) cubic-bezier(.22,1,.36,1);}
/* Pre-paint gate for the entrance-animated hero image. HERO_ANIM_JS parks the
   start-state (zoom on this element, clip on the card around it) at
   end-of-body; on a slow/early-painting device the image would otherwise flash
   at full size first, then jump to the clip. Hiding it here (only when JS is
   present — html.wh-js is set in <head>) reserves the box silently until the
   script shows it. No-JS never gets .wh-js, so its fully-visible fallback is
   untouched; HERO_ANIM_JS restores opacity:1 on every path (incl.
   reduced-motion and the 9s failsafe).
   ⚠ THIS CLASS STAYS ON .ph AND MUST NOT BE MOVED TO THE CARD, even though
   since 20 September 2026 it is the CARD that is clipped. On the card this
   rule (two classes, one type) is outweighed by the scroll-reveal's
   .js-rv .wh-rv.is-in [data-rv]{opacity:1} (three classes), which the hero
   feature card carries — so the gate would stop gating and nothing would say
   so. See cardOf() in HERO_ANIM_JS. */
html.wh-js .wh-hero-anim{opacity:0;}
/* Gated on a real pointer move (html.wh-hover-armed, set by HOVERARM_JS): if the
   cursor happens to be over a hero image at load, :hover is already true, so the
   image would SNAP to the zoom the instant the entrance settles. Requiring an
   actual pointer move means it only zooms once the visitor moves the cursor —
   and then it eases in via the .ph transition instead of jumping. Clicking still
   follows the link regardless. */
/* (pointer:fine) as well as (hover:hover): defence-in-depth so a touch primary
   pointer can't match even if a stray :hover sticks. The load-bearing guard is
   still wh-hover-armed (HOVERARM_JS only arms on a real mouse pointer), because
   iPad desktop-mode reports both hover and fine with no mouse attached. */
@media (hover:hover) and (pointer:fine){html.wh-hover-armed .hero .card:hover .ph{transform:scale(var(--wh-hero-zoom,1.05));}}
.hero a.card:focus-visible .ph{transform:scale(var(--wh-hero-zoom,1.05));}
@media (prefers-reduced-motion: reduce){.hero .card .ph{transition:none;}.hero .card:hover .ph,.hero a.card:focus-visible .ph{transform:none;}}
.card .ph video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}
.card .c-in{position:relative;z-index:2;padding:22px 24px;}
.card .c-in .tag{margin-bottom:8px;}
.hero .c-in .hero-crest{display:block;line-height:0;margin:0 0 12px;}
.hero .c-in .hero-crest .eyecrest{display:block;width:var(--hero-crest,44px);height:var(--hero-crest,44px);margin:0;filter:drop-shadow(0 2px 6px rgb(8 16 34 / .28));}
@media(max-width:640px){.hero .c-in .hero-crest .eyecrest{width:min(var(--hero-crest,44px),40px);height:min(var(--hero-crest,44px),40px);}}
.card h3{color:#fff;font-size:calc(var(--h-sub-scale,1) * 18px);line-height:1.2;}
/* The two hero mini cards carry a slightly larger main heading than a generic
   card (≈1.35× the 18px base) so they read with more presence beside the
   feature card, without touching the feature card's own heading scale. */
/* ── THE SMALL CARDS' TYPE, AS TOKENS ──────────────────────────────────────
   Every fallback below is what this rule computed before, so a hero that sets
   none of them renders exactly as it does today. The block writes overrides
   onto its own section, so one hero can be quieter without moving another. */
.hero-mini .card h3{font-size:var(--mini-title-size,calc(var(--h-sub-scale,1) * 24.3px));
  line-height:var(--mini-title-lh,1.2);
  font-weight:var(--mini-title-weight,var(--h-sub-weight,var(--wt-heading,600)));
  font-family:var(--mini-title-face,var(--h-sub-face,var(--serif)));
  letter-spacing:var(--mini-title-track,var(--h-sub-track,var(--tracking-heading,-.01em)));
  text-transform:var(--mini-title-case,var(--h-sub-case,none));}
/* A second line on a mini card, matching the feature card's line 3. */
.hero-mini .card h3 .fh3{display:block;font-family:var(--body);font-weight:400;
  letter-spacing:0;line-height:var(--mini-sub-lh,1.45);color:rgba(255,255,255,.86);
  font-size:var(--mini-sub-size,calc(var(--h-sub-scale,1) * 15.5px));
  margin-top:var(--mini-fh3-mt,9px);}
/* Same cap as the feature card's, on the mini cards' own measurement. The
   120px ceiling stays: a mini card's rule was never allowed the full setting,
   and this only ever narrows it further. */
.hero-mini .card h3 .fh3::before{content:"";display:block;
  width:min(var(--hero-rule-w,200px),120px,var(--hero-rule-mw,100%));max-width:100%;height:1px;
  background:var(--hero-rule-col,rgba(255,255,255,.28));
  margin:0 0 var(--mini-rule-mb,8px);}
.hero-mini .card h3 .fh3:first-child::before{display:none;}
/* Mini cards match the feature card's effective bottom gap (30px padding +
   16px h3 margin = 46px), expressed the same way so the two stay in step. */
.hero-mini .card .c-in{padding-bottom:30px;}
.hero-feature.card{min-height:100%;}
.hero-feature .c-in{padding:30px 32px;}
.hero-feature .tag{color:var(--pink);}
.hero-feature h3{font-size:calc(var(--h-sub-scale,1) * clamp(24px,2.4vw,32px));line-height:1.08;margin-bottom:16px;max-width:18ch;}
.hero-feature h3:last-child{margin-bottom:0;}
/* No-JS fallback: the underline sits on each LINE span, so it sweeps per line
   exactly as featureHeading() describes — not one bar under the whole block. */
.hero-feature h3 > .ul{display:block;background-size:0 2px;-webkit-box-decoration-break:slice;box-decoration-break:slice;}
/* The wrapper itself never paints: only the middle line sweeps. */
.hero-feature h3 > .ul,a.hero-feature:hover h3 > .ul{background-image:none;background-size:0 2px;padding-bottom:0;}
/* ONLY the middle line sweeps. Its 11px padding-bottom lets the 2px bar land
   exactly on the 1px rule above line 3 (whose top margin is pulled back by the
   same 11px), so spacing is unchanged and the bar paints over the separator.
   position:relative;z-index:1 is what makes "over" true: both are backgrounds of
   non-positioned blocks, and .fh3::before comes later in tree order, so it was
   washing rgba(255,255,255,.28) across the top half of the pink bar. */
/* ⚠ THE BAR IS AS WIDE AS THE WORDS, NOT AS WIDE AS THE COLUMN.
   Dylan, 25 September 2026: "in the hero card the underline that appears
   extends too far to the right of the text it is underlining."

   .fh2 is display:block, so its background painted across the whole of h3's
   content box — which is h3's max-width:18ch, not the length of the title. On
   "Founded in Oxford" at 1440px that was about 35px of pink past the final d.

   width:fit-content shrink-wraps the box to the text and changes nothing else:
   the line still sits on its own line, still wraps at the same place (a
   shrink-to-fit box is still bounded by the space available), and the sweep
   still grows from 0 to 100% of it. The -webkit- value is first so a browser
   that only knows that one takes it and a browser that knows both takes the
   standard one underneath.

   ⚠ WHAT THIS DOES NOT DO: a title that wraps onto two lines gets ONE bar the
   width of the widest line, not a bar per line. .ul carries
   box-decoration-break:slice for the block case and the per-line treatment
   belongs to SEQ_JS, which deliberately skips .hero-feature. Left as is rather
   than half-built: one bar under a wrapped title is the behaviour that was
   already there, minus the overhang that was reported. */
.hero-feature h3 > .ul .fh2{position:relative;z-index:1;background-image:linear-gradient(var(--ul-c),var(--ul-c));background-repeat:no-repeat;background-position:0 100%;background-size:0 2px;--ul-c:transparent;--ul-in:background-size .5s cubic-bezier(.22,1,.36,1),--ul-c 0s;transition:var(--ul-out);padding-bottom:var(--fh2-pb,11px);width:-webkit-fit-content;width:fit-content;max-width:100%;}
a.hero-feature:hover h3 > .ul .fh2{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}
.hero-feature h3 .fh1,.hero-feature h3 .fh2,.hero-feature h3 .fh3{display:block;}
.hero-feature h3 .fh1{font-size:calc(var(--h-sub-scale,1) * clamp(19.2px,1.92vw,25.6px));}
/* Line 2 is the name: Aleo serif, the statement of the card. */
.hero-feature h3 .fh2{font-size:calc(var(--h-sub-scale,1) * clamp(29px,2.9vw,38px));}
/* Line 3 is the credential: body face, footnote weight, under a short rule.
   The three overrides are required because h1..h4 sets serif/600/-.01em. */
.hero-feature h3 .fh3{font-family:var(--body);font-weight:400;letter-spacing:0;line-height:1.45;color:rgba(255,255,255,.86);font-size:calc(var(--h-sub-scale,1) * clamp(17px,1.7vw,20.5px));}
/* Rule colour, width and the gap below it are tokens now; the fallbacks are
   the old literals, so an untouched hero is unchanged. HERORULE_JS makes the
   gap ABOVE match the one below when the block asks for it. */
/* ⚠ THE RULE NEVER OUTRUNS THE TEXT ABOVE IT.
   Dylan, 25 September 2026: "in the hero card the underline that appears
   extends too far to the right of the text it is underlining."

   --hero-rule-w is a flat number of pixels chosen in the block settings; the
   title beside it is whatever the words and the width of the window make it.
   Those two agree only by luck, and on "Founded in Oxford" at 1440px the rule
   ran about 35px past the final d.

   --hero-rule-fw is the measured width of the widest line ABOVE the rule,
   published per card by HERORULE_JS. min() takes whichever is smaller, so the
   setting still shortens the rule and can no longer lengthen it past the ink.
   The 100% fallback is what the old max-width:100% did, so a page whose script
   has not run yet renders exactly as it did before. */
.hero-feature h3 .fh3::before{content:"";display:block;
  width:min(var(--hero-rule-w,200px),var(--hero-rule-fw,100%));max-width:100%;height:1px;
  background:var(--hero-rule-col,rgba(255,255,255,.28));
  margin:-2px 0 var(--fh3-rule-mb,8px);}
/* No rule floating above the first line when there is nothing above it. */
.hero-feature h3 .fh3:first-child::before{display:none;}


/* Item list ("what we do"): one testimonial beside the lead + numbered list. */
/* ~30% more breathing room at the TOP of the services list than the bottom. */
/* THE CIRCLE'S SHADOW FALLS ONTO THE NEXT SECTION ON PURPOSE, so say so.
   .wd-circle is position:relative with a 64px shadow offset 30px DOWN, which
   lands well past the foot of this section — and it only stayed visible there
   by accident of paint order: a positioned element paints after every
   unpositioned background, including the next section's. That accident breaks
   the moment anything in the next section takes a transform, because a
   transform makes an element paint in the same late phase and it is later in
   tree order — which is why the shadow vanished for the length of a fade-in
   and then came back. Claiming the layer explicitly makes it true rather than
   lucky. 1 is enough: every piece of chrome on the page sits at 80 or above. */
/* ── HOW MUCH ROOM THE SECTION TAKES, ABOVE AND BELOW ──────────────────────
   Dylan, 26 September 2026, asked for this twice: once as a per-placement
   choice and once as a site-wide one. Both exist, and they meet here.

   --wd-blockt and --wd-blockb are MULTIPLIERS on the spacing the design already
   has, not replacements for it. That is deliberate: the padding is a clamp, so
   it is three numbers rather than one, and "twice as large" has to double the
   floor and the preferred width as well as the cap or a phone would not move.
   Multiplying the whole clamp doubles all three at once and keeps the section
   responsive, which a pair of pixel values could not.

   ⚠ THE 1 IS A var() FALLBACK AND MUST NOT BECOME A DECLARATION ON THIS RULE.
   Writing '.whatdo{--wd-blockt:1}' would look equivalent and would silently
   break the site-wide setting: an element's own declaration beats an inherited
   one, so the ':root' value compile.ts emits would never reach the section and
   the global control would do nothing on any page. The three layers resolve as
   fallback -> inherited ':root' -> the section's own style attribute, in that
   order of increasing strength, with no '!important' anywhere. */
.whatdo{position:relative;z-index:1;background:var(--paper);padding:calc(var(--wd-blockt,1) * clamp(42px,4.2vw,65px)) 0 calc(var(--wd-blockb,1) * clamp(32px,3.2vw,50px));}
/* Optional white background. Declared BEFORE the .wh-il-anim navy rule so, when
   both classes are present, the animated navy still wins the base background;
   the higher-specificity ::before override below recolours the card instead. */
.whatdo.is-white{background:#fff;}
.wd-split{display:grid;grid-template-columns:0.95fr 1.3fr;gap:clamp(24px,3.2vw,52px);align-items:center;}
.wd-split.wd-noquote{grid-template-columns:1fr;}
.whatdo .lead{text-align:left;max-width:none;margin:0;}
.whatdo .lead .eyecrest{margin-left:0;margin-right:0;}
.whatdo .lead .eyebrow{display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,14px);}
.whatdo .lead h2{font-size:calc(var(--h-section-scale,1) * clamp(27px,3.1vw,41px));}
.whatdo .lead .purpose{font-size:15px;color:var(--muted);margin:var(--h-section-gap,26px) 0 0;max-width:54ch;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.62);}
.wd-testimonial{display:flex;flex-direction:column;justify-content:center;min-height:290px;margin:0;padding:clamp(24px,2.4vw,36px);background:var(--white);border-top:3px solid var(--pink);border-radius:2px;box-shadow:0 22px 44px -30px rgba(20,38,63,.4);}
.wd-testimonial blockquote{font-family:var(--q-face,var(--serif));font-style:var(--q-style,normal);font-weight:var(--q-weight,inherit);font-size:calc(var(--q-scale,1) * clamp(19px,1.9vw,24px));line-height:1.34;letter-spacing:var(--q-track,inherit);text-transform:var(--q-case,none);color:var(--q-colour,var(--navy));}
.wd-testimonial figcaption{display:flex;align-items:center;gap:11px;margin-top:clamp(22px,2.4vw,30px);}
.wd-testimonial figcaption img{width:38px;height:38px;flex:0 0 38px;border-radius:50%;object-fit:cover;}
.wd-attrib{display:flex;flex-direction:column;line-height:1.35;}
.wd-attrib strong{font-family:var(--body);font-size:13.5px;font-weight:600;color:var(--navy);}
.wd-attrib span{font-size:12.5px;color:var(--muted);}
.wd-list{margin-top:clamp(18px,2vw,26px);border-top:1px solid var(--line);}
.wd-item{position:relative;display:grid;grid-template-columns:40px 1fr auto;align-items:start;gap:12px;padding:var(--wd-padt,14px) var(--wd-padr,18px) var(--wd-padb,14px) var(--wd-padl,18px);border-bottom:1px solid var(--line);text-decoration:none;color:inherit;background:transparent;transition:background-color .26s ease;}
.wd-item .n{font-family:var(--serif);font-weight:700;font-size:12.5px;letter-spacing:0;line-height:1.9;white-space:nowrap;color:var(--pink);}
.wd-item .n .pfx{font-size:1em;font-weight:700;letter-spacing:0;vertical-align:baseline;margin-right:.22em;}
.wd-item h3{font-size:calc(var(--h-sub-scale,1) * 19px);margin:0 0 4px;line-height:1.2;}
.wd-item .wd-crest{display:inline-block;width:15px;height:15px;margin-left:10px;vertical-align:-2px;filter:drop-shadow(0 2px 4px rgba(8,16,34,.2));}
.wd-item .wd-sig{font-family:var(--body);font-size:9.5px;font-weight:700;letter-spacing:.17em;text-transform:uppercase;color:var(--pink);margin-left:9px;vertical-align:3px;white-space:nowrap;}
@media(max-width:640px){.wd-item .wd-sig{display:block;margin-left:0;margin-top:5px;vertical-align:baseline;}}


.wd-item p{font-size:12.8px;color:var(--muted);margin:0;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.55);}
.wd-item .arw{align-self:center;color:var(--pink);opacity:0;transform:translateX(-6px);transition:opacity .26s ease,transform .26s ease;}
/* Hover keeps only the underline sweep + arrow reveal — no background tint. */
.wd-item:hover .arw,.wd-item:focus .arw,.wd-item:focus-within .arw{opacity:1;transform:none;}
.wd-item:hover .ul{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}
.wd-item:focus-visible{outline:2px solid var(--pink);outline-offset:2px;}
/* No quote: no empty card; the list runs full width in two text columns. */
.wd-noquote .wd-list{display:grid;grid-template-columns:1fr 1fr;column-gap:clamp(24px,3vw,48px);}
/* Category-links layout (new Services-list version): a single clean column of
   page links — no numbers, no descriptions. Overrides the two-column list. */
.wd-list.wd-cats{display:block;grid-template-columns:none;}
/* align-items:center is a FIX, not a preference: .wd-item sets align-items to
   start, and the disc is taller than the 25px line of text, so the disc set the
   row height while the title stayed pinned to the top of it — 16px above, 29.7px
   below, 13.7px off centre. Scoped to .wd-cat so the individual-services layout,
   which has a description under each title and genuinely wants top alignment, is
   untouched. Row height 70px → 60px (12 + 36 + 12). */
.wd-item.wd-cat{grid-template-columns:1fr auto;padding-top:var(--wd-padt,12px);padding-bottom:var(--wd-padb,12px);align-items:center;}
.wd-item.wd-cat h3{margin:0;font-size:calc(var(--h-sub-scale,1) * 21px);}
/* ⚠ CATEGORY LIST: RULES BETWEEN ROWS ONLY (Dylan, 15 October 2026): *"please
   remove the top and the bottom lines from the service categories. Just keep the
   ones in between."* Scoped to .wd-cats, so the individual-services list keeps
   its top and bottom rules. */
.wd-list.wd-cats{border-top:0;}
.wd-cats .wd-cat:last-child{border-bottom:0;}
/* .arwbtn already supplies the circle, the centring, the 1.5px border and the
   transition, so this only overrides size and colour. */
.wd-item .wd-catbtn{align-self:center;width:36px;height:36px;background:var(--navy);border-color:var(--navy);color:#fff;}
/* One pixel right: the arrow's head carries its weight, so centred by the box it
   reads a touch left (Dylan, 25 October 2026; measured 1px left on his screen). */
.wd-item .wd-catbtn .gwarr{width:17px;height:17px;transform:translateX(1px);}
.wd-item:hover .wd-catbtn,.wd-item:focus .wd-catbtn,.wd-item:focus-within .wd-catbtn{background:var(--pink);border-color:var(--pink);}
/* SET APART, design 5A (Dylan, 24 October 2026): "I like that it's
   lower-profile, similar to how it is in the burger mega menu." With a category
   flagged "Set apart" (Private & Organisational), the other rows lose their
   lines, and one line with room either side stands above it. Its name takes the
   burger's page-link voice (the same --mn-page-* chain as the side-lines burger)
   and its disc is an outline that fills pink on hover. Only the rows and lines:
   the heading, introduction, picture and row padding are unchanged.
   ⚠ CHANGED 25 October 2026 to his option B: "use the same font/colour as the
   ones above" at their size, and "the right arrow circle is the same style
   (navy background usually, pink on hover)". So only the line and the gap set
   Private apart now; its name and disc are the other rows'. A list with
   nothing set apart is exactly as before. The staggered lines are ::after, and
   STAGGER_CSS (blocks/whatWeDo.ts) drops those the same way.
   ⚠ 26 October 2026: "make that padding slightly smaller", the space above
   and below the line, a third less (was 22px above, 20px below). */
.wd-cats:has(.is-apart) .wd-cat{border-bottom:0;}
.wd-cats .wd-cat.is-apart{margin-top:15px;padding-top:13px;border-top:1px solid var(--line);}


/* NOTE: the hidden items and the 120vh cover are gated on .js-il, added by
   IL_REVEAL_JS. With JavaScript off (and for crawlers) the list renders fully
   revealed instead of a navy panel over everything below it. */
/* The animated variant restates the padding, so it takes the same multipliers —
   otherwise the setting would work on an ordinary services list and do nothing
   on one with the intro reveal switched on, which is the sort of inconsistency
   nobody would think to test for. */
.whatdo.wh-il-anim{position:relative;background:var(--navy-ink);padding:calc(var(--wd-blockt,1) * clamp(32px,3.2vw,50px)) 0 calc(var(--wd-blockb,1) * clamp(32px,3.2vw,50px));}
/* The expanding card is measured by IL_REVEAL_JS against the RIGHT-hand lead
   (.wd-lead), so left/width are variables too — without JS it falls back to the
   old centred box. */
.whatdo.wh-il-anim::before{content:"";position:absolute;z-index:0;background:var(--paper);left:var(--il-card-left,50%);transform:translateX(var(--il-card-tx,-50%));top:var(--il-card-top,calc(var(--wd-blockt,1) * clamp(32px,3.2vw,50px)));width:var(--il-card-w,min(640px,calc(100% - 32px)));height:var(--il-card-h,320px);border-radius:2px;box-shadow:0 18px 44px -22px rgba(0,0,0,.28);transition:top var(--il-dur,900ms) var(--il-ease,cubic-bezier(.35,0,.8,.5)),left var(--il-dur,900ms) var(--il-ease,cubic-bezier(.35,0,.8,.5)),width var(--il-dur,900ms) var(--il-ease,cubic-bezier(.35,0,.8,.5)),height var(--il-dur,900ms) var(--il-ease,cubic-bezier(.35,0,.8,.5)),border-radius 160ms linear calc(var(--il-dur,900ms) - 160ms);}
.whatdo.wh-il-anim.is-open::before{top:0;left:0;transform:translateX(0);width:100%;height:100%;border-radius:0;box-shadow:none;}
/* White background: recolour the expanding card (higher specificity than the
   base ::before rule above, so it wins over the paper default). */
.whatdo.is-white.wh-il-anim::before{background:#fff;}
.whatdo.wh-il-anim.js-il::after{content:"";position:absolute;left:0;right:0;top:100%;height:var(--il-cover-h,120vh);background:var(--navy-ink);z-index:5;transform:scaleY(1);transform-origin:center bottom;transition:transform var(--il-cover-dur,700ms) linear var(--il-dur,900ms);pointer-events:none;}
.whatdo.wh-il-anim.js-il.is-open::after{transform:scaleY(0);}
.whatdo.wh-il-anim.js-il + *{transform:scale(1.1);transform-origin:top center;transition:transform var(--il-zoom-dur,700ms) linear var(--il-dur,900ms);}
.whatdo.wh-il-anim.js-il.is-open + *{transform:scale(1);}
.whatdo.wh-il-anim .wrap{position:relative;z-index:1;background:transparent;}
.whatdo.wh-il-anim.js-il .purpose,.whatdo.wh-il-anim.js-il .wd-testimonial,.whatdo.wh-il-anim.js-il .wd-photo,.whatdo.wh-il-anim.js-il .wd-item{opacity:0;transform:translateY(32px);transition:opacity .8s ease,transform .8s cubic-bezier(.22,1,.36,1);}
.whatdo.wh-il-anim.js-il.is-open .wd-testimonial,.whatdo.wh-il-anim.js-il.is-open .wd-photo{opacity:1;transform:none;transition-delay:calc(var(--il-dur,900ms)*0.4);}
/* The stagger delay must apply to the REVEAL properties only. Listing it per
   property keeps the hover lift (transform) instant once revealed — a blanket
   transition-delay made the 3px lift hang for up to a second after mouse-out. */
.whatdo.wh-il-anim.js-il.is-open .purpose{opacity:1;transform:none;transition-delay:calc(var(--il-dur,900ms)*0.4);}
.whatdo.wh-il-anim.js-il.is-open .wd-item{opacity:1;transform:none;transition-delay:calc(var(--il-dur,900ms)*0.55 + var(--i)*110ms);}
/* Once revealed, hover-driven properties carry NO delay. */
.whatdo.wh-il-anim.js-il.is-open .wd-item.is-shown{transition:opacity .8s ease,transform .26s ease,background-color .26s ease,box-shadow .26s ease;transition-delay:0s;}
@media (hover:hover){
html.wh-hover-armed .whatdo.wh-il-anim.js-il.is-open a.wd-item:hover{transform:translateY(-3px);transition:background-color .26s ease,box-shadow .26s ease,transform .26s ease;transition-delay:0s;}
}
@media (prefers-reduced-motion: reduce){.whatdo.wh-il-anim::before{top:0;left:0;transform:translateX(0);width:100%;height:100%;border-radius:0;box-shadow:none;transition:none;}.whatdo.wh-il-anim .purpose,.whatdo.wh-il-anim .wd-testimonial,.whatdo.wh-il-anim .wd-photo{opacity:1;transform:none;transition:none;}.whatdo.wh-il-anim .wd-item,.whatdo.wh-il-anim.is-open a.wd-item:hover{opacity:1;transform:none;transition:background-color .26s ease,box-shadow .26s ease;transition-delay:0s;}.whatdo.wh-il-anim.js-il::after{transform:scaleY(0);transition:none;}.whatdo.wh-il-anim.js-il + *{transform:none;transition:none;}}
/* Reduced motion: keep the background/shadow reveal, drop the lift. */
@media (prefers-reduced-motion: reduce){.wd-item{transition:background-color .26s ease,box-shadow .26s ease;}a.wd-item:hover{transform:none;}}

.insights{background:var(--white);padding:clamp(40px,4.5vw,70px) 0 clamp(42px,5vw,74px);}
.ins-head{display:flex;align-items:flex-start;justify-content:space-between;gap:40px;margin-bottom:var(--h-section-gap,26px);flex-wrap:wrap;}
.ins-head-l{max-width:48ch;}
.ins-head-l .eyebrow{display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,14px);}
/* "RECENT INSIGHTS" SITS ON ONE LINE.
   The 16ch measure here was the generic section-heading measure, and at the
   live section scale it broke this heading across two lines. It can be dropped
   for THIS heading specifically because the text is not author-editable — the
   registry fixes it at INSIGHTS_HEADING and offers no field to change it — so
   there is no longer sentence that nowrap could push out of the column.
   Below 560px one line genuinely does not fit on a phone, so it wraps again
   there rather than overflowing the page. */
.ins-head h2{font-size:calc(var(--h-section-scale,1) * clamp(30px,4.4vw,52px));max-width:none;white-space:nowrap;line-height:1.04;}
@media(max-width:560px){.ins-head h2{white-space:normal;max-width:16ch;}}
/* THE WORDS ON THE LEFT, THE DISC ON THE RIGHT EDGE OF THE COLUMN.
   It used to be disc-then-words as one tight pair, floating in the middle of a
   26ch box — so the disc lined up with nothing, while the circular thumbnails
   in the list directly above it all sit hard against the column's right edge.
   space-between in a full-width row puts it exactly there, in the same
   vertical line as those thumbs, and leaves the label starting where every
   other line in the column starts. */
/* THE WORDS SIT AGAINST THE ARROW, NOT ACROSS THE COLUMN FROM IT. This was
   space-between, which pinned the label to the far left of a full-width foot
   and the disc to the far right, so the two read as unrelated things that
   happened to share a row. flex-end keeps the disc where it was — hard right,
   under the list it belongs to — and brings the label up beside it. Centred
   rather than top-aligned now they are adjacent: against a 42px disc the old
   11px nudge was doing the centring by hand, and would push a one-line label
   below the arrow the moment they were next to each other. */
.explore{display:flex;align-items:center;justify-content:flex-end;gap:12px;width:100%;}
.explore .t{max-width:26ch;text-align:right;}
.explore .arwbtn{border-color:var(--navy);background:var(--navy);color:#fff;width:42px;height:42px;font-size:16px;}
.explore:hover .arwbtn{background:var(--pink);border-color:var(--pink);}
.explore .t{font-size:14px;font-weight:600;color:var(--ink);line-height:1.42;}
/* Stretch both columns so the feature can bottom-align its body text with the
   last list item's body text: the picture takes up the slack. */
.ins-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(34px,4.6vw,64px);align-items:stretch;}
.feature{display:flex;flex-direction:column;padding-bottom:var(--ins-padb,22px);}
/* min-height keeps the old proportions; flex-grow absorbs the difference in
   column height so the text below it lands level with the right-hand list. */
.feature .pic{position:relative;flex:1 1 auto;height:auto;min-height:clamp(180px,17vw,250px);margin-bottom:22px;border-radius:2px;overflow:hidden;}
.kick{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;color:var(--pink);}
.kick.pink{color:var(--pink);}
.feature h3{font-size:calc(var(--h-sub-scale,1) * clamp(25px,2.9vw,36px));line-height:1.13;margin:10px 0 var(--h-sub-gap,12px);}
.feature p{color:var(--muted);font-size:16px;max-width:46ch;}
.feature .ins-body{color:var(--muted);font-size:16px;max-width:46ch;}
.feature .ins-body p{margin:0 0 10px;}
.feature .ins-body p:last-child{margin-bottom:0;}
.feature .ins-body ul,.feature .ins-body ol{margin:8px 0 10px 20px;}
.feature .ins-body li{margin:3px 0;}
.feature .ins-body a{color:var(--pink);text-decoration:underline;}
.feature .ins-body strong{font-weight:700;}
.feature .ins-body em{font-style:italic;}
.feature .ins-body h2,.feature .ins-body h3{font-family:var(--serif);color:var(--navy);
  line-height:1.2;margin:16px 0 7px;}
.feature .ins-body h2{font-size:calc(var(--h-section-scale,1) * 22px);}.feature .ins-body h3{font-size:calc(var(--h-sub-scale,1) * 18px);}
.feature .ins-body blockquote{margin:12px 0;padding-left:13px;border-left:2px solid var(--pink);
  color:var(--navy);font-family:var(--serif);font-style:italic;}
.ins-list{display:flex;flex-direction:column;}
/* The "all insights" link at the foot of a column: a divider matching the list
   rows, then the link, dropped to the very bottom so both columns stay level. */
.ins-foot{margin-top:auto;padding-top:var(--ins-padt,22px);border-top:1px solid var(--line);}
.ins-col{display:flex;flex-direction:column;}
.ins-col>.feature{flex:1 1 auto;}
.item{display:flex;gap:22px;justify-content:space-between;align-items:flex-start;padding:var(--ins-padt,22px) 0 var(--ins-padb,22px);border-top:1px solid var(--line);}
/* NO RULE ABOVE THE FIRST POST. The border is a separator BETWEEN posts; on the
   top one it drew a line under the column's own heading, which the heading does
   not want and which the left column has no counterpart for. The foot keeps its
   own rule — that one genuinely separates the list from the link beneath it. */
.ins-list > .item:first-child{border-top:0;}
.it-txt{flex:1;}
.it-txt .kick{margin-bottom:8px;}
.it-txt h4{font-size:calc(var(--h-sub-scale,1) * 21px);line-height:1.18;}
.it-txt p{color:var(--muted);font-size:14px;margin-top:7px;max-width:44ch;}
.it-thumb{flex:0 0 auto;width:112px;height:82px;border-radius:2px;overflow:hidden;}
/* Circular list thumbnails — the design default, set globally on site chrome
   via "insightsThumbStyle" (a block may opt out with "rectangle"). Scoped under
   BOTH ".insights" and the modifier because ".it-thumb" is an unscoped, shared
   card class. The top/left/right/bottom longhands before "inset" are deliberate
   — same as the tint scrims — so the ring still lands on Safari older than
   14.1. The ring is an inset box-shadow, not a border: Chrome floors
   border-width to whole CSS pixels, which would make the half-pixel thickness
   steps no-ops; box-shadow spread is not snapped. */
.insights.ins-thumb--circle .it-thumb{width:92px;height:92px;border-radius:50%;position:relative;}
/* BOTH COLUMNS CENTRED, not just the disc.
   The disc is 92px and a two-line excerpt is 88 — so with the row hung from
   the top, the taller column set the height and the shorter one left its slack
   at the BOTTOM. Equal top and bottom padding then measured equal and looked
   unequal, which is the complaint. Centring the row puts half the slack each
   side of whichever column is shorter, and centres the disc into the bargain.
   Scoped to the circle style: the rectangle crops are designed top-aligned
   with the headline, and that is still what they get. */
.insights.ins-thumb--circle .item{align-items:center;}
.insights.ins-thumb--circle .it-thumb::after{content:"";position:absolute;top:var(--ins-ring-i,5px);left:var(--ins-ring-i,5px);right:var(--ins-ring-i,5px);bottom:var(--ins-ring-i,5px);inset:var(--ins-ring-i,5px);border-radius:50%;box-shadow:inset 0 0 0 var(--ins-ring-w,1.5px) var(--ins-ring-c,rgb(217 113 149 / .85));pointer-events:none;}
.slider{background:var(--navy-ink);color:#fff;position:relative;padding-bottom:clamp(70px,8vw,110px);}
.slider .band{height:clamp(240px,30vw,380px);position:relative;}
.slider .shead{position:relative;z-index:3;background:var(--navy-ink);margin-top:clamp(-150px,-14vw,-104px);padding:clamp(30px,4vw,52px) 0 clamp(26px,3vw,40px) clamp(30px,4vw,56px);
   margin-right:calc(-50vw + 50%);padding-right:calc(50vw - 50% + clamp(22px,4vw,48px));max-width:min(880px,84%);}
.slider .sband{background:var(--band-bg,var(--navy-ink));}
.slider.no-band .shead{margin-top:0;margin-right:0;padding-right:clamp(30px,4vw,56px);}
.slider .shead .eyebrow{color:var(--eyebrow-lit);display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,14px);}
.slider .shead h2{color:#fff;font-size:calc(var(--h-section-scale,1) * clamp(26px,3.4vw,44px));letter-spacing:.01em;}
.slider .shead p{color:rgba(255,255,255,.66);max-width:52ch;margin-top:12px;font-size:15px;}
.slider .trackwrap{position:relative;z-index:4;margin-top:clamp(28px,3vw,40px);margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);}
.track{display:flex;gap:20px;overflow-x:auto;scrollbar-width:none;padding:0 0 6px clamp(22px,4vw,48px);scroll-snap-type:x proximity;scroll-padding-left:clamp(22px,4vw,48px);scroll-padding-right:clamp(22px,4vw,48px);}
.track::-webkit-scrollbar{height:0;}
.scard{flex:0 0 clamp(240px,25vw,320px);background:#fff;scroll-snap-align:start;border-radius:2px;overflow:hidden;}
.scard .spic{height:clamp(180px,17vw,220px);position:relative;}
.scard .scap{padding:18px 20px 22px;}
.scard .scap .nm{font-family:var(--serif);font-weight:600;color:var(--heading);font-size:19px;}
.scard .scap .meta{font-size:13px;color:var(--muted);margin-top:4px;}
.scard .scap .view{display:inline-block;margin-top:14px;font-size:12px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--navy);padding-bottom:3px;--ul-c:transparent;background:linear-gradient(var(--ul-c),var(--ul-c)) no-repeat 0 100% / 0% 2px;transition:var(--ul-out);}
.scard:hover .view{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}
.snav{position:absolute;top:clamp(90px,9vw,110px);left:0;right:0;display:flex;justify-content:space-between;pointer-events:none;z-index:6;padding:0 clamp(12px,2vw,26px);box-sizing:border-box;}
.snav .arwbtn{background:rgba(255,255,255,.92);border:1.5px solid transparent;color:var(--navy);width:48px;height:48px;padding:0;font:inherit;font-size:18px;line-height:1;box-shadow:0 10px 24px -12px rgba(0,0,0,.5);pointer-events:auto;cursor:pointer;}
/* Do NOT set pointer-events:none — the arrows overlap the region cards, so a tap
   on a greyed-out arrow would fall through and navigate. The disabled attribute
   already swallows the click; keep the button hit-testable. */
.snav .arwbtn:disabled{opacity:.35;cursor:default;}
.snav.is-static{display:none;}
/* ── HOW FAR THIS BLOCK'S PARTS RISE ───────────────────────────────────────
   Dylan, 25 September 2026: "With the feature + testimonials fade in upwards,
   I'd also like them to travel more of a vertical distance when they are
   fading in upwards."

   ⚠ THE BLOCK HAS TWO RISES, AND HE MEANT BOTH. The site-wide reveal brings
   the picture and then the quote column in (translateY of --rv-lift, 18px by
   default); the block's own "Scroll reveal" toggle then walks each quote up
   one after another from a hard-coded 22px. A control that moved only one of
   them would have answered half the sentence and left the other half looking
   like a bug.

   So ONE number drives both, and its default is 40px — the same rise the
   services list settles on, so the two neighbouring sections travel the same
   distance rather than each having its own idea. The old 18/22 are gone
   deliberately: he asked for more, not for a switch left where it was.

   --ft-lift is declared on .feattest rather than on the animated selector so
   the override written by featureRevealStyle() (compile.ts) has one place to
   land for both rules. */
.feattest{background:var(--paper);--ft-lift:40px;}
/* Dylan, 16 October 2026: paper or white, per placement (the block's own
   Background field). Paper stays the rule above, so a placement with no answer
   is unchanged. On white the editorial cards (themselves white) keep their
   border and shadow, which is what separates them from the ground. */
.feattest.ft--white{background:var(--white);}
.feattest.wh-rv{--rv-lift:var(--ft-lift,40px);}
/* The same two multipliers as the services list, under their own names. The
   padding is on .wrap rather than on the section here, and a custom property
   declared on the section is inherited by it, so both the site-wide ':root'
   value and the section's own style attribute reach this rule unchanged.
   ⚠ Same warning as .whatdo: the 1 is a var() FALLBACK. A declaration on
   '.feattest' would beat the inherited ':root' one and kill the global. */
.feattest .wrap{padding-top:calc(var(--ft-blockt,1) * clamp(48px,6vw,88px));padding-bottom:calc(var(--ft-blockb,1) * clamp(48px,6vw,88px));}
.ft-grid{display:grid;grid-template-columns:2.9fr 1fr;gap:clamp(28px,3.6vw,56px);align-items:stretch;}
.ft-img{min-height:clamp(320px,42vw,540px);border-radius:2px;overflow:hidden;}
.ft-quotes{display:flex;flex-direction:column;justify-content:center;gap:clamp(28px,3.25vw,45px);}
.ft-q{margin:0;}
.ft-q .q{font-family:var(--q-face,var(--serif));font-style:var(--q-style,italic);font-weight:var(--q-weight,500);font-size:calc(var(--q-scale,1) * clamp(16px,1.5vw,19px));line-height:1.4;letter-spacing:var(--q-track,inherit);text-transform:var(--q-case,none);color:var(--q-colour,var(--navy));}
.ft-q figcaption{margin-top:9px;font-size:12px;letter-spacing:.02em;color:var(--muted);}
.ft-q figcaption strong{color:var(--ink);font-weight:600;}
/* Flip = swap sides on desktop: testimonials narrow-left (right-aligned),
   image wide-right. Explicit grid placement is used rather than the order
   property, which proved fragile. */
.feattest.ft-flip .ft-grid{grid-template-columns:1fr 2.9fr;}
.feattest.ft-flip .ft-img{grid-column:2;grid-row:1;}
.feattest.ft-flip .ft-quotes{grid-column:1;grid-row:1;text-align:right;}
/* Small screens: always stack image ABOVE, testimonials BELOW, text left-aligned
   — regardless of the flip setting (which is a desktop-only side-swap). */
@media (max-width:820px){
  .ft-grid,.feattest.ft-flip .ft-grid{grid-template-columns:1fr;}
  .ft-img{min-height:clamp(240px,50vw,360px);}
  .ft-img,.feattest.ft-flip .ft-img{grid-column:1;grid-row:1;}
  .ft-quotes,.feattest.ft-flip .ft-quotes{grid-column:1;grid-row:2;text-align:left;}
}
/* ─── The EDITORIAL testimonial style (.qs-editorial) ───
 *
 * A second dressing for the two testimonial blocks, chosen per block. It adds
 * a class and nothing else: the figure, the blockquote, the figcaption and
 * every word inside them are the same as in the classic style, which is why
 * the choice cannot touch what a crawler reads.
 *
 * It reads the --q-* variables like the classic style does, so the global
 * quote face, weight, size, spacing, case and colour still govern both. Only
 * the FALLBACKS differ — upright rather than italic, because a framed card
 * wants a steadier face than an open one.
 */
.feattest.qs-editorial .ft-quotes{gap:clamp(18px,1.9vw,26px);}
.feattest.qs-editorial .ft-q{position:relative;background:var(--white);border:1px solid var(--line);border-left:2px solid var(--ring);border-radius:2px;padding:clamp(20px,2vw,28px) clamp(20px,2.1vw,30px) clamp(18px,1.8vw,24px);box-shadow:0 18px 40px -34px rgba(20,38,63,.55);}
.feattest.qs-editorial .ft-q .q{font-style:var(--q-style,normal);line-height:1.5;}
/* The ornament is decoration, so it is given an EMPTY speech alternative:
   a reader that supports it says nothing, and one that does not never sees
   generated content in the first place. It is never in the document text. */
.feattest.qs-editorial .ft-q .q::before{content:"\201C" / "";color:var(--brass);font-family:var(--serif);font-size:1.9em;line-height:0;vertical-align:-.28em;margin-right:.12em;opacity:.62;}
.feattest.qs-editorial .ft-q figcaption{margin-top:14px;padding-top:12px;border-top:1px solid var(--line);font-size:11px;letter-spacing:.13em;text-transform:uppercase;}
.feattest.qs-editorial .ft-q figcaption strong{display:block;font-size:12.5px;letter-spacing:.06em;text-transform:none;}
.feattest.qs-editorial .ft-q figcaption .sep{display:none;}
.feattest.qs-editorial .ft-q figcaption .det{display:block;margin-top:3px;}
/* Flipped, the accent rule has to move to the other edge or it points away
   from the text it belongs to. */
.feattest.ft-flip.qs-editorial .ft-q{border-left:1px solid var(--line);border-right:2px solid var(--ring);}
@media (max-width:900px){.feattest.ft-flip.qs-editorial .ft-q{border-left:2px solid var(--ring);border-right:1px solid var(--line);}}

.voice.qs-editorial .founder{gap:clamp(24px,3.2vw,44px);}
/* A soft-cornered portrait rather than a disc: the framed look is squarer
   throughout, and the pink hairline ring belongs to the round one. */
.voice.qs-editorial .fv-photo{width:clamp(132px,15vw,196px);height:clamp(158px,18vw,236px);border-radius:3px;box-shadow:0 20px 44px -22px rgba(20,38,63,.55);}
.voice.qs-editorial .fv-photo::after{inset:6px;border-radius:2px;border-color:rgba(255,255,255,.5);}
.voice.qs-editorial .fv-body{border-left:2px solid var(--ring);padding-left:clamp(20px,2.2vw,32px);}
.voice.qs-editorial .fv-q{font-style:var(--q-style,normal);line-height:1.36;}
.voice.qs-editorial .fv-who{margin-top:20px;padding-top:14px;border-top:1px solid var(--fv-line,var(--line));font-size:11px;letter-spacing:.14em;text-transform:uppercase;}
.voice.qs-editorial .fv-who strong{display:block;font-size:14px;letter-spacing:.03em;text-transform:none;}
.voice.qs-editorial .fv-who .sep{display:none;}
.voice.qs-editorial .fv-who .det{display:block;margin-top:4px;}
@media (max-width:680px){.voice.qs-editorial .fv-body{border-left:0;border-top:2px solid var(--ring);padding-left:0;padding-top:22px;}}

/* Scroll reveal (toggle ON -> .wh-ft-anim). Hidden state is gated on .js-ft,
   added by FT_REVEAL_JS, so no-JS visitors and crawlers see the quotes. */
.feattest.wh-ft-anim.js-ft .ft-q .q,.feattest.wh-ft-anim.js-ft .ft-q figcaption{opacity:0;transform:translateY(var(--ft-lift,40px));transition:opacity 620ms ease,transform 620ms cubic-bezier(.22,1,.36,1);}
.feattest.wh-ft-anim.js-ft.is-in .ft-q .q{opacity:1;transform:none;transition-delay:calc(var(--i,0)*280ms);}
.feattest.wh-ft-anim.js-ft.is-in .ft-q figcaption{opacity:1;transform:none;transition-delay:calc(var(--i,0)*280ms + 90ms);}
/* Once shown, drop the delay so nothing lingers on later interaction. */
.feattest.wh-ft-anim.js-ft.is-shown .ft-q .q,.feattest.wh-ft-anim.js-ft.is-shown .ft-q figcaption{transition-delay:0s;}
@media (prefers-reduced-motion: reduce){.feattest.wh-ft-anim.js-ft .ft-q .q,.feattest.wh-ft-anim.js-ft .ft-q figcaption{opacity:1;transform:none;transition:none;}}
.seqbar{position:absolute;height:2px;background:var(--pink);transform:scaleX(0);transform-origin:left center;transition:transform .34s cubic-bezier(.33,0,.25,1);pointer-events:none;z-index:1;}
/* ⚠ THE BARS LEAVE BY FADING TOO, and the fade is written in script rather
   than here because the transition each bar carries is set inline, per bar,
   with its own duration and stagger. SEQ_JS switches the inline
   transition-property to opacity on the way out and leaves the transform
   alone, so the line stays where it was drawn and goes out all at once. A
   staggered fade would be the reverse gesture again, in a different
   costume. */
/* The hero feature card is exempt: only its middle line sweeps, and it does so
   in CSS (the .fh2 rule above) — SEQ_JS skips it. */
.js-seq a.feature .ul,.js-seq a.item .ul,.js-seq a.ncard .ul,.js-seq a.card:not(.hero-feature) .ul{background:none;padding-bottom:1px;}
.trust{position:relative;background:var(--navy-ink);}
.trust .bg{position:absolute;top:0;bottom:0;left:0;width:58%;}
.trust .wrap{position:relative;z-index:2;padding-top:clamp(40px,4.5vw,72px);padding-bottom:clamp(40px,4.5vw,72px);}
.trust-card{margin-left:auto;width:min(520px,52%);background:var(--white);border-top:4px solid var(--pink);box-shadow:0 30px 64px -30px rgba(20,38,63,.5);padding:clamp(34px,3.2vw,52px) clamp(30px,2.8vw,48px) clamp(22px,2.4vw,36px);}
/* ON A PHONE THE PHOTO GOES ABOVE THE CARD (device review, 18 October 2026).
   The card was 52% of the width at every size, so on a 320-414px phone it was
   140-200px wide: the body copy ran one or two words a line and the heading's
   longest word ran out of the card. Below 760px the photo becomes a band at
   the top and the card takes the full width, rising 48px over the photo's foot
   so the two still overlap as they do on a wide screen. Nothing changes above
   760px. */
@media(max-width:760px){
  .trust .bg{position:relative;width:auto;height:clamp(220px,58vw,340px);}
  .trust .wrap{padding-top:0;}
  .trust-card{width:auto;margin-left:0;margin-top:-48px;position:relative;}
}
.trust-card .eyebrow{display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,14px);}
/* Was restating face, weight, leading, colour and tracking literally, which
   froze it at 600/-.01em — so it never followed the theme's heading weight and
   would not have followed the section tier either. Reduced to what is genuinely
   its own: its size and its margin. Every removed value equalled the inherited
   one, so this changes nothing on an untouched site. */
.trust-card h2{font-size:calc(var(--h-section-scale,1) * clamp(27px,2.7vw,40px));margin-bottom:var(--h-section-gap,16px);}
/* ─── THE HEADING GIVES UP ITS BOTTOM MARGIN ONLY TO AN EYEBROW THAT EXISTS ───
 *
 * When the eyebrow is placed below, the heading's bottom margin is zeroed so
 * the author's eyebrow gap is the whole distance rather than their number plus
 * whatever the heading carried. Right — but it used to be done with a GLOBAL
 * variable, and a global cannot tell the difference between a block that has
 * an eyebrow and one that does not.
 *
 * On a block with no eyebrow the margin was zeroed anyway and nothing arrived
 * to replace it: the heading sat directly on the body copy, on every such
 * block, the moment anybody flipped a site-wide setting they had no reason to
 * connect to it.
 *
 * :has() asks the only question that matters — is there an eyebrow in here —
 * and where it is unsupported the heading simply keeps its normal margin,
 * which is the safe way round to be wrong. */
@supports selector(:has(*)){
.trust-card:has(>.eyebrow) h2{margin-bottom:var(--eb-headmb,var(--h-section-gap,16px));}
.notfound .wrap:has(>.eyebrow) h1{margin-bottom:var(--eb-headmb,var(--h-title-gap,20px));}
}
.trust-card p{color:var(--muted);font-size:15.5px;margin-bottom:24px;max-width:46ch;}
.trow{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:15px 2px;border-bottom:1px solid var(--line);font-weight:700;color:var(--navy);font-size:15px;transition:border-color .2s;}
.trow .ch{color:var(--pink);font-size:22px;font-weight:700;line-height:1;transition:transform .2s ease;}
/* Hover-only: a raw :hover here strands the chevron slid-out after a tap on
   touch (incl. iPad desktop-mode). The border tint is harmless on tap, but keep
   both in the guard so the row's hover state is coherent. */
@media (hover:hover){.trow:hover .ch{transform:translateX(6px);}.trow:hover{border-bottom-color:var(--pink);}}
/* ⚠ DOUBLED, 25 September 2026. Dylan: "With the single testimonial block,
   I'd also like to double the padding at the top and the bottom of the block."
   Was clamp(38px,4.5vw,66px); every one of the three numbers is doubled, not
   only the maximum, so the section grows by the same proportion on a phone as
   on a wide screen. Doubling the cap alone would have left the narrow layout
   untouched, which is where a quote most needs the air.
   ⚠ THIS MOVES BYTES ON LIVE PAGES, deliberately. The whole-document and
   markup-plus-stylesheet golden guards in megaMenu.test.ts were updated in the
   same commit; the MARKUP-ONLY guard was not, and that it did not move is the
   evidence this is a stylesheet change and nothing else. */
/* ── THE SINGLE TESTIMONIAL'S THREE GROUNDS ────────────────────────────────
   Dylan, 26 September 2026: "for the 'single testimonial' block, I'd like to be
   able to change the background colour between the three standard options; if
   navy, then this should change the contents colours accordingly so everything
   is still visible."

   FOUR TOKENS, ONE PER THING THAT CAN GO INVISIBLE, and every default is the
   value the rule below ALREADY had — so a block on paper renders exactly as it
   did and only the sheet's text changed. That is the whole reason this is a
   contract rather than a set of .voice--navy overrides bolted on: a rule added
   to this block tomorrow reads a token and is legible on all three grounds
   without anybody remembering, which is what STORY_PAGES_CSS's own ground
   contract does for the service blocks.

   WHAT WAS MEASURED, because "make everything visible" needs a list rather than
   an intention. Against navy-ink #14263f:
     .fv-who        --muted #5b6672   about 2.0:1 — UNREADABLE.
     .fv-who strong --ink   #2b2d31   about 1.2:1 — INVISIBLE.
     .fv-q          --q-colour, and this site's stored value IS #14283f, the
                    navy itself — 1.0:1. The exact defect written up for the
                    pull quote in aQuoteOnNavyIsStillReadable.test.ts, whose own
                    'what this does not cover' note predicted that '.voice .fv-q'
                    would need this treatment the day it gained a ground. It has.
     .eyebrow       --eyebrow, which is pink by default and fine, but BRASS at
                    its paper value is about 4:1 — under the 4.5 that 12px bold
                    uppercase needs. --eyebrow-lit is the site's existing
                    dark-ground answer and is what two other dark surfaces
                    (.slider .shead, .enqf-side) already read.
     .fv-who border --line #e7dee1, pale pink: visible on navy but far brighter
                    than a hairline should be, so it takes the site's usual
                    dark-ground rgba(255,255,255,.22).
     the portrait's drop shadow is rgba(20,38,63,.5) — which IS navy-ink — so on
                    navy the disc lost its lift entirely. Two explicit rules
                    below, because the two shadow geometries differ and one
                    colour token would have had to change one of the alphas.

   ⚠ NOT ADAPTED, DELIBERATELY, AND SAID OUT LOUD: the editorial style's
   border-left reads --ring, the roundel's ring colour. Pink, silver and brass
   are all light accents and a 2px graphic needs 3:1 rather than 4.5:1, so all
   three clear it on navy. If a future palette makes that ring dark it will need
   a token like the rest.
   ⚠ --fv-ring, the hairline INSIDE the portrait, is untouched: it is white by
   default and pink by choice, and both read on every ground.
   ⚠ --q-colour IS OVERRIDDEN ON THE SECTION, NOT READ THROUGH A NEW FALLBACK,
   for the reason .svp.svp--navy gives: paletteCss emits --q-colour on :root when
   the author has chosen a quote colour, and ':root' has the same weight as one
   class — so a single-class rule would win only by sitting later in the
   concatenated sheet. '.voice.voice--navy' is two classes and wins wherever it
   sits. Leaving the .fv-q rule itself untouched is also what keeps paper and
   white byte-identical. */
.voice{--fv-quiet:var(--muted);--fv-name:var(--ink);--fv-line:var(--line);
  --fv-eyebrow:var(--eyebrow);
  background:var(--paper);padding:clamp(76px,9vw,132px) 0;}
.voice.voice--white{background:var(--white);}
.voice.voice--navy{background:var(--navy-ink);
  --fv-quiet:rgba(255,255,255,.68);--fv-name:#fff;--fv-line:rgba(255,255,255,.22);
  --fv-eyebrow:var(--eyebrow-lit);--q-colour:var(--fv-name);}.voice.voice--tertiary{background:var(--tertiary,var(--navy-ink));}
/* The lift the disc loses when its shadow is the colour of the ground. */
.voice.voice--navy .fv-photo{box-shadow:0 16px 34px -16px rgba(0,0,0,.55);}
.voice.voice--navy.qs-editorial .fv-photo{box-shadow:0 20px 44px -22px rgba(0,0,0,.6);}
.voice .founder{display:flex;align-items:center;gap:clamp(28px,4vw,58px);max-width:940px;margin:0 auto;}
.voice .fv-photo{flex:0 0 auto;position:relative;width:clamp(116px,13vw,172px);height:clamp(116px,13vw,172px);border-radius:50%;background-size:cover;background-position:center 22%;box-shadow:0 16px 34px -16px rgba(20,38,63,.5);}
/* THE LINE INSIDE THE PORTRAIT, settable site-wide (Global -> block defaults)
   and white by default. It was a fixed pink; white reads as a lining on a
   photograph rather than a second brand mark competing with the roundel. */
.voice .fv-photo::after{content:"";position:absolute;inset:5px;border-radius:50%;border:1.5px solid var(--fv-ring,rgba(255,255,255,.85));}
/* The colour is stated here rather than left to the global .eyebrow rule, so the
   navy ground can move it: .voice .fv-body .eyebrow is three classes against
   that rule's one, and the fallback resolves to exactly what it inherited. */
.voice .fv-body .eyebrow{display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,13px);color:var(--fv-eyebrow,var(--eyebrow));}
.voice .fv-q{font-family:var(--q-face,var(--serif));font-style:var(--q-style,italic);font-weight:var(--q-weight,500);font-size:calc(var(--q-scale,1) * clamp(21px,2.5vw,32px));line-height:1.32;text-transform:var(--q-case,none);color:var(--q-colour,var(--navy));margin:0;letter-spacing:var(--q-track,-.01em);}
.voice .fv-who{margin-top:18px;font-size:14px;letter-spacing:.03em;color:var(--fv-quiet,var(--muted));}
.voice .fv-who strong{color:var(--fv-name,var(--ink));font-weight:600;}
@media (max-width:680px){ .voice .founder{flex-direction:column;text-align:center;} }
/* A QUOTATION WITH NO PORTRAIT IS CENTRED, NOT LEFT-HANGING.
   Dylan, 20 September 2026. The block stops emitting .fv-photo when the
   testimonial has no picture (see founderNote.ts); without these three rules
   the remaining body simply sits against the left edge of a 940px row, which
   looks like a portrait that failed rather than a quote that never had one.
   text-align cascades to the blockquote and the figcaption, which is why the
   .fv-who line below it centres too. The editorial style draws a rule down the
   left of .fv-body to separate it FROM the portrait; with no portrait there is
   nothing to separate, so it is taken off in both its desktop and its
   narrow-screen form - matching one of them only would leave the line back on
   a phone, which is the mutation this rule pair exists to make fail. */
.voice.voice--noportrait .founder{justify-content:center;text-align:center;max-width:820px;}
.voice.voice--noportrait .fv-body{align-items:center;}
.voice.qs-editorial.voice--noportrait .fv-body{border-left:0;padding-left:0;}
@media (max-width:680px){.voice.qs-editorial.voice--noportrait .fv-body{border-top:0;padding-top:0;}}
/* ── THE BAND'S SPACING DOUBLED ON 25 SEPTEMBER 2026 ───────────────────────
   Dylan: "a choice between what it is currently, and a choice of the padding
   at the top and the bottom being twice the size (which should become the
   default)."

   So the base rule carries the doubled figure — every term of the clamp twice
   what it was, so the band grows at the same rate as the window rather than
   snapping to a new shape at one width — and .cta-snug carries the old one for
   a block that asks for it. Written this way round because the default is the
   one that must apply to a band nobody has touched. */
.cta-band{position:relative;color:#fff;text-align:center;padding:clamp(180px,22vw,300px) 0;overflow:hidden;}
.cta-band.cta-snug{padding:clamp(90px,11vw,150px) 0;}
.cta-band .ph{position:absolute;inset:0;}
.cta-band .inner{position:relative;z-index:2;}
.cta-band .eyebrow{color:#fff;opacity:.9;display:block;margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,0px);}
/* NO TOP MARGIN OF ITS OWN. It carried 14px, which sat ON TOP of the roundel's
   own margin-bottom (--crest-gap, 12px by default) — so the gap between the
   disc and the heading was 26px here and 12px in every other block that draws
   the same pair, and the site-wide crest-gap setting could only ever move part
   of it. The 14px predates the roundel appearing in this band. With it gone
   the one variable governs the gap here exactly as it does everywhere else,
   and a band with no roundel is spaced by its own generous padding, which is
   what was holding the heading off the top edge all along. */
.cta-band h2{color:#fff;font-size:calc(var(--h-section-scale,1) * clamp(30px,4.4vw,56px));max-width:18ch;margin:0 auto;}
/* The button carries the heading→button gap itself, at the standard section
   spacing, so it holds whatever the eyebrow does — above the heading, below it,
   or absent — instead of relying on the h2 bottom margin the eyebrow flow zeroes. */
/* ⚠ A CHIP, NOT A RECTANGLE WITH ROUNDED CORNERS, 26 September 2026. Dylan:
   "Within the Call to Action section, please make the 'Contact us' button be a
   chip rather than a rectangle with rounded corners."
   999px, which is this stylesheet's own idiom for a pill — .wh-cal-chip,
   .wh-fsv-btn, .enqf-btn and .wh-enq all say the same thing —
   rather than a new number. Only the corner moves: the fill, the ink, the type
   and the 15px/32px padding the block sets inline are untouched, so the chip is
   the same button in a different silhouette.
   ⚠ SCOPED TO THIS BAND, AND THAT IS THE WHOLE OF THE CHANGE. The shared .btn
   rule is used by the hero, the services list, the trust card, the insights
   head and the menu; rounding it there would answer a question nobody asked
   about five other blocks. .cta-band .inner>.btn is three classes and an
   element against one class, so it wins on SPECIFICITY and not on source order
   — which matters, because the shared rule is declared about 1,880 lines
   earlier and an equal-weight rule here would be relying on the concatenation
   order of BASE_CSS to keep working.
   ⚠ AND THE FIRST DRAFT OF THIS COMMENT PUT THE SELECTOR IN BACKTICKS, WHICH
   ENDED THE TEMPLATE LITERAL. The file's own warning says it has already cost
   three edits; this is the fourth. The error pointed at this line and read
   '".btn is not a function', which is the shape to recognise. Single quotes or
   nothing, in every comment in this file. */
.cta-band .inner>.btn{margin-top:var(--h-section-gap,26px);border-radius:999px;}
footer{background:var(--chrome);color:#b9bcc4;padding:74px 0 40px;font-size:14px;}
.fgrid{display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:32px;}
footer .crest{width:46px;height:46px;margin-bottom:10px;}
/* ── THE FOOTER'S COPY OF THE HEADER LOGO ────────────────────────────────────
   Dylan, 28 September 2026: the footer shows the same logo as the top of the page
   instead of the roundel, slogan and name. compile.ts emits
   <a class="fbrand wh-brand-lg"> with the header's own logoHtml inside it.

   ⚠ THE SIZE IS SET HERE, ON THE LINK, AND NOT ON .wh-lg — which is the opposite
   of how the masthead and the sticky bar do it, on purpose. Those two set
   font-size on .wh-lg, the LOCKUP. But a flattened logo is not a .wh-lg: it is
   a bare <img class="wh-lg-img"> whose height is calc(var(--lg-rh,1)*1em*…), so
   its 1em comes from whatever ANCESTOR sets a font-size. Sizing .wh-lg here
   would therefore size a live lockup correctly and leave a flattened one at the
   inherited 14px of the footer's body text — the same class of defect --lg-ih was
   added to fix in the masthead (see logoStyle in compile.ts). One declaration on the
   link sizes both kinds, because both read their em from it.

   26px against the 22px the wordmark used to be: a logo carries a roundel and often
   a strapline inside that em, so matching the old NAME's size would have made the
   whole lockup smaller than the name it replaced.

   White, for the same reason the masthead forces white: the lockup's stored word
   colours are chosen against a light bar and the footer is navy. ⚠ This reaches a
   live lockup only. A flattened logo is a bitmap and keeps its baked-in colours;
   compile.ts says so at the point it decides. */
footer .fbrand{display:inline-flex;align-items:center;font-size:26px;color:#fff;text-decoration:none;margin-bottom:10px;}
footer .fbrand:hover{text-decoration:none;}
footer .fbrand .wh-lg{color:#fff;}
/* The footer's own logo must not inherit the body's 1.7 line-height: a lockup sets
   its own, and the extra leading would push the roundel off its words. */
footer .fbrand,footer .fbrand .wh-lg{line-height:1;}
footer .fbrandrow{display:flex;align-items:flex-end;gap:22px;margin-bottom:10px;}
footer .fbrandrow>.fbrand{margin-bottom:0;}
footer .fbrandrow>.fbrand,footer .fbrandrow>div{flex:none;}
footer .fbadge{flex:0 1 auto;min-width:0;display:block;font-size:26px;background:#fff;border-radius:2px;box-shadow:0 2px 8px rgba(0,0,0,.22);padding:.3em .38em;line-height:0;}
footer .fbadge img{display:block;width:auto;height:auto;max-height:1.9em;max-width:100%;}
@media(max-width:900px){.fgrid>div:has(>.fbrandrow){grid-column:1 / -1;}}
/* ── THE FOOTER'S BRAND TYPE IS THE HEADER'S ──────────────────────────────
   Every fallback below is exactly what this rule computed before the tokens
   existed, so a site with no header logo renders byte-for-byte as it did.
   footerBrandTypeStyle() in compile.ts fills them from the TOP header's logo
   when there is one — see the note there for what it does and does not copy.
   Tracking is carried in 1/1000 em, like the logo studio's own --lg-t*, so the
   two can never disagree about units; 0 here and 340 on .tm are today's values
   written in those units. */
footer .nm{font-family:var(--ft-nm-face,var(--serif));font-size:22px;color:#fff;font-weight:var(--ft-nm-weight,600);font-style:var(--ft-nm-style,normal);letter-spacing:calc(var(--ft-nm-track,0)*.001em);text-transform:var(--ft-nm-case,none);}
/* Match the masthead strapline (.masthead .cm) weight + tracking exactly so the
   footer slogan reads as the same type — only the colour differs (pink here). */
/* The pink is Dylan's, said so on 25 September 2026, and is deliberately NOT
   tokenised: the logo's own datum colour would be the header's, and the footer
   wants this one. Only the type follows the header. */
footer .tm{font-family:var(--ft-tm-face,inherit);font-size:10.5px;font-weight:var(--ft-tm-weight,700);font-style:var(--ft-tm-style,normal);letter-spacing:calc(var(--ft-tm-track,340)*.001em);text-transform:var(--ft-tm-case,uppercase);color:var(--pink);}
footer h4{font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:#7d818b;margin-bottom:15px;font-weight:700;}
footer a{display:block;width:fit-content;margin-bottom:9px;color:#b9bcc4;}
/* THE FOOTER'S ENQUIRE (Dylan, 26 October 2026): the header corner's pink pill,
   the same size, fill, ink and hover, at the right-hand end of the logo row and
   level with the bottom of the wordmark. Below 900px it drops under the logo.
   Its face is the header pill's too (Dylan, 30 October 2026: the footer's
   Enquire "seems to have a different font to the one in the header"): it
   inherited the footer's body face, the pill reads the menu row's face. The two !importants (7 November 2026, a pink strip on hover) beat the a:not() underline, as .wh-enq's do. */
footer a.fenq{grid-column:-2;grid-row:1;justify-self:end;align-self:end;display:inline-flex;align-items:center;height:44px;padding:0 20px;margin:0;border-radius:999px;background:var(--btn-fill,var(--pink));color:var(--btn-ink,#fff);font-family:var(--mn-row-face,inherit);font-style:normal;text-transform:none;font-size:13px;font-weight:600;line-height:1;white-space:nowrap;text-decoration:none;background-image:none!important;padding-bottom:0!important;transition:background-color .18s ease;}
footer a.fenq:hover,footer a.fenq:focus-visible{background:color-mix(in srgb,var(--btn-fill,var(--pink)) 86%,#000);color:var(--btn-ink,#fff);text-decoration:none;}
footer a.fenq.fenq--below{grid-row:auto;}
/* THE INDEPENDENCE DISCLAIMER.
   Its own row above the copyright rule rather than a fourth item inside it:
   the rule is a flex row of short spans that wraps, and seventy-five words
   dropped into it would either stretch to the full width of the footer at
   12px — a line length nobody reads — or wrap into a block that pushes the
   copyright and the legal links apart. A measure is set for the same reason.
   Slightly dimmer than the rule beneath it and slightly smaller, because it
   is the footer's quietest text; still well clear of the ground it sits on,
   since a disclaimer nobody can read is not a disclaimer. */
.fdisc{margin-top:40px;padding-top:20px;border-top:1px solid rgba(255,255,255,.14);color:#b3b8c1;font-size:12px;line-height:1.7;}
/* ⚠ THE MEASURE IS ON THE PARAGRAPH, NOT ON THE ROW, and the first attempt had
   it the wrong way round. A max-width on .fdisc shortens the rule above it too,
   so the divider stopped dead about half way across the footer and read as a
   broken border rather than as a deliberate column. The row spans the footer;
   only the sentence is held to a length somebody can read. */
.fdisc p{max-width:92ch;}
/* ...and the rule below it then needs neither the gap nor a second line. */
/* ⚠ TWO COLUMNS BELOW THE DIVIDER. Dylan, 17 October 2026: the disclaimer and
   the copyright on the left, the footer's legal links (Privacy policy and the rest)
   in a column on the right, top-aligned with the disclaimer. The rule stays on
   .fdisc, which still spans the footer, so the divider is unchanged. Below 900px
   the grid is one column and the links fall between the disclaimer and the
   copyright, in source order (compile.ts). */
.fdisc{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:56px;align-items:start;}
.fdisc > p{grid-column:1;grid-row:1;}
/* Dylan, 17 October 2026: the disclaimer and copyright a shade brighter (above:
   #9aa0ab to #b3b8c1, #7d818b to #a4a9b2), and the links a little bigger and
   brighter than the footer's other links, stacked as a list so new ones line up
   under Privacy policy. */
.fdisc > .footer-legal{grid-column:2;grid-row:1 / span 2;flex-direction:column;gap:0;}
.fdisc > .footer-legal a{font-size:15.5px;color:#e4e6ea;}
.fdisc > .footer-legal a:hover,.fdisc > .footer-legal a:focus-visible{color:#fff;}
.fdisc > .frule{grid-column:1;grid-row:2;margin-top:18px;padding-top:18px;border-top:0;}
.frule{border-top:1px solid rgba(255,255,255,.14);margin-top:46px;padding-top:22px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px;color:#a4a9b2;font-size:12px;}
/* ── the burger ──
   There is ONE burger, in the fixed corner, in both menu designs, and it opens
   the full-screen sheet. The base rule stays because .wh-burger is still the
   shared hook the runtime syncs aria on; the round corner variant declares its
   own display in CORNER_CSS.
   Gone with the slide-in panel they styled: every .wh-mnav rule, the
   html.wh-mnav-open scroll lock, and the two rules that positioned a burger
   INSIDE each header. */
.wh-burger{display:none;flex-direction:column;justify-content:center;gap:5px;width:44px;height:44px;padding:0;background:none;border:0;cursor:pointer;}
.wh-burger span{display:block;width:22px;height:2px;margin:0 auto;background:currentColor;transition:transform .22s,opacity .18s;}
.masthead .inner{position:relative;}
@media(max-width:900px){
  .hero-grid{grid-template-columns:1fr;grid-template-rows:auto auto auto;}
  .hero-lead,.hero-mini,.hero-feature{grid-column:1;}
  .hero-feature.card{grid-row:2;min-height:360px;}
  .hero-mini{grid-row:3;}
  .wd-split,.wd-noquote .wd-list{grid-template-columns:1fr;}
  .ins-grid{grid-template-columns:1fr;}
  .fgrid{grid-template-columns:1fr 1fr;}
  footer a.fenq{grid-column:1 / -1;grid-row:auto;justify-self:start;}
  .fdisc{grid-template-columns:1fr;}
  .fdisc > p,.fdisc > .footer-legal,.fdisc > .frule{grid-column:1;grid-row:auto;}
  .fdisc > .footer-legal{margin-top:18px;}
  /* THE BURGER DESIGN BELOW 900px. This is the ONLY place the breakpoint is
     written: NAVFIT_JS asks the computed style whether the row is on screen
     rather than repeating the number, and publishes wh-nav-tight when it is
     not — so everything that defers to the row (the corner's audience prompt)
     answers to one fact rather than to a copy of this media query. */
  .masthead .nav{display:none;}
  .drophdr .nav{display:none;}
  /* .wh-burger{display:flex} lived here to reveal the header-embedded burgers
     below the breakpoint. The only .wh-burger left is the corner's round one,
     which declares display:flex itself in CORNER_CSS — emitted after this
     sheet's base rule, so it wins at every width with or without this. */
}
.intro{background:var(--white);padding:clamp(50px,5.8vw,88px) 0;}
.intro figure{margin:0;}
.intro .imark{display:block;font-family:var(--serif);font-weight:700;color:var(--pink);font-size:clamp(64px,7vw,104px);line-height:.72;margin-bottom:clamp(-30px,-1.65vw,-10px);user-select:none;}
.intro blockquote{font-family:var(--q-face,var(--serif));font-style:var(--q-style,normal);font-weight:var(--q-weight,500);color:var(--q-colour,var(--navy));font-size:calc(var(--q-scale,1) * clamp(23px,2.8vw,36px));line-height:1.24;letter-spacing:var(--q-track,-.01em);text-transform:var(--q-case,none);max-width:30ch;text-wrap:balance;margin:0;}
.intro blockquote em{font-style:italic;color:var(--pink-deep);}
.intro figcaption{display:flex;align-items:center;gap:12px;margin-top:clamp(28px,3vw,40px);}
.intro .iph{width:36px;height:36px;flex:0 0 36px;border-radius:50%;background-position:center top;}
.intro .iwho{font-family:var(--body);font-size:13px;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.4);color:var(--muted);letter-spacing:.02em;}
.intro .iwho strong{font-weight:600;color:var(--navy);}
@media (max-width:900px){.intro blockquote{max-width:none;}}

/* Services list, photograph mode: a large circle that bleeds off the LEFT edge.
   The two numbers below are load-bearing together — width:min(--dmax,125%) with
   margin-left:calc(100% - <the same>) sends any overflow leftwards, and 125% is
   what caps the bleed at 20% of the diameter. The circle stays in normal flow
   with aspect-ratio (never absolute, never a vh height), so the section grows to
   fit it and it can never be clipped top or bottom. */
/* overflow-x:clip trims the circle's horizontal bleed off the LEFT edge, while
   overflow-y stays visible so the circle's drop-shadow can spill BELOW the
   section instead of being cropped at its bottom. position/z-index then lift the
   section (and the escaped shadow) above the following section so the shadow
   paints on top of it rather than being hidden behind it. This also lets the
   Scroll-reveal ::after panel (120vh below) show, so both features coexist. */
.whatdo.is-photo{
  overflow-x:clip;
  overflow-y:visible;
  position:relative;
  z-index:1;
  --pad:clamp(22px,4vw,48px);
  --gutter:calc(max(0px,(100vw - 1280px)/2) + var(--pad));
  --dmax:620px;
}
/* The grid is pulled out to the viewport's left edge, so the circle's column
   starts at x=0 and a -25% margin puts exactly 20% of it off the page. */
.wd-split.wd-photo-mode{
  grid-template-columns:1fr 1.05fr;
  gap:0 clamp(32px,4.4vw,72px);
  /* ⚠ START, NOT CENTRE. Dylan, 24 September 2026: the picture should "always
     be right-aligned within that LHS column and top-aligned too". Centring was
     invisible while the circle was the tallest thing in the row; the moment it
     can be made smaller it would float in the middle of a column it no longer
     fills, which is the thing that reads as unfinished. Right-alignment needs
     no rule: .wd-circle's margin-left is already calc(100% - width), which IS
     right-alignment, and unlike an "auto" margin it still holds when the width
     EXCEEDS the column - which is how the big circle bleeds off the page.
     ⚠ No backticks in this comment: it sits inside a JS template literal and a
     backtick closes it. That is not hypothetical - the first draft of this very
     comment quoted "auto" in backticks and the file stopped parsing. */
  align-items:start;
  margin-left:calc(-1 * var(--gutter));
}
/* ── THE SERVICES LIST ARRIVES IN TWO BEATS, NOT ONE ──────────────────────
   Dylan, 25 September 2026: "I'd like the LHS image to fade in more before the
   RHS heading/text etc., and for the vertical fade in upwards distance to be
   larger."

   Both are one rule, because the shared reveal runtime already expresses both
   as inherited custom properties and this section only has to say what its own
   numbers are. The left column carries data-rv="0" and the lead column
   data-rv="1", and the delay is index x --rv-step - so raising the STEP widens
   the gap between the picture and the words without touching either index, and
   without disturbing any other section's timing.

   ⚠ SCOPED TO .whatdo ON PURPOSE. --rv-step and --rv-lift are site-wide
   settings with their own global control; putting these values on :root would
   re-time every revealing section on the site to suit this one. Inheritance
   does the rest: the [data-rv] children read the nearest declaration.

   160ms -> 340ms and 18px -> 40px. Both are overridable from Global -> Block
   settings, where they sit beside the other services-list controls. */
.whatdo.wh-rv{--rv-step:340ms;--rv-lift:40px;}
.wd-photo{min-width:0;}
.wd-circle{
  width:min(var(--dmax),125%);
  aspect-ratio:1;
  margin-left:calc(100% - min(var(--dmax),125%));
  border-radius:50%;
  background-position:center;
  background-size:cover;
  background-repeat:no-repeat;
  position:relative;
  box-shadow:0 30px 64px -34px rgba(20,38,63,.5);
}
/* aspect-ratio is Safari 15+; on older iOS the circle would get width but auto
   (0) height and vanish. padding-bottom:100% reproduces the 1:1 box there. */
@supports not (aspect-ratio:1){.wd-circle{height:0;padding-bottom:100%;}}
.wd-circle svg{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;width:100%;height:100%;pointer-events:none;}
/* Optional clockwise ring draw (photoRingAnim). The circle has no rotation, so
   the sweep starts at 3 o'clock (quarter past); .is-drawn is added by the
   observer when the section scrolls into view. Reduced-motion shows it drawn. */
.wd-ring--draw circle{stroke-dasharray:1188;stroke-dashoffset:1188;}
.wd-ring--draw.is-drawn circle{animation:whWdRingDraw 900ms cubic-bezier(.33,0,.2,1) both;}
@keyframes whWdRingDraw{to{stroke-dashoffset:0;}}
@media (prefers-reduced-motion:reduce){.wd-ring--draw circle{stroke-dashoffset:0;animation:none;}}

/* ── THE QUIET ALTERNATIVE ────────────────────────────────────────────────
   Dylan, 24 September 2026: "toggle between the current design and one which
   is more mellow with the image being smaller ... 2px rounded corners, but it
   should be smaller."

   Three things change together, and they have to: the circle is large BECAUSE
   it bleeds off the page, so shrinking it without also cancelling the negative
   margin would leave it stranded in the middle of a gutter it no longer fills.
   So the card shape cancels the bleed, gives the columns back an even weight,
   and drops the 1:1 crop for a portrait-ish 4:5 that suits a smaller picture.

   ⚠ The ring is dropped in the MARKUP rather than hidden here, because a ring
   is a circle: over a rectangle it reads as a stray outline, not as an option
   somebody left on. */
.wd-split.wd-photo-mode.wd-card-mode{
  grid-template-columns:.82fr 1.18fr;
  /* ⚠ A TIGHTER GUTTER THAN THE CIRCLE'S, and the reason is the bleed. The
     circle starts off the left edge of the page, so it needs a wide gutter to
     stop the photograph running into the list. The card sits inside the margin,
     so the same gutter reads as a hole. Dylan asked for "some, but not too
     much". */
  gap:0 clamp(24px,2.6vw,44px);
  margin-left:0;
  /* ⚠ NO align-items HERE - IT INHERITS start FROM .wd-photo-mode, AND THE
     FIRST VERSION OF THIS RULE OVERRODE IT WITH center.
     ⚠ No backticks anywhere in this comment: it sits inside a JS template
     literal and a backtick closes it. The first draft of this very comment
     quoted start and center in backticks and the file stopped parsing - the
     third time that trap has been sprung in this file, which is why the
     warning is repeated here rather than left to the other two.

     Dylan, 25 September 2026: "when you decrease the size of the circle or
     rectangle image in the services list block, it should still stay right- and
     top-aligned within the LHS column."

     The circle obeyed that and the rectangle did not, because this rule is more
     specific than the one that sets start and quietly won. It was invisible
     while the picture was the tallest thing in the row — centring and
     top-aligning look identical when an item fills its track — and appeared the
     moment the size control made it shorter, which is exactly when somebody
     would be looking. Removing the declaration is the fix; adding
     align-items:start here would work too and would be a second place for the
     same answer to drift. */
}
.wd-circle.wd-card{
  width:min(100%,var(--wd-card-w,380px));
  aspect-ratio:4 / 5;
  /* ⚠ RIGHT-ALIGNED, NOT LEFT. Dylan, 25 September 2026: the picture "doesn't
     seem to be right-aligned within its LHS column fully - there's a lot of
     space (there should be some, but not too much) between it and the RHS
     column."

     The first version of this rule set margin-left:0, which pins the card to
     the LEFT edge of a column that is wider than it - so every pixel the card
     is narrower than its column became dead space in the middle of the
     section, exactly where the eye expects the two halves to meet.

     ⚠ auto IS CORRECT HERE, AND IS WRONG FOR THE CIRCLE. The circle uses
     calc(100% - width) because it may be WIDER than its column (that is how it
     bleeds off the page) and an auto margin collapses to 0 in that case. The
     card can never exceed its column - min(100%,...) guarantees it - so auto is
     both simpler and exactly equivalent here. The two rules differ because the
     two shapes differ, not by oversight. */
  margin-left:auto;
  margin-right:0;
  border-radius:2px;
  box-shadow:0 18px 44px -28px rgba(20,38,63,.42);
}
@supports not (aspect-ratio:4 / 5){.wd-circle.wd-card{height:0;padding-bottom:125%;}}

/* -- HOW TALL THE PICTURE IS, AND WHERE IT SITS DOWN THE COLUMN -----------
   Dylan, 25 September 2026: "you should also be able to make it the full
   height of the LHS column (i.e. stretching from the top of the roundel on the
   RHS to the bottom of the services list on the RHS, with the normal padding at
   the top and the bottom of the section)", and separately: "whilst the default
   should be top right aligned within the column, i'd also like to have the
   option to make it vertically centred within the column".

   THE PADDING LOOKS AFTER ITSELF, which is why there is no padding rule here.
   .wd-photo is a GRID ITEM, so stretching it fills the row - and the row lives
   inside .wrap, inside the section's own vertical padding. Filling the row
   therefore runs from the top of the words to the bottom of them and stops,
   which is exactly what he described, without this rule knowing what the
   padding is.

   ⚠ THE TWO HEIGHT MODES MUST CANCEL THE ASPECT RATIO, NOT JUST SET A HEIGHT.
   A box with a definite width, a definite height AND an aspect-ratio is not
   over-specified in CSS - the ratio is simply ignored - so this would work
   without the aspect-ratio:auto. It is written anyway because the
   @supports-not fallback above is NOT a ratio: it is height:0 with
   padding-bottom:125%, and height:0 would win on a browser old enough to need
   it and collapse the picture to nothing. Setting padding-bottom back to 0 is
   the half that actually matters. */
.wd-photo.wd-fill{align-self:stretch;}
.wd-photo.wd-fill .wd-circle,.wd-photo.wd-hset .wd-circle{aspect-ratio:auto;padding-bottom:0;}
.wd-photo.wd-fill .wd-circle{height:100%;}
.wd-photo.wd-hset .wd-circle{height:var(--wd-card-h,380px);}
/* ⚠ center, NOT auto MARGINS. The circle already uses margin-left to sit
   against the right of its column, so a vertical answer written as a margin
   would be a second thing reading the same property. align-self is the grid's
   own word for it and touches nothing the horizontal rules depend on.
   The block refuses to emit this alongside wd-fill, so the two never fight. */
.wd-photo.wd-mid{align-self:center;}

@media(max-width:900px){
  /* ⚠ AND THE ROW GAP HAS TO COME BACK, or the roundel sits flush against the
     bottom edge of the picture. Dylan reported exactly that on 26 September
     2026 with a screenshot, and it is measured: rowGap computed to 0px at
     390px, so the crest's top and the photo's bottom were the same pixel.

     The zero is correct where it is written — .wd-photo-mode sets
     gap:0 <column-gap> for a TWO-COLUMN row, where there are no rows to space
     and a row gap would do nothing. It only becomes wrong here, at the one
     breakpoint that turns those columns into rows, and that is why it was
     invisible on every desktop width. Same clamp the unstacked .wd-split
     uses, so the spacing matches the rest of the block rather than inventing
     a number. */
  .wd-split.wd-photo-mode{grid-template-columns:1fr;margin-left:0;row-gap:clamp(24px,3.2vw,52px);}
  .wd-circle{width:min(100%,420px);margin-left:auto;margin-right:auto;}
  /* ⚠ THE ROW GAP AGAIN, FOR THE CARD SHAPE. Dylan, 17 October 2026: "beneath
     the image in the services list block, there is no space between that and
     the roundel immediately below." Measured at 390px: the card's bottom and the
     crest's top were the same pixel, while the circle had its 24px. The row-gap
     above is written on .wd-photo-mode, and the desktop card-mode rule
     (gap:0 <column-gap>) is one class more specific, so it won and zeroed the
     row gap for the rectangle only. Same clamp, so both shapes match. */
  .wd-split.wd-photo-mode.wd-card-mode{grid-template-columns:1fr;row-gap:clamp(24px,3.2vw,52px);}
  .wd-circle.wd-card{width:min(100%,340px);margin-left:auto;margin-right:auto;}
  /* ⚠ THE FULL-HEIGHT MODE MUST BE UNDONE WHEN THE COLUMNS STACK, OR THE
     PICTURE DISAPPEARS ON A PHONE. Once the grid is one column the row's height
     comes from the item itself, so height:100% resolves against an indefinite
     parent and falls back to auto - and .wd-circle has no content, so auto is
     ZERO. With the aspect ratio also cancelled there is nothing left to give it
     a height at all: a blank gap where the photograph was, on the layout most
     visitors use. The fixed height is left alone deliberately; a number
     somebody chose is still a number on a phone. */
  .wd-photo.wd-fill{align-self:auto;}
  .wd-photo.wd-fill .wd-circle{height:auto;aspect-ratio:4 / 5;}
  .wd-photo.wd-mid{align-self:auto;}
}
/* ⚠ AND THE SAME UNDO FOR A BROWSER WITH NO aspect-ratio, which the line above
   cannot reach: there the ratio is inert and the card's height comes from
   padding-bottom:125%, which the fill mode had to zero. Without this the phone
   layout on Safari 14 and older shows the gap this whole media block exists to
   prevent. Its own at-rule rather than a declaration in the block above,
   because the two cases need OPPOSITE answers and one rule cannot hold both. */
@supports not (aspect-ratio:4 / 5){
  @media(max-width:900px){
    .wd-photo.wd-fill .wd-circle{height:0;padding-bottom:125%;}
  }
}
/* ── ON A PHONE THE PICTURE RUNS THE FULL WIDTH OF THE WORDS ──────────────
   Dylan, 17 October 2026: "In the services list block on mobile, the image
   doesn't appear to be the same width as the rest of the content." The stacked
   layout above caps the circle at 420px and the card at 340px and centres
   them, so on a phone wider than the cap (a 430px phone has 386px of content)
   the picture sat inset from the heading and the list on both sides. At 640px
   and below it takes 100% of the column instead, so its edges are the
   heading's and the list's edges. ⚠ 641-900px (tablets, stacked) keeps the
   capped, centred picture: a full-width 4:5 card there would be taller than
   the screen. The FIXED height mode still sets its own height. */
@media(max-width:640px){
  /* ⚠ WIDTH ONLY. The 900px block already gives both shapes auto margins, and
     at 100% there is no free space for them to share, so they are 0 in effect.
     Writing margin-left:0 here would also trip the guard that the card is never
     pinned left on desktop (heroEntranceAndQuietIntro), which reads the rule
     text and cannot see the media query. */
  .wd-circle,.wd-circle.wd-card{width:100%;}
  /* ⚠ THE ROUNDEL MATCHES THE CALL TO ACTION'S. Dylan, 17 October 2026: "the
     call to action block roundel size on mobile should be the model." The
     roundel is var(--crest-w,42px) everywhere; when the site sizes it as a
     share of the heading (an em value) it resolves against this font-size,
     and the services list's was 27px against the call to action's 30px, so
     the two roundels differed on a phone. Written as the SAME expression as
     .cta-band .eyecrest rather than as a number, so the two cannot drift
     apart if either clamp or the heading scale changes. Unset, both stay the
     fixed 42px and this changes nothing. */
  .whatdo .lead .eyecrest{font-size:calc(var(--h-section-scale,1) * clamp(30px,4.4vw,56px));}
}

/* Insights: the tall ratio exists only to bottom-align the caption with the
   right-hand column. Once the columns stack there is nothing to align to, so the
   ratio reverts to its natural value and the image is no longer very deep. */
@media(max-width:900px){
  .feature{display:block;padding-bottom:0;}
  .feature .pic{flex:none;height:auto;aspect-ratio:auto;}
}

/* ------------------------------------------------------------------ */
/* Enquiry form: full-bleed split — navy panel left, hairline fields   */
/* right. No boxed inputs anywhere: a label above, a transparent       */
/* ground, and a 1px rule underneath that turns pink on focus.         */
/* ------------------------------------------------------------------ */
.wh-enqf{padding:0;}
/* ── THE SINGLE "SERVICES" SUBMENU ──────────────────────────────────────────
   N1771, design 4 revision 3, ported from
   docs/branches/claude-gallant-gates-5ppgmv/services-submenu-designs.html.
   Dylan chose it and sent three rounds of refinements; every one of them is a
   line below, and each is marked so a future reader does not "tidy" one away.

   It reuses .navgrp / .navgrp-btn / .mm, so the hover intent, the hand-over
   between items, the retract with the sticky header and the fold measurement
   all keep working without being re-earned.                                   */
/* ⚠ THE TOP PADDING CARRIES THE OVERHANGING LOGO'S CLEARANCE, and the whole of
   the scoping is that --wh-svc-clear is only ever PUBLISHED on the sticky bar.

   Dylan, 1 October 2026: *"You may also wish to move all of the content within
   the submenu itself down so that there is a bit of padding between the logo
   (however big it may be, bearing in mind that this can be changed) and the
   content within the box itself. (Don't just move the entire submenu container
   down, this wouldn't match the description. Move the submenu content itself
   down, i.e. the content within the box.)"* — so the padding moves and the
   panel's own top edge stays flush under the bar, which is what he asked for in
   the parenthesis.

   And, separately: *"Please ensure that there are no equivalent layout changes
   in the submenu when it appears from the masthead."* That is why there is no
   '.drophdr' in this selector and no masthead override either. svcClear() in
   MEGAMENU_JS sets --wh-svc-clear on the DROP-IN HEADER and on nothing else, so
   the masthead's panel resolves the fallback 0px and is byte-for-byte the layout
   it had. A scoped rule would have needed a second rule the day somebody added a
   third header; a variable that is simply absent needs none.

   ⚠ "HOWEVER BIG IT MAY BE" IS WHY IT IS MEASURED RATHER THAN TYPED. The logo's
   size and its padding above are both site settings, so the overhang is whatever
   those two come out at — a number here would be right for one site and wrong
   for the next one that changes either.

   28px is unchanged and is the gap itself: the panel's own designed space below
   the bar, which becomes the space below the logo when a logo hangs into it. */
/* ⚠⚠ THE TWO HEADERS ARE DELIBERATELY DIFFERENT HERE, AND THE FIRST DRAFT OF
   THIS RULE GOT THAT WRONG by giving both the centred wrap. Dylan, on seeing it:
   *"In this instance, I don't want the space at either side to expand according
   to how wide the window is. I just want it to be horizontally slightly more
   compressed than it is at present."*

   The reason is what each one IS. The sticky bar's panel is a FULL-BLEED SHELF
   spanning the window, so its contents need the site container or they run to
   the glass. The masthead's panel is already a CARD with margins of its own —
   it does not touch the edges, so centring a narrower column inside it would
   add a second set of gutters inside the first and read as a box in a box.

   So the shared base is only a modest inset, and the wrap belongs to '.drophdr'
   alone — the same scoping, and for the same reason, as '--wh-svc-clear' two
   rules up: the masthead resolves the fallback and is left as it was. */
.svcgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(214px,1fr));gap:0;
  padding:calc(28px + var(--wh-svc-clear,0px)) var(--wh-svc-gut,clamp(10px,1.2vw,22px)) 26px;}

/* ⚠ THE STICKY PANEL ONLY: the site container, centred, with the gutters growing
   as the window does. Dylan: *"the bit of the submenu that actually has content
   in it be a fixed width, with padding expanding on the LHS and the RHS
   depending on how wide the page is."* The background still spans the window —
   it is the shelf the bar sits on, and stopping it at 1280px would read as a
   floating card rather than part of the header.

   --wh-svc-wrap defaults to --wrap so the menu follows the site container, which
   is the alignment being asked for; a site whose header is far wider than its
   body can widen the menu alone without widening every band on every page. */
/* ⚠ AND THE CAP SCALES WITH HOW MANY CATEGORIES THERE ARE, because there will
   not always be three. Dylan, 1 October 2026: *"there may be more or less
   categories of service in the future, so it should also be future-proofed."*

   Columns are '1fr', so they divide whatever width the grid has. Pinned at a
   flat 1280px that is right for three and wrong at both ends: ONE category would
   stretch a single short list across the whole container with nothing beside it,
   which reads as a layout fault rather than a menu.

   So the width is the site container OR the natural width of the columns there
   actually are, whichever is SMALLER. 430px is the width three columns settle at
   inside the wrap today, so three is unchanged to the pixel — one category gets
   430px, two get 860px, and four or more hit the container and divide it.

   ⚠ --svc-cols IS WRITTEN BY BOTH BUILDERS, and the default of 3 is a fallback
   rather than an assumption: the compiler stamps it on the baked panel and
   svcColsInto() stamps it on the rebuilt one, so a cell swap re-stamps it with
   the new cell's count rather than keeping the baked cell's.

   ⚠ WHAT THIS DOES NOT FIX, out loud: 'auto-fit' still wraps when the columns
   would fall below 214px — six categories inside the wrap become five and one,
   and that trailing single column stretches the full width because it is still
   '1fr'. Equal columns ACROSS ROWS needs a chosen column count per breakpoint
   rather than auto-fit, which is a different design and a product decision about
   how a six-category menu should read. Five categories is the most that stays on
   one row, and at that point this is worth revisiting rather than patching. */
.drophdr .svcgrid{max-width:min(var(--wh-svc-wrap,var(--wrap)),calc(var(--svc-cols,3) * 430px));
  margin-left:auto;margin-right:auto;--wh-svc-gut:clamp(22px,4vw,48px);}

.svcol{padding:0 clamp(18px,2.2vw,26px);border-left:1px solid var(--line);
  display:flex;flex-direction:column;}
.svcol:first-child{border-left:0;}
/* ⚠ STICKY ONLY, and it is the half that makes the alignment deliberate rather
   than merely narrower. '.svcol' pads both sides so the dividers sit in a
   gutter; left on the outer columns INSIDE a centred wrap, "Admissions" would
   indent ~20px further than the body text it is meant to line up with, and
   agreeing with the body text is the whole point of the wrap. The masthead card
   keeps its outer padding, because there the padding IS the compression. */
.drophdr .svcol:first-child{padding-left:0;}
.drophdr .svcol:last-child{padding-right:0;}
/* ⚠ THE SERIF, AND IT IS THE h3/QUOTATION FACE RATHER THAN --serif. Dylan asked
   for the serif "woven in"; --serif is built from theme.fonts.heading, which on
   this site is a SANS, so reading it here would have quietly produced the body
   face. --h-sub-face is the one he meant. Same mistake cost a cycle on N1781. */
.svccat{font-family:var(--h-sub-face,var(--serif));font-weight:600;
  font-size:19.5px;color:var(--navy);margin:0 0 13px;letter-spacing:-.01em;
  line-height:1.22;}
/* ── THE HEADING AS THE WAY THROUGH ─────────────────────────────────────────
   Dylan, 1 October 2026: the chip at the foot of each column goes, and instead
   "a right arrow … appears next to the service category title at the top of the
   submenu when you hover over it, alongside an underline effect (the same one
   that is used elsewhere). The title itself should become the thing that takes
   you through to the service category page."

   ⚠ 'display:block' — .svcol is a flex column, so an inline <a> would shrink to
   its text and the hover target would be the words rather than the heading's
   line. It also keeps the 'margin:0 0 13px' above doing what it did when this
   was a <div>.
   ⚠ 'text-decoration:none' — the heading is an anchor now and the browser's own
   underline is NOT the site's; the .ul span is.
   ⚠ 'width:max-content;max-width:100%' — without it the block is the column's
   full width and the arrow sits far out to the right of a short title, detached
   from the words it belongs to. max-width keeps a long title wrapping inside the
   column rather than widening the grid.                                       */
a.svccat{display:block;text-decoration:none;width:max-content;max-width:100%;}
/* The same two rules the service rows carry, named for the heading — one
   mechanism for the underline and one for the arrow, which is what "the same one
   that is used elsewhere" has to mean if it is to stay true. focus-visible as
   well as hover, so a keyboard reader arriving at the heading sees both. */
a.svccat:hover .ul,a.svccat:focus-visible .ul{background-size:100% 2px;
  --ul-c:var(--pink);transition:var(--ul-in);}
a.svccat:hover .wh-fs-arw,a.svccat:focus-visible .wh-fs-arw{opacity:1;transform:none;}
/* Sized off the heading rather than the rows: the arrow belongs to this text.
   ⚠ vertical-align, not a flex alignment — see the note on .svcrow's arrow; the
   heading is inline flow inside its block for the same reason. */
a.svccat .wh-fs-arw{margin-left:10px;vertical-align:1px;font-size:17px;}
.svclist{display:flex;flex-direction:column;}
/* ⚠ INLINE FLOW, NOT FLEX — this is what puts the signature mark on the title's
   baseline for free. sigMark emits .wd-crest and .wh-sig as INLINE elements, so
   they share the baseline; a flex row has to be told to align, and told again
   every time either font size changes. */
/* ⚠ 14px → 15px, 2 October 2026, at Dylan's word: *"please make the service items
   text slightly bigger — at present it's slightly difficult to use."* Slightly is
   the instruction and one step is slightly; the padding is deliberately left
   alone, so the column's rhythm is unchanged and only the type grows. The arrow
   beside a row is sized from '--mn-service-scale' rather than from this, so the
   two do not drift. */
/* ⚠ 15px → 17px, 18 October 2026, Dylan: *"make it so that the service links
   themselves are 2 sizes bigger"*. Two of the 2 October steps, and again only
   the type: padding, column widths and headings are unchanged. */
.svcrow{display:block;font-family:var(--body);font-size:17px;color:var(--ink);
  text-decoration:none;padding:7px 0;}
/* ⚠ NO BACKGROUND ON HOVER — he asked for the site's underline instead. The .ul
   span and its --ul-c are the same mechanism the top menu rows already use, so
   there is one underline in this site rather than two that can drift. */
.svcrow:hover .ul,.svcrow:focus-visible .ul{background-size:100% 2px;
  --ul-c:var(--pink);transition:var(--ul-in);}
/* ⚠ THE SIGNATURE CREST HAD NO SIZE HERE, AND AN INLINE <svg> WITH NO SIZE FILLS
   ITS CONTAINER. sigMark emits WD_CREST, whose only other rules are in contexts
   that size it — .nrow/.wh-fs-row at 13px, .wd-item at 15px, .svp-plate-sig at
   18px. This panel was never given one, so the roundel rendered at 100% of the
   column: measured 255x255 in a 255px column in Chromium, and about 530px on the
   live site's wider columns. Dylan, 1 October 2026: "The P roundels are MUCH too
   big and mess everything up." They also pushed the row's own text to the bottom
   of a 530px line box, which is why the names and the word SIGNATURE appeared to
   overlap.
   13px matches .nrow — the classic band's rows are the nearest thing to these,
   and one signature size across the site beats two that can drift. */
.svcrow .wd-crest{display:inline-block;width:13px;height:13px;margin-left:8px;
  vertical-align:-2px;flex:0 0 auto;
  filter:drop-shadow(0 2px 4px rgba(8,16,34,.2));}
.svcrow .wh-sig{font-family:var(--body);font-size:9px;font-weight:700;
  letter-spacing:.16em;text-transform:uppercase;color:var(--pink);
  margin-left:4px;white-space:nowrap;}
/* ── THE ARROW ON HOVER ──────────────────────────────────────────────────────
   Dylan, 1 October 2026: *"when you hover over a service in the submenu, ideally
   having the same right arrow that currently appears next to them in the mega
   menu appear."*

   It IS that arrow: compile.ts emits '.wh-fs-arw', whose colour, glyph size,
   hidden resting state (opacity 0, translateX(-6px)) and easing are all declared
   once, in MEGA_MENU_CSS, for the full-screen menu's service rows. Nothing is
   restated here. The only thing this panel has to say is WHEN it shows, because
   the mega menu says that against '.wh-fs-row:hover' and these rows are
   '.svcrow' — (0,2,0) in both, so neither out-ranks the other and both apply in
   their own panel.

   ⚠ focus-visible as well as hover, matching '.svcrow:hover .ul' directly above:
   a keyboard reader moving down the column gets the same arrow. The mega menu's
   own rule is hover-only, which is a gap there rather than a precedent.         */
.svcrow:hover .wh-fs-arw,.svcrow:focus-visible .wh-fs-arw{opacity:1;transform:none;}
/* The gap the mega menu gets from its grid. These rows are inline flow (see
   above), so the arrow needs telling — and 'vertical-align' keeps it on the
   name's baseline rather than the signature mark's. */
.svcrow .wh-fs-arw{margin-left:9px;vertical-align:-1px;
  font-size:calc(var(--mn-service-scale,1) * 15px);}
/* ⚠ THESE COLOURS HAVE TO OUT-SPECIFY THE HEADER THEY ARE DRAWN IN, and until
   1 October 2026 they did not. '.masthead .nav a' is (0,2,1) and sets
   rgba(255,255,255,.85) for a nav over the dark hero; '.svcrow' is (0,1,0). So
   every service name in this panel was 85% WHITE ON PAPER when the menu was
   opened from the masthead — invisible — while the sticky header, which sets a
   dark #3c4757, looked correct. That is exactly why Dylan saw it from one header
   and not the other: "When I open it from the masthead, the text itself isn't
   visible?"
   '.mm .svcol a.svcrow' is (0,3,1) and wins in both, without either header
   being named — a rule that named them would need a third edit the next time a
   header is added. */
.mm .svcol a.svcrow{color:var(--navy);}
.mm .svcol a.svcrow .wh-sig{color:var(--pink);}
.mm .svcol .svccat{color:var(--ink);}
/* ⚠ AND THE SAME RULE SETS THE CAPITALS, WHICH IS THE HALF THAT WAS MISSED ON
   1 OCTOBER AND IS THE SAME DEFECT AS THE COLOUR ABOVE.

   '.masthead .nav a' / '.drophdr .nav a' do not only set a colour; they set
   'text-transform:var(--mn-row-case,none)' as well, at (0,2,1), and they MATCH
   a.svcrow directly — a service row is an <a> inside .nav, so the header's row
   typography lands on it whole. This site sets --mn-row-case:uppercase for its
   menu, so every service name in the panel was SHOUTED. Measured in Chromium
   against Dylan's own configuration: computed text-transform "uppercase".

   Dylan: "I'd feel much more comfortable if the services that are listed in the
   submenu weren't all caps either, and were also the navy blue that the titles
   are currently. Then changing the titles (e.g. Private Tuition & Mentoring) to
   black."

   So the two swap: the rows take the navy the headings had, the headings take
   --ink. ⚠ The headings were NEVER uppercase — .svccat is a <div>, so the
   header's '.nav a' rule never reached it, which is exactly why he saw caps on
   one and not the other.

   ⚠ THE FIX IS A DECLARATION, NOT AN INHERITANCE. text-transform inherits, so
   the instinct is to set 'none' on .svcgrid and let it flow down. That does
   nothing: .masthead .nav a matches the ANCHOR, and a matched declaration beats
   an inherited one whatever the ancestor says. It has to be declared on a
   selector that out-specifies (0,2,1) on the anchor itself. */
/* ⚠ a.svccat IS HERE FOR THE SAME REASON AND IT IS NOT DECORATIVE. The heading
   became an <a> on 1 October 2026, and '.masthead .nav a' (0,2,1) sets uppercase
   on every anchor in that nav — so the moment the heading stopped being a <div>
   it would have gone to capitals in the masthead and stayed sentence case in the
   sticky bar, which is the exact asymmetry the note above this rule records
   happening to the service rows. '.svcall' stays named although nothing emits it:
   see the blocklist note in BASE_CSS. */
.mm .svcol a.svcrow,.mm .svcol a.svcall,.mm .svcol a.svccat{text-transform:none;letter-spacing:normal;white-space:normal;}
/* ⚠⚠ AND THE FACE, THE SIZE AND THE WEIGHT HAD TO COME WITH IT — THE RULE ABOVE
   FIXED ONE THIRD OF ITS OWN CLASS AND LEFT THE REST FOR A DAY.

   '.masthead .nav a,.drophdr .nav a' sets THREE things at (0,2,1) — face, slant
   and capitals — and '.masthead .nav a' sets THREE MORE beside its colour: size,
   weight and tracking. '.svccat' is (0,1,0) and loses to both. So the moment the
   heading became an anchor it took the row's BODY face instead of the serif,
   dropped from 19.5px to 13.5px and from weight 600 to 500: the heading stopped
   being a heading and became another row in the list. Dylan, 2 October 2026:
   *"the titles themselves are no longer serif and bigger than the rest."*

   ⚠ THE LESSON IS THE SHAPE OF THE MISS RATHER THAN THESE SIX PROPERTIES. The
   note above this one PREDICTED the class — it says in as many words that the
   heading would go to capitals the moment it stopped being a <div> — and then
   fixed only the property it had thought of. Changing an element's TAG inside a
   styled region inherits every rule that region sets on that tag, so the safe
   move is to restate the whole of what the element WAS, not the part that comes
   to mind. A seventh declaration added to the row rules above would take this
   heading with it again, silently.

   These are '.svccat''s own values, restated rather than re-tokenised so the two
   places agree by construction. This is also the block a different heading
   treatment would change — see docs/branches/claude-gifted-gauss-fd3zp8 for the
   three Dylan was shown. */
.mm .svcol a.svccat{font-family:var(--h-sub-face,var(--serif));
  font-size:19.5px;font-weight:600;letter-spacing:-.01em;line-height:1.22;
  font-style:normal;white-space:normal;}
/* ⚠⚠ 'white-space:normal' IS THE FOURTH PROPERTY OF THAT SHARED RULE AND I HAD
   COUNTED THREE — the same miss, a second time, on the same two elements.
   Dylan, 2 October 2026: *"the submenu on small screen sizes — the text gets cut
   off at one side."*

   '.masthead .nav a,.drophdr .nav a,.navgrp-btn' opens with 'white-space:nowrap'
   and the note above it calls it "face, slant and capitals". It is face, slant,
   capitals AND nowrap. A nav row must not wrap; a four-column panel of category
   names must. With nowrap inherited, "Full Strategy & Management" cannot break,
   so it overflows its grid track — the tracks are 'minmax(214px,1fr)' and equal,
   so each heading simply runs out of its column into the next one and is clipped
   at the panel's edge. That is precisely his screenshot: Private Tuition &
   Mentoring colliding with Homeschooling, and Full Strategy & Management cut off.

   ⚠ THE SERVICE ROWS NEED IT TOO and for the same reason — "Travelling &
   Residential Tutors" is longer than a narrow track — so it is set beside their
   capitals rather than only here. Both are below.

   ⚠ AND THE REAL FIX IS THE TEST, NOT THIS LINE. Restating "the properties I
   thought of" has now failed twice. The guard enumerates the row rules'
   declarations and requires the panel to answer every one, so the next property
   added to them fails a test instead of reaching a visitor. */
/* ── THE "ALL OF …" CHIP — IT IS THE MEGA MENU'S About PILL ──────────────────
   ⚠ THIS BLOCK USED TO DRAW A CHIP OF ITS OWN, and that was the mistake. It
   carried .svcall/.svcall-t/.svcall-a with their own border, radius, size and
   hover — a second outlined chip written to LOOK like the mega menu's pill — and
   a chip written to look like another chip drifts, which is exactly what
   happened. On 1 October its radius was taken from 999px to 2px on the reasoning
   that '.mm .btn' is 2px; the thing it was meant to match was never '.mm .btn'.

   Dylan, the same day: *"The chips at the bottom don't match the ones in the
   mega menu. The ones in the mega menu are proper chips … The ones in the
   submenu are rectangles with slightly (but not completely rounded corners).
   Also, the ones in the submenu are in all caps. Just replicate what you've
   already got in the mega menu when it comes to those buttons, please don't
   create new things."*

   So compile.ts now emits the pill's OWN classes —
   '.wh-fs-row .wh-fs-row--about .wh-fs-about--pill', with '.wh-fs-mid' /
   '.wh-fs-name' / '.wh-fs-arw' inside — and every visible property comes from
   MEGA_MENU_CSS. There is one definition of that control in this stylesheet now.
   Delete nothing from here expecting the chip to keep its shape: it has none of
   its own.

   ⚠ WHAT IS LEFT IS NOT COSMETIC AND MUST NOT BE TIDIED AWAY. Three facts are
   true of the chip HERE and nowhere else:

   · 'align-self:flex-start' — .svcol is a flex column, and a flex item's default
     'stretch' would pull the pill to the column's full width. 'inline-flex' does
     not save it: stretch acts on the cross size regardless of display.
   · '.svcall' stays in the class list because the site-wide anchor-underline
     sweep names it in its exclusion list (see BASE_CSS). Drop the class and
     every chip grows a 2px line across its whole box.
   · the capitals, with the rows, in the rule further up — '.masthead .nav a'
     reaches the chip too.

   ⚠ AND ONE CORRECTION TO WHAT THIS BLOCK USED TO CLAIM. It said
   "margin-top:auto pins it to the foot of its column, so the chips line up
   across columns of different lengths". It did not: '.svclist+.svcall' set
   margin-top:16px at (0,2,0) and the chip ALWAYS follows the list, so the auto
   never applied and the chips have always sat 16px under their own list. The
   pill's own 'margin-top:16px' reproduces that exactly, which is why both rules
   could go. Whether they SHOULD line up is a design question nobody has asked;
   it is N1866 rather than a silent change here.

   ⚠ THE ARROW NOW TURNS ON HOVER, where this chip's did not. That was Dylan's
   instruction about a control that no longer exists; "replicate the pill" is the
   later and more specific one, and the pill turns. Said out loud because it is a
   visible change nobody asked for in those words.                              */
/* ⚠ '.svcall{align-self:flex-start}' WAS HERE AND IS DELIBERATELY GONE, 1 October
   2026, with the chip it held in place. It is recorded rather than simply removed
   because the reason it existed is a trap: .svcol is a flex column, and a flex
   item's default 'stretch' pulls a chip to the column's full width — 'inline-flex'
   does not save it, because stretch acts on the cross size whatever the display
   is. Anybody putting a chip back in this column needs that line and will not
   guess it from the symptom. The heading above is a flex item too, which is why
   'a.svccat' sets its own width rather than relying on the default.            */
/* ── OPENING AND CLOSING, IN THE STICKY BAR ──────────────────────────────────
   Dylan, 1 October 2026: *"when the submenu opens from the sticky menu, I
   ideally want it to expand downwards from the top gracefully, before the items
   within it appear (ideally one column at a time from left to right). And then
   when you close it all of the items to disappear at once and the container to
   expand upwards from bottom to top very quickly. Ensure that there is proper
   handling for the shadows. I am mindful that there is a shadow at the bottom of
   the sticky menu and at the bottom of the submenu, so when the submenu has not
   yet been expanded/the submenu has been collapsed, it's possible that, unless
   you're careful, two shadows will be appearing on top of each other at that
   stage … ensure that this doesn't happen … or any instance where neither shadow
   is displayed on the page."*

   SCOPED TO .drophdr, with the clearance above and for the same reason: he asked
   that the masthead's panel get no equivalent layout change, and the masthead's
   is an inset, rounded card rather than a strip continuing the bar. It keeps the
   fade it has.

   ── THE CONTAINER: grid-template-rows, 0fr to 1fr ──────────────────────────
   ⚠ NOT max-height AND NOT clip-path, and both were tried in the head before
   this was written, because each fails one half of the request.

   · 'height' cannot transition from or to 'auto', and the panel's height is
     whatever its tallest column comes to.
   · 'max-height' to a guessed ceiling animates the GUESS, so the easing lands
     wrong by however much the guess overshot, and the close starts with a pause.
   · 'clip-path' would animate truly — but it clips the box's shadow away with
     everything else, so the panel's shadow would vanish on the first frame of
     the close and the bar's would not be back until the last. That is Dylan's
     "neither shadow", built in.

   0fr to 1fr animates the REAL height — the row is sized by its content at both
   ends — so the box genuinely grows and shrinks, and its shadow travels with its
   bottom edge the whole way. That is the property the shadow rule below depends
   on.

   ⚠ .mm-body NEEDS overflow:hidden AND min-height:0. A grid item's automatic
   minimum size is its content, so without min-height:0 the row refuses to go
   below the content's height and the 0fr end does nothing at all — the classic
   way this technique silently no-ops.

   ⚠ A BROWSER THAT WILL NOT INTERPOLATE fr JUMPS, which is the old behaviour and
   is honest: the panel appears and disappears at full size, as it did before
   this block existed. Nothing is hidden or broken by the fallback.

   ── THE SHADOWS ────────────────────────────────────────────────────────────
   The band wears --wh-bar-shadow, the sticky bar's OWN shadow (tokenised where
   the bar declares it). So:

   · closed — the panel is visibility:hidden and casts nothing; the bar's shadow
     is the only one. ONE.
   · the first frame of opening — the panel is zero pixels tall and its shadow
     sits exactly where the bar's does. They coincide, to the pixel, because they
     are the same shadow. ONE, visibly.
   · open — the panel is opaque and several hundred pixels tall, so it covers the
     bar's shadow completely and casts its own below itself. ONE.
   · closing — the reverse of opening, continuously. ONE throughout.

   ⚠ THAT IS A PROPERTY OF THE TWO BEING IDENTICAL, NOT OF ANY TIMING. The
   obvious implementation is to turn the bar's shadow OFF while the panel is open
   and back on when it closes, which needs two delays to be right and is wrong
   during every transition between them — too early and there is no shadow at
   all, too late and there are two. Making them the same shadow removes the
   question instead of answering it, and there is no :has() in it, so a browser
   without :has() behaves identically.

   ⚠ WHAT THIS DOES NOT COVER, out loud: while the panel is between 1 and about
   3px tall the bar's shadow is not yet fully covered, so for those two frames
   there are two identical shadows within three pixels of each other. That is
   indistinguishable from one slightly heavier shadow and lasts under 20ms; it is
   recorded rather than fixed because fixing it means reintroducing the timing
   this design exists to avoid.

   ── THE COLUMNS ────────────────────────────────────────────────────────────
   In after the container, one at a time from the left: --i is the column's own
   index (compile.ts counts the columns it actually draws, and the cell-swap
   runtime counts the same way). Out all together and fast, with no stagger at
   all, which is what "all of the items disappear at once" means — a reversed
   stagger is the same gesture backwards and reads as the panel unbuilding
   itself.

   ⚠ THE OPEN DELAY INCLUDES --wh-mm-delay, the hover-intent wait, so the stagger
   is measured from when the panel actually starts to appear rather than from
   when the pointer arrived. Those are now 120ms apart.                        */
.drophdr .navgrp[data-wh-services] > .mm{
  --wh-svc-open:.40s; --wh-svc-close:.17s; --wh-svc-items-out:.10s;
  --wh-svc-stagger:70ms;
  display:grid;grid-template-rows:0fr;
  box-shadow:var(--wh-bar-shadow,0 8px 24px -16px rgba(20,38,63,.4));
  transition:grid-template-rows var(--wh-svc-close) cubic-bezier(.4,0,1,1) var(--wh-svc-items-out),
             opacity 0s linear calc(var(--wh-svc-close) + var(--wh-svc-items-out)),
             visibility 0s linear calc(var(--wh-svc-close) + var(--wh-svc-items-out));}
.drophdr .navgrp[data-wh-services] > .mm > .mm-body{overflow:hidden;min-height:0;}
.drophdr .navgrp[data-wh-services]:hover > .mm,
.drophdr .navgrp[data-wh-services]:focus-within > .mm{
  grid-template-rows:1fr;
  transition:grid-template-rows var(--wh-svc-open) cubic-bezier(.22,1,.36,1) var(--wh-mm-delay,0ms),
             opacity 0s linear var(--wh-mm-delay,0ms),
             visibility 0s linear var(--wh-mm-delay,0ms);}
/* Keyboard focus never waits — the same exemption the panel's own visibility
   rule makes, restated here because this rule replaces that one's timing. */
.drophdr .navgrp[data-wh-services]:focus-within > .mm{
  transition-delay:0s,0s,0s;}
/* THE HEADER LEAVING TAKES THE PANEL WITH IT, and it has to be said again here:
   .drophdr:not(.show) .mm is (0,3,0) and loses to the hover rule above, so
   without this a panel that was open when the bar retracted would keep its row
   at 1fr and come back fully grown. Last, so it wins its tie with the hover
   rule, and instant, because the bar is already on its way out. */
/* A HELD PANEL KEEPS ITS HEIGHT. .mm-hold means "the reader has left but may be
   coming straight back" — the forgiveness window, and the swap window. It already
   keeps the panel opaque and visible; with the container animated it has to keep
   the container open too, or a hold shows a full-opacity strip of nothing. No
   transition, because a hold is a freeze rather than a movement: whatever the
   panel's height was when the pointer left is what it keeps. */
.drophdr .navgrp[data-wh-services].mm-hold > .mm{
  grid-template-rows:1fr;transition:none;}
/* ⚠ AFTER THE HOLD, so a retracting header beats it. .mm-hold is script-owned
   and expires on its own, but a bar that has gone must not leave a held panel
   behind it — the same precedence .mm-shut and the retract already have over
   .mm-hold for opacity. */
.drophdr:not(.show) .navgrp[data-wh-services] > .mm{
  grid-template-rows:0fr;transition-duration:0s;transition-delay:0s;}
.drophdr .navgrp[data-wh-services] .svcol{
  opacity:0;
  transition:opacity var(--wh-svc-items-out,.10s) linear;}
.drophdr .navgrp[data-wh-services]:hover .svcol,
.drophdr .navgrp[data-wh-services]:focus-within .svcol{
  opacity:1;
  transition:opacity .28s ease
    calc(var(--wh-mm-delay,0ms) + var(--wh-svc-open,.40s) * .55
         + var(--i,0) * var(--wh-svc-stagger,70ms));}
/* ⚠ A HELD PANEL KEEPS ITS LINKS, NOT ONLY ITS BOX. Dylan, 18 October 2026:
   crossing the space under another menu item, "the submenu container remains
   open. However, the text/contents disappear." The hold above kept the
   container's height and nothing told the columns, which fade on :hover alone,
   so they faded out inside a box that stayed. A hold is a freeze, so no
   transition. After the :hover rule, which it ties with, so it wins. */
.drophdr .navgrp[data-wh-services].mm-hold .svcol{opacity:1;transition:none;}
@media (prefers-reduced-motion: reduce){
  .drophdr .navgrp[data-wh-services] > .mm,
  .drophdr .navgrp[data-wh-services]:hover > .mm,
  .drophdr .navgrp[data-wh-services]:focus-within > .mm,
  .drophdr .navgrp[data-wh-services] .svcol,
  .drophdr .navgrp[data-wh-services]:hover .svcol,
  .drophdr .navgrp[data-wh-services]:focus-within .svcol{
    transition-duration:.01ms!important;transition-delay:0ms!important;}
}
/* ⚠ THE BAND BEHIND THE BRAND IS FOR THE SPLIT LAYOUT ONLY. .drophdr::before
   draws a 40% paper column that belongs to the per-category design's left
   panel; this submenu's paper is the full width of the panel, so a 40% band
   would show as a lighter rectangle behind the brand and nothing else.

   ⚠ AND IT IS KEYED ON THE PANEL EXISTING, NOT ON A LIST OF ITS STATES — 1
   October 2026, because the list was the defect. It used to name three:
   '.mm-warm', ':hover' and '.mm-out'. The rules that TURN THE BAND ON name
   FIVE — ':hover', ':focus-within', '.mm-warm', '.mm-hold' and '.mm-out' (see
   the block beside .drophdr::before) — so two of them had no answer here.

   ':focus-within' is the one that showed. A mouse click on the trigger focuses
   the button, so the band wiped in from the left across 42% of the sticky bar,
   behind the brand, on a click rather than a hover — which is exactly Dylan's
   *"sometimes when I open the submenu, the paper effect from the old submenu
   design still appears in the sticky header itself animating in from left."*
   The word is "sometimes" because hovering alone never did it.

   Measured in Chromium with focus and the pointer moved away: ::before computed
   display:block, clip-path inset(0px), width 768px at a 1920 viewport.

   The two designs are mutually exclusive at the compiler — 'servicesRow' is
   'catHtml' OR 'singleServicesHtml()', never both — so the presence of a
   .navgrp--svc anywhere in this header is sufficient and complete: it is true in
   every state of the new design, and false in every state of the old one. A
   state list has to be kept in step with a rule four thousand lines away, and
   this one went stale the first time anybody added to that rule.             */
.drophdr:has(.navgrp--svc)::before{display:none;}
/* ── THE STARRED CATEGORY ────────────────────────────────────────────────────
   Dylan, 18 October 2026. A category starred in Services lists (stored as
   'apart', where the "Set apart" switch it replaced kept it) is drawn LAST
   (starredLast in nav.ts) and in a box of its own, in one of two styles he chose
   and asked to toggle between (Services lists → Starred category style):
     ribbon (D, the default) — a white column hanging from the top edge of the
       menu to a little below its foot, with a pink band across its top;
     card (B, .svc-star--card on the panel body) — a raised white card inside
       the menu, soft shadow, no outline.
   No words are added. The grey divider before it goes: the box is the divider.
   Its shadow's strength is a setting too (Services lists → Shadow, 0 to 200%):
   --svc-star-shade on the panel body multiplies the shadow's opacity, so 0 is
   no shadow and 2 is twice the designed one. ⚠ The pink band is an inset
   shadow and is deliberately NOT multiplied: it is the ribbon, not its shadow.
   Mockups: /mnt/project-files/mockups/services-menu-separation/.
   ⚠ This replaces version 2's pink divider and extra room (#583), which was never
   switched on. The full-screen menu's treatment, further down, is unchanged.
   ⚠ THE RIBBON'S TOP IS THE GRID'S OWN TOP PADDING, logo clearance included, so
   it meets the bar under the sticky header and the top edge of the masthead's
   card exactly. Its foot is 74px past the grid's 26px bottom padding: 48px of
   ribbon below the menu. */
.svcol.is-apart{border-left:0;position:relative;z-index:2;background:#fff;
  margin:calc(-1 * (28px + var(--wh-svc-clear,0px))) 0 -74px clamp(8px,1.1vw,16px);
  padding:calc(28px + var(--wh-svc-clear,0px)) clamp(16px,1.8vw,26px) 22px;
  border-radius:0 0 var(--wh-mm-radius,2px) var(--wh-mm-radius,2px);
  box-shadow:inset 0 3px 0 var(--pink),0 24px 50px -18px rgb(15 25 42 / calc(.32 * var(--svc-star-shade,1)));}
/* The sticky row takes the outer columns' side padding away to line up with the
   body text; the box needs its own back, or the words touch its edges. */
.drophdr .svcol.is-apart{padding-left:clamp(16px,1.8vw,26px);padding-right:clamp(16px,1.8vw,26px);}
.svc-star--card .svcol.is-apart{margin-top:-12px;margin-bottom:-10px;
  padding-top:12px;padding-bottom:10px;border-radius:var(--wh-mm-radius,2px);
  box-shadow:0 14px 34px -16px rgb(15 25 42 / calc(.28 * var(--svc-star-shade,1)));}
/* ⚠ LETTING THE RIBBON OUT OF THE PANEL, which three things clip, each for a
   reason that still holds. Every rule here names a ribbon (a starred column in a
   body that is not .svc-star--card), so a site with no star is untouched.
   1. '.mm' clips its drop-shadow filter 70px below itself; 140px leaves room for
      the 48px ribbon and its own shadow.
   2. The masthead's panel and its body hide overflow to keep their corners; the
      body carries the radius itself instead, so the corners are unchanged.
   3. The sticky panel's body hides overflow BECAUSE its height animates (0fr to
      1fr): visible during the opening would spill every column below a panel
      still growing. So it is let out only once the opening has finished, and
      shut again once the columns have faded on the way out. 'allow-discrete' is
      what lets 'overflow' wait for a delay; a browser without it flips at once,
      while the columns are still transparent, and the most it can show is a few
      frames of rows below a nearly grown panel. */
.navgrp[data-wh-services] > .mm:has(.mm-body:not(.svc-star--card) .svcol.is-apart){
  clip-path:inset(-15px -70px -140px -70px);}
.masthead .navgrp[data-wh-services] > .mm:has(.mm-body:not(.svc-star--card) .svcol.is-apart){overflow:visible;}
.masthead .navgrp[data-wh-services] > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
  overflow:visible;border-radius:var(--wh-mm-radius,2px);}
.drophdr .navgrp[data-wh-services] > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
  transition:overflow 0s linear var(--wh-svc-items-out,.10s);transition-behavior:allow-discrete;}
.drophdr .navgrp[data-wh-services]:hover > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart),
.drophdr .navgrp[data-wh-services].mm-hold > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
  overflow:visible;
  transition-delay:calc(var(--wh-mm-delay,0ms) + var(--wh-svc-open,.40s));}
.drophdr .navgrp[data-wh-services]:focus-within > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
  overflow:visible;transition-delay:var(--wh-svc-open,.40s);}
.drophdr:not(.show) .navgrp[data-wh-services] > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
  overflow:hidden;transition-delay:0s;}
/* With reduced motion the panel opens at once (the block further up zeroes its
   journey), so the ribbon is let out at once too rather than waiting out a
   journey that no longer happens and then dropping into place (Codex on #593). */
@media (prefers-reduced-motion: reduce){
  .drophdr .navgrp[data-wh-services] > .mm > .mm-body:not(.svc-star--card):has(.svcol.is-apart){
    transition-delay:0s!important;}
}
@media(max-width:760px){
  .svcol{border-left:0;border-top:1px solid var(--line);padding-top:16px;
    margin-top:6px;}
  .svcol:first-child{border-top:0;margin-top:0;padding-top:0;}
  /* Stacked, both styles are a box in the column's place: no hang, since there
     is nothing beside it to hang past. */
  .svcol.is-apart,.drophdr .svcol.is-apart,.svc-star--card .svcol.is-apart{border-top:0;
    margin:18px 0 0;padding:16px 16px 14px;border-radius:var(--wh-mm-radius,2px);}
}
.enqf-split{display:grid;grid-template-columns:0.85fr 1.15fr;align-items:stretch;}
.enqf-side{background:var(--navy-ink);color:#fff;padding:clamp(30px,4.4vw,64px);}
.enqf-side .eyecrest{width:52px;height:52px;margin:0 0 clamp(16px,2vw,24px);}
.enqf-side .eyebrow{display:block;color:var(--eyebrow-lit);margin-top:var(--eb-mt-section,0px);margin-bottom:var(--eb-mb-section,12px);}
.enqf-side h2{font-size:calc(var(--h-section-scale,1) * clamp(26px,3vw,40px));color:#fff;margin:0;}
.enqf-lede{font-size:15px;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.62);color:rgb(255 255 255 / .74);margin:14px 0 0;max-width:42ch;}
.enqf-steps{list-style:none;margin:clamp(26px,3.4vw,44px) 0 0;padding:0;border-top:1px solid rgb(255 255 255 / .16);}
.enqf-steps li{display:grid;grid-template-columns:44px 1fr;gap:12px;align-items:start;padding:15px 0;border-bottom:1px solid rgb(255 255 255 / .16);opacity:.4;transition:opacity .3s ease;}
.enqf-steps li.is-on{opacity:1;}
.enqf-steps .n{font-family:var(--serif);font-weight:700;font-size:12.5px;letter-spacing:0;line-height:1.9;white-space:nowrap;color:var(--pink);}
.enqf-steps .n .pfx{font-size:1em;font-weight:700;letter-spacing:0;vertical-align:baseline;margin-right:.22em;}
.enqf-steps .tx{display:flex;flex-direction:column;line-height:1.4;}
.enqf-steps .tx strong{font-family:var(--body);font-size:14px;font-weight:600;color:#fff;}
.enqf-steps .tx span{font-size:12.5px;color:rgb(255 255 255 / .66);margin-top:3px;}
/* ── THE LEFT PANEL ON PAPER ─────────────────────────────────────────────────
   Dylan, 30 September 2026: the masthead, the footer and this panel were all
   navy, "so it all just feels quite oppressive". The block now offers a ground,
   and this is the light one. Navy stays the default, so every page that has not
   asked is byte-identical.

   ⚠ EVERY COLOUR BELOW IS AN EXISTING TOKEN, and that is the point rather than a
   convenience: "make the text visible on the other ground" is a MAPPING from the
   dark set to the light set the site already uses everywhere else, not a new
   palette. --eyebrow is the light-ground twin of --eyebrow-lit and predates this.

   ⚠ NO RULE BETWEEN THE COLUMNS, at Dylan's explicit instruction — he was shown a
   version with a hairline and asked for it without. That leaves --paper (#fbf7f7)
   meeting --white (#ffffff), four points apart: a real edge, but a quiet one that
   may read as almost nothing on a dim screen, so the panel can look like it
   simply ends. He was told that before choosing. It is one setting, so adding a
   rule later touches nothing else.

   ⚠ DOUBLE-CLASS SELECTORS where the navy rule is equally specific (h2, .eyebrow),
   so this block wins on SPECIFICITY rather than on source order. A single class
   here would tie with .enqf-side h2 and depend on staying below it forever. */
.enqf-side.enqf-side--paper{background:var(--paper);color:var(--ink);}
.enqf-side.enqf-side--paper h2{color:var(--navy-ink);}
.enqf-side.enqf-side--paper .eyebrow{color:var(--eyebrow);}
.enqf-side--paper .enqf-lede{color:var(--muted);}
.enqf-side--paper .enqf-steps{border-top-color:var(--line);}
.enqf-side--paper .enqf-steps li{border-bottom-color:var(--line);}
.enqf-side--paper .enqf-steps .tx strong{color:var(--navy-ink);}
.enqf-side--paper .enqf-steps .tx span{color:var(--muted);}
/* .enqf-steps .n stays var(--pink) on BOTH grounds — it is the one colour in this
   panel that already worked either way, and pinning it here would only create a
   second place to keep in step. */
.enqf-main{background:var(--white);padding:clamp(30px,4.4vw,64px);display:flex;flex-direction:column;justify-content:center;}
.enqf-pane{animation:whEnqfIn .3s ease both;}
@keyframes whEnqfIn{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
@media (prefers-reduced-motion:reduce){.enqf-pane{animation:none;}}
.enqf-rows{display:grid;grid-template-columns:1fr 1fr;column-gap:clamp(20px,2.6vw,40px);row-gap:clamp(18px,2.2vw,30px);}
.enqf-field label,.enqf-msg label{display:block;font-family:var(--body);font-size:10.5px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin-bottom:8px;}
.enqf-opt{font-weight:600;letter-spacing:.1em;color:var(--muted);opacity:.7;text-transform:none;}
.enqf-box{border-bottom:1px solid var(--line);transition:border-color .2s ease;}
.enqf-box:focus-within{border-color:var(--pink);}
.enqf-box input,.enqf-box textarea{display:block;width:100%;box-sizing:border-box;border:0;outline:0;background:transparent;padding:8px 0 10px;font-family:var(--body);font-size:15px;color:var(--navy);border-radius:0;}
.enqf-box textarea{min-height:196px;resize:vertical;line-height:1.6;}
.enqf-box textarea::placeholder{color:var(--muted);opacity:.7;}
.enqf-box input[aria-invalid="true"],.enqf-box textarea[aria-invalid="true"]{color:var(--navy);}
.enqf-box:has(input[aria-invalid="true"]),.enqf-box:has(textarea[aria-invalid="true"]){border-color:var(--pink);}
/* Error text in a DARKER tone than the brand pink: #d97195 on white is 3.1:1,
   under the 4.5:1 WCAG AA minimum for text this size — the least-readable text
   on the page must not be the message a visitor needs to fix their submission.
   The pink stays on the hairline/border states, where 3:1 (non-text) is met. */
.enqf-fieldmsg{display:block;font-size:11.5px;color:#b0466b;margin-top:6px;letter-spacing:0;text-transform:none;font-weight:600;}
/* Phone field: dial-code picker on the left of the same hairline. The native
   <select> sits invisible over a compact "flag +code" face, so the full country
   list opens on tap while the closed control stays small. */
.enqf-phonebox{display:flex;align-items:center;gap:10px;}
.enqf-phonebox input{flex:1;min-width:0;}
.enqf-dial{position:relative;display:inline-flex;align-items:center;flex:none;}
.enqf-dial[hidden]{display:none;}
.enqf-dial-face{display:inline-flex;align-items:center;gap:5px;padding:8px 0 10px;font-family:var(--body);font-size:15px;color:var(--navy);white-space:nowrap;}
.enqf-dial-face::after{content:"";width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid var(--muted);margin-left:2px;}
.enqf-dial select{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;font-size:16px;border:0;}
/* The select is opacity:0, so its native focus ring is invisible — give the
   FACE a distinct ring when the select holds keyboard focus, so a keyboard
   user can tell the country picker apart from the adjacent number input
   (which shows the shared pink hairline instead). */
.enqf-dial:focus-within .enqf-dial-face{outline:2px solid var(--navy);outline-offset:2px;border-radius:4px;}
/* iOS zooms any focused control under 16px — the visible inputs were 15px, so
   focusing them zoomed the page while the (already 16px) invisible dial select
   did not. Touch only, so desktop density is unchanged. */
@media(pointer:coarse){
  .enqf-box input,.enqf-box textarea{font-size:16px;}
}
/* No rule between the message and the consents — the field keeps its own
   hairline and the consent block simply follows. */
.enqf-turnstile{margin:18px 0 0;max-width:100%;overflow-x:auto;}
.enqf-consent{display:grid;grid-template-columns:1fr 1fr;column-gap:clamp(22px,2.8vw,44px);row-gap:18px;margin-top:clamp(20px,2.4vw,30px);}
.enqf-ticks{display:flex;flex-direction:column;gap:14px;}
.enqf-tick{display:grid;grid-template-columns:18px 1fr;gap:11px;align-items:start;font-size:13px;line-height:1.5;color:var(--navy);cursor:pointer;}
.enqf-tick input{appearance:none;-webkit-appearance:none;width:18px;height:18px;margin:1px 0 0;border:1px solid var(--line);border-radius:2px;background:var(--white);position:relative;cursor:pointer;transition:border-color .18s ease;}
.enqf-tick input:hover{border-color:var(--pink);}
.enqf-tick input:focus-visible{outline:2px solid var(--pink);outline-offset:2px;}
.enqf-tick input:checked{border-color:var(--pink);}
.enqf-tick input:checked::after{content:"";position:absolute;left:4px;top:8px;width:4px;height:8px;border-right:2px solid var(--pink);border-bottom:2px solid var(--pink);transform:translateY(-5px) rotate(45deg);}
.enqf-tick input[aria-invalid="true"]{border-color:var(--pink);}
.enqf-tick b{color:var(--pink);font-weight:700;}
.enqf-fine{font-size:11.5px;line-height:1.6;color:var(--muted);}
.enqf-fine p{margin:0 0 8px;font-size:11.5px;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.6);}
.enqf-fine p:last-child{margin-bottom:0;}
.enqf-fine a{color:var(--navy);text-decoration:underline;}
.enqf-foot{display:flex;align-items:center;gap:18px;margin-top:clamp(22px,2.6vw,34px);}
.enqf-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;height:44px;padding:0 24px;border-radius:999px;background:var(--navy);color:#fff;border:1.5px solid var(--navy);font-family:var(--body);font-size:13px;font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;transition:background-color .18s ease,border-color .18s ease,color .18s ease;}
.enqf-btn:hover{background:var(--pink);border-color:var(--pink);color:#fff;}
.enqf-btn[disabled]{opacity:.55;cursor:default;}
.enqf-back{background:none;border:0;padding:0;font-family:var(--body);font-size:13px;font-weight:600;color:var(--muted);cursor:pointer;transition:color .18s ease;}
.enqf-back:hover{color:var(--navy);}
/* The one message a visitor MUST be able to read: it is the only thing
   standing between them and a submission that will not go through. 13px at
   the body weight, in a colour dark enough to pass contrast but light
   enough to read as an error, was legible in theory and squinted at in
   practice. Heavier, a shade larger, and given a tinted rule so it reads as
   a block rather than as another line of fine print. */
.enqf-error{font-size:13.5px;font-weight:600;line-height:1.45;color:#a33c62;margin:16px 0 0;
  padding:9px 12px;border-left:3px solid #d97195;background:rgba(217,113,149,.09);border-radius:0 4px 4px 0;}
.enqf-done{max-width:46ch;}
.enqf-done .eyecrest{width:52px;height:52px;margin:0 0 18px;filter:drop-shadow(0 6px 18px rgba(8,16,34,.14));}
.enqf-done h3{font-size:calc(var(--h-sub-scale,1) * clamp(22px,2.4vw,30px));margin:0 0 var(--h-sub-gap,12px);}
.enqf-done p{font-size:15px;line-height:calc(var(--leading-body,1.7) / 1.7 * 1.62);color:var(--muted);margin:0;}
/* Off-screen, never display:none — a bot fills a hidden input and skips this. */
.enqf-hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;}
@media(max-width:700px){
  .enqf-consent{grid-template-columns:1fr;}
}
@media(max-width:900px){
  .enqf-split{grid-template-columns:1fr;}
  .enqf-rows{grid-template-columns:1fr;}
  .enqf-box textarea{min-height:150px;}
}
:root{--navy:#284570;--navy-deep:#1d3555;--navy-ink:#14283f;--pink:#d97195;--pink-deep:#c46280;--paper:#fbf7f7;--white:#ffffff;--ink:#2b2d31;--muted:#5b6672;--line:#e7dee1;--brass:#876c3c;--chrome:#0f1f38;--heading:#14283f;--heading-deep:#0e1c2c;--eyebrow:#d97195;--eyebrow-lit:#d97195;--ring:#d97195;--serif:'Red Hat Display',Georgia,serif;--body:'Ysabeau Office','Helvetica Neue',Arial,sans-serif;--wt-heading:300;--wt-body:400;--tracking-heading:-0.045em;--tracking-eyebrow:0em;--tracking-body:-0.02em;--leading-body:1.25;--align-body:justify;--hyphens-body:auto;--eb-flow:flex;--eb-headmb:0px;--eb-mt-title:15px;--eb-mt-section:14px;--eb-weight:700;--eb-scale:0.900;--h-section-gap:30px;--h-title-face:'Red Hat Display',Georgia,serif;--h-title-weight:300;--h-title-style:normal;--h-title-track:-0.045em;--h-title-case:none;--h-title-scale:1.500;--h-section-face:'Red Hat Display',Georgia,serif;--h-section-weight:400;--h-section-style:normal;--h-section-track:-0.045em;--h-section-case:none;--h-section-scale:1.700;--h-sub-face:'Literata',Georgia,serif;--h-sub-weight:600;--h-sub-style:normal;--h-sub-track:-0.045em;--h-sub-case:none;--h-sub-scale:0.900;--h-sub-colour:#284570;--q-face:'Literata',Georgia,serif;--q-weight:300;--q-style:normal;--q-track:-0.042em;--q-colour:#14283f;--mn-row-face:'Red Hat Display',Georgia,serif;--mn-row-weight:600;--mn-row-track:0em;--mn-row-case:uppercase;--mn-row-scale:0.899;--mn-category-face:'Red Hat Display',Georgia,serif;--mn-category-weight:400;--mn-category-style:normal;--mn-category-track:-0.02em;--mn-category-colour:#0e1c2c;--mn-page-face:'Red Hat Display',Georgia,serif;--mn-page-weight:600;--mn-page-scale:0.860;--mn-page-colour:#14283f;--mn-service-face:'Red Hat Display',Georgia,serif;--mn-service-weight:700;--mn-service-scale:0.700;--mn-service-colour:#14283f;}

:root{--wh-gutter:clamp(22px,4vw,48px);--wh-corner-w:168px;}
/* With the Enquire pill switched off (chrome.megaMenuEnquire === false) the
   corner is the 44px burger alone, so the reservation shrinks to match. Driven
   off the modifier class rather than a second number in compile.ts; browsers
   without :has() simply keep the wider reservation, which is only ever slack. */
html:has(.wh-corner--solo){--wh-corner-w:44px;}html:not(.wh-nav-tight):not(.wh-mastnav-tight):not(.wh-dropnav-tight) .wh-corner--noburger .wh-burger{display:none;}html:not(.wh-nav-tight):not(.wh-mastnav-tight):not(.wh-dropnav-tight):has(.wh-corner--noburger.wh-corner--solo){--wh-corner-w:0px;}
/* Clearance for the fixed corner (Enquire pill + burger). The corner really is
   over the utility strip at scroll 0, so the strip's content box is shortened to
   keep clear of it; the headers no longer render a <nav> at all in this mode, so
   there is nothing else to push. Reserved on the ROW'S PADDING rather than as a
   fixed margin on .right, so it tracks the corner's own clamp() at every width —
   a flat 216px reserved more than the whole content box at 390px and wrapped the
   strip onto a second line. */
.topstrip .row{padding-right:calc(clamp(22px,4vw,48px) + var(--wh-corner-w));}
/* Same clearance for the CLASSIC design's header rows. In that design the two
   headers DO draw a nav row, and the drop-in one right-aligns it — straight
   under the fixed Enquire pill and burger, which is exactly the overlap in the
   screenshot. Reserved on the row's own padding (like the strip above), so the
   links end where the corner begins and the corner's clamp() is tracked at
   every width. .wh-hdr-nav is on the header ONLY when a row is drawn: in
   mega-menu mode the row is empty and there is nothing to keep clear of.
   --wh-corner-w starts as the values below and is replaced by the corner's
   MEASURED width as soon as FSBURGER_JS runs, because the static number cannot
   know how wide "Not a family?" renders in this audience's wording. The
   masthead's row is centred and wraps well clear of the corner, so it needs no
   reservation of its own. */
/* THE SPACE BEFORE THE CORNER IS THE ROW'S OWN GAP, NOT A NUMBER OF ITS OWN.
   Measured at 1440px: 28px between every pair of menu items, and 14px between
   the last one and the corner — so the space before "Not a family?" read as
   exactly half the space before everything else, which is what Dylan kept
   seeing. It is now --wh-navgap, hoisted onto the header for the purpose, so
   the two can never drift apart again and both track the same clamp() as the
   window resizes. */
.drophdr.wh-hdr-nav .inner{padding-right:calc(clamp(22px,4vw,48px) + var(--wh-corner-w) + var(--wh-navgap,14px));}

/* THE CORNER — the Enquire pill and the one and only burger as a single fixed
   unit. Root-level and position:fixed, so it is never inside a stacking
   context (.drophdr is fixed WITH a transform, so a nested child would paint at
   the header's z-index 70, below the sheet at 240) and its coordinates are
   identical at every scroll offset. Its right offset is the headers' OWN
   horizontal padding — the same clamp() the .wrap uses. ONE colour on the group
   drives both children (the pill's ring and the burger's bars are
   currentColor); the :not(:hover) negation is scoped to EACH CHILD, never the
   group — on the group, hovering either child (or the gap between them) stripped
   the dark colour off the other one and left it white on white. */

/* z-index 250 sits above the sheet (240), which itself sits above the cookie
   notice (210) and the popovers (200): an aria-modal dialog must not have live
   controls floating on top of it and outside its Tab trap. */
/* ⚠ THE CORNER IS CENTRED ON THE BAR BY ARITHMETIC, NOT BY A CONSTANT — 28
   SEPTEMBER 2026, AND THE CONSTANT WAS WRONG THE MOMENT THE BAR STOPPED BEING
   64px.

   Dylan, with a screenshot of an 80px bar: "the enquire button and burger menu
   icon still appear in the top right where they should, but they are not
   vertically centred within the sticky menu. Please make it so that their
   position in the sticky menu itself (and by extension, their position at the
   top of the page before the sticky menu has appeared) is always vertically
   centred within the sticky menu bar, however tall that sticky menu bar might
   be."

   WHAT IT USED TO SAY: top:calc(10px + var(--wh-mm-h,0px)). That 10px is
   (64 - 44) / 2 — the corner centred in the bar the stylesheet had when it was
   written — with the arithmetic already done and thrown away. --drop-h was added
   later (see the note beside .drophdr .inner, which lists the seven rules that
   were derived from a literal 64 and now read the variable). This rule was the
   eighth and was missed, because the corner is NOT in either header: it is a
   root-level fixed element, so nothing that swept the header's own rules could
   have found it.

   MEASURED IN CHROMIUM at 1440x900, as the corner's centre against the shown
   bar's centre, before the fix:

     bar 64px (the default)   corner centre 32.0   bar centre 32.5   off by -0.5
     bar 80px (his setting)   corner centre 32.0   bar centre 40.5   off by -8.5

   The half-pixel at 64px is the bar's 1px bottom border, not an error. The 8.5px
   is the complaint, and it grows with every pixel added to the bar.

   ⚠ AND THE HEIGHT BECAME A VARIABLE FOR ONE REASON: the centring has to know
   it. The phone breakpoint shortens the corner to 40px, and it used to do that
   by restating height:40px — which the calc() above could not see, so a centred
   corner would have gone back off-centre at 900px wide. It now sets
   --wh-corner-h and the top follows it. ⚠ That IS a behaviour change on a phone,
   deliberately: the 40px corner sat at top:10 (centre 30) in a 64px bar (centre
   32), 2px high, and is now centred. Nobody reported those 2px; correcting them
   is the same fix, and leaving them would mean the rule is only true on a
   desktop.

   ⚠ WHAT THIS DOES NOT CHANGE, and must not: the --wh-mm-h term. The
   audience-mismatch notice sits in flow above the masthead and pushes this
   corner down by its height; that is deliberate and is documented below. It is
   ADDED to the centring rather than folded into it, so the corner is centred on
   the bar and then displaced by the notice, which is the order the two facts
   actually apply in. */
.wh-corner{position:fixed;z-index:250;top:calc(var(--wh-mm-h,0px) + (var(--drop-h,64px) - var(--wh-corner-h,44px)) / 2);right:clamp(22px,4vw,48px);height:var(--wh-corner-h,44px);
  display:flex;align-items:center;gap:12px;color:#fff;
  /* The resting half of the crossfade below. Stated rather than left to the
     initial values, because the lit rule has to have something to return TO. */
  opacity:1;visibility:visible;
  transition:opacity var(--wh-corner-ms,220ms) ease var(--wh-corner-wait,120ms),visibility 0s var(--wh-corner-wait,120ms);}
/* The corner does NOT move when the sheet opens. It used to snap back to
   top:10px on the grounds that the mismatch notice is hidden behind the sheet,
   but that made every corner control (Enquire, "Not a family?", the burger) —
   and the brand beside them — jump up by the notice's height on open and back
   down on close. The sheet now carries the SAME --wh-mm-h offset on its own
   header row and panel instead, so the two brand rows coincide and nothing
   moves; the top band of the sheet is simply paper. */
/* ⚠ THE PILL AND THE BURGER LEFT THIS RULE ON 25 SEPTEMBER 2026, and that is
   the point of the change rather than a side-effect of it. Both are now SOLID
   — pink and navy discs with white content — so they no longer take their
   colour from what is behind them, and flipping them to navy over a navy fill
   would have made the Enquire label and the burger bars vanish. Only the text
   button, which has no fill, still needs to know what it is sitting on. */
/* ⚠ AND THE LAST TWO USERS OF html.wh-past-navy IN THIS BLOCK WENT WITH THE
   PROMPT on 27 September 2026. They turned the text button navy once the corner
   had left the navy masthead. The class itself is very much alive and is now read
   by the BURGER instead, which is a stronger use of the same one fact: see the
   white-disc block below. */
/* ── THE ENQUIRE PILL: A SOLID BLOCK, NOT A RING ───────────────────────────
   Dylan, 25 September 2026: "The background colour of the enquire button
   should normally be #D97195, and the enquire text should normally be white.
   There should be no outline for the button. The background colour should
   likewise get slightly darker when you hover over it."

   #D97195 is --pink, so the pill is written against the palette rather than
   against the literal: a site that retheme its pink gets a pill that follows.
   --btn-fill / --btn-ink are still honoured because a site CAN set a button
   colour (see paletteCss), and they used to drive this pill's hover; they now
   drive its resting state, which is the same setting answering the same
   question one state earlier.

   It is still not a .btn — that is the square block used in page content — and
   it is still in the plain anchor sweep's blast radius, so it still opts out
   of that here. */
.wh-enq{display:inline-flex;align-items:center;height:44px;padding:0 20px;border-radius:999px;
  border:0;background:var(--btn-fill,var(--pink));color:var(--btn-ink,#fff);
  font-family:var(--mn-row-face,inherit);font-style:normal;text-transform:none;
  font-size:13px;font-weight:600;line-height:1;white-space:nowrap;transition:.18s;
  background-image:none;padding-bottom:0;}
/* "Slightly darker", for whatever colour the pill actually is. --pink-deep is
   the palette's own darker pink and is the answer whenever nothing overrides
   the fill; color-mix covers the case where something does, and is written as
   a second declaration so a browser that cannot parse it simply keeps the
   first. Both are a hover state on a solid button, so neither can make the
   label unreadable. */
.wh-enq:hover{background:var(--pink-deep);color:var(--btn-ink,#fff);}
.wh-enq:hover{background:color-mix(in srgb,var(--btn-fill,var(--pink)) 86%,#000);}
/* ── AND WHITE WITH NAVY TEXT WHILE THE MENU'S PICTURE PANEL IS SHOWING ──────
   Dylan, item 7 of nine, 26 September 2026: *"Just keep the one in the top
   right, which should have a background of #D97195 usually, but whose background
   should turn white with navy text on top when the RHS image appears when
   hovering over a menu item."*

   ⚠ THE FIRST HALF OF THAT WAS ALREADY TRUE WHEN HE ASKED, which is worth
   recording rather than re-implementing: the rule above already fills the pill
   with var(--btn-fill,var(--pink)), and --pink IS #d97195 in DEFAULT_THEME. It
   became that on 25 September, one day earlier, in his own words then ("the
   enquire button … with a pink background"). So only the lit state is new.

   html.wh-fs-lit is written by FULLSCREEN_MENU_JS whenever a picture is up in
   the sheet's right-hand panel — the same one fact that switches the scrim, so
   the pill and the scrim cannot disagree. It sits on the ROOT because the corner
   is a root-level fixed element and is not inside #wh-fs at all.

   SPECIFICITY, MEASURED RATHER THAN ASSUMED. (0,3,0) here against (0,1,0) for
   the base rule and (0,2,0) for the two hover rules, so it wins in every state
   and the hover below has to be stated for the lit case in its own right. It is
   NOT an !important: there is nothing here to fight.

   The hover keeps the pink it has always had rather than darkening the white,
   because white-going-greyer over a photograph reads as the button failing. */
/* ── ⚠ 27 SEPTEMBER 2026: THE WHOLE CORNER LEAVES INSTEAD, AND THE TWO RULES
      THE PARAGRAPHS ABOVE DESCRIBE ARE GONE ────────────────────────────────────
   The paragraphs are KEPT because they are the argument for a decision that has
   now been reversed, and a reader who finds only the reversal cannot tell whether
   the first version was careless. It was not: with a scrim over the photograph,
   white-on-dark was the right answer.

   Dylan, 27 September 2026: *"I'd also like to make it so that when the image
   appears, all of the stuff in the top right (e.g. viewing as a family, enquire,
   the x button) fade out as the image fades in, and vice versa if you stop
   hovering over a submenu item."*

   ONE RULE ON THE CORNER, NOT THREE ON ITS CHILDREN. He named the chip, the pill
   and the X, which is everything in there; the corner is their only common
   ancestor and it is the element html.wh-fs-lit can already reach. Three rules
   could disagree about a fourth control somebody adds later, and this one cannot.

   ⚠ 220ms OUT, AGAINST THE PICTURE'S 340ms, AND THE ASYMMETRY IS THE SAFETY.
   The photograph fades in over .34s (see .wh-fs-img). Matching that exactly would
   read as a cleaner crossfade and would hold the controls half-visible over a
   half-arrived photograph for the whole of it — and with the scrim gone there is
   no longer any guarantee about what is under them. Leaving first means the
   controls are clear before the picture is established: at 220ms the photograph
   is only part-way in, and for the whole of the overlap the ground beneath the
   corner is still mostly the panel's own WHITE, which is the one ground the
   controls' own navy-and-pink colours are drawn for.
   Coming back, --wh-corner-wait holds them out for 120ms while the picture
   starts to go, so they do not arrive on top of a photograph that is still there.

   ⚠ visibility, NOT opacity ALONE, and this is the accessibility half. The corner
   floats OVER an aria-modal dialog at z-index 250, and focusables() in
   FULLSCREEN_MENU_JS collects its controls into the sheet's Tab cycle. An
   opacity-0 button is still focusable, so a keyboard reader would Tab onto an
   invisible chip. visibility:hidden takes it out of the tab order and the
   accessibility tree; the 0s transition is DELAYED to the end of the fade going
   out (so it fades rather than vanishing) and to the start of the wait coming
   back (so nothing is focusable while it is still invisible).
   visibility keeps the box, so --wh-corner-w does not move and no menu row
   reflows — the same reason the prompt beside it used visibility, back when
   there was a prompt.

   ⚠ WHAT THIS DELIBERATELY COSTS, SAID OUT LOUD: while a submenu row is hovered
   there is no visible X and no clickable one. Escape still closes the sheet, and
   moving the pointer off the row brings the corner back. That is what he asked
   for; it is recorded here so nobody has to rediscover it as a bug.

   ⚠ AND IT CANNOT HAPPEN TO A KEYBOARD READER, which is worth knowing before
   anybody "fixes" that: html.wh-fs-lit is written only from rcol's mouseover
   handler (and the mouseleave that clears it). Nothing on the focus path lights
   the panel, so a reader who never moves a pointer never loses the corner. */
/* ── ⚠ 18 OCTOBER 2026: THE CORNER STAYS, AND THE RULE ABOVE IS GONE ──────────
   Dylan: *"When that photo appears, the two buttons … disappear. Please make it
   so that they don't disappear - the photo just appears behind them."* The
   corner already floats over the sheet at z-index 250, so the photograph is
   behind it as soon as nothing hides it. Taking the fade-out away is the whole
   change. The Enquire pill and the X keep their own solid fills, which is what
   keeps them legible over a photograph without a scrim. html.wh-fs-lit is
   still written by the runtime and now changes nothing in the corner. The
   reduced-motion line that names it is left alone, because it is harmless. */
/* Low-profile "Not a {audience}?" button, LEFT of the Enquire pill. No outline,
   a touch lighter than the pill; opens the shared #wh-modal audience callout
   (same one the classic site uses). Follows the corner's colour states like the
   pill/burger, and drops out on the narrowest screens where the corner is tight. */
/* gap:5px sets the distance between the label and its caret. */
/* THE FALLBACK IS THE BODY FACE, NOT inherit. Every other item in the row
   resolves --mn-row-face against var(--body); this one resolved it against
   whatever it happened to be sitting inside. With the menu-row face left unset
   — which is the default — the links took the body face and this took the
   header's, so the audience switch was visibly a different typeface from the
   links beside it. One fallback, and the two agree by construction. */
/* ⚠ THE PROMPT'S OWN FADE-OUT ON html.wh-fs-open IS GONE, AND THE PATTERN IN IT
   IS NOT: opacity plus visibility, with the visibility change delayed to the end
   of the fade so the hidden control leaves the tab order without vanishing, and
   the box kept so --wh-corner-w cannot move. That is exactly the shape the whole
   corner now uses for its crossfade with the picture, above. One control's fix
   became the group's. */
/* ⚠ A wh-nav-tight HIDE AND A 560px HIDE STOOD HERE and went with the prompt on
   27 September 2026. They answered a request of Dylan's from 25 September — do not
   show "not a family?" once the links have folded into the burger — which the
   removal of the control answers at every width instead. wh-nav-tight itself is
   unaffected and is still what the masthead row reads. */
/* ── THE BURGER: THE ROUNDEL, WITH BARS ON IT ──────────────────────────────
   Dylan, 25 September 2026: "I'd instead like to make it so that that
   background colour normally is the same navy blue as we are using as the
   background colour of the roundels, with the icon colour white, and so that
   when you hover over it the background colour simply becomes a slightly
   darker shade of navy blue. There should be no outline for the button."

   THE ROUNDEL'S NAVY IS --navy-deep, and that is not a guess: faviconSvg() and
   ROUND_BLUE both draw their disc with the literal #1d3552, which is what
   --navy-deep is set to in BASE_CSS. Written as the token so the two cannot
   drift apart if the palette is ever retheme; if a future reader changes one,
   the burger follows the roundel, which is the relationship Dylan described.

   "Slightly darker" is --navy-ink (#14263f), the palette's own next step down,
   rather than a one-off literal.

   The bars are white in every state now, so the colour no longer changes with
   what is behind the corner and the two colour-flip rules above no longer name
   this button. box-sizing:border-box means dropping the 1.5px ring leaves the
   disc exactly 44px, so nothing in the corner moves. */
/* ⚠ THIS IS THE MEGA-MENU STATE AND THE SCROLLED STATE, NOT THE TOP OF THE PAGE.
   Since 27 September 2026 the top of the page is a WHITE disc with a navy icon —
   see the block below, which is where the reasoning for both lives. Dylan asked
   for this appearance to be kept exactly as it is, so nothing here changed except
   the transition, and that only so the two states can cross safely. */
.wh-burger--round{
  display:flex;width:44px;height:44px;border-radius:50%;
  border:0;background:var(--navy-deep);padding:0;
  flex-direction:column;align-items:center;justify-content:center;gap:5px;
  cursor:pointer;color:#fff;
  /* linear, NOT the default ease, and the arithmetic in the block below is void
     without it: it reads the fraction of the fade that has elapsed as the
     fraction of the colour path travelled, which is only true of linear. */
  --wh-burger-ms:180ms;
  transition:background-color var(--wh-burger-ms) linear,color 0s calc(var(--wh-burger-ms) * .58);
}
.wh-burger--round span{width:18px;height:2px;margin:0;background:currentColor;}
/* Hover: a darker navy disc, white bars — in every state, including when it is
   an X. The bars no longer change colour at all, so the note that used to live
   here about NOT transitioning the bars' colour (a 180ms crossfade dropped the X below
   1.5:1 against the white sheet for ~150ms) is now true by construction. */
.wh-burger--round:hover{background:var(--navy-ink);color:#fff;}
.wh-burger--round:focus-visible{outline:2px solid var(--pink);outline-offset:3px;}
/* ── A WHITE DISC WITH A NAVY ICON, AT THE TOP OF THE PAGE ONLY ──────────────
   Dylan, 27 September 2026, with a screenshot of the burger vanishing into a navy
   page: *"I'd like to modify so that the burger menu button in the top right,
   when it's on the main page, has a white background and a navy blue icon within
   that. This applies only when its at the top of the page... When it appears on
   top of the mega menu instead, keep it as it is currently please (it should
   simply fade between one state and another)."*

   ⚠ THERE ARE THREE STATES, NOT TWO, AND HE ASKED ABOUT TWO OF THEM. Measured
   from the runtime rather than assumed:
     1. at the top of the page, menu shut      → WHITE disc, navy icon   (new)
     2. scrolled, the sticky bar showing       → navy disc, white icon   (unchanged)
     3. the mega menu open                     → navy disc, white icon   (unchanged)
   He named 1 and 3. State 2 is the one he did NOT ask about, so it is left alone,
   and the whole of this rule is the two negations that leave it alone.

   html.wh-past-navy IS the site's own fact for "no longer over the navy masthead"
   — FSBURGER_JS toggles it at the masthead's bottom edge, offset by the corner's
   own height, and .masthead is background:var(--navy-ink). So :not(.wh-past-navy)
   is "at the top of the page" without inventing a second measurement that could
   disagree with the sticky bar's.

   :not(.wh-fs-open) is state 3. wh-fs-open is written on the root the INSTANT the
   sheet starts to open and removed in finish() once it has fully gone, which is
   exactly the window in which the corner is over paper rather than over the page.
   .wh-corner.is-dark would have been the obvious choice and is the WRONG one: it
   lands 50ms after the open, which is the same moment the sheet's lower edge
   crosses the corner, so a white disc and a white sheet would meet.
   ⚠ And with the "Not a {audience}?" prompt gone (27 September), is-dark now has
   no rule reading it at all. FULLSCREEN_MENU_JS still writes it. Left as it is
   rather than ripped out with an unrelated change; it is noted so the next reader
   does not spend the morning looking for what it drives.

   ⚠ WHY THE ICON IS SNAPPED AND THE DISC IS FADED, WHICH LOOKS WRONG AND IS NOT.
   This is one colour pair traversed in opposite directions: the disc goes white →
   navy while the bars go navy → white. Fade both and at the half-way point they
   are THE SAME COLOUR — contrast 1:1, the icon simply gone — and no choice of
   timing function avoids it, because any symmetric pair meets in the middle.
   So the disc fades and the bars change in ONE step, at the point on the disc's
   path where the two possible bar colours are equally legible:
     navy-deep #1d3552 has relative luminance 0.0341; white has 1.0 (12.5:1).
     Equal contrast needs (L+0.05) = sqrt(1.05 x 0.0841) = 0.2972, so L = 0.2472,
     which is 58% of the way from white to navy — and gives 3.53:1 in BOTH
     directions, the best any single step can do. Above 3:1, which is what WCAG
     1.4.11 asks of a non-text control, in every frame.
   The delay is therefore .58 of the duration when the disc is heading TO navy
   (the rule above) and .42 when it is heading TO white (here) — the same point on
   the path, reached from the other end. A transition is read from the state being
   moved TO, which is why the two numbers live on two different rules.
   ⚠ It is written as calc() against --wh-burger-ms so that changing the duration
   cannot silently move the snap off the safe point. Changing the .58 / .42 pair
   without redoing the luminance arithmetic will.

   ⚠ AND OPENING THE MENU IS THE ONE EDGE THAT CANNOT AFFORD 180ms. The sheet's
   lower edge crosses the corner about 50ms in — the same figure the prompt's
   fade-out was timed against, back when there was a prompt. A 180ms fade to navy
   would still be nearly white when the white paper arrived under it. Overriding
   --wh-burger-ms rather than restating the transition keeps the duration and both
   delays in step, which two hand-written transition lists would not.

   ⚠ WHAT THIS DOES NOT KNOW, AND IT IS A REAL GAP: a masthead set to the OVERLAY
   style (.masthead.wh-hdr-overlay) is transparent, so "at the top of the page"
   there means "over whatever the first block is" — which may be a light
   photograph, where a white disc is as invisible as the navy one he photographed.
   Nothing in the page publishes the colour behind the corner, only whether the
   masthead is still under it, so this cannot be answered here. Reported rather
   than guessed at.

   SPECIFICITY, COUNTED. This is (0,4,1) against the base rule's (0,1,0) — but
   also against the base HOVER at (0,2,0), which it therefore outweighs. That is
   why the hover has to be restated below in its own right, or hovering the burger
   at the top of the page would keep the white background and lose nothing else.
   No !important: there is nothing here to fight. */
html:not(.wh-past-navy):not(.wh-fs-open) .wh-corner .wh-burger--round{
  background:#fff;color:var(--navy);
  transition:background-color var(--wh-burger-ms) linear,color 0s calc(var(--wh-burger-ms) * .42);}
/* "Slightly darker", the same idea as the navy disc's hover, for a white one:
   --paper first so a browser without color-mix still darkens, then the mix, which
   is the pattern .wh-enq:hover already uses. The icon stays navy throughout — a
   hover is not a state change, and inverting the button on hover is exactly what
   this rule exists to prevent. */
html:not(.wh-past-navy):not(.wh-fs-open) .wh-corner .wh-burger--round:hover{background:var(--paper);color:var(--navy);}
html:not(.wh-past-navy):not(.wh-fs-open) .wh-corner .wh-burger--round:hover{background:color-mix(in srgb,#fff 92%,var(--navy-deep));color:var(--navy);}
/* The open edge, 50ms: see the arithmetic above. (0,3,0) beats the base rule, and
   the white rule above excludes this state, so the two can never both apply. */
html.wh-fs-open .wh-corner .wh-burger--round{--wh-burger-ms:50ms;}
/* ── THE X STAYS WHITE OVER THE NAVY PICTURE PANEL ──────────────────────────
   Dylan, 20 October 2026: "if the navy is turned on, then the top right burger
   menu x exit button doesn't change colour to navy - it stays white. And
   likewise, it should stay the same colour of white when you close the burger
   mega menu too." The corner sits over the picture panel, so a navy disc would
   vanish into it. With the panel's navy switched on (.wh-fs--navy, see THE
   NAVY PICTURE PANEL in MEGA_MENU_CSS) the disc is the top-of-page white with
   a navy X for the whole time wh-fs-open is on, which runs from the first
   frame of the open to the last frame of the close, and it does not fade on
   either edge (transition:none), so nothing flips on the way in or out.
   ⚠ Only where the panel shows (above 1200px). Below that there is no panel
   and the X keeps today's navy disc over the white sheet.
   ⚠ What this does NOT change: once the menu has fully closed, the disc
   returns to whatever the page under it calls for, as it does today (white at
   the top of the page, navy once scrolled past the masthead). */
@media (min-width:1201px){
  html.wh-fs-open:has(#wh-fs.wh-fs--navy) .wh-corner .wh-burger--round{background:#fff;color:var(--navy);transition:none;}
  html.wh-fs-open:has(#wh-fs.wh-fs--navy) .wh-corner .wh-burger--round:hover{background:var(--paper);color:var(--navy);}
  html.wh-fs-open:has(#wh-fs.wh-fs--navy) .wh-corner .wh-burger--round:hover{background:color-mix(in srgb,#fff 92%,var(--navy-deep));color:var(--navy);}
}
/* Burger → X */
.wh-burger--round span{transition:transform .26s cubic-bezier(.4,0,.2,1),opacity .18s ease;}
.wh-burger--round.is-x span:nth-child(1){transform:translateY(7px) rotate(45deg);}
/* THE MIDDLE BAR IS COLLAPSED, NOT MERELY MADE TRANSPARENT.
   Dylan: "when I click on the burger menu icon it does its animation, but the
   middle line still seems to be visible at either side... when I move the
   cursor away it becomes an x again."

   The two stubs are this bar's ends. It is 18px wide; the rotated arms occupy a
   box of 18·cos45 + 2 = 14.1px, so exactly 1.95px of it stands proud on each
   side — which is the size and the position of what he photographed.

   opacity:0 alone was doing the hiding, and opacity alone is a compositing
   instruction: the element still has its layer, its geometry and its paint, and
   the engine is free to show a stale one. I could not reproduce it in headless
   Chromium — computed opacity is 0 in every state I could construct, hovered,
   dark, menu-open and mid-transition — which is what a GPU-level artifact looks
   like from here: real on the hardware, absent in software rasterisation.

   So this stops relying on opacity being honoured. scaleX(0) leaves the bar
   with no width to draw, so there is nothing for any layer, stale or fresh, to
   put on screen. It rides the transform transition the arms already use, so it
   collapses from its centre as they converge on it — the same gesture, and now
   one that cannot leave anything behind. */
.wh-burger--round.is-x span:nth-child(2){opacity:0;transform:scaleX(0);}
.wh-burger--round.is-x span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
@media(max-width:900px){
  /* The pill's label would crowd the burger at this width. */
  :root{--wh-corner-w:148px;}
  html:has(.wh-corner--solo){--wh-corner-w:40px;}
  .wh-corner{--wh-corner-h:40px;}
  .wh-enq{padding:0 14px;font-size:12px;height:40px;}
  /* (The "Viewing as" chip's phone rules went with the chip, 12 October 2026.) */
}


.wh-doors{position:relative;display:flex;flex-direction:column;overflow:hidden;
  background:#0c0e11;
  min-height:calc(100svh - var(--wh-doors-off,0px));
  padding-top:var(--wh-doors-top,0px);
  --wh-dr-sc:1; --wh-dr-a:1;
  --wh-dr-r:2px;
  --wh-dr-pt:24px; --wh-dr-px:24px; --wh-dr-pb:64px; --wh-dr-gap:7px; --wh-dr-col:16px;
  --wh-dr-gob:18px; --wh-dr-gosz:38px;
  --wh-dr-name:clamp(21px,2.2vw,27px);
  --wh-dr-hlpy:20px; --wh-dr-hlpx:clamp(20px,2.6vw,38px); --wh-dr-hlgap:8px;
  --wh-dr-ico:27px;
  /* ONE curve for the whole door, so the lift, the shadow and the disc arrive
     together. Slower out than in — a card that settles back faster than it rose
     reads as sprung rather than sluggish. */
  --wh-dr-tr:transform .42s cubic-bezier(.2,.7,.2,1),box-shadow .42s cubic-bezier(.2,.7,.2,1);}
.wh-dr-cap:hover,.wh-dr-hl:hover{--wh-dr-tr:transform .3s cubic-bezier(.34,1.4,.5,1),box-shadow .3s ease-out;}
/* No 100svh in this browser: 100vh is close enough and never zero. */
@supports not (height:100svh){.wh-doors{min-height:calc(100vh - var(--wh-doors-off,0px));}}
.wh-doors .wh-fsvideo-video,.wh-doors .wh-fsvideo-poster,.wh-doors .wh-fsvideo-fallback{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  background-size:cover;background-position:center;}
.wh-doors .wh-fsvideo-video{opacity:0;transition:opacity .5s ease;z-index:1;}
.wh-doors .wh-fsvideo-video.is-playing{opacity:1;}
/* Under reduced motion the clip must not cross-fade in either. */
@media(prefers-reduced-motion:reduce){.wh-doors .wh-fsvideo-video{transition:none;}}
/* ── the scrim: a BLACK wash, never blue ── */
.wh-dr-scrim{position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(180deg,
    rgb(0 0 0 / calc(var(--wh-dr-a1) * var(--wh-dr-sc))) 0%,
    rgb(0 0 0 / calc(var(--wh-dr-a2) * var(--wh-dr-sc))) 32%,
    rgb(0 0 0 / calc(var(--wh-dr-a3) * var(--wh-dr-sc))) 64%,
    rgb(0 0 0 / calc(var(--wh-dr-a4) * var(--wh-dr-sc))) 100%);}
.wh-doors.wh-dr-sc-vig {--wh-dr-a1:.72;--wh-dr-a2:.30;--wh-dr-a3:.46;--wh-dr-a4:.92;}
.wh-doors.wh-dr-sc-base{--wh-dr-a1:.28;--wh-dr-a2:.20;--wh-dr-a3:.54;--wh-dr-a4:1;}
.wh-doors.wh-dr-sc-even{--wh-dr-a1:.5;--wh-dr-a2:.5;--wh-dr-a3:.5;--wh-dr-a4:.5;}
.wh-dr-stack{position:relative;z-index:3;flex:1;display:flex;flex-direction:column;}
/* ── slogan ── */
.wh-dr-slogan{flex:1;display:flex;align-items:center;justify-content:center;
  padding:clamp(24px,5vw,60px) var(--wh-gutter);text-align:center;}
.wh-dr-slg{display:flex;flex-direction:column;align-items:center;justify-content:center;
  width:100%;max-width:var(--wrap);color:#fff;margin:0;
  font-family:var(--serif);font-weight:600;
  font-size:clamp(44px,8.4vw,112px);letter-spacing:-.025em;line-height:1.02;}
.wh-dr-l1,.wh-dr-l2{display:block;}
.wh-dr-rule{display:none;}
@keyframes whDrUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}}
.wh-dr-l1{animation:whDrUp .85s cubic-bezier(.22,1,.36,1) both;}
.wh-dr-l2{animation:whDrUp .85s cubic-bezier(.22,1,.36,1) .13s both;}
.wh-dr-rule{animation:whDrUp .85s cubic-bezier(.22,1,.36,1) .26s both;}
/* 1 · two-tone */
.wh-dr-s-two .wh-dr-l2{color:var(--pink);}
/* 2 · kicker */
.wh-dr-s-kicker .wh-dr-slg{gap:clamp(12px,1.6vw,20px);}
.wh-dr-s-kicker .wh-dr-l1{font-family:var(--body);font-weight:700;text-transform:uppercase;
  font-size:clamp(12px,1.5vw,17px);letter-spacing:.42em;text-indent:.42em;
  color:rgb(255 255 255 / .84);}
.wh-dr-s-kicker .wh-dr-l2{font-size:clamp(50px,9.6vw,132px);}
/* 3 · italic (the default) */
.wh-dr-s-italic .wh-dr-l2{font-style:italic;letter-spacing:-.015em;}
.wh-dr-s-italic .wh-dr-rule-b{display:block;flex:0 0 auto;
  width:min(220px,32vw);height:3px;background:var(--pink);
  margin-top:clamp(20px,2.6vw,34px);}
/* 4 · outline */
.wh-dr-s-outline .wh-dr-l1{color:transparent;
  -webkit-text-stroke:clamp(1px,.13vw,2.2px) rgb(255 255 255 / .95);}
/* 5 · masthead */
.wh-dr-s-masthead .wh-dr-slg{flex-direction:row;align-items:center;gap:0;}
.wh-dr-s-masthead .wh-dr-rule{display:block;flex:1 1 0;min-width:20px;height:1px;
  background:linear-gradient(90deg,rgb(255 255 255 / 0),rgb(255 255 255 / .6));
  margin-right:clamp(16px,2.6vw,36px);}
.wh-dr-s-masthead .wh-dr-rule-b{margin:0 0 0 clamp(16px,2.6vw,36px);
  background:linear-gradient(90deg,rgb(255 255 255 / .6),rgb(255 255 255 / 0));}
.wh-dr-s-masthead .wh-dr-l1,.wh-dr-s-masthead .wh-dr-l2{
  white-space:nowrap;font-size:clamp(28px,5vw,78px);}
.wh-dr-s-masthead .wh-dr-l1{margin-right:.24em;}
/* ── the chooser ── */
/* margin-top:auto so the doors sit at the BASE of the hero whatever is above
   them. The slogan is the flex:1 spacer that normally pushes them down; with no
   slogan the doors used to sit at the top of a full-height section with the
   rest of the viewport left as empty ground. */
.wh-dr-choose{margin-top:auto;padding:0 var(--wh-gutter) clamp(26px,4vw,52px);}
.wh-dr-lead{display:block;text-align:center;font-size:11px;font-weight:700;
  letter-spacing:.24em;text-transform:uppercase;color:rgb(255 255 255 / .66);
  margin-bottom:clamp(14px,2vw,22px);}
.wh-dr-in{max-width:var(--wrap);margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);}
.wh-dr-l-capsules .wh-dr-in{gap:var(--wh-dr-col);}
/* THE MOTION LIVES ON THE DOOR ITSELF, whatever element it is.
   A door with no destination is a <div>, not an <a> — that is deliberate, an
   anchor with no href is a tap target that does nothing — and this transition
   used to be declared only on the anchor selector. So an unlinked door had no
   transition at all and its hover lift SNAPPED, which is exactly how every door
   behaves until its link is set.
   Two declarations, not one: the site-wide anchor underline-sweep selector
   outranks anything reasonable and pins transition-property to background-size
   on ANY <a>, so the anchor case still needs !important to win it back. Same
   escape hatch the masthead brand link uses (.masthead .brandtop). */
.wh-dr-cap,.wh-dr-hl{transition:var(--wh-dr-tr);}
/* THE PADDING NEEDS !important AND THE BACKGROUND ALREADY HAD IT.
   The site-wide underline sweep is an anchor selector carrying SIXTEEN :not()
   clauses, which is sixteen classes of specificity — more
   than any sane selector here can reach. It sets a 2px bottom padding to make
   room for the 2px rule it draws, and that was landing on these cards: a door
   with a link had 13px of padding above its name and 2px below, while a door
   WITHOUT one rendered as a <div>, escaped the rule entirely and kept both. The
   same card, two different heights, depending on whether it went anywhere. */
.wh-doors a.wh-dr-cap,.wh-doors a.wh-dr-hl{
  background-image:none!important;padding-bottom:var(--wh-dr-pb)!important;
  transition:var(--wh-dr-tr)!important;}
/* The hairline layout pads on its own token, not the capsule's. */
.wh-doors a.wh-dr-hl{padding-bottom:var(--wh-dr-hlpy)!important;}
/* ── 1 · capsules ── */
.wh-dr-cap{position:relative;display:flex;flex-direction:column;align-items:flex-start;
  gap:var(--wh-dr-gap);border-radius:var(--wh-dr-r);color:var(--ink);
  background:rgb(251 247 247 / var(--wh-dr-a));
  backdrop-filter:blur(calc(6px + (1 - var(--wh-dr-a)) * 14px)) saturate(1.06);
  -webkit-backdrop-filter:blur(calc(6px + (1 - var(--wh-dr-a)) * 14px)) saturate(1.06);
  padding:var(--wh-dr-pt) var(--wh-dr-px) var(--wh-dr-pb);
  box-shadow:0 16px 40px -26px rgb(0 0 0 / .78),
             inset 0 0 0 1px rgb(255 255 255 / calc((1 - var(--wh-dr-a)) * .55));}
.wh-dr-cap:hover{transform:translateY(-9px);
  box-shadow:0 26px 54px -26px rgb(0 0 0 / .85),
             inset 0 0 0 1px rgb(255 255 255 / calc((1 - var(--wh-dr-a)) * .7));}
/* The arrow leans the way it points, on the same curve as the card. */
.wh-dr-go .gwarr{transition:transform .3s cubic-bezier(.34,1.4,.5,1);}
.wh-dr-cap:hover .wh-dr-go .gwarr{transform:translateX(2px);}
/* As the card thins out the small type deepens, so contrast holds. */
.wh-dr-l-capsules .wh-dr-who{font-size:10px;font-weight:700;letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--pink);
  color:color-mix(in srgb, var(--pink-deep) calc((1 - var(--wh-dr-a)) * 230%), var(--pink));}
.wh-dr-l-capsules .wh-dr-name{font-family:var(--serif);font-weight:600;
  font-size:var(--wh-dr-name);line-height:1.12;
  color:var(--navy);
  color:color-mix(in srgb, var(--navy-ink) calc((1 - var(--wh-dr-a)) * 230%), var(--navy));}
.wh-dr-l-capsules .wh-dr-blurb{font-size:13.5px;line-height:1.5;
  color:var(--muted);
  color:color-mix(in srgb, var(--ink) calc((1 - var(--wh-dr-a)) * 230%), var(--muted));}
/* ── THE QUIETER WORDS AFTER A DOOR'S TITLE ────────────────────────────────
   Dylan, 26 September 2026: "put it to the RHS of 'Families' within the same
   line of text, in thinner text and slightly transparent."

   THREE THINGS AND NO MORE. Lighter weight, slightly see-through, and the SAME
   SIZE AND FACE as the title — he asked for thinner and transparent and did not
   ask for smaller, and the doors block is where he said "to the RHS" while
   asking for a SMALLER line underneath in the insights doors. Two different
   requests, kept different.

   ⚠ opacity RATHER THAN A COLOUR, on purpose. The capsule layout paints the
   title with a color-mix against the card's own opacity and the hairline layout
   paints it white; a colour here would have to be written twice and would be
   wrong on whichever ground was added next. Fading what is already there is
   correct on every ground by construction.
   ⚠ AND 400 IS A WEIGHT THE FACE ACTUALLY HAS. Aleo ships 400 and the Google
   request fetches it (fontLoading.test.ts pins the URL); asking for a weight a
   face lacks makes the browser fake it, which looks subtly wrong rather than
   obviously wrong. */
.wh-dr-trail{font-weight:400;opacity:.62;}
.wh-dr-go{position:absolute;left:var(--wh-dr-px);bottom:var(--wh-dr-gob);
  width:var(--wh-dr-gosz);height:var(--wh-dr-gosz);
  background:var(--navy);border-color:var(--navy);color:#fff;}
.wh-dr-go .gwarr{width:16px;height:16px;}
.wh-dr-cap:hover .wh-dr-go{background:var(--pink);border-color:var(--pink);}
/* ── density ── */
.wh-doors.wh-dr-d-airy{
  --wh-dr-pt:32px; --wh-dr-px:32px; --wh-dr-pb:82px; --wh-dr-gap:11px; --wh-dr-col:22px;
  --wh-dr-gob:26px; --wh-dr-gosz:42px; --wh-dr-name:clamp(22px,2.4vw,30px);
  --wh-dr-hlpy:28px; --wh-dr-hlgap:11px;}
.wh-doors.wh-dr-d-tight{
  --wh-dr-pt:17px; --wh-dr-px:20px; --wh-dr-pb:54px; --wh-dr-gap:4px; --wh-dr-col:11px;
  --wh-dr-gob:13px; --wh-dr-gosz:34px; --wh-dr-name:clamp(19px,2vw,24px);
  --wh-dr-hlpy:13px; --wh-dr-hlgap:5px;}
.wh-doors.wh-dr-d-compact{
  --wh-dr-pt:13px; --wh-dr-px:18px; --wh-dr-pb:13px; --wh-dr-gap:4px; --wh-dr-col:9px;
  --wh-dr-gosz:32px; --wh-dr-name:clamp(18px,1.9vw,22px);
  --wh-dr-hlpy:9px; --wh-dr-hlgap:3px;}
/* Compact drops the description and puts the arrow on the title's own line. */
.wh-dr-d-compact .wh-dr-cap{flex-flow:row wrap;align-items:center;}
.wh-dr-d-compact .wh-dr-cap .wh-dr-who{order:0;flex:0 0 100%;}
.wh-dr-d-compact .wh-dr-cap .wh-dr-ico{order:1;margin:0 10px 0 0;--wh-dr-ico:22px;}
.wh-dr-d-compact .wh-dr-cap .wh-dr-name{order:2;flex:1 1 auto;}
.wh-dr-d-compact .wh-dr-blurb{display:none;}
.wh-dr-d-compact .wh-dr-cap .wh-dr-go{order:3;position:static;margin-left:auto;}
/* ── icons ── */
.wh-dr-ico{display:block;line-height:0;margin:0 0 4px;}
.wh-dr-ico svg{display:block;width:var(--wh-dr-ico);height:var(--wh-dr-ico);}
.wh-dr-ico .wh-dr-pk{stroke:var(--pink);}
.wh-dr-ico .wh-dr-pkf{fill:var(--pink);}
.wh-doors.wh-dr-i-round,.wh-doors.wh-dr-i-emblem{--wh-dr-ico:34px;}
.wh-dr-cap .wh-dr-ico{color:var(--navy);}
.wh-dr-hl .wh-dr-ico{color:#fff;margin-bottom:6px;}
/* ── 2 · hairlines ── */
.wh-dr-hl{position:relative;display:flex;flex-direction:column;align-items:flex-start;
  gap:var(--wh-dr-hlgap);color:#fff;padding:var(--wh-dr-hlpy) var(--wh-dr-hlpx);}
.wh-dr-hl + .wh-dr-hl::before{content:"";position:absolute;left:0;top:6%;bottom:6%;width:1px;
  background:linear-gradient(180deg,rgb(255 255 255 / 0),rgb(255 255 255 / .42) 22%,
             rgb(255 255 255 / .42) 78%,rgb(255 255 255 / 0));}
.wh-dr-l-hairlines .wh-dr-who{font-size:10px;font-weight:700;letter-spacing:.2em;
  text-transform:uppercase;color:var(--pink);}
.wh-dr-l-hairlines .wh-dr-name{font-family:var(--serif);font-weight:600;
  font-size:clamp(24px,2.7vw,34px);line-height:1.1;}
.wh-dr-l-hairlines .wh-dr-blurb{font-size:13.5px;line-height:1.55;
  color:rgb(255 255 255 / .74);max-width:34ch;}
.wh-dr-go-t{display:inline-flex;align-items:center;gap:9px;margin-top:4px;
  font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  color:rgb(255 255 255 / .6);transition:color .22s ease,gap .22s ease;}
.wh-dr-go-t .gwarr{width:16px;height:16px;}
.wh-dr-hl:hover .wh-dr-go-t{color:var(--pink);gap:15px;}
/* ── responsive ── */
@media(max-width:900px){
  .wh-dr-in{grid-template-columns:1fr;}
  .wh-dr-hl + .wh-dr-hl::before{left:0;right:0;top:0;bottom:auto;width:auto;height:1px;
    background:linear-gradient(90deg,rgb(255 255 255 / 0),rgb(255 255 255 / .4) 20%,
               rgb(255 255 255 / .4) 80%,rgb(255 255 255 / 0));}
}
@media(max-width:760px){
  .wh-dr-slogan{padding-top:clamp(30px,7vw,60px);padding-bottom:clamp(30px,7vw,60px);}
  /* The masthead one-liner has nowhere to run on a phone — stack it instead. */
  .wh-dr-s-masthead .wh-dr-slg{flex-direction:column;gap:15px;}
  .wh-dr-s-masthead .wh-dr-rule{flex:0 0 auto;width:120px;margin:0;
    background:linear-gradient(90deg,rgb(255 255 255 / 0),rgb(255 255 255 / .6),rgb(255 255 255 / 0));}
  .wh-dr-s-masthead .wh-dr-l1,.wh-dr-s-masthead .wh-dr-l2{font-size:clamp(38px,11vw,58px);}
  .wh-dr-s-masthead .wh-dr-l1{margin-right:0;}
}
@media(prefers-reduced-motion:reduce){
  .wh-dr-l1,.wh-dr-l2,.wh-dr-rule{animation-duration:.01ms!important;animation-delay:0ms!important;}
  .wh-dr-cap,.wh-dr-hl,.wh-doors a.wh-dr-cap,.wh-doors a.wh-dr-hl,
  .wh-dr-go .gwarr{transition-duration:.01ms!important;}
}
/* NO CLIP AND NO POSTER YET. The block is routinely inserted before the footage
   is uploaded, and a full screen of #0c0e11 reads as a broken video rather than
   as a hero waiting for one. Carry the brand instead — the same navy the site
   opens with — so an unfinished hero still looks deliberate. */
.wh-doors.wh-dr-nomedia{background:
  radial-gradient(120% 90% at 50% 0%, rgb(255 255 255 / .07), rgb(255 255 255 / 0) 60%),
  linear-gradient(170deg, var(--navy) 0%, var(--navy-deep) 55%, var(--navy-ink) 100%);}
/* Nothing to darken, so the scrim would only mute the ground. */
.wh-doors.wh-dr-nomedia .wh-dr-scrim{display:none;}

/* The editor preview is a short banner, not a viewport-height hero. */
.wh-doors.wh-dr-preview{min-height:0;padding-top:0;}
.wh-doors.wh-dr-preview .wh-dr-slogan{padding-block:clamp(30px,6vw,64px);}

/* ── transparent page header ──
   The masthead leaves the flow and sits over the first block. It is NOT fixed:
   it scrolls away with the hero, and .drophdr is what comes back on scroll. */
.masthead.wh-hdr-overlay{position:absolute;top:0;left:0;right:0;z-index:60;
  background:transparent;pointer-events:none;}
/* ⚠ START BELOW THE UTILITY STRIP, NOT AT THE TOP OF THE PAGE — N1783,
   29 SEPTEMBER 2026, and the symptom was nothing like the cause.

   Dylan: "please check how the masthead logo is being rendered when the masthead
   has no background and displays an image behind … the top of the logo seems to
   be being cut off." It was, and cleanly, straight across the roundel.

   position:absolute resolves against body (body{position:relative} below), so
   top:0 is the top of the PAGE — above the utility strip, which is an earlier
   sibling in normal flow. Two consequences, and only the second is visible:

     · the whole transparent masthead sat one strip-height too high, so the brand
       was never where the author placed it relative to the hero; and
     · .topstrip is position:relative z-index:61 against this header's 60, so the
       strip PAINTED OVER the part of the logo that had been pushed up behind it
       — a clean horizontal cut at the strip's lower edge, which is exactly what
       the screenshot shows. A solid masthead is in the flow and never had it.

   ⚠ Raising this z-index above 61 would have hidden the cut and kept the fault:
   the logo would then be drawn over the audience and location chips. The offset
   is the defect; the clipping is only how it became visible.

   The general sibling combinator means this applies only when a strip is
   actually drawn — with the utility bar switched off the header still belongs at
   the very top, and top:0 above is right.

   The 44px fallback is .topstrip .row's own declared height, so the page is
   correct before any script runs. It is a FALLBACK and not the answer: that row
   is flex-wrap:wrap, so it grows past 44px when the chips wrap on a narrow
   screen, and MASTFIT_JS republishes the measured height as --wh-strip-h.

   ⚠ And this CORRECTS --wh-doors-top rather than needing a change beside it. The
   opener reserves var(--wh-mast-h) at its own top; while the header floated a
   strip-height higher, that reservation over-counted by exactly the strip, which
   is why an overlay hero's content sat lower than the author asked for. */
.topstrip ~ .masthead.wh-hdr-overlay{top:var(--wh-strip-h,44px);}
.masthead.wh-hdr-overlay .inner{pointer-events:none;}
.masthead.wh-hdr-overlay .brandtop,.masthead.wh-hdr-overlay .nav{pointer-events:auto;}
/* The corner (Enquire + burger) is fixed at z-index 250 and already clears this. */
body{position:relative;}

/* ── HOW MUCH ROOM THE OPENING BLOCK LEAVES ──
   Keyed off the MASTHEAD'S OWN class and its position in the markup, never off a
   class on the document body. The hub preview renders compileInner() into
   #wh-root inside its own shell — it never calls compilePage() — so a body class
   would be absent there, and the author would see the brand sitting on top of
   the slogan in the editor and correct on the live page. Everything below uses
   only what compileInner emits, so the preview and the published page are
   arranged identically.

   Scoped to the FIRST child because this is about the page's opener. A doors block placed
   further down has nothing above it to account for and takes a whole viewport;
   subtracting the masthead there would leave it short by the height of a header
   that scrolled away long ago. */
.masthead.wh-hdr-overlay ~ main > .wh-doors:first-child{
  --wh-doors-top:var(--wh-mast-h,168px);--wh-doors-off:0px;}
/* Solid header: the masthead is in flow above, so the opener takes what is left
   and the two together fill exactly one viewport. */
main > .wh-doors:first-child{--wh-doors-off:var(--wh-mast-h,168px);}
@media(max-width:900px){
  /* No nav row at this width; the brand column is shorter. */
  .masthead.wh-hdr-overlay ~ main > .wh-doors:first-child{--wh-doors-top:var(--wh-mast-h,140px);}
}


/* Height is measured against the STICKY drop-in header (64px), NOT the masthead:
   once the visitor has scrolled, the drop-in bar is what overlays this block.
   ⚠ This used to say "64px is the .drophdr .inner height - keep the two in
   step", which asked a future reader to remember. It now reads --drop-h, so
   they are in step by construction. It is the fifth
   hard-coded call site of that number in this file; if anyone tokenises it,
   this rule is one of the five to update. */
.wh-pc{background:var(--paper);display:flex;flex-direction:column;
  min-height:calc(100svh - var(--drop-h,64px));padding:var(--pc-gap,12px);
  --pc-gap:12px;--pc-round:44px;--pc-zoom:1.06;--pc-zoom-ms:760ms;}
@supports not (height:100svh){.wh-pc{min-height:calc(100vh - var(--drop-h,64px));}}
.wh-pc-in{flex:1;min-height:0;display:grid;grid-template-columns:repeat(3,1fr);
  gap:var(--pc-gap);}
.wh-pc-card{position:relative;display:block;overflow:hidden;
  background:var(--navy-ink);isolation:isolate;}
/* The site-wide anchor underline-sweep selector outranks anything reasonable and
   pins transition-property to background-size on ANY anchor. Same escape hatch
   the masthead brand link uses (.masthead .brandtop). */
/* Same sixteen-:not() sweep as the doors above — see DOORS_CSS. Without the
   !important a linked card is 2px taller than an unlinked one. */
.wh-pc a.wh-pc-card{background-image:none!important;padding-bottom:0!important;}
/* THE PICTURE ZOOMS - never the card. The wrapper is what clips it. */
.wh-pc-pic{position:absolute;inset:0;display:block;overflow:hidden;}
.wh-pc-pic img{width:100%;height:100%;object-fit:cover;display:block;
  transform:scale(1);transform-origin:center 42%;
  transition:transform var(--pc-zoom-ms) cubic-bezier(.22,1,.36,1);
  will-change:transform;}
.wh-pc-card:hover .wh-pc-pic img,
.wh-pc-card:focus-visible .wh-pc-pic img{transform:scale(var(--pc-zoom));}
.wh-pc-scrim{position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,
    rgb(0 0 0 / .30) 0%, rgb(0 0 0 / .10) 34%,
    rgb(0 0 0 / .34) 68%, rgb(0 0 0 / .78) 100%);
  transition:opacity .5s ease;}
.wh-pc-card:hover .wh-pc-scrim{opacity:.88;}
.wh-pc-body{position:relative;z-index:2;height:100%;display:flex;flex-direction:column;
  align-items:center;justify-content:flex-end;text-align:center;color:#fff;
  gap:clamp(10px,1.3vw,16px);
  padding:clamp(28px,4vw,54px) clamp(20px,2.6vw,40px) clamp(34px,4.4vw,60px);}
.wh-pc-round{display:block;line-height:0;margin-bottom:clamp(2px,.5vw,6px);}
/* EYECREST / EYECREST_W carry class="eyecrest", and BASE_CSS already sizes that
   at 42px with a 12px bottom margin - both are overridden here. */
.wh-pc-round .eyecrest{width:var(--pc-round);height:var(--pc-round);margin:0;
  filter:drop-shadow(0 3px 8px rgb(8 16 34 / .3));
  transition:transform .5s cubic-bezier(.22,1,.36,1);}
.wh-pc-card:hover .wh-pc-round .eyecrest{transform:translateY(-3px);}
.wh-pc-eye{display:block;font-size:11px;font-weight:600;letter-spacing:.24em;
  text-transform:uppercase;color:rgb(255 255 255 / .88);}
/* ── THE CARD'S TITLE, AND THE QUIETER LINE UNDER IT ───────────────────────
   Dylan, 26 September 2026: "add the capability to have slightly smaller text
   underneath the main text too".

   ⚠ THE TITLE'S SIZE BECAME A VARIABLE SO THE SECOND LINE CAN TRACK IT. Both are
   clamps, and two independent clamps are two things that drift: somebody tuning
   the title at one breakpoint would leave the line beneath it the wrong size at
   that width only, which is the sort of thing nobody notices until a screenshot.
   .62 of the title, at every width, by construction. The rendered title size is
   unchanged — the variable's value IS the clamp it replaced, and the narrow
   override below sets the same variable rather than the font-size.
   ⚠ 400, WHICH ALEO HAS. Asking a face for a weight it lacks makes the browser
   fake it; the Google request fetches Aleo 400 (fontLoading.test.ts pins it). */
.wh-pc-body{--pc-name:clamp(28px,3.1vw,46px);}
.wh-pc-name{display:block;font-family:var(--serif);font-weight:600;
  font-size:var(--pc-name,clamp(28px,3.1vw,46px));line-height:1.06;letter-spacing:-.012em;}
.wh-pc-sub{display:block;font-family:var(--serif);font-weight:400;
  font-size:calc(var(--pc-name,clamp(28px,3.1vw,46px)) * .62);line-height:1.12;
  letter-spacing:-.008em;opacity:.82;margin-top:-2px;}
/* Outlined pill, matching the fixed corner's Enquire. */
.wh-pc-btn{display:inline-flex;align-items:center;gap:12px;height:46px;padding:0 22px;
  border:1.5px solid rgb(255 255 255 / .72);color:#fff;
  font-size:11.5px;font-weight:600;letter-spacing:.2em;text-transform:uppercase;
  transition:background-color .22s ease,border-color .22s ease,gap .22s ease;}
.wh-pc-btn .gwarr{width:16px;height:16px;}
.wh-pc-card:hover .wh-pc-btn{background:var(--pink);border-color:var(--pink);gap:18px;}
@media(max-width:1000px){
  .wh-pc{min-height:0;}
  .wh-pc-in{grid-template-columns:1fr;}
  .wh-pc-card{min-height:62vh;min-height:62svh;}
  /* ⚠ THE VARIABLE, NOT THE font-size, so the quieter line under the title
     follows it. Setting font-size here would leave .wh-pc-sub reading the
     DESKTOP clamp on a phone — the exact drift the variable exists to prevent,
     and invisible at every width except the narrow ones. The value is the one
     this rule has always had. */
  .wh-pc-body{--pc-name:clamp(30px,7vw,44px);}
}
@media(max-width:760px){
  .wh-pc-body{padding:clamp(24px,6vw,40px) clamp(18px,5vw,30px) clamp(28px,7vw,44px);}
}
@media(prefers-reduced-motion:reduce){
  .wh-pc-pic img,.wh-pc-scrim,.wh-pc-round .eyecrest,.wh-pc-btn{transition:none;}
  .wh-pc-card:hover .wh-pc-pic img{transform:none;}
}


/* ── the shared frame ───────────────────────────────────────────────────── */
.svp{position:relative;}
.svp--paper{background:var(--paper);}
.svp--white{background:var(--white);}
.svp--navy{background:var(--navy-ink);}.svp.svp--tertiary{background:var(--tertiary,var(--navy-ink));}
/* A room, not a strip: each section holds most of a screen and centres its
   content in it, so a service page reads as a sequence of rooms. `min()`
   rather than a flat 560px so a short viewport is never forced to scroll
   through empty ground to reach the words. */
.svp-room{padding:clamp(38px,4.5vw,66px) 0;min-height:min(560px,90vh);
  display:flex;flex-direction:column;justify-content:center;}
.svp-room>.wrap{width:100%;}
.svp-crest{display:block;}
/* BASE_CSS sizes .eyecrest at 42px with a 12px bottom margin. The service
   sections use the small cut and space it from the heading themselves. */
.svp-crest .eyecrest{width:30px;height:30px;margin:0;}
.svp-muted{color:var(--muted);}
.svp-body{color:var(--ink);}
.svp .eyebrow{display:block;margin-top:14px;}
.svp-line{border:0;border-top:1px solid var(--line);}
/* ⚠ FACE DROPPED with .svp-faq-spoken's, 20 September 2026, and for the same
   reason: a run-in question is still an h3, so its typeface is the sub-heading
   tier's to choose. The 700 weight IS this element's own — a run-in heading
   sits on the same line as its answer and has nothing but weight to separate
   them — so it stays. */
.svp-runin{font-weight:700;color:var(--heading-deep,var(--navy-ink));}
/* A covered rectangle and nothing else — deliberately not `.ph`, which carries
   the whole photographic-card language (scrims, hover zoom, tone filters). */
.svp-ph{background-size:cover;background-position:center;background-color:var(--line);
  border-radius:2px;position:relative;overflow:hidden;}
/* Ground flip, once, for all eight blocks. */
.svp--navy h1,.svp--navy h2,.svp--navy h3,.svp--navy .svp-sentence,
.svp--navy .svp-voice,.svp--navy .svp-voice-long,.svp--navy .svp-runin{color:#fff;}
.svp--navy .svp-body{color:rgba(255,255,255,.85);}
.svp--navy .svp-muted{color:rgba(255,255,255,.68);}
.svp--navy .svp-line,.svp--navy .svp-ckrow,.svp--navy .svp-profile-note{
  border-color:rgba(255,255,255,.22);}
.svp--navy .svp-dcap{color:#fff;}
.svp--navy .svp-ph{background-color:rgba(255,255,255,.12);}

/* ── 01 · service hero ──────────────────────────────────────────────────── */
.svp-wrap2{display:grid;grid-template-columns:1fr .95fr;gap:clamp(28px,5vw,72px);
  align-items:center;}
.svp-wrap2.rev{grid-template-columns:.95fr 1fr;}
.svp-hero h1{font-family:var(--h-hero-face,var(--h-title-face,var(--serif)));font-size:clamp(30px,4.6vw,54px);margin-top:18px;}
.svp-hero-lede{margin-top:22px;max-width:48ch;font-size:15px;line-height:1.55;}
.svp-hero-imgs{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start;}
.svp-hero-imgs .svp-ph{aspect-ratio:3/4.2;}
/* The offset is what makes the pair read as a gallery rather than a grid. It
   always rides the photograph FURTHEST from the words; the template decides
   which one that is and marks it. */
.svp-hero-imgs .svp-ph.off{margin-top:56px;}
.svp-enq{display:inline-flex;align-items:center;gap:12px;margin-top:26px;
  color:var(--navy);text-decoration:none;
  background-image:none!important;padding-bottom:0!important;}
.svp-enq .t{font-family:var(--h-sub-face,var(--serif));font-weight:700;font-size:17px;
  letter-spacing:0;color:inherit;line-height:1.25;}
.svp-enq .arwbtn{width:36px;height:36px;font-size:15px;
  border-color:var(--pink);background:var(--pink);color:#fff;}
.svp-enq .arwbtn .gwarr{width:15px;height:15px;}
.svp-enq:hover .arwbtn{background:var(--pink-deep);border-color:var(--pink-deep);}
.svp--navy .svp-enq{color:#fff;}
/* THE SERVICE HERO'S ENQUIRE BUTTON IS THE HEADER'S ENQUIRE PILL. Dylan, 16
   October 2026: "the "enquire now" button within that is the same #D97195
   background with white text chip that is used elsewhere as a button." It
   first shipped with the navy masthead button's type (the serif at 600, 15px);
   he saw that and said "it doesn't have the enquire now text in the same
   font/weight/size as the other enquire buttons". So every declaration here is
   .wh-enq's (the header corner's Enquire): 44px tall, 0 20px, the menu row's
   face falling back to the body face (as the row's other items do), 13px, 600,
   line-height 1, the Buttons card's fill, ink and hover. ⚠ Only the service
   hero carries --chip: .svp-enq is also the related-insights "more" link,
   which keeps its words-and-arrow look. padding-bottom is !important because
   .svp-enq zeroes it with !important against the site's link underline; here
   the height does the work, so 0 is right and the rule only restates it. */
.svp-enq.svp-enq--chip{gap:0;height:44px;padding:0 20px!important;border-radius:999px;
  background:var(--btn-fill,var(--pink));color:var(--btn-ink,#fff);line-height:1;white-space:nowrap;
  transition:background-color var(--btn-ms,.18s) ease,color var(--btn-ms,.18s) ease;}
.svp-enq--chip .t{font-family:var(--mn-row-face,var(--body));font-style:normal;text-transform:none;
  font-size:13px;font-weight:600;letter-spacing:0;line-height:1;color:inherit;}
.svp--navy .svp-enq.svp-enq--chip{color:var(--btn-ink,#fff);}
.svp-enq.svp-enq--chip:hover{background:var(--pink-deep);color:var(--btn-ink,#fff);}
.svp-enq.svp-enq--chip:hover{background:color-mix(in srgb,var(--btn-fill,var(--pink)) 86%,#000);}

/* ── 02 · introduction ──────────────────────────────────────────────────── */
.svp-intro-grid{display:grid;gap:clamp(28px,5vw,72px);}
.svp-intro--rail .svp-intro-grid{grid-template-columns:1.5fr .8fr;}
.svp-intro--portrait .svp-intro-grid{grid-template-columns:1.25fr .85fr;align-items:center;}

.svp-essay--quote{position:relative;align-self:stretch;min-width:0;}
.svp-intro-main{display:flow-root;}
.svp-intro--portrait .svp-essay--quote{display:flex;flex-direction:column;justify-content:center;}
.svp-intro .svp-intro-quote{position:absolute;left:0;right:0;bottom:0;margin:0;max-width:none;--svp-intro-quote-gap:28px;}
.svp-intro-quote blockquote{margin:0;padding:0;border:0;line-height:1.5;text-align:left;text-wrap:balance;}
.svp-intro-quote[hidden]{display:block;visibility:hidden;pointer-events:none;}
.svp-intro-quote-by{display:flex;align-items:center;gap:10px;margin-top:14px;font-style:normal;font-size:12.5px;font-weight:600;letter-spacing:.06em;color:var(--muted);}
.svp-intro-quote-by::before{content:"";flex:0 0 22px;height:1px;background:var(--pink);}
.svp--navy .svp-intro-quote-by{color:rgba(255,255,255,.72);}


.cqt{--cqt-ink:var(--q-colour,var(--navy-ink));--cqt-who:var(--muted);--cqt-rule:var(--line);padding:37px 0;background:var(--paper);color:var(--cqt-ink);}
.cqt.svp--white{background:#fff;}
.cqt.svp--navy{--cqt-ink:#fff;--cqt-who:rgba(255,255,255,.72);--cqt-rule:rgba(255,255,255,.22);background:var(--navy-ink);}
.cqt>.wrap{width:calc(100% - 68px);max-width:1056px;margin:0 auto;padding:0;}
.cqt-figure{margin:0;min-height:126px;display:flex;justify-content:center;flex-direction:column;}
.cqt .cqt-quote{font-family:var(--q-face,var(--serif));font-weight:var(--q-weight,400);font-style:var(--q-style,normal);font-size:calc(var(--q-scale,1) * 24px);line-height:1.4;letter-spacing:var(--q-track,-.042em);color:var(--cqt-ink);text-transform:var(--q-case,none);margin:0;border:0;padding:0;text-wrap:balance;overflow-wrap:anywhere;-webkit-hyphens:none;hyphens:none;}
.cqt .cqt-who{font-family:var(--body);font-size:15px;line-height:1.4;letter-spacing:var(--tracking-body);color:var(--cqt-who);overflow-wrap:anywhere;}
.cqt-name{font-weight:600;color:var(--cqt-ink);}
.cqt-sep{white-space:pre;}
.cqt-detail{font-weight:400;}
.cqt--quiet-centre{text-align:center;}
.cqt--quiet-centre .cqt-quote{max-width:890px;margin:0 auto;}
.cqt--quiet-centre .cqt-who{margin-top:22px;}
.cqt--signature .cqt-figure{display:grid;grid-template-columns:minmax(0,1fr) 214px;gap:40px;align-items:center;}
.cqt--signature .cqt-quote{font-size:calc(var(--q-scale,1) * 23px);line-height:1.42;text-align:left;}
.cqt--signature .cqt-who{border-left:1px solid var(--cqt-rule);padding-left:28px;display:flex;gap:13px;align-items:center;min-height:66px;min-width:0;text-align:left;}
.cqt-roundel{display:flex;flex:0 0 26px;line-height:0;color:var(--pink);}
.cqt-roundel .eyecrest{width:26px;height:26px;}
.cqt--signature .cqt-attribution{display:flex;flex-direction:column;gap:2px;min-width:0;overflow-wrap:anywhere;}
.cqt--signature .cqt-sep{display:none;}
.cqt--signature.cqt--anonymous .cqt-figure{grid-template-columns:minmax(0,1fr);}
@media(max-width:1199px){.cqt--signature .cqt-figure{gap:30px;grid-template-columns:minmax(0,1fr) 190px;}}
@media(max-width:700px){
  .cqt{padding:29px 0;}
  .cqt>.wrap{width:calc(100% - 44px);}
  .cqt-figure{min-height:0;}
  .cqt .cqt-quote{font-size:calc(var(--q-scale,1) * 21px);line-height:1.42;}
  .cqt--quiet-centre .cqt-who{margin-top:19px;}
  .cqt--signature .cqt-figure{display:block;}
  .cqt--signature .cqt-who{border-left:0;border-top:1px solid var(--cqt-rule);padding:18px 0 0;margin-top:21px;min-height:0;gap:11px;}
  .cqt--signature .cqt-attribution{display:block;}
  .cqt--signature .cqt-sep{display:inline;}
  .cqt .cqt-who{font-size:14.5px;}
}

.svp-intro .svp-body,.svp-incl .svp-body{font-size:16.5px;line-height:1.55;margin-top:20px;}
.svp-intro--portrait .svp-body{max-width:58ch;}
/* THE DROP CAP. Floated, with a line-height under 1 and a small top nudge, so
   its cap height lines up with the first line of the paragraph rather than
   sitting above it. */
.svp-dcap{float:left;font-family:var(--serif);font-weight:600;font-size:72px;
  line-height:.76;color:var(--navy);padding:0 12px 0 0;margin-top:3px;}
/* THE INTRODUCTION'S DROP CAP IS THE NAVY MASTHEAD'S. Dylan, 17 October 2026:
   "the dropcap within the introduction block. It's not the Literata font like
   the hero: navy masthead block's dropcap is. Also, it extends too high above
   the top of the top line, and it doesn't extend to the bottom of the third
   line. Also ensure that when you make it bigger ... there is still enough
   padding/space to the right of it". So it takes the masthead initial's face
   (the sub-heading serif, Literata on the live site) and its arithmetic
   (heroDesigns.ts, .hm-t-initial): from the first line's tallest letters
   (Ysabeau's ascenders, .70em above the baseline) to the third line's
   baseline, two 1.55em lines lower, over Literata's cap height (.69, measured
   on the F) is 5.51em, 91px at this paragraph's 16.5px. -.158em lifts the cap
   top onto the first line's ascenders; -.1em off the foot ends the float above
   the fourth line, so that line runs full width. The 12px on its right is
   kept: measured in Chromium at 1440 and 390 wide, it leaves the same room
   beside the bigger letter as beside the old one. ⚠ The numbers hold for
   Ysabeau and Literata at this 1.55 line height; change either and measure
   again.
   ⚠ CORRECTED THE SAME EVENING, with the masthead's initial (heroDesigns.ts
   says why). Dylan, from his iPhone: "Similar issue with the drop cap height".
   Safari drew the A about a pixel above the first line's ascenders and a
   pixel below the third baseline. Now (2 × 1.55 + .62) / .69 = 5.39em, lifted
   -.166em: in Chromium the A's apex meets the ascenders and its feet stand a
   pixel above the baseline, where Safari's extra puts them on it. ⚠ Only the Introduction block: the standfirst's cap (.svp-sf-body, which has its own rule since 18 October under "02 · the standfirst")
   and the phone size below are the shared rule's, untouched. */
/* 22 October 2026: the real loaded Literata 600 C still stood 2.5–3px
   above Ysabeau Office's ascenders in WebKit. Its rounded ink was 65.5px tall,
   so moving it down alone put its foot below the third baseline. The smaller
   size and lower offset together align C within 0.5px at the top and 0.8px at
   the foot at 1440/390, DPR2. Flat A/F/W/L remain within 0.5px at the top and
   1.7px at the foot; rounded letters retain their optical overshoot. The 12px
   side padding and negative bottom margin keep line four clear. These are
   introduction measurements; the hero and standfirst retain their own rules. */
.svp-intro .svp-dcap{font-family:var(--h-sub-face,var(--serif));font-style:normal;letter-spacing:0;
  font-size:calc((2 * 1.55em + .62em) / .71);line-height:1;margin:-.14em 0 -.1em;}
.svp-rail{border-left:1px solid var(--line);padding-left:28px;}
.svp--navy .svp-rail{border-left-color:rgba(255,255,255,.22);}
.svp-rail-label{font-size:10.5px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--pink);margin-bottom:14px;}
/* ⚠ WAS A <p> WITH <br>s UNTIL 20 SEPTEMBER 2026, now a <ul> — Dylan asked
   for the facts to be bullet points. 'line-height:2' was doing the work of the
   spacing between facts when they were one paragraph; as list items they get
   real spacing instead, so the leading comes back to a readable 1.5 and the
   air moves to the gap between items where it belongs. The marker is the same
   pink square as the checklist blocks ('.svp-ck') rather than a disc, so the
   page has one bullet shape and not two. */
.svp-rail-facts{font-size:13.5px;line-height:1.5;color:var(--muted);-webkit-hyphens:none;hyphens:none;
  list-style:none;margin:0;padding:0;}
.svp-rail-facts li{position:relative;padding-left:16px;margin-bottom:11px;}
.svp-rail-facts li:last-child{margin-bottom:0;}
.svp-rail-facts li::before{content:"";position:absolute;left:0;top:.5em;
  width:6px;height:6px;background:var(--pink);border-radius:1px;}
.svp--navy .svp-rail-facts{color:rgba(255,255,255,.68);}
.svp-rail-img{aspect-ratio:1/1;margin-top:24px;}
.svp-portrait-img{aspect-ratio:4/4.6;}
.svp-caption{font-size:12px;letter-spacing:.02em;margin-top:10px;}

/* ── 03 · what's included ───────────────────────────────────────────────── */
.svp-incl-grid{display:grid;grid-template-columns:.9fr 1.1fr;gap:clamp(28px,5vw,72px);
  align-items:stretch;}
.svp-incl-img{min-height:440px;}
.svp-incl-body{display:flex;flex-direction:column;justify-content:center;}
/* The three service-section titles share the existing included-title size.
   Keep the related-services selector scoped: related insights uses its own size. */
.svp-intro h2,.svp-incl h2,.svp-rel .svp-rel-headings h2{font-size:clamp(24px,3vw,36px);margin-top:16px;}
/* ⚠ THE SERVICE HERO'S INTRODUCTION, WHICH USED TO BE A CHECKLIST.
   Dylan, 20 September 2026: "I don't want that to be bullet points - I want it
   to be a paragraph." Measure is capped at 58ch because the hero's text column
   is narrow already and prose set to the full column width reads as a wall;
   the checklist it replaced never needed a measure because each row was one
   short line. */
.svp-hero-lede{font-size:15.5px;line-height:1.6;color:var(--muted);max-width:58ch;
  margin-top:18px;-webkit-hyphens:none;hyphens:none;}
.svp-hero-lede+.svp-hero-lede{margin-top:12px;}
.svp--navy .svp-hero-lede{color:rgba(255,255,255,.78);}

.svp-cklist{margin-top:20px;}
/* The rule belongs to the row ABOVE it, and the last row drops it — so the list
   ends on words rather than on a line hanging under them. */
.svp-ckrow{display:flex;gap:12px;align-items:baseline;padding:13px 0;
  border-bottom:1px solid var(--line);}
.svp-ckrow:last-child{border-bottom:0;}
.svp-ck{width:7px;height:7px;flex:0 0 7px;background:var(--pink);border-radius:1px;
  position:relative;top:-1px;}
.svp-ckt{font-size:14.5px;color:var(--ink);line-height:1.45;}
.svp--navy .svp-ckt{color:rgba(255,255,255,.85);}

/* ── 04 · how we work ───────────────────────────────────────────────────── */
.svp-process-head{margin-bottom:28px;}
.svp-process h2{font-size:clamp(24px,3vw,36px);margin-top:16px;}
.svp-acts{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,2.4vw,30px);}
.svp-act-img{height:240px;}
.svp-act-row{display:flex;gap:12px;align-items:baseline;margin-top:15px;}
.svp-num{font-family:var(--serif);font-weight:700;font-size:12.5px;color:var(--pink);
  white-space:nowrap;}
.svp-act-body h3{font-size:17px;line-height:1.25;}
.svp-act-line{font-size:13px;margin-top:5px;line-height:1.5;}

/* ── 05 · heritage & numbers ────────────────────────────────────────────── */
/* The one service section that is NOT `svp-room`: the photograph is the ground,
   so the height comes from a min-height and flex centring rather than from
   vertical padding, and the section itself must stay transparent or the flat
   ground colour would cover the picture. */
.svp-stats{background:transparent;position:relative;min-height:min(520px,84vh);
  display:flex;align-items:center;overflow:hidden;}
.svp-stats-img{position:absolute;inset:0;border-radius:0;}
.svp-wash{position:absolute;inset:0;}
.svp--navy .svp-wash{background:linear-gradient(180deg,rgba(20,40,63,.55),rgba(20,40,63,.88));}
.svp--paper .svp-wash{background:rgba(251,247,247,.88);}
.svp--white .svp-wash{background:rgba(255,255,255,.9);}
.svp-stats-in{position:relative;max-width:900px;width:100%;}
.svp-sentence{font-family:var(--serif);font-weight:400;color:var(--heading-deep,var(--navy-ink));
  font-size:clamp(22px,2.8vw,34px);line-height:1.35;letter-spacing:-.01em;margin-top:20px;}
/* The figures are woven into the sentence, marked up as EM by the author. No
   angle brackets in this comment: it ships inside a style element, and a
   literal tag in a comment is exactly what broke a tag-counting test once. */
.svp-sentence em{font-style:normal;font-weight:600;color:var(--pink);}

/* ── 06 · people ────────────────────────────────────────────────────────── */
.svp-people-grid{display:grid;gap:clamp(28px,5vw,72px);align-items:center;}
.svp-people--portrait .svp-people-grid{grid-template-columns:.8fr 1.2fr;}
.svp-people--length .svp-people-grid{grid-template-columns:1.25fr .75fr;}
.svp-people-photo{aspect-ratio:4/4.8;}
.svp-cred{border-left:2px solid var(--pink);padding:10px 0 2px 14px;margin-top:14px;}
.svp-cred h3{font-size:16px;}
.svp-cred-role{font-size:12.5px;margin-top:3px;line-height:1.5;color:var(--muted);}
.svp--navy .svp-cred-role{color:rgba(255,255,255,.68);}
.svp-voice{font-family:var(--serif);font-weight:400;color:var(--heading-deep,var(--navy-ink));
  font-size:clamp(18px,2.2vw,26px);line-height:1.4;letter-spacing:-.01em;
  margin-top:20px;max-width:40ch;-webkit-hyphens:none;hyphens:none;}
.svp-support{font-size:13.5px;margin-top:18px;max-width:48ch;line-height:1.55;}
.svp-voice-long{font-family:var(--serif);font-size:16px;line-height:1.6;margin-top:20px;
  max-width:56ch;color:var(--ink);}
.svp-who{font-size:13.5px;color:var(--muted);letter-spacing:.02em;margin-top:16px;}
.svp-who strong{font-weight:600;color:var(--navy);}
.svp--navy .svp-who{color:rgba(255,255,255,.66);}
.svp--navy .svp-who strong{color:#fff;}
/* The portrait is deliberately wider than its column and runs off the right
   edge; the section clips it. */
.svp-ringed{position:relative;width:clamp(280px,34vw,420px);justify-self:end;
  margin-right:clamp(-140px,-10vw,-48px);}
.svp-ringed-img{display:block;aspect-ratio:1/1;border-radius:50%;}
.svp-ring{position:absolute;inset:7px;border-radius:50%;
  border:1.5px solid rgba(217,113,149,.75);}
.svp-people-head{margin-bottom:26px;}
.svp-people--cards h2{font-size:clamp(24px,3vw,36px);margin-top:16px;}
.svp-profiles{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(16px,2.4vw,28px);}
.svp-profile{background:var(--white);border-radius:2px;overflow:hidden;
  box-shadow:0 14px 24px -18px rgba(20,40,63,.3);}
.svp-profile-img{height:190px;border-radius:0;}
.svp-profile-body{padding:18px 20px 20px;}
.svp-profile-body h3{font-size:16.5px;}
.svp-profile-cred{font-size:12.5px;color:var(--muted);margin-top:3px;line-height:1.45;}
.svp-profile-note{font-size:12.5px;color:var(--ink);margin-top:10px;line-height:1.5;
  border-top:1px solid var(--line);padding-top:10px;}
/* The cards keep their white ground on every section ground, so their own ink
   must NOT follow the navy flip. */
.svp--navy .svp-profile h3{color:var(--heading);}
.svp--navy .svp-profile-cred{color:var(--muted);}
.svp--navy .svp-profile-note{color:var(--ink);border-top-color:var(--line);}
.svp-people-foot{font-size:12.5px;margin-top:16px;}

/* ── 07 · the address book ──────────────────────────────────────────────── */
.svp-ab-in{max-width:880px;}
.svp-ab h2{font-size:clamp(24px,3.2vw,38px);margin-top:16px;max-width:20ch;}
.svp-ab-body{font-size:14.5px;margin-top:20px;max-width:60ch;line-height:1.6;}
.svp-ab-hr{margin-top:34px;}
.svp-ab-foot{display:flex;gap:22px;padding-top:14px;flex-wrap:wrap;
  font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;}

/* ── 08 · related services & reading ────────────────────────────────────── */
.svp-rel-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;
  flex-wrap:wrap;margin-bottom:26px;}
.svp-rel h2{font-size:clamp(24px,3.2vw,38px);margin-top:14px;}
.svp-all{display:inline-flex;align-items:center;gap:12px;font-weight:600;font-size:14px;
  color:var(--navy);text-decoration:none;letter-spacing:-.01em;
  background-image:none!important;padding-bottom:0!important;}
.svp-all .arwbtn{width:36px;height:36px;font-size:15px;
  border-color:rgba(20,40,63,.3);background:transparent;color:var(--navy);}
.svp-all .arwbtn .gwarr{width:15px;height:15px;}
.svp-all:hover .arwbtn{border-color:var(--pink);background:var(--pink);color:#fff;}
/* ⚠ THE WORDS GET THE SITE'S HOVER UNDERLINE; THE ANCHOR CANNOT.
   Dylan, 4 October 2026, of the FAQ block's "Something we have not answered?
   Ask us": "it doesn't have the same underline effect as elsewhere in the
   published marketing website."

   .svp-all turns the base anchor sweep off with two !important declarations,
   and that is correct for the anchor: it is display:inline-flex and contains
   the round arrow button, so a line under IT would run under the arrow too.
   The sweep therefore goes on the .ul span around the label, which is exactly
   what a.svp-card:hover .ul does two thousand lines up.

   ⚠ THIS FIXES THREE LINKS, NOT ONE. svp-all is emitted by the FAQ ask link,
   featuredInsights and whoWritesThis, and none of the three had the underline
   — a finding names an example, not a site. Two of them were already emitting
   the .ul span with nothing to drive it, so the markup had been half right for
   some time and looked finished.

   ⚠ AND NO BACKTICKS IN THIS COMMENT, because the whole stylesheet is one
   template literal: a backtick here ends it, and the first draft of this note
   broke the build in three places at once.

   Inside the hover media query like every other sweep here, so a touch device
   does not paint it on tap and leave it stuck. */
@media (hover:hover){.svp-all:hover .ul{background-size:100% 2px;--ul-c:var(--pink);transition:var(--ul-in);}}
.svp--navy .svp-all{color:#fff;}
.svp--navy .svp-all .arwbtn{border-color:rgba(255,255,255,.4);color:#fff;}
.svp-rel-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(14px,2vw,24px);}
.svp-rel-grid>*{min-height:400px;}
/* Plates are sized by their photographs' aspect ratio, not by the row. */
.svp-rel-grid--plates>*{min-height:0;}
.svp-card{position:relative;overflow:hidden;border-radius:2px;display:flex;
  flex-direction:column;justify-content:flex-end;color:#fff;text-decoration:none;
  background-image:none!important;padding-bottom:0!important;}
.svp-card-img{position:absolute;inset:0;border-radius:0;}
.svp-card-scrim{position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(15,31,56,0) 34%,rgba(15,31,56,.72) 100%);}
.svp-card-in{position:relative;z-index:2;padding:22px;}
.svp-card-tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:#f2b8cd;margin-bottom:8px;}
.svp-card-t{display:block;font-family:var(--serif);font-weight:600;font-size:18px;
  line-height:1.2;letter-spacing:0;color:#fff;}
.svp-plate-img{aspect-ratio:4/3;}
.svp-plate-cap{font-size:12.5px;margin-top:10px;line-height:1.5;}
.svp-insight{background:var(--white);border:1px solid var(--line);border-radius:2px;
  display:flex;flex-direction:column;justify-content:space-between;padding:22px;
  box-shadow:0 14px 24px -18px rgba(20,40,63,.3);text-decoration:none;color:inherit;
  background-image:none!important;padding-bottom:22px!important;}
.svp-insight-tag{display:inline-block;font-size:10.5px;font-weight:700;letter-spacing:.16em;
  text-transform:uppercase;color:var(--pink);}
.svp-insight h3{font-size:19px;margin-top:12px;line-height:1.35;}
.svp-insight-lede{font-size:13px;margin-top:10px;line-height:1.5;}
.svp-insight-foot{display:flex;justify-content:space-between;align-items:center;
  border-top:1px solid var(--line);padding-top:12px;margin-top:18px;}
.svp-insight-meta{font-size:12px;color:var(--muted);}
.svp-insight .arwbtn{width:36px;height:36px;font-size:15px;
  border-color:rgba(20,40,63,.3);background:transparent;color:var(--navy);}
.svp-insight .arwbtn .gwarr{width:15px;height:15px;}
.svp-insight:hover .arwbtn{border-color:var(--pink);background:var(--pink);color:#fff;}
/* The insight card keeps its white ground on navy, so its ink stays as it is. */
.svp--navy .svp-insight h3{color:var(--heading);}
.svp--navy .svp-insight .svp-insight-lede{color:var(--muted);}

/* ── 09 · testimonials, one at a time ───────────────────────────────────── */
.svp-car{min-height:min(540px,80vh);display:flex;align-items:center;
  padding:clamp(38px,4.5vw,66px) 0;}
.svp-car-grid{display:grid;grid-template-columns:auto 1fr auto;
  gap:clamp(18px,3vw,48px);align-items:center;width:100%;}
/* A single quote has no arrows, so the stage takes the whole row. */
.svp-car--one{grid-template-columns:1fr;}
.svp-car-stage{text-align:center;min-width:0;}
/* Keep every quote in one grid cell: the tallest reserves the space beneath
   the mark, including after a font load or a resize. Hidden slides still size
   the grid, but visibility, aria-hidden and inert keep them unreadable and
   unfocusable. No cloned quotations or stale measured pixel heights. */
.svp-slides{display:grid;}
/* CENTRED, not start (Dylan, 24 October 2026): every quote, its mark and its
   name sit in the middle of the space the tallest one reserves, so a short
   quote no longer hangs from the top. The dots stay put below. */
.svp-slides>.svp-slide{grid-area:1/1;min-width:0;align-self:center;}
.svp-slides>.svp-slide[hidden]{display:block;visibility:hidden;pointer-events:none;}
.svp-car{--svp-car-out:160ms;--svp-car-in:520ms;--svp-car-stagger:80ms;}
@keyframes svp-car-enter{from{opacity:0;transform:translateY(18px);}to{opacity:1;transform:none;}}
@keyframes svp-car-leave{from{opacity:1;transform:none;}to{opacity:0;transform:translateY(-10px);}}
.svp-car-in .svp-car-mark,.svp-car-in .svp-slide:not([hidden]) blockquote,.svp-car-in .svp-slide:not([hidden]) .svp-who{
  animation:svp-car-enter var(--svp-car-in) cubic-bezier(.22,1,.36,1) both;}
.svp-car-in .svp-slide:not([hidden]) blockquote{animation-delay:var(--svp-car-stagger);}
.svp-car-in .svp-slide:not([hidden]) .svp-who{animation-delay:calc(var(--svp-car-stagger) * 2);}
.svp-car-out .svp-car-mark,.svp-car-out .svp-slide:not([hidden]) blockquote,.svp-car-out .svp-slide:not([hidden]) .svp-who{
  animation:svp-car-leave var(--svp-car-out) ease both;}
@media(prefers-reduced-motion:reduce){
  .svp-car-in .svp-car-mark,.svp-car-in .svp-slide:not([hidden]) blockquote,.svp-car-in .svp-slide:not([hidden]) .svp-who,
  .svp-car-out .svp-car-mark,.svp-car-out .svp-slide:not([hidden]) blockquote,.svp-car-out .svp-slide:not([hidden]) .svp-who{animation:none;transform:none;opacity:1;}
}
/* NAVY MARK AND DOTS (Dylan, 24 October 2026): "make it so that the " mark at
   the top and the dots themselves are navy, whilst the buttons to the left and
   the right are still #D97195". The navy is the quote's own ink, so the mark,
   the words and the dot read as one colour. The navy ground keeps its pink
   (navy on navy would vanish). */
.svp-car-mark{display:block;font-family:var(--q-face,var(--serif));font-weight:700;color:var(--q-colour,var(--heading-deep,var(--navy-ink)));
  font-size:60px;line-height:.72;margin-bottom:-4px;}
/* Same variables the site's other quote surface reads, so the Brand styles
   quote settings govern this one too rather than it having a look of its own. */
.svp-car blockquote{font-family:var(--q-face,var(--serif));font-style:var(--q-style,normal);
  font-weight:var(--q-weight,400);
  font-size:calc(var(--q-scale,1) * clamp(21px,2.7vw,31px));line-height:1.35;
  letter-spacing:var(--q-track,-.01em);text-transform:var(--q-case,none);
  color:var(--q-colour,var(--heading-deep,var(--navy-ink)));
  max-width:40ch;margin:0 auto;-webkit-hyphens:none;hyphens:none;text-wrap:balance;}
.svp--navy .svp-car blockquote{color:#fff;}
.svp--navy .svp-car-mark{color:var(--pink);}
.svp-car .svp-who{margin-top:16px;}
/* THE GAP AND THE BORDER ARE ONE NUMBER BETWEEN THEM. What the eye sees
   between two dots is gap + the two transparent borders, and what a finger has
   to hit is the border box. Trading one against the other therefore enlarges
   the target without moving anything: 2 + 10 + 10 is the same 22px of visible
   space that 7 + 7.5 + 7.5 was, and the centres stay 29px apart, but the box
   grows from 22px square to 27px. Whole pixels on purpose — half a pixel of
   border rounds, and the measured box came out a pixel short of its sum.
   MEASURED at 21x21 in Chromium — under WCAG 2.2 AA's 24px floor. 29px is the
   ceiling here: a larger target than the pitch would overlap its neighbour,
   and a target that catches the wrong dot is worse than a small one. */
.svp-dots{display:flex;gap:2px;justify-content:center;margin-top:24px;}
.svp-dot{width:7px;height:7px;border-radius:50%;padding:0;border:0;cursor:pointer;
  background:transparent;box-sizing:content-box;position:relative;
  border:10px solid transparent;background-clip:padding-box;}
/* THE DOTS GROW AND SHRINK (Dylan, 24 October 2026): "when you change
   testimonial, the old testimonial dot shrinks and changes colour and the new
   one grows and changes colour". The mark is a circle centred on the button,
   so it can grow without moving anything: 7px at rest, and at the full 27px
   the current dot has always shown (the old background shorthand painted the
   whole button, N607; this draws that size on purpose). Its size and colour
   ease over the quote's own entrance (--svp-car-in, same curve), and the swap
   happens as the new quote starts to rise, so the two move together. */
.svp-dot::before{content:"";position:absolute;left:50%;top:50%;width:7px;height:7px;
  border-radius:50%;transform:translate(-50%,-50%);background-color:rgba(20,40,63,.18);
  transition:width var(--svp-car-in,520ms) cubic-bezier(.22,1,.36,1),height var(--svp-car-in,520ms) cubic-bezier(.22,1,.36,1),background-color var(--svp-car-in,520ms) ease;}
.svp-dot.on::before{width:27px;height:27px;background-color:var(--q-colour,var(--heading-deep,var(--navy-ink)));}
.svp--navy .svp-dot::before{background-color:rgba(255,255,255,.25);}
.svp--navy .svp-dot.on::before{background-color:var(--pink);}
@media(prefers-reduced-motion:reduce){.svp-dot::before{transition:none;}}
/* All three grounds share the header pill's filled controls and hover. */
.svp-car .arwbtn{cursor:pointer;padding:0;border:0;
  background:var(--btn-fill,var(--pink));color:#fff;
  transition:background-color var(--btn-ms,.18s) ease;}
.svp-car .arwbtn:hover{border:0;
  background:var(--pink-deep);color:#fff;}
.svp-car .arwbtn:hover{
  background:color-mix(in srgb,var(--btn-fill,var(--pink)) 86%,#000);}
.svp-car .arwbtn:focus-visible{outline:2px solid var(--navy-ink);outline-offset:3px;}
.svp-car.svp--navy .arwbtn:focus-visible,.svp--navy .svp-car .arwbtn:focus-visible{outline-color:#fff;}
/* The arrow glyph points one way; the previous button is the same glyph turned
   round, so there is one SVG in the engine rather than two. */
.svp-car-prev .gwarr{transform:scaleX(-1);}

/* ── 09b · student voices, on a curved horizon ──────────────────────────────
   Circles standing on a hill: the voice being read is whole and above the
   line, its neighbours half sunk behind it, and advancing walks the whole
   company sideways along the arc.

   THE GROUND IS A VARIABLE, AND IT HAS TO BE. The horizon is not a drawn
   curve — it is a shape painted in the SECTION'S OWN COLOUR laid over the row
   of circles, so the part of a face below the line simply is not there. That
   works only while the shape and the section agree on what colour the ground
   is, so the three .svp grounds each set --stuv-ground and the horizon reads
   it. Hard-coding var(--paper) renders correctly on paper and puts a pale
   rectangle across the middle of the navy one. */
/* --stuv-travel / --stuv-out / --stuv-in are the WORDS' swap, not the hill's;
   the rules that consume them, and why they are separate from --stuv-ms, are
   written out above .stuv-slidebox.stuv-stacked. Declared here so one section
   can be retimed without reaching into the transition shorthand. */
.stuv{--stuv-ground:var(--paper);--stuv-ring:rgb(217 113 149 / .85);
  --stuv-travel:44px;--stuv-out:340ms;--stuv-in:420ms;
  padding:clamp(52px,6vw,86px) 0 clamp(40px,4.6vw,64px);}
/* The ring inside the circles, per block. White reads over a dark photograph
   and over the navy ground where the pink can disappear into it. It moves the
   RING ONLY — the horizon hairline and the arrows are the section's, not the
   portrait's, and a setting that quietly moved three things would be a setting
   nobody could predict. */
.stuv--ring-white{--stuv-ring:rgb(255 255 255 / .92);}
.svp--white.stuv{--stuv-ground:var(--white);}
.svp--navy.stuv{--stuv-ground:var(--navy-ink);}
.stuv-head{display:flex;flex-direction:column;align-items:center;text-align:center;}
.stuv-head .eyecrest{margin-left:auto;margin-right:auto;}
.stuv h2{font-size:calc(var(--h-section-scale,1) * clamp(26px,3.4vw,44px));text-wrap:balance;}

/* --stuv-w is a seat's width and every other measurement here is a multiple of
   it, so the hill scales as one thing. --stuv-gap is how far along the hill a
   neighbour stands, as a multiple of that width; the arrows are positioned
   from the same number, which is what keeps them in the space between the
   circles at every size rather than at one size somebody checked. */
.stuv-stage{--stuv-w:clamp(190px,21vw,300px);--stuv-gap:1.12;--stuv-arw:46px;
  position:relative;height:clamp(250px,22.5vw,306px);margin-top:clamp(30px,3.4vw,46px);
  isolation:isolate;overflow:hidden;}
/* ⚠ overflow:hidden IS LOAD-BEARING. The horizon is a 400px shape inside a
   306px stage; unclipped it hangs a quarter of a metre below and paints the
   section's own ground over the quote, the caption and the dots. The bug
   renders as a perfect crop above a blank space, which reads as "the script
   did not run" rather than as an overflow. */
.stuv-seat{--stuv-w:clamp(190px,21vw,300px);--stuv-gap:1.12;
  position:absolute;left:50%;top:30px;width:var(--stuv-w);
  margin-left:calc(var(--stuv-w) / -2);
  transform:translate3d(var(--x,0),var(--y,0),0) scale(var(--s,1));
  opacity:var(--o,1);z-index:var(--z,1);
  transition:transform var(--stuv-ms,680ms) cubic-bezier(.22,1,.36,1),
             opacity var(--stuv-ms,680ms) cubic-bezier(.22,1,.36,1);
  will-change:transform;margin-bottom:0;}
/* Every seat starts 30px down because the active one rises 26px, and a
   clipping stage would otherwise shave its crown. */
.stuv-seat[data-rel="0"] {--x:0;                                       --y:-26px; --s:1;   --o:1;   --z:3;}
.stuv-seat[data-rel="-1"]{--x:calc(var(--stuv-w) * var(--stuv-gap) * -1);    --y:58px;  --s:.6;  --o:1;   --z:2;}
.stuv-seat[data-rel="1"] {--x:calc(var(--stuv-w) * var(--stuv-gap));         --y:58px;  --s:.6;  --o:1;   --z:2;}
.stuv-seat[data-rel="-2"]{--x:calc(var(--stuv-w) * var(--stuv-gap) * -1.65); --y:128px; --s:.42; --o:.62; --z:1;}
.stuv-seat[data-rel="2"] {--x:calc(var(--stuv-w) * var(--stuv-gap) *  1.65); --y:128px; --s:.42; --o:.62; --z:1;}
/* Parked rather than removed, so a seat never pops into being at the edge of
   the frame — it is already there, small and low, and walks up the hill. */
.stuv-seat[data-rel="off"]{--x:0;--y:150px;--s:.34;--o:0;--z:0;pointer-events:none;}

.stuv-disc{position:relative;width:100%;aspect-ratio:1/1;border-radius:50%;overflow:hidden;
  background:var(--navy-deep);box-shadow:0 26px 48px -26px rgba(20,38,63,.55);}
.stuv-disc .stuv-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
/* ⚠ THE PORTRAIT IS AN <img>, AND IT NEEDS THIS RULE TO EXIST. svpPh emits a
   background-image div normally and an <img class="svp-ph-img"> when it is
   asked for a lazy one — and the ONLY .svp-ph-img rule in this stylesheet is
   scoped to .svp-orb. Outside that block the image has no sizing at all and
   renders at its intrinsic size, spilling out of the circle that is supposed
   to clip it. Found by rendering the block rather than by reading it. */
.stuv-disc .stuv-ph .svp-ph-img{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;
  width:100%;height:100%;object-fit:cover;object-position:center;display:block;}
/* THE RING, the same one every other circle on this site wears: a 1.5px
   hairline inset 4%, drawn as an inset box-shadow on a pseudo element exactly
   as in .svp-orb .svp-disc::after and .insights.ins-thumb--circle .it-thumb.
   A portrait here and a place on the Approach diagram are the same object. */
.stuv-disc::after{content:"";position:absolute;top:4%;left:4%;right:4%;bottom:4%;inset:4%;
  border-radius:50%;z-index:2;pointer-events:none;
  box-shadow:inset 0 0 0 1.5px var(--stuv-ring);
  transition:box-shadow var(--stuv-ms,680ms) cubic-bezier(.22,1,.36,1);}
/* A voice that is not being read has a quieter ring — still a circle in the
   brand's language, just not the one speaking. */
.stuv-seat:not([data-rel="0"]) .stuv-disc::after{box-shadow:inset 0 0 0 1.5px rgb(20 40 63 / .22);}
.svp--navy .stuv-seat:not([data-rel="0"]) .stuv-disc::after{box-shadow:inset 0 0 0 1.5px rgb(255 255 255 / .28);}

/* THE HORIZON. Painted in the section's own ground so the join is invisible,
   with a hairline top edge in the ring colour so the curve reads as a drawn
   line rather than as an accident of the crop. The elliptical radius is very
   shallow on purpose: it should read as the brow of a hill, not as a circle. */
.stuv-horizon{position:absolute;left:50%;transform:translateX(-50%);
  top:clamp(216px,19.5vw,272px);width:190%;height:400px;z-index:4;pointer-events:none;
  background:var(--stuv-ground);border-radius:100% 100% 0 0 / 34% 34% 0 0;
  border-top:1px solid rgb(217 113 149 / .3);}
.svp--navy .stuv-horizon{border-top-color:rgb(217 113 149 / .42);}

/* ⚠ THE ARROWS SIT IN THE GAP, AND THE GAP IS ARITHMETIC. The centre disc's
   edge is at 0.5w from the middle. A neighbour's centre is at --stuv-gap x w
   and it is scaled to 0.6, so its inner edge is at (gap - 0.3)w. The space
   between them runs from 0.5w to (gap - 0.3)w and an arrow belongs at the
   midpoint of that, w(0.2 + gap)/2 — 0.66w at gap 1.12, in a space 0.32w
   wide, which takes a 46px button with 25px of air each side. Expressed as
   the flex gap between the pair (centre-to-centre 1.32w, less the button) so
   there is one expression and they cannot drift apart. */
.stuv-arws{position:absolute;left:0;right:0;top:clamp(180px,15.6vw,226px);z-index:6;
  display:flex;justify-content:center;
  gap:calc(var(--stuv-w,300px) * 1.32 - var(--stuv-arw,46px));pointer-events:none;}
/* Pink fill, white mark, no outline. The border is removed outright rather
   than made transparent and the 1.5px given back as padding — .arwbtn draws a
   1.5px border, and simply recolouring it would leave the ring; dropping it
   without the padding grows the drawn arrow by 3px and moves the button's
   centre, which is visible against the circles either side. */
.stuv-arws .arwbtn{pointer-events:auto;background:var(--pink);color:#fff;
  border:0;padding:1.5px;width:var(--stuv-arw,46px);height:var(--stuv-arw,46px);
  cursor:pointer;box-shadow:0 10px 22px -14px rgba(20,38,63,.45);}
.stuv-arws .arwbtn:hover{background:var(--pink-deep);}
.stuv-arws .arwbtn:focus-visible{outline:2px solid var(--navy-ink);outline-offset:3px;}
.svp--navy .stuv-arws .arwbtn:focus-visible{outline-color:#fff;}
.stuv-prev .gwarr{transform:scaleX(-1);}

.stuv-words{margin-top:clamp(18px,2vw,26px);text-align:center;}
/* The opening mark, in --q-face rather than --serif, like .svp-car-mark since
   29 October 2026 (N608): on --serif it was a sans quotation mark in front of
   a serif quote wherever the quote face had been moved. */
.stuv-mark{display:block;font-family:var(--q-face,var(--serif));font-weight:600;
  color:rgb(217 113 149 / .5);font-size:54px;line-height:.5;margin-bottom:10px;}
.stuv-slidebox{padding-bottom:6px;}
/* ⚠ THE SIX QUOTES ARE STACKED IN ONE GRID CELL, AND THAT IS WHAT KEEPS THE
   DOTS STILL. Swapping one quote for another of a different length changes the
   height of the box holding it, so the dots — and the bottom of the whole
   section — jump as the reader moves through. Reserving "the tallest quote" as
   a number would need the text measured, which cannot be done at compile time
   and would be wrong the moment somebody edits a quote. Putting every figure
   in the SAME grid cell asks the browser instead: a track is as tall as its
   tallest item, so the box is permanently the height of the longest quote
   whichever one is showing, and it re-derives itself when the words change.
   The hidden ones are hidden with VISIBILITY, not display, because display:none
   takes them out of the track and the height collapses again — which is the
   bug this exists to fix.
   The class is added by the runtime, so without JavaScript the markup's own
   'hidden' attributes still show the first quote alone.
   ⚠ SINGLE QUOTES, NOT BACKTICKS, in this comment and every other one in this
   file — the whole stylesheet is one template literal, so a backtick ends it.
   This comment originally used them and did exactly that; the error points at
   the CSS on the next line rather than at the quote, which is why the file's
   own warning says it has already cost two edits. Three now. */
/* ⚠ ONE QUOTE LEAVES BEFORE THE NEXT ARRIVES, 26 September 2026, at Dylan's
   request: a longer travel, and "the old one has to finish fading out before
   the new one can fade in".
   MEASURED FROM THE RULES THIS REPLACES, rather than described: travel 18px;
   the leaving quote faded over 408ms (--stuv-ms 680ms x .6) from 0ms, and the
   arriving one over the same 408ms from 122ms (x .18) — so the two were BOTH
   visible for 286ms. That is the cross-fade.
   Now: travel 44px, and the arriving quote's delay IS the leaving quote's
   duration, one variable in both places so they cannot drift back into an
   overlap. Out over --stuv-out, in over --stuv-in after it; 760ms in all.
   ⚠ --stuv-ms IS NOT REUSED HERE, and that is why there are two new variables.
   It times the HILL — seats walking sideways, the ring in each disc — and
   sharing it would weld "the words take longer" to "the company walks slower".
   ⚠ visibility, NOT display, and its delay is --stuv-out so the leaving quote
   drops out of the reading order the moment it is transparent. display:none
   stops a grid item sizing the track and the dots jump — see above. */
.stuv-slidebox.stuv-stacked{display:grid;}
.stuv-slidebox.stuv-stacked>.stuv-slide{grid-area:1/1;
  visibility:hidden;opacity:0;
  transform:translateY(calc(var(--stuv-dir,1) * var(--stuv-travel,44px)));
  transition:opacity var(--stuv-out,340ms) linear,
             transform var(--stuv-out,340ms) cubic-bezier(.4,0,1,1),
             visibility 0s linear var(--stuv-out,340ms);}
.stuv-slidebox.stuv-stacked>.stuv-slide.on{visibility:visible;opacity:1;transform:none;
  transition:opacity var(--stuv-in,420ms) linear var(--stuv-out,340ms),
             transform var(--stuv-in,420ms) cubic-bezier(.22,1,.36,1) var(--stuv-out,340ms),
             visibility 0s;}
.stuv-slide{margin:0;}
/* The same --q-* variables every other quote surface on the site reads, so
   Global -> Text styles governs this section too rather than it having a
   private look that drifts the first time that panel is touched. */
.stuv-q{font-family:var(--q-face,var(--serif));font-style:var(--q-style,normal);
  font-weight:var(--q-weight,400);
  font-size:calc(var(--q-scale,1) * clamp(19px,2.2vw,27px));line-height:1.38;
  letter-spacing:var(--q-track,-.01em);text-transform:var(--q-case,none);
  color:var(--q-colour,var(--heading-deep,var(--navy-ink)));
  max-width:42ch;margin:0 auto;-webkit-hyphens:none;hyphens:none;text-wrap:balance;}
.svp--navy .stuv-q{color:#fff;}
.stuv-who{font-size:14px;color:var(--muted);margin-top:14px;line-height:1.4;}
/* The name in the face this site gives every card title, which is what a name
   under a portrait is. */
.stuv-who strong{font-family:var(--h-sub-face,var(--serif));
  font-weight:var(--h-sub-weight,var(--wt-heading,600));
  letter-spacing:var(--h-sub-track,var(--tracking-heading,-.01em));
  color:var(--h-sub-colour,var(--navy));}
.svp--navy .stuv-who{color:rgba(255,255,255,.66);}
.svp--navy .stuv-who strong{color:#fff;}

/* The dots: a 7px mark inside a 27px target, the same trade .svp-dot makes.
   ⚠ background-COLOR, not the background shorthand — the shorthand resets
   background-clip to border-box and the mark then paints the whole 27px box.
   That is exactly the live defect registered as N607 on .svp-dot.on, and this
   block does not repeat it. */
.stuv-dots{display:flex;gap:2px;justify-content:center;margin-top:26px;}
.stuv-dot{width:7px;height:7px;border-radius:50%;padding:0;cursor:pointer;
  background-color:rgba(20,40,63,.18);box-sizing:content-box;
  border:10px solid transparent;background-clip:padding-box;
  transition:background-color .2s,transform .2s;}
.stuv-dot.on{background-color:var(--pink);transform:scale(1.25);}
.stuv-dot:focus-visible{outline:2px solid var(--pink);outline-offset:-6px;border-radius:50%;}
.svp--navy .stuv-dot{background-color:rgba(255,255,255,.25);}
.svp--navy .stuv-dot.on{background-color:var(--pink);}

@media(max-width:760px){
  /* ⚠ THE PROPORTIONS STAY AND THE HILL SHRINKS. An earlier draft pulled the
     neighbours in to 0.84w on small screens to stop the clip cutting flat
     edges down their sides — but 0.84 leaves about seven pixels between the
     circles, and the arrows live in that space. So the gap is unchanged, the
     discs get smaller, the buttons get smaller with them, and the STAGE RUNS
     TO THE SCREEN EDGES so a neighbour that overflows is cut by the edge of
     the phone. A circle disappearing off the side reads as "there are more";
     a circle with a flat edge two thirds of the way across reads as a fault. */
  .stuv-stage,.stuv-seat{--stuv-w:clamp(112px,30vw,150px);}
  .stuv-stage{--stuv-arw:38px;height:246px;
    margin-left:calc(-50vw + 50%);margin-right:calc(-50vw + 50%);}
  .stuv-horizon{top:206px;width:260%;}
  .stuv-arws{top:172px;}
  .stuv-seat[data-rel="-2"],.stuv-seat[data-rel="2"]{--o:0;--s:.3;}
}
.svp-ph>.svp-ph-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:center;display:block;border-radius:inherit;}

/* Reduced motion: every travel becomes a cut, and nothing is left invisible. */
@media(prefers-reduced-motion:reduce){
  .stuv-seat,.stuv-disc::after,.stuv-slidebox.stuv-stacked>.stuv-slide,
  .stuv-slidebox.stuv-stacked>.stuv-slide.on,.stuv-dot{
    transition-duration:1ms!important;transition-delay:0ms!important;}
}

/* ── 10 · questions we are asked ────────────────────────────────────────── */
/* One block, three designs: the ledger (svp-faq--ledger), answered by
   (svp-faq--answered) and the cards (svp-faq--cards). The heading is an h2
   like every service section; every question is an h3, styled per design
   below, so the outline is right whichever design a page chooses. */
.svp-faq-head{max-width:60ch;}
.svp-faq h2{font-size:clamp(24px,3vw,36px);margin-top:16px;}
.svp-faq-stand{font-size:14.5px;line-height:1.5;margin-top:18px;max-width:60ch;}
.svp-faq-entry{padding:16px 0;border-bottom:1px solid var(--line);}
.svp-faq-entry:last-child{border-bottom:0;}
.svp-faq-entry .svp-body{font-size:14.5px;line-height:1.5;text-align:left;}
.svp--navy .svp-faq-entry{border-color:rgba(255,255,255,.22);}
/* A · the ledger. The question is an h3 and the answer a p, both set INLINE so
   the question runs into its answer; the heading styles that BASE_CSS gives
   every h3 (size, tracking, case) are reset here so it reads as a run-in. */
.svp-faq-two{display:grid;grid-template-columns:1fr 1fr;gap:0 clamp(28px,5vw,64px);margin-top:22px;}
.svp-faq--ledger .svp-faq-entry h3{display:inline;font-size:14.5px;line-height:1.5;
  letter-spacing:0;text-transform:none;font-style:normal;}
.svp-faq--ledger .svp-faq-entry p{display:inline;}
/* The ask link: the related-services go-link, arrow filled pink like the hero's. */
.svp-faq-ask{margin-top:26px;}
/* The shared link's navy rule supplies a pale border colour. Removing the
   border itself keeps this filled circle plain on every ground, including
   the matching contact link in What's included. Keyboard focus stays on the
   link and retains its normal focus indicator. */
.svp-faq-ask .arwbtn{border:0;background:var(--pink);color:#fff;}
.svp-faq-ask:hover .arwbtn{border:0;background:var(--pink-deep);}
/* C · answered by. The portrait stays beside the list on a long page. */
.svp-faq-grid{display:grid;grid-template-columns:.85fr 1.25fr;gap:clamp(28px,5vw,72px);align-items:start;}
/* ⚠ NOT STICKY ANY MORE, 20 September 2026. Dylan: "in the LHS image of the
   FAQs/questions we are asked block, the image just stays fixed - it doesn't
   move as you move down the page/whatever effect it has currently."

   'position:sticky' is what that effect WAS: the portrait detached from the
   layout at 24px from the top and travelled down the viewport alongside the
   answers, which reads as the image moving. Removing it puts the photograph
   back in normal flow, where it sits still on the page and scrolls away with
   everything around it. 'align-items:start' on the grid above is kept — it is
   what stops the photograph stretching to the height of a long answer list. */
.svp-faq-aside{position:static;}
.svp-faq-photo{aspect-ratio:4/4.8;}
.svp-faq-aside .svp-who{margin-top:12px;}
.svp-faq-list{margin-top:12px;}
/* THE QUESTION IS AN H3 AND NOW LOOKS LIKE ONE.
   Dylan, 20 September 2026: *"in the questions we are asked block, please make
   it so that the questions themselves aren't in italics. Ensure also that they
   are in the same font as H3 (but what they are tagged as is up to you)."*

   It was already tagged as an h3 — the answer to his parenthesis is that
   nothing needed re-tagging — and then overrode almost everything the h3 rule
   gives it: the face, the slant, the weight, the tracking and the case. Those
   five overrides are gone, so the element takes the sub-heading tier exactly as
   every other h3 on the site does, and a site that later chooses a different
   sub-heading face gets it here too rather than being quietly excepted.

   What is kept is this block's own layout rather than its type: the 17px size
   and the 1.35 leading, which belong to the rhythm of a question-and-answer
   list, and the deeper ink and left alignment it has always had. Losing those
   would be re-designing the block, which is not what was asked.

   ⚠ The typographic quotation marks in the template are NOT part of this. They
   were not mentioned, and they are the one thing left that says the question is
   spoken; removing them would be a design decision nobody made. */
.svp-faq-spoken{font-size:17px;line-height:1.35;color:var(--heading-deep,var(--navy-ink));
  text-align:left;}
.svp-faq-spoken+.svp-body{margin-top:8px;}
.svp--navy .svp-faq-spoken{color:#fff;}
/* D · the cards. 3 × 2, white on every ground, the sixth card pink. */
.svp-faq-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:28px;}
.svp-faq-card{background:var(--white);border:1px solid var(--line);border-radius:2px;
  padding:22px 22px 20px;display:flex;flex-direction:column;gap:10px;text-decoration:none;
  color:inherit;background-image:none!important;padding-bottom:20px!important;}
.svp--navy .svp-faq-card{border-color:transparent;}
.svp-faq-card h3{font-size:16.5px;line-height:1.25;}
.svp-faq-card p{font-size:14px;line-height:1.5;color:var(--muted);text-align:left;margin:0;}
/* White cards keep their own ink on every ground, so the navy flip must not
   reach them — the same exception .svp-profile makes. */
.svp--navy .svp-faq-card h3{color:var(--heading);}
.svp--navy .svp-faq-card p{color:var(--muted);}
/* THE QUESTIONS ARE THE DROP CAPS' NAVY (Dylan, 18 October 2026): "With all
   the questions in the questions we are asked block ... please make the
   questions themselves the same dark navy as is used for the drop caps
   elsewhere." That is --navy-ink (#14283f on the live site), the Our Story
   standfirst's cap, not --navy (#284570), the masthead and Introduction caps'
   lighter blue. Before this the spoken and run-in questions took
   --heading-deep, which on a 'deep' heading tone is the navy mixed 30% toward
   black and read as black; the cards took the sub-heading colour.
   Every design: the ledger's run-in, the answered-by spoken question, and
   each card's title. Answers are untouched.
   ⚠ Off the navy ground only for the ledger and answered-by: Dylan approved
   brand pink for their questions on navy on 22 October, after the preview.
   The cards are white on every ground, so their
   questions are navy on every ground, except the pink ask card, which keeps
   its white. Scoped to .svp-faq because .svp-runin is shared with other blocks. */
.svp-faq:not(.svp--navy) .svp-runin,.svp-faq:not(.svp--navy) .svp-faq-spoken,
.svp-faq-card:not(.svp-faq-card--ask) h3{color:var(--navy-ink);}
.svp-faq.svp--navy .svp-runin,.svp-faq.svp--navy .svp-faq-spoken{color:var(--pink);}
.svp-faq-card--ask{background:var(--pink);border-color:var(--pink);justify-content:space-between;}
.svp-faq-card--ask h3,.svp-faq-card--ask p,
.svp--navy .svp-faq-card--ask h3,.svp--navy .svp-faq-card--ask p{color:#fff;}
.svp-faq-card--ask .arwbtn{width:36px;height:36px;font-size:15px;align-self:flex-end;
  background:#fff;border-color:#fff;color:var(--pink);}
.svp-faq-card--ask .arwbtn .gwarr{width:15px;height:15px;}
.svp-faq-card--ask:hover .arwbtn{background:var(--navy-ink);border-color:var(--navy-ink);color:#fff;}

/* The two portrait/checklist layouts keep their original column proportions
   when the photograph moves right. Their DOM order stays photo then text, so
   the existing stacked layout and reading order are unchanged on small screens. */
@media(min-width:901px){
.svp-incl--photo-right .svp-incl-grid{grid-template-columns:1.1fr .9fr;}
.svp-incl--photo-right .svp-incl-img{grid-column:2;grid-row:1;}
.svp-incl--photo-right .svp-incl-body{grid-column:1;grid-row:1;}
.svp-faq--photo-right .svp-faq-grid{grid-template-columns:1.25fr .85fr;}
.svp-faq--photo-right .svp-faq-aside{grid-column:2;grid-row:1;}
.svp-faq--photo-right .svp-faq-body{grid-column:1;grid-row:1;}
/* The introduction's At a glance rail or portrait on the left: the same column
   widths mirrored, and the rail's dividing rule moves to its inner edge. */
.svp-intro--side-left.svp-intro--rail .svp-intro-grid{grid-template-columns:.8fr 1.5fr;}
.svp-intro--side-left.svp-intro--portrait .svp-intro-grid{grid-template-columns:.85fr 1.25fr;}
.svp-intro--side-left .svp-intro-grid>.svp-essay{grid-column:2;grid-row:1;}
.svp-intro--side-left .svp-intro-grid>.svp-rail,.svp-intro--side-left .svp-intro-grid>.svp-portrait{grid-column:1;grid-row:1;}
.svp-intro--side-left .svp-rail{border-left:0;padding-left:0;border-right:1px solid var(--line);padding-right:28px;}
.svp--navy.svp-intro--side-left .svp-rail{border-right-color:rgba(255,255,255,.22);}
}

/* ── one collapse, for every section ────────────────────────────────────── */
@media(max-width:900px){
  .svp-room{min-height:0;}
  .svp-wrap2,.svp-wrap2.rev,.svp-intro--rail .svp-intro-grid,
  .svp-intro--portrait .svp-intro-grid,.svp-incl-grid,
  .svp-people--portrait .svp-people-grid,.svp-people--length .svp-people-grid{
    grid-template-columns:1fr;}
  /* Mirrored or not, the words come first once the columns stack. */
  .svp-wrap2.rev>*:first-child{order:2;}
  .svp-acts,.svp-profiles,.svp-rel-grid{grid-template-columns:1fr;}
  .svp-rel-grid>*{min-height:280px;}
  .svp-incl-img{min-height:280px;}
  /* The facts rail's left rule reads as a stray vertical once it is full width. */
  .svp-rail{border-left:0;padding-left:0;padding-top:24px;border-top:1px solid var(--line);}
  .svp--navy .svp-rail{border-top-color:rgba(255,255,255,.22);}
  /* Nothing to run off the edge of any more. */
  .svp-ringed{width:100%;justify-self:stretch;margin-right:0;}
  .svp-stats{min-height:420px;}
  .svp-dcap{font-size:58px;}
  /* The carousel keeps its three columns: collapsing it would either stack two
     arrows vertically or drop them, and the arrows are how a reader on a phone
     gets to the second quote. They shrink instead, and the gap closes. */
  .svp-car-grid{gap:clamp(8px,3vw,18px);}
  .svp-car .arwbtn{width:38px;height:38px;font-size:15px;}
  .svp-car .arwbtn .gwarr{width:15px;height:15px;}
  /* Questions we are asked: one column, and nothing sticky once it is. */
  .svp-faq-two,.svp-faq-grid,.svp-faq-cards{grid-template-columns:1fr;}
  .svp-faq-aside{position:static;}
}
@media(max-width:560px){
  .svp-hero-imgs{grid-template-columns:1fr;}
  .svp-hero-imgs .svp-ph.off{margin-top:0;}
  /* Only the primary photograph on a phone (Dylan, 26 October 2026). */
  .svp-hero-imgs .svp-ph--alt{display:none;}
}


/* ── the ground contract ──────────────────────────────────────────────
   Three tokens per ground: the ink for headings, for body and for the
   quiet meta line, plus the rule colour. Declared on .svp itself so a
   nested block inherits its own section's ground and never the page's. */
.svp{--wh-on-strong:var(--heading-deep,var(--navy-ink));--wh-on-body:var(--ink);
  --wh-on-quiet:var(--muted);--wh-on-line:var(--line);
  --wh-on-accent:var(--pink);--wh-on-field:var(--white);}
.svp--white{--wh-on-field:var(--paper);}
.svp--navy{--wh-on-strong:#fff;--wh-on-body:rgba(255,255,255,.85);
  --wh-on-quiet:rgba(255,255,255,.68);--wh-on-line:rgba(255,255,255,.22);
  --wh-on-accent:#f2b8cd;--wh-on-field:rgba(255,255,255,.1);}
/* ⚠ THE GLOBAL QUOTE COLOUR IS A LIGHT-GROUND COLOUR, AND ON NAVY IT
   DISAPPEARS. Brand styles → Quotations offers ONE colour for the whole site,
   and its picker shows it against the ordinary paper ground, so that is the
   ground somebody chooses it for. This site's is #14283f — the navy ink
   itself — so a pull quote on a navy band was navy on navy: invisible, with
   nothing in the markup or the editor to say so.

   Two of the three quote surfaces that can sit on this ground had already
   answered it, each by hand and each separately: '.svp-car blockquote' with a
   '.svp--navy' colour override, and Service Intro's feature quote with an
   inline ';color:#fff' in its template. The pull quote was the one nobody came
   back for — rule 2.5 exactly, a finding that named an example rather than a
   site. So the answer belongs in the CONTRACT, where a quote surface added
   tomorrow inherits it without anybody having to remember.

   TWO CLASSES ON PURPOSE. '--q-colour' is the one ground token that is also
   declared in ':root' (paletteCss emits it when the author has chosen one),
   and ':root' and '.svp--navy' have the SAME specificity — so a single class
   would win only because STORY_PAGES_CSS happens to be concatenated after
   ':root{…}' in stylesheet(). That is true today and is not a thing to rely
   on: somebody reordering those fragments would turn every navy quote back to
   invisible and no test that reads the DOM would notice. '.svp.svp--navy' is
   0,2,0 and wins wherever it sits.

   '--wh-on-strong' rather than a literal #fff, so this stays in step if the
   ground's heading ink ever moves. An untouched site resolves to exactly the
   value it already resolved to, because that is the fallback the pull quote
   already named. */
.svp.svp--navy{--q-colour:var(--wh-on-strong);}

/* ── section padding for the strips ───────────────────────────────────
   '.svp-room' carries the padding AND a min-height, which is right for a
   section that should hold most of a screen and wrong for a band. These are
   bands, so they take the same vertical rhythm without the height. */
.svp-ins-mast,.svp-feat,.svp-cdoors,.svp-band,.svp-bylines-sec,.svp-sub,
.svp-svcplates{padding:clamp(34px,4.2vw,64px) 0;}

/* ── 01 · the cover ───────────────────────────────────────────────────
   A photograph that is the whole screen, with the page's own header drawn
   over it. With no photograph chosen the section falls back to its ground,
   which is why the scrim is on the image and not on the section. */
.svp-cover{position:relative;min-height:min(620px,92vh);display:flex;
  flex-direction:column;justify-content:flex-end;overflow:hidden;}
.svp-cover .svp-cover-img{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-cover .svp-cover-scrim{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;
  background:linear-gradient(0deg,rgba(15,25,42,.78),rgba(15,25,42,.3) 56%,rgba(15,25,42,.1));}
.svp-cover>.wrap{position:relative;z-index:2;width:100%;
  padding-top:clamp(40px,6vw,92px);padding-bottom:clamp(40px,6vw,92px);}
/* With a photograph the ink is white whatever the ground; without one the
   section keeps its ground's own ink. ':has' is progressively enhanced — a
   browser without it shows the ground ink over the scrim, which is the
   pre-existing behaviour of every other photographic section here. */
.svp-cover:has(.svp-cover-img) h1,.svp-cover:has(.svp-cover-img) .svp-cover-lede{color:#fff;}
.svp-cover--centre>.wrap{text-align:center;}
.svp-cover--centre h1,.svp-cover--centre .svp-cover-lede{margin-left:auto;margin-right:auto;}
.svp-cover h1{font-family:var(--h-hero-face,var(--h-title-face,var(--serif)));font-size:clamp(32px,5.4vw,66px);line-height:1.04;max-width:18ch;
  color:var(--wh-on-strong);text-wrap:balance;}
.svp-cover .svp-cover-lede{margin-top:20px;max-width:56ch;font-size:16px;line-height:1.55;
  color:var(--wh-on-body);}
.svp-cover .svp-crest{margin-bottom:16px;}
.svp-cover--centre .svp-crest .eyecrest{margin-left:auto;margin-right:auto;}

/* ── 02 · the standfirst ──────────────────────────────────────────────
   Paragraphs in the heading face at near-heading size, the first opened by a
   drop cap. '.svp-dcap' already flips on navy in SERVICE_PAGES_CSS.

   ⚠ THE MARGINS ARE SET HERE RATHER THAN LEFT TO THE BROWSER, and that is
   because of what happened when this block gained a second paragraph on
   21 September 2026. The default paragraph margin is 1em — at a 27px face, 27px
   above AND below — so two standfirst paragraphs would have been pushed
   further apart than the standfirst is from the byline under it, and the
   block would have read as two blocks. */
.svp-standfirst>.wrap{max-width:920px;}
.svp-standfirst .svp-sf-body{font-family:var(--serif);font-weight:400;
  font-size:clamp(19px,2.2vw,27px);line-height:1.45;letter-spacing:-.01em;
  color:var(--wh-on-strong);color:color-mix(in srgb,var(--wh-on-strong) 86%,transparent);margin:0;}
.svp-standfirst .svp-sf-body + .svp-sf-body{margin-top:.62em;}
/* THE STANDFIRST'S DROP CAP (Dylan, 18 October 2026, of the Our Story
   introduction): "ensure that the initial/drop cap uses literata. At present,
   it's also slightly too tall - it extends too high above the top line of
   text. Can you also make the initial navy? And make the main text itself a
   slightly lighter/more subtle shade of grey, only subtly."
   So the cap takes the other two caps' face (the sub-heading serif, Literata
   on the live site) and their arithmetic, over TWO lines here rather than
   three: one 1.45em line plus .62em (the same allowance the other two use for
   Safari setting Literata a touch large) over Literata's cap height, .69.
   The old cap was 72px at line-height .76 in the heading face, and in
   Chromium at 1440 wide its top stood 4px above the first line's ascenders.
   Measured in Chromium with the live faces at 1440, 1000 and 390 wide, -.11em
   puts the A's apex within a pixel or two of the first line's ascenders and
   its feet within a pixel of the second baseline, with line three running
   full width. Not measured in Safari (none here).
   THE COLOUR IS --navy (#284570), THE MASTHEAD INITIAL'S. It was --navy-ink
   (#14283f) from the morning of 18 October, my reading of "make the initial
   navy" when the cap was already --navy; that was wrong. Dylan, the same
   afternoon: "The initial /drop cap in the navy masthead is a really nice
   shade of blue. The initial in the intro block within the our story block
   and elsewhere isn't that shade of blue." So all three caps (masthead,
   Introduction, standfirst) are --navy again. ⚠ Only off the navy ground:
   '.svp--navy .svp-dcap' keeps it white there, and this rule must not
   out-rank it.
   THE TEXT IS 86% OF ITS GROUND'S STRONG INK, mixed toward transparent so it
   lightens on paper and dims white the same subtle amount on navy. A browser
   without color-mix keeps the first declaration, the full ink.
   ⚠ The numbers hold for Red Hat Display at 1.45 and Literata; change either
   and measure again. */
.svp-standfirst .svp-sf-body .svp-dcap{font-family:var(--h-sub-face,var(--serif));font-style:normal;letter-spacing:0;
  font-size:calc((1.45em + .62em) / .69);line-height:1;margin:-.11em 0 -.1em;}
.svp-standfirst:not(.svp--navy) .svp-sf-body .svp-dcap{color:var(--navy);}
.svp-standfirst .svp-sf-by{display:flex;align-items:center;gap:11px;margin-top:24px;
  font-size:12.5px;color:var(--wh-on-quiet);}
.svp-standfirst .svp-sf-by .svp-crest .eyecrest{width:20px;height:20px;}
/* Top and bottom spacing (Dylan, 17 October 2026; blocks/standfirst.ts says
   why). A tuned block stops being a room and takes each gap as padding: the
   gap it showed as a room, max(padding, (room − words) / 2), with --sf-c the
   words' height measured by STANDFIRST_SPACE_JS, then halved or quartered on
   the sides that ask. Without --sf-c it falls back to the padding alone.
   Below 900px the room has no minimum height, so the gap is the padding.
   ⚠ The two numbers here must stay the same as '.svp-room' above. */
.svp-standfirst.sf-tuned{--sf-pad:clamp(38px,4.5vw,66px);
  --sf-gap:max(var(--sf-pad),calc((min(560px,90vh) - var(--sf-c,9999px)) / 2));
  min-height:0;padding-top:var(--sf-gap);padding-bottom:var(--sf-gap);}
@media(max-width:900px){.svp-standfirst.sf-tuned{--sf-gap:var(--sf-pad);}}
.svp-standfirst.sf-t2{padding-top:calc(var(--sf-gap) / 2);}
.svp-standfirst.sf-t4{padding-top:calc(var(--sf-gap) / 4);}
.svp-standfirst.sf-b2{padding-bottom:calc(var(--sf-gap) / 2);}
.svp-standfirst.sf-b4{padding-bottom:calc(var(--sf-gap) / 4);}

/* ── 03 · the chapter room ────────────────────────────────────────────
   Half photograph, half words. The template owns the side; the page only
   writes the words and picks the pictures. */
.svp-chapter .svp-chapter-grid{display:grid;gap:clamp(28px,5vw,72px);align-items:center;
  grid-template-columns:.9fr 1.1fr;}
.svp-chapter--left .svp-chapter-grid{grid-template-columns:1.1fr .9fr;}
/* ⚠ THESE TWO WERE THE WRONG WAY ROUND until 20 September 2026, and the bug
   was INVISIBLE because it restated the default. Source order is words-then-art,
   so the art auto-places into column 2 already; putting the art in column 2 and
   the words in column 1 for the "photographs on the left" design therefore
   changed nothing at all. Selecting it moved no photograph, in the preview or on
   the published page — one stylesheet serves both.
   The swapped track widths on the line above are the tell: 1.1fr first means the
   WIDE track is meant to be first and to hold the pictures. A flip like this is
   only real if the two designs place the art in DIFFERENT columns, which is what
   the guard in storyInsightsCategoryBlocks.test.ts now asserts. */
.svp-chapter--left .svp-chapter-art{grid-column:1;grid-row:1;}
.svp-chapter--left .svp-chapter-words{grid-column:2;grid-row:1;}
.svp-chapter-art{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start;}
.svp-chapter-art.is-single{grid-template-columns:1fr;}
.svp-chapter-art .svp-ph{aspect-ratio:3/4.2;}
/* The offset is what makes the pair read as a gallery rather than a grid; it
   always rides the photograph furthest from the words. */
.svp-chapter-art .svp-ph.off{margin-top:52px;}
.svp-chapter-num{font-family:var(--serif);font-weight:700;font-size:13px;
  color:var(--wh-on-accent);letter-spacing:.02em;}
.svp-chapter h2{font-size:clamp(24px,3.4vw,42px);line-height:1.07;margin-top:12px;
  max-width:17ch;text-wrap:balance;}
.svp-chapter .svp-chapter-passage{margin-top:20px;max-width:54ch;font-size:15px;
  line-height:1.62;color:var(--wh-on-quiet);}
.svp-chapter .svp-chapter-passage+.svp-chapter-passage{margin-top:16px;}
/* STACKED, THE PHOTOGRAPHS COME FIRST ON BOTH DESIGNS (Dylan, 18 October
   2026: About the Founders, "Photographs on the right", should show its
   photographs above the words on a phone, as Featured Consultants on the left
   already did). Until then only the left design was placed here, so the right
   design fell back to source order, words first. Grid rows, not 'order', so a
   screen reader still reads the words first. Side by side nothing changes.
   ⚠ The words are given NO row on purpose: the art is placed first because its
   row is definite, and the words then auto-place into the next free row. With
   no photographs they take row 1, rather than sitting below an empty row and
   its gap, which a fixed 'grid-row:2' did. */
@media (max-width:860px){
  .svp-chapter .svp-chapter-grid,.svp-chapter--left .svp-chapter-grid{grid-template-columns:1fr;}
  .svp-chapter .svp-chapter-art,.svp-chapter .svp-chapter-words{grid-column:1;}
  .svp-chapter .svp-chapter-art{grid-row:1;}
  .svp-chapter--left .svp-chapter-words{grid-row:auto;}
  .svp-chapter-art .svp-ph.off{margin-top:0;}
}

/* ── 04 · the pull quote ──────────────────────────────────────────────
   A band between two chapters. One quote, one attribution, no photograph. */
.svp-pull{padding:clamp(54px,7vw,106px) 0;text-align:center;}
.svp-pull .svp-pull-mark{display:block;font-family:var(--serif);font-weight:700;
  color:var(--pink);font-size:60px;line-height:.72;margin-bottom:6px;}
.svp-pull blockquote{font-family:var(--q-face,var(--serif));font-style:var(--q-style,normal);
  font-weight:var(--q-weight,400);
  font-size:calc(var(--q-scale,1) * clamp(22px,3.2vw,40px));line-height:1.3;
  letter-spacing:var(--q-track,-.01em);text-transform:var(--q-case,none);
  color:var(--q-colour,var(--wh-on-strong));max-width:26ch;margin:0 auto;
  -webkit-hyphens:none;hyphens:none;text-wrap:balance;}
.svp-pull .svp-pull-who{margin-top:20px;font-size:13px;color:var(--wh-on-quiet);}

/* ── 05 · recent locations ────────────────────────────────────────────
   One block, three designs, one place at the centre of each. */
/* ⚠ THIS BLOCK OPTS OUT OF THE ROOM'S VERTICAL RHYTHM, and Dylan's report is
   why: "there is currently far too much padding at the top and the bottom of
   the block". Two separate things were adding it up.

   The first was inside the diagram and is fixed where it was caused — see the
   grow step in recentLocations.ts. The square stage was sized for the worst
   case and the solver then pulled every place inward, so the ink spanned only
   about three quarters of it and the remaining quarter read as padding.

   The second is here. '.svp-room' gives every service-page section
   clamp(38px,4.5vw,66px) AND a min-height of min(560px,90vh) with the content
   centred in it, which is right for a room holding a paragraph and wrong for a
   block that is already 820px tall and brings its own margins. The min-height
   can only ever add empty ground to this one, so it goes; the padding comes
   down to the band figure the strips use. */
.svp-loc.svp-room{padding:clamp(22px,2.6vw,40px) 0;min-height:0;}
.svp-loc-head{display:flex;align-items:flex-end;justify-content:space-between;
  gap:clamp(14px,2.4vw,34px);flex-wrap:wrap;margin-bottom:26px;}
.svp-loc h2{font-size:clamp(24px,3.2vw,38px);max-width:18ch;}
.svp-loc-note{font-size:13px;color:var(--wh-on-quiet);max-width:34ch;line-height:1.5;}
/* ── THE FOOTNOTE'S ALIGNMENT IS A SETTING ─────────────────────────────────
   Dylan, 25 September 2026: "With the recent locations block, please also have
   a choice to make the footnote left-aligned, centre-aligned, or right-aligned
   in the global settings for that block."

   ⚠ THE FALLBACK IS inherit, NOT left, and that is what keeps an untouched
   site byte-identical. The footnote is a paragraph, so the site-wide paragraph
   rule already governs it — including the justification setting — and writing
   left here would quietly override that for every site that has never opened
   the control. inherit is exactly what it resolves to today. */
.svp-loc-foot{margin-top:18px;font-size:12.5px;color:var(--wh-on-quiet);text-align:var(--loc-foot-align,inherit);}

/* 05a · the constellation — the centre across the middle three of five
   columns, the rest ringed round it. */
.svp-con{display:grid;grid-template-columns:repeat(5,1fr);gap:clamp(9px,1.3vw,16px);}
.svp-con .svp-lp{position:relative;overflow:hidden;border-radius:2px;aspect-ratio:1/1;}
.svp-con .svp-lp .svp-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-con .svp-lp-scrim{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;
  background:linear-gradient(180deg,rgba(15,31,56,0) 34%,rgba(15,31,56,.74) 100%);}
.svp-con .svp-lp-cap{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:10px 12px;}
.svp-con .svp-lp-nm{display:block;font-family:var(--serif);font-weight:600;font-size:14px;
  line-height:1.15;color:#fff;}
.svp-con .svp-lp-ct{display:block;font-size:10px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:rgba(255,255,255,.72);margin-top:3px;}
/* The pair either side of the centre fill its height rather than staying square. */
.svp-con .svp-lp.tall{aspect-ratio:auto;height:100%;}
.svp-con .svp-lp.core{grid-column:2 / span 3;aspect-ratio:16/10;}
.svp-con .svp-lp.core .svp-lp-cap{padding:clamp(16px,2vw,26px);}
.svp-con .svp-lp.core .svp-lp-nm{font-size:clamp(26px,3.4vw,44px);line-height:1.05;}
.svp-con .svp-lp.core .svp-lp-ct{font-size:12.5px;margin-top:9px;color:#f2b8cd;
  letter-spacing:.01em;text-transform:none;font-weight:400;max-width:44ch;line-height:1.45;}
@media (max-width:1000px){
  .svp-con{grid-template-columns:repeat(3,1fr);}
  .svp-con .svp-lp.core{grid-column:1 / span 3;}
  .svp-con .svp-lp.tall{aspect-ratio:1/1;height:auto;}
}
@media (max-width:620px){
  .svp-con{grid-template-columns:repeat(2,1fr);}
  .svp-con .svp-lp.core{grid-column:1 / span 2;aspect-ratio:4/3;}
}

/* 05b · the orbit — the same idea in the site's own circular language, the
   92px insight thumbnail and its 1.5px pink ring grown up. */
/* —— THE CLOCK FACE ——
   Replaced the three stacked rows on 20 September 2026. The compiler places
   every disc as a PERCENTAGE of this square, so the layout needs no script and
   no measurement; all this sheet does is draw them and move them.
   'container-type:inline-size' is what makes 'cqw' mean "a share of THIS
   block", so a name scales with the diagram rather than with the viewport and
   reads the same in a narrow column as at full bleed.
   ⚠ SINGLE QUOTES, NOT BACKTICKS, in this comment and every other one in this
   file. A backtick here ends the template literal the whole stylesheet lives
   in, and the error it produces points at the CSS rather than at the quote.
   This is the second time it has cost an edit. */
.svp-orb{position:relative;width:100%;max-width:min(100%,820px);margin:0 auto;
  aspect-ratio:1/1;container-type:inline-size;}
.svp-orb .svp-onode{position:absolute;transform:translate(-50%,-50%);
  display:flex;flex-direction:column;align-items:center;text-align:center;}
.svp-orb .svp-disc{position:relative;width:100%;aspect-ratio:1/1;border-radius:50%;
  overflow:hidden;}
.svp-orb .svp-disc .svp-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-orb .svp-disc::after{content:"";position:absolute;top:4%;left:4%;right:4%;bottom:4%;
  inset:4%;border-radius:50%;box-shadow:inset 0 0 0 1.5px rgba(217,113,149,.85);z-index:2;}
.svp-orb .svp-onode--core .svp-disc::after{top:3%;left:3%;right:3%;bottom:3%;inset:3%;
  box-shadow:inset 0 0 0 2px rgba(217,113,149,.9);}
/* 13px is the floor, not the size: a third ring on a phone would otherwise set
   its names at four or five pixels, which is a caption nobody can read. */
.svp-orb .svp-lp-nm{display:block;font-family:var(--serif);font-weight:600;
  font-size:max(13px,var(--wh-cap,1.4cqw));line-height:1.2;margin-top:.5em;
  color:var(--wh-on-strong);white-space:nowrap;}
.svp-orb .svp-onode--core .svp-lp-nm{margin-top:.42em;}
.svp-orb .svp-lp-ct{display:block;font-size:max(10px,calc(var(--wh-cap,1.4cqw) * .72));
  letter-spacing:.1em;text-transform:uppercase;color:var(--wh-on-quiet);margin-top:.3em;
  white-space:nowrap;}

/* ⚠ THE ENTRANCE STARTS AT THE CENTRE AND OPENS OUTWARDS, and it is driven by
   one class on the stage rather than by a class per disc, so the browser has a
   single style recalculation to do however many places there are.
   The resting state is the LAID-OUT one: a page whose script never runs, or
   whose observer never fires, shows the finished diagram rather than a pile of
   discs stacked at the centre. 'is-in' is added, never required.

   ⚠ AND IT DID NOT, UNTIL 21 SEPTEMBER 2026. The sentence above was written
   before the thing it describes existed. The collapsed state scaled each disc
   to .3 where it already stood, so every place shrank and grew ON ITS OWN SPOT
   and nothing travelled: Dylan, looking at it, said they "seem to be fading in
   but not moving outwards". A comment claiming the behaviour is how it went
   nine days without being noticed, which is the argument for the guard that
   now reads the compiled markup rather than this file.

   The travel is '--wh-dx'/'--wh-dy', written per disc by the block in 'cqw'
   — the offset from that place to the centre place, in shares of the stage's
   width. The stage is already a container ('container-type:inline-size', for
   the captions), so those units resolve without measuring anything. */
.svp-orb[data-wh-orbit] .svp-onode{
  transition:transform .62s cubic-bezier(.22,.72,.3,1) calc(var(--wh-ring,0) * .11s),
             opacity .44s ease-out calc(var(--wh-ring,0) * .11s);}
/* The two collapsed states share one geometry and differ ONLY in whether
   getting there is animated. 'is-armed' is the state the script writes before
   the diagram has ever been seen, so it must land instantly — a transition
   there would animate the page's own setup. 'is-out' is the reversal, which is
   the same picture arrived at over time. */
.svp-orb[data-wh-orbit].is-armed .svp-onode,
.svp-orb[data-wh-orbit].is-out .svp-onode{
  opacity:0;
  transform:translate(-50%,-50%) translate(var(--wh-dx,0cqw),var(--wh-dy,0cqw)) scale(.3);}
.svp-orb[data-wh-orbit].is-armed .svp-onode{transition:none;}
/* The centre has nowhere to travel to, so it only grows — and from further
   up, because a disc this size appearing from a third of itself reads as a
   pop rather than as an opening. */
.svp-orb[data-wh-orbit].is-armed .svp-onode--core,
.svp-orb[data-wh-orbit].is-out .svp-onode--core{
  opacity:0;transform:translate(-50%,-50%) scale(.6);}
/* GOING BACK IN IS THE SAME GESTURE PLAYED BACKWARDS, so the outermost ring
   leaves FIRST and the centre last. The entrance delays by the ring number;
   the exit delays by how far the ring is from the outside. '--wh-rmax' is the
   number of rings this particular diagram has, written on the stage by the
   block — without it the exit would have to guess, and a guess that is wrong
   by one puts a dead beat at one end of the sequence. */
.svp-orb[data-wh-orbit].is-out .svp-onode{
  transition-delay:calc((var(--wh-rmax,0) - var(--wh-ring,0)) * .11s);}
.svp-orb[data-wh-orbit].is-in .svp-onode{opacity:1;transform:translate(-50%,-50%) scale(1);}
/* —— ON PHONES, A SECOND LAYOUT ——
   Dylan, 18 October 2026: on his phone the names were "a bit big" and "getting
   cut off": California under Lyon, Hong Kong under Lisbon, Paris into Boston.
   The clock face is solved in shares of the stage's width, but a name has a
   floor in pixels, so on a narrow stage the names outgrow the gaps the solver
   left. recentLocations.ts (orbitPhoneLayout) therefore solves a second
   arrangement with the names counted in, at the sizes below and the narrowest
   phone stage, and writes it on each place as '--wh-px', '--wh-py' and
   '--wh-pd', and on the stage as '--wh-ph' (its height as a share of its
   width; it may be taller than square). Here it replaces the desktop one.
   ⚠ '!important' IS NEEDED: the desktop position is in each place's own style
   attribute, and only an important declaration beats an inline one.
   ⚠ The two type sizes must stay the same as PHONE_NAME_PX and
   PHONE_CAPTION_PX in recentLocations.ts, or the layout is solved for names of
   the wrong size, and the two line heights the same as orbitPhoneBox's (the
   caption's would otherwise follow the site's body line-height, up to 2),
   and 90cqw the same as PHONE_TEXT_MAX: a long caption (the
   centre's often is) wraps at that width instead of running off the screen.
   The place is a centred flex column, so text wider than its disc overhangs
   both sides equally. Above 600px nothing here applies. */
@media (max-width:600px){
  .svp-orb[data-wh-phone]{aspect-ratio:100 / var(--wh-ph,100);}
  .svp-orb[data-wh-phone] .svp-onode{left:var(--wh-px)!important;top:var(--wh-py)!important;
    width:var(--wh-pd)!important;--wh-dx:var(--wh-pdx)!important;--wh-dy:var(--wh-pdy)!important;}
  .svp-orb[data-wh-phone] .svp-lp-nm{font-size:11px;line-height:1.2;}
  .svp-orb[data-wh-phone] .svp-lp-ct{font-size:10px;line-height:1.5;}
  .svp-orb[data-wh-phone] .svp-lp-nm,.svp-orb[data-wh-phone] .svp-lp-ct{white-space:normal;
    width:max-content;max-width:90cqw;}
}
@media (prefers-reduced-motion: reduce){
  .svp-orb[data-wh-orbit] .svp-onode,
  .svp-orb[data-wh-orbit].is-armed .svp-onode,
  .svp-orb[data-wh-orbit].is-out .svp-onode{
    opacity:1;transform:translate(-50%,-50%);transition:none;}
}
/* The lazily-loaded photograph inside a disc. It fills the placeholder exactly
   as the background-image it replaces did: same box, same crop, same rounding
   from the disc's own overflow:hidden. */
.svp-orb .svp-ph-img{position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;display:block;}
.svp-orb .svp-core .svp-lp-ct{font-size:11.5px;color:var(--wh-on-accent);margin-top:7px;
  text-transform:none;letter-spacing:.01em;}
@media (max-width:620px){
  .svp-orb .svp-orow--mid .svp-core{flex:0 0 100%;order:-1;}
  .svp-orb .svp-orow--mid .svp-sat{padding-top:0;}
}

/* 05c · the offset plates — the plate gallery with the centre grown to four
   times the others and the rest dropped alternately. */
.svp-off{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(14px,2vw,26px);
  align-items:start;}
.svp-off .svp-lp-img{position:relative;overflow:hidden;border-radius:2px;aspect-ratio:1/1;}
.svp-off .svp-lp-img .svp-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-off .svp-lp-nm{display:block;font-family:var(--serif);font-weight:600;font-size:15px;
  line-height:1.2;margin-top:11px;color:var(--wh-on-strong);}
.svp-off .svp-lp-ct{display:block;font-size:11px;color:var(--wh-on-quiet);margin-top:3px;
  line-height:1.45;}
.svp-off .svp-lp.drop{margin-top:clamp(20px,3.4vw,52px);}
.svp-off .svp-lp.core{grid-column:1 / span 2;grid-row:1 / span 2;}
.svp-off .svp-lp.core .svp-lp-nm{font-size:clamp(24px,3vw,38px);margin-top:16px;}
.svp-off .svp-lp.core .svp-lp-ct{font-size:13.5px;margin-top:7px;max-width:40ch;}
@media (max-width:820px){
  .svp-off{grid-template-columns:repeat(2,1fr);}
  .svp-off .svp-lp.core{grid-column:1 / span 2;grid-row:auto;}
  .svp-off .svp-lp.drop{margin-top:0;}
}

/* ── 06 · the insights masthead ───────────────────────────────────────── */
.svp-ins-mast h1{font-family:var(--h-hero-face,var(--h-title-face,var(--serif)));font-size:clamp(30px,4.6vw,54px);max-width:15ch;line-height:1.05;}
.svp-ins-mast--centre{text-align:center;}
.svp-ins-mast--centre h1,.svp-ins-mast--centre .svp-ins-mast-lede{margin-left:auto;margin-right:auto;}
.svp-ins-mast-lede{margin-top:16px;max-width:58ch;font-size:15.5px;line-height:1.6;
  color:var(--wh-on-quiet);}
.svp-ins-mast-grid{display:flex;justify-content:space-between;align-items:flex-end;
  gap:clamp(20px,3vw,44px);flex-wrap:wrap;}
.svp-ins-mast-rail{border-left:1px solid var(--wh-on-line);padding-left:28px;min-width:186px;}
.svp-ins-mast-fig{font-family:var(--serif);font-weight:700;font-size:clamp(28px,3.4vw,42px);
  color:var(--pink);line-height:1;}
.svp-ins-mast-rail p{font-size:13px;color:var(--wh-on-quiet);line-height:1.6;}

/* ── 07 · featured insights ───────────────────────────────────────────
   One block, three weights: the lead at full width, a pair of cards, and
   three picks in type alone. The cascade is the point, so each design is
   deliberately smaller than the one above it. */
.svp-feat-lead{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(26px,4.4vw,64px);
  align-items:center;}
.svp-feat-lead .svp-ph{aspect-ratio:16/10;}
.svp-feat-lead h2,.svp-feat-lead h3{font-size:clamp(26px,3.4vw,42px);line-height:1.08;
  max-width:17ch;text-wrap:balance;}
.svp-feat-lede{margin-top:16px;max-width:46ch;font-size:15px;line-height:1.6;
  color:var(--wh-on-quiet);}
.svp-feat-meta{margin-top:18px;font-size:13px;color:var(--wh-on-quiet);}
.svp-feat-pair{display:grid;grid-template-columns:1fr 1fr;gap:clamp(18px,2.4vw,30px);}
.svp-feat-picks{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(18px,2.4vw,30px);}
.svp-feat-pick{border-top:1px solid var(--wh-on-line);padding-top:16px;display:block;
  background-image:none!important;padding-bottom:0!important;}
.svp-feat-pick h3{font-size:clamp(17px,1.7vw,21px);line-height:1.22;margin-top:8px;}
.svp-feat-pick .svp-feat-meta{margin-top:8px;}
.svp-feat-head{display:flex;justify-content:space-between;align-items:flex-end;
  gap:14px;flex-wrap:wrap;margin-bottom:18px;}
.svp-feat-head h2{font-size:clamp(17px,1.8vw,21px);}
@media (max-width:860px){
  .svp-feat-lead,.svp-feat-pair,.svp-feat-picks{grid-template-columns:1fr;}
}

/* A photographic card that carries a headline and a line of standfirst —
   the seconds, and the category doors. Shared so they cannot drift. */
.svp-tile{position:relative;overflow:hidden;border-radius:2px;display:flex;
  flex-direction:column;justify-content:flex-end;color:#fff;min-height:300px;
  text-decoration:none;background-image:none!important;padding-bottom:0!important;}
.svp-tile .svp-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-tile-scrim{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;
  background:linear-gradient(180deg,rgba(15,31,56,0) 34%,rgba(15,31,56,.74) 100%);}
.svp-tile-in{position:relative;z-index:2;padding:22px;}
.svp-tile-t{display:block;font-family:var(--serif);font-weight:600;font-size:19px;
  line-height:1.2;color:#fff;}
.svp-tile-s{font-size:12.5px;color:rgba(255,255,255,.78);margin-top:7px;line-height:1.45;}
.svp-tile-meta{display:block;font-size:12px;color:rgba(255,255,255,.66);margin-top:10px;}

/* ── 08 · category doors ──────────────────────────────────────────────
   They jump to that category further DOWN THIS PAGE; they do not open a
   page of their own. The arrow is turned to say so, and the label repeats
   it in words — Dylan: "I'm not sure what opening it would do? I don't
   want a separate page." */
.svp-doors{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(18px,2.4vw,30px);}

/* ---- 09b - the insights doors ----------------------------------------
   Shown at the foot of a shared insight post to a reader we cannot name.
   It reuses .svp-tile wholesale; everything below is the difference.

   (!) THE TITLE IS AN h3 HERE, NOT A SPAN. category_doors draws its titles as
   spans carrying font-family:var(--serif), which looks right and quietly
   ignores every heading setting the operator has made in the hub. A real
   heading tag picks up --h-sub-face, its weight, tracking and case for free,
   so a site that changes its heading face changes this too. The rule below
   therefore RESTORES what the span had taken - the size and the white - while
   leaving the face to the cascade. */
/* (!) AN UNSET PHOTOGRAPH MUST NOT BE PALE, and it is by default. .svp-ph falls
   back to background-color:var(--line) — #e7dee1, a pale pink-grey — which is
   right for a gallery tile on paper and wrong here, because this tile carries
   WHITE type under a light scrim. With no picture set the door would have been
   white on near-white. That is not a hypothetical: all three photographs are
   unset today, so it is what every door would have looked like on the day this
   shipped. Deep navy instead, which the scrim and the white type are drawn for,
   and which a real photograph covers completely. */
.svp-idoors .svp-ph{background-color:var(--navy-ink);}
.svp-idoors-head{margin-bottom:clamp(18px,2.4vw,28px);}
.svp-idoors-head h2{font-size:clamp(21px,2.9vw,31px);line-height:1.12;max-width:20ch;
  text-wrap:balance;margin:0;color:var(--wh-on-strong);}
.svp-idoors .svp-tile-t{margin:0;font-size:clamp(17px,2vw,21px);line-height:1.12;
  color:#fff;letter-spacing:var(--h-sub-track,var(--tracking-heading,-.01em));}
/* The white has to be restated because .svp-tile-in sits on a photograph while
   h3's own colour comes from --h-sub-colour, which is chosen for paper. */
/* ── THE SMALLER SECOND LINE OF A DOOR'S TITLE ─────────────────────────────
   Dylan, 26 September 2026: "I'd like 'Families' to have a smaller bit of text
   underneath that says '& Family Offices'. Likewise … '& Global Mobility' … is
   underneath 'Corporate' in a smaller font too."

   UNDERNEATH and SMALLER, which is a different request from the audience doors'
   trailing text on the same line — both were asked for in the same message and
   they are kept different on purpose.

   ⚠ A BLOCK-LEVEL SPAN *INSIDE* THE h3, so the heading's text is unchanged for a
   crawler and a screen reader while the eye reads two lines. See insightsDoors.ts.
   ⚠ em, NOT px: the door title is a clamp, so a fixed size would be right at one
   width and wrong at the other two. .78em holds the ratio everywhere.
   ⚠ AND THE FACE IS INHERITED. The title reads the site's --h-sub-* settings, so
   naming a family here would make the second line ignore every heading setting
   the operator has made — the mistake this block's own h3-not-a-span comment was
   written about. Only the weight, the size and the transparency move. */
.svp-idoors .svp-tile-sub{display:block;font-size:.78em;font-weight:400;
  opacity:.72;margin-top:2px;letter-spacing:0;}

/* The site's own enquire link at a smaller size: the words, then a filled pink
   disc to their RIGHT, no border and no underline. Repeated three times across
   a row here, so it is set down from the 17px the service hero uses. */
.svp-enq--sm{margin-top:14px;gap:9px;}
.svp-enq--sm .t{font-size:13px;font-weight:700;}
.svp-enq--sm .arwbtn{width:28px;height:28px;font-size:12px;}
.svp-enq--sm .arwbtn .gwarr{width:11px;height:11px;}
/* On a photograph the words are white and the disc keeps its pink, which is the
   same pairing .svp--navy .svp-enq already uses. */
.svp-idoors .svp-enq--sm{color:#fff;}
.svp-idoors .svp-enq--sm .t{color:#fff;}
.svp-doors .svp-tile{min-height:250px;}
.svp-door-jump{display:inline-flex;align-items:center;gap:9px;margin-top:16px;
  font-size:12px;font-weight:600;letter-spacing:.02em;color:rgba(255,255,255,.86);}
.svp-door-jump .arwbtn{width:30px;height:30px;font-size:12px;
  border-color:rgba(255,255,255,.4);background:transparent;color:#fff;}
.svp-door-jump .arwbtn .gwarr{width:12px;height:12px;transform:rotate(90deg);}
@media (max-width:860px){.svp-doors{grid-template-columns:1fr;}}

/* ── THE SPREAD (related_insights) ──────────────────────────────────────────
   One piece carries the weight and two or three defer to it. The design Dylan
   picked out of three on 20 September 2026.

   Every colour goes through the --wh-on-* ground contract rather than being
   written down, so the block is correct on paper, white AND navy without a
   single per-ground override below. */
.svp-spread{display:grid;grid-template-columns:1.2fr 1fr;
  gap:clamp(24px,3.4vw,42px);align-items:start;}
.svp-rel-head{margin-bottom:clamp(18px,2.4vw,28px);}
.svp-rel-head h2{font-size:clamp(21px,2.9vw,31px);line-height:1.12;max-width:22ch;
  text-wrap:balance;margin:0;color:var(--wh-on-strong);}
/* The shared insights heading has no crest. Related services does, so its
   title keeps the same 16px clearance as the other service-page sections. */
.svp-rel .svp-rel-headings h2{margin-top:16px;}

/* The cards are anchors, so every inherited link decoration has to be turned
   off explicitly — the site underlines links inside prose with a background
   image, which is why background-image is suppressed and not just
   text-decoration. Same shape as .svp-enq above. */
.svp-rel-lead,.svp-rel-item{text-decoration:none;color:inherit;
  background-image:none!important;padding-bottom:0!important;}

.svp-rel-lead{display:block;}
.svp-rel-pic{display:block;position:relative;width:100%;aspect-ratio:4/3;
  border-radius:2px;overflow:hidden;}
.svp-rel-lead .svp-rel-t{font-size:clamp(19px,2.4vw,27px);line-height:1.14;
  margin:10px 0 0;color:var(--wh-on-strong);}
.svp-rel-d{margin:9px 0 0;line-height:1.45;color:var(--wh-on-body);
  font-size:15px;max-width:44ch;}

.svp-rel-rest{display:flex;flex-direction:column;gap:clamp(16px,1.8vw,22px);}
.svp-rel-item{display:flex;gap:15px;align-items:flex-start;}
.svp-rel-txt{display:block;min-width:0;}
.svp-rel-item .svp-rel-t{font-size:16px;line-height:1.2;margin:0;
  color:var(--wh-on-strong);}

/* The circular thumb with its inset ring is the site's existing insights
   thumbnail, at a smaller size. The ring is INSET rather than a border so it
   reads as drawn on the photograph rather than around it. */
.svp-rel-thumb{position:relative;width:64px;height:64px;border-radius:50%;
  overflow:hidden;flex:none;}
.svp-rel-thumb .svp-ph{border-radius:50%;}
.svp-rel-thumb::after{content:"";position:absolute;inset:5%;border-radius:50%;
  border:1.5px solid var(--pink);opacity:.85;pointer-events:none;}

.svp-rel-kick{display:block;font-size:12px;font-weight:600;letter-spacing:.16em;
  text-transform:uppercase;color:var(--wh-on-body);opacity:.7;margin-top:7px;}
.svp-rel-lead .svp-rel-kick{margin-top:14px;}

.svp-rel .svp-enq{margin-top:clamp(22px,2.6vw,32px);}

/* ⚠ ONE COLUMN BELOW 760px, and the lead keeps its picture. An earlier
   instinct was to drop the lead image on a phone to save height; that turns
   the block into an undifferentiated list and loses the one thing the design
   is for. The list simply follows the lead instead. */
@media(max-width:760px){
  .svp-spread{grid-template-columns:1fr;gap:26px;}
}

/* ── THE POST (insight_post) ────────────────────────────────────────────────
   One column, measure-limited, because it is the only block on this site whose
   job is to be READ rather than scanned. Everything else here is a section; this
   is a document. */
.svp-post .wrap{max-width:760px;}
.svp-post-head{margin-bottom:clamp(20px,2.6vw,30px);}
.svp-post-kick{display:block;font-size:12px;font-weight:600;letter-spacing:.16em;
  text-transform:uppercase;color:var(--wh-on-body);opacity:.7;margin-bottom:10px;}
.svp-post-t{font-size:clamp(27px,4.2vw,44px);line-height:1.08;margin:0;
  max-width:20ch;text-wrap:balance;color:var(--wh-on-strong);}
.svp-post-date{display:block;margin-top:14px;font-size:13px;
  color:var(--wh-on-body);opacity:.7;}
.svp-post-pic{position:relative;width:100%;aspect-ratio:16/9;border-radius:2px;
  overflow:hidden;margin-bottom:clamp(20px,2.6vw,32px);}
.svp-post-pic .svp-ph{position:absolute;inset:0;border-radius:2px;}

/* ⚠ THE BODY'S TAGS ARE THE SANITISER'S ALLOW-LIST, AND NOTHING ELSE CAN
   ARRIVE. p, h2, h3, blockquote, ul, ol, li, strong, em, a — sanitizeInsightHtml drops every other
   tag and keeps its text, so styling anything further would be styling markup
   that cannot exist. */
.svp-post-body{font-size:17px;line-height:1.62;color:var(--wh-on-body);}
.svp-post-body p{margin:0 0 1.05em;}
.svp-post-body p:last-child{margin-bottom:0;}
.svp-post-body ul,.svp-post-body ol{margin:0 0 1.05em;padding-left:1.35em;}
.svp-post-body li{margin:0 0 .45em;}
.svp-post-body strong{color:var(--wh-on-strong);font-weight:600;}
.svp-post-body h2,.svp-post-body h3{font-family:var(--serif);color:var(--wh-on-strong);
  line-height:1.18;text-wrap:balance;}
.svp-post-body h2{font-size:calc(var(--h-section-scale,1) * clamp(25px,3vw,34px));margin:1.35em 0 .48em;}
.svp-post-body h3{font-size:calc(var(--h-sub-scale,1) * clamp(20px,2.25vw,25px));margin:1.2em 0 .42em;}
.svp-post-body blockquote{margin:1.25em 0;padding-left:1em;border-left:3px solid var(--pink);
  color:var(--wh-on-strong);font-family:var(--serif);font-style:italic;font-size:1.08em;}
.svp-post-body a{color:var(--wh-on-strong);text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px;}
.svp-post-body a:hover{color:var(--pink-deep);}

/* ── 09 · the category band ───────────────────────────────────────────
   A featured piece for the category on the left, the rest of it in two
   tight columns on the right, and — when there are more than fit — a
   VIEW MORE button that reveals the next page in place.

   Every row is in the markup from the start and the overflow is hidden
   with an attribute, so a reader with no JavaScript, and a crawler, get
   the whole list. That is the same contract the testimonial carousel
   keeps, and the reason neither needs a fetch. */
.svp-band-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  padding-bottom:10px;border-bottom:2px solid var(--wh-on-strong);margin-bottom:20px;}
.svp-band-head h2{font-size:clamp(20px,2.4vw,28px);line-height:1.15;}
.svp-band-count{font-size:11px;font-weight:600;color:var(--wh-on-quiet);white-space:nowrap;
  letter-spacing:.06em;text-transform:uppercase;}
.svp-band-grid{display:grid;grid-template-columns:.8fr 1.2fr;gap:clamp(26px,4.4vw,64px);}
.svp-band-feature{display:block;background-image:none!important;padding-bottom:0!important;}
.svp-band-feature .svp-ph{aspect-ratio:4/3;}
.svp-band-feature h3{font-size:21px;line-height:1.22;margin-top:14px;}
.svp-band-cols{display:grid;grid-template-columns:1fr 1fr;gap:0 clamp(26px,4.4vw,64px);
  align-content:start;}
.svp-crow{display:block;padding:11px 0;border-bottom:1px solid var(--wh-on-line);
  background-image:none!important;padding-bottom:11px!important;}
.svp-crow .svp-crow-t{display:block;font-family:var(--serif);font-weight:600;font-size:15.5px;
  line-height:1.25;color:var(--wh-on-strong);}
.svp-crow .svp-crow-d{display:block;font-size:11.5px;color:var(--wh-on-quiet);margin-top:4px;}
/* THE HIDDEN TAIL. 'hidden' alone would be overridden by the display above, so
   the attribute is matched explicitly and wins on specificity. */
.svp-crow[data-wh-more="hidden"]{display:none;}
.svp-band-foot{display:flex;justify-content:flex-end;margin-top:18px;}
.svp-more{display:inline-flex;align-items:center;gap:10px;font-family:var(--body);
  font-size:13px;font-weight:600;letter-spacing:.02em;cursor:pointer;
  background:transparent;color:var(--wh-on-strong);
  border:1.5px solid var(--wh-on-line);border-radius:999px;padding:9px 18px;
  transition:background-color .18s ease,border-color .18s ease,color .18s ease;}
.svp-more:hover{border-color:var(--pink);background:var(--pink);color:#fff;}
.svp-more:focus-visible{outline:2px solid var(--pink);outline-offset:2px;}
.svp-more .svp-more-n{font-family:var(--serif);font-weight:700;color:var(--wh-on-accent);}
.svp-more:hover .svp-more-n{color:#fff;}
/* Nothing left to reveal: the button removes itself rather than sitting there
   disabled, because a control that cannot do anything is not information. */
.svp-more[hidden]{display:none;}
@media (max-width:1000px){.svp-band-grid{grid-template-columns:1fr;}}
@media (max-width:620px){.svp-band-cols{grid-template-columns:1fr;}}

/* ── 10 · who writes this ─────────────────────────────────────────────── */
.svp-bylines{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(18px,2.4vw,30px);}
.svp-byline{display:flex;align-items:center;gap:14px;
  background-image:none!important;padding-bottom:0!important;}
.svp-byline-pic{flex:0 0 auto;width:62px;height:62px;border-radius:50%;overflow:hidden;
  position:relative;}
.svp-byline-pic .svp-ph{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-byline h3{font-size:16px;line-height:1.2;}
.svp-byline p{font-size:12px;color:var(--wh-on-quiet);margin-top:3px;}
@media (max-width:860px){.svp-bylines{grid-template-columns:1fr 1fr;}}
@media (max-width:480px){.svp-bylines{grid-template-columns:1fr;}}

/* ── 11 · the subscribe band ──────────────────────────────────────────
   The approved compact stack: heading above the field, then privacy beside
   verification. An unconnected band keeps its honest placeholder. */
/* --sub-pb is the bottom padding, kept as a variable because the space held for
   Cloudflare's box and the form's messages is measured against it (below);
   --sub-m is the least room ever left under that box or a message. */
.svp-sub{--sub-pb:44px;--sub-m:6px;padding-top:44px;padding-bottom:var(--sub-pb);}
.svp-sub .wrap{max-width:780px;container:svp-sub-wrap / inline-size;}
.svp-sub-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:27px;align-items:start;}
.svp-sub h2{font-size:clamp(22px,2.8vw,34px);max-width:none;}
.svp-sub-form{container:svp-sub / inline-size;min-width:0;}
.svp-sub-details{display:grid;grid-template-columns:minmax(0,1fr);gap:14px;margin-top:14px;align-items:start;}
.svp-sub-details .svp-sub-privacy{margin:0;max-width:none;}
.svp-sub-note{margin-top:14px;max-width:48ch;font-size:14px;line-height:1.55;
  color:var(--wh-on-quiet);}
.svp-sub-row{display:flex;gap:12px;flex-wrap:wrap;align-items:stretch;}
/* CHIPS, NOT RECTANGLES. Dylan, 18 October 2026, with a screenshot of the band:
   "make it so that the subscribe button is a chip of similar style to the chips
   used elsewhere (same font size, etc) and that the type box ... is also
   chip-like (a longer chip to be sure, but with the same radius at the ends)."
   Both read the same three variables as every other chip on the site (the
   Buttons card's chip size and padding, see buttonCss), so changing the site's
   chips changes these too. ⚠ 999px for the reason given at .navpill: the browser
   clamps it to the true half-height, so it cannot go stale when the type moves.
   The field's border is 1.5px to match .btn's, so the two chips are the same
   height side by side. The field gets 4px more side padding than the button
   because its text starts flush left and would otherwise sit in the curve. */
.svp-sub-field{flex:1 1 160px;min-width:0;display:flex;align-items:center;background:var(--wh-on-field);
  border:1.5px solid var(--wh-on-line);border-radius:999px;
  padding:var(--btn-chip-pady,9px) calc(var(--btn-chip-padx,16px) + 4px);
  font-size:var(--btn-chip-size,12.5px);line-height:1.4;color:var(--wh-on-quiet);}
.svp-sub-row .btn{border-radius:999px;padding:var(--btn-chip-pady,9px) var(--btn-chip-padx,16px);
  font-size:var(--btn-chip-size,12.5px);line-height:1.4;white-space:nowrap;}
/* "a bit more padding between the roundel and the title" (same message). */
.svp-sub .svp-crest{margin-bottom:18px;}
/* The point-of-collection line: what the address is for, how to leave, and the
   notice. Small and quiet, like the foot note; the link is underlined because
   colour alone does not mark a link on navy. */
.svp-sub-privacy{margin-top:12px;max-width:52ch;font-size:11.5px;line-height:1.5;color:var(--wh-on-quiet);}
/* The global link sweep paints a second, lower line. Keep one native underline
   here so wrapped notice links retain the same offset in every state. */
.svp-sub-privacy a{color:inherit;text-decoration:underline;text-underline-offset:2px;
  background-image:none!important;}
.svp-sub-privacy a:hover,.svp-sub-privacy a:focus-visible{color:var(--wh-on-strong);text-decoration-color:var(--pink);}
.svp-sub-foot{margin-top:12px;font-size:11.5px;color:var(--wh-on-quiet);}
/* 37.5, not 32: with the 36 above it, the band now looks as deep below its
   last line as above its heading (measured 38.8 and 38.8 on a phone). */
@media (max-width:860px){.svp-sub{--sub-pb:37.5px;padding-top:36px;}.svp-sub-grid{gap:25px;}}
/* THE LIVE FORM (N3261), drawn only when the band has an endpoint. The field
   and button keep the chip rules above; these lines only undo what a browser
   does to a real <input> and <button> (its own font, colour and cursor) so the
   live strip looks exactly like the placeholder Dylan approved. The message
   line is quiet on success and pink on a problem; the honeypot is off-screen,
   not display:none, so bots still see and fill it. */
input.svp-sub-field{font-family:inherit;color:var(--wh-on-strong);margin:0;min-width:0;}
input.svp-sub-field::placeholder{color:var(--wh-on-quiet);opacity:1;}
input.svp-sub-field:focus-visible{outline:2px solid var(--wh-on-strong);outline-offset:2px;}
input.svp-sub-field[aria-invalid="true"]{border-color:var(--pink);}
.svp-sub-row button.btn{font-family:inherit;cursor:pointer;}
.svp-sub-row button.btn[disabled]{opacity:.6;cursor:default;}
/* Cloudflare documents flexible/normal at >=300 × 65 and compact at
   150 × 140. Reserve the whole footprint before loading, without hiding or
   clipping an interactive challenge. The compact base also works without
   container-query support; SUBSCRIBE_JS measures this same box.
   ⚠ HEIGHT, NOT MIN-HEIGHT, AND NO LINE HEIGHT. Dylan, 26 October 2026: "the
   block still gets slightly taller when the Cloudflare box appears". The
   widget's iframe sits on a line of text whose descent hung 4px below it, so
   the 65px minimum became 69 (and 140 became 144) the moment Cloudflare drew
   it: measured in Chromium with the iframe straight in the box, in a wrapper
   and in a shadow root. line-height:0 takes the descent away, and the fixed
   height holds the band even if Cloudflare's markup ever adds more. Overflow
   stays visible, so a challenge is never clipped. */
.svp-sub-ts{width:150px;height:140px;line-height:0;justify-self:start;}
@container svp-sub (min-width:300px){.svp-sub-ts{width:300px;height:65px;}}
@container svp-sub (min-width:600px){
  .svp-sub-details{grid-template-columns:minmax(0,1fr) 300px;gap:24px;}
  .svp-sub-ts{justify-self:end;}
}
/* WHAT THE FORM SAYS BACK, under the privacy line in the left column. Dylan,
   26 October 2026: "if an error message shows up, that distorts things. Have
   it so that the error message appears slightly bolder than it is at the
   moment, but in the LHS column underneath the privacy notice text, and so
   that it also doesn't distort the height of the section." So the message
   shares a grid cell with an invisible copy of the longest thing the band can
   say (::after), and the cell is that tall from the first paint at every
   width; a shorter message sits inside it. On a phone the column stacks above
   Cloudflare's box, so the message is still right under the privacy line.
   600, not the body's 400: in Ysabeau Office 500 could not be told from 400
   at this size. ⚠ The copy is SUBSCRIBE_JS's longest sentence word for word,
   and it never hyphenates, so it is never shorter than a message it stands
   in for. theSubscribeBandHoldsItsHeight.test.ts fails if a longer sentence
   is added, here or in insights-subscribe, without moving the copy to it.
   1.4 and a 6px gap, not 1.5 and 8px, so that two lines of it fit in the
   bottom padding (EVEN AT REST, below). */
.svp-sub-say{display:grid;grid-template-columns:minmax(0,1fr);
  grid-template-rows:auto minmax(calc(var(--sub-pb) - var(--sub-m)),auto);align-content:start;min-width:0;}
.svp-sub-say>.svp-sub-privacy{grid-row:1;grid-column:1;}
.svp-sub-say::after,.svp-sub-msg{grid-row:2;grid-column:1;align-self:start;margin:6px 0 0;
  font-size:13px;line-height:1.4;font-weight:600;}
.svp-sub-say::after{content:"The security check could not load. Please turn off any ad or tracker blocker, refresh the page and try again.";
  visibility:hidden;-webkit-hyphens:manual;hyphens:manual;}
.svp-sub-msg{color:var(--wh-on-strong);}
.svp-sub-msg.is-error{color:var(--pink);}
/* EVEN AT REST. Dylan, 26 October 2026, 16:56: "the amount of padding below is
   now too big - it looks uneven. I'm fine with it looking uneven once the
   cloudflare card has appeared (i.e. not enough space below when the
   cloudflare thing has appeared), but not before."
   So the space held for the box and the messages is taken out of the bottom
   padding instead of being added to it. The details pull the band's bottom
   edge up by the padding less --sub-m, and the message row is never shorter
   than that, so the band ends at whichever is lowest: the privacy line plus
   the usual padding (at rest, which is what you see), or the box or the
   longest message plus --sub-m. Nothing moves when either appears.
   ⚠ ONLY WHEN THE FORM ENDS ITS COLUMN. A foot note under the form would sit
   in the space the box and messages use, so with one the space stays where
   it was, under the details and above the note. */
.svp-sub-form:last-child .svp-sub-details{margin-bottom:calc(var(--sub-m) - var(--sub-pb));}
/* PHONES (one column): Cloudflare's box goes under the privacy line and the
   message space, and the two need about four times the bottom padding, so
   they cannot be taken out of it. Until the visitor first touches the field
   (SUBSCRIBE_JS adds is-armed), neither is held, and the band is even; then
   both are, once, before Cloudflare draws anything or a message shows. */
@container svp-sub (max-width:599.98px){
  .svp-sub-form:not(.is-armed) .svp-sub-ts,.svp-sub-form:not(.is-armed) .svp-sub-say::after{display:none;}
  .svp-sub-form:not(.is-armed) .svp-sub-say{grid-template-rows:auto;}
  .svp-sub-form:not(.is-armed) .svp-sub-details{margin-bottom:0;}
}
/* TABLETS: two columns, but the smaller screens' 36px top. Two lines of a
   message need 48px under the privacy line, more than the bottom padding,
   so a band with a form takes 10px more at the top and is even (measured
   48.8 above, 49.3 below, at 768). */
@media (max-width:860px){@container svp-sub-wrap (min-width:600px){.svp-sub-grid:has(.svp-sub-form){padding-top:10px;}}}
.svp-sub-form.is-done input.svp-sub-field{opacity:.7;}
.svp-sub-hp{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;}

/* ── 12 · service plates ──────────────────────────────────────────────
   Every service in a category, four across. Eight tiles rather than six,
   so a whole category fits without the tiles becoming postage stamps —
   Dylan: "I want this to display all of the services within any given
   category." */
.svp-plates{display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(12px,1.7vw,22px);}
.svp-plates .svp-tile{min-height:clamp(260px,29vw,360px);}
.svp-plates .svp-tile-t{font-size:clamp(16px,1.6vw,19px);}
.svp-plates .svp-tile-s{font-size:12px;margin-top:6px;}
.svp-plate-sig{display:inline-flex;align-items:center;gap:7px;margin-bottom:8px;}
.svp-plate-sig .wd-crest{width:18px;height:18px;flex:0 0 auto;}
.svp-plate-sig .svp-sig-t{font-size:9px;font-weight:700;letter-spacing:.17em;
  text-transform:uppercase;color:#f2b8cd;}
@media (max-width:1100px){.svp-plates{grid-template-columns:repeat(3,1fr);}}
@media (max-width:820px){.svp-plates{grid-template-columns:repeat(2,1fr);}}
@media (max-width:480px){.svp-plates{grid-template-columns:1fr;}}

/* ── the address book's two new designs ───────────────────────────────
   Variation 2 counts the KINDS of people in the book. Nobody is named —
   that is the block's whole discipline — but a count is a fact, and a
   fact is what makes the paragraph believable. */
.svp-ab-grid{display:grid;grid-template-columns:.9fr 1.1fr;gap:clamp(26px,4.4vw,64px);}
.svp-ab-disc{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;
  background:var(--wh-on-line);}
.svp-ab-disc>div{background:var(--wh-on-field);padding:clamp(18px,2.2vw,28px);}
.svp--white .svp-ab-disc>div{background:var(--white);}
.svp--navy .svp-ab-disc>div{background:var(--navy-ink);}
.svp-ab-fig{font-family:var(--serif);font-weight:700;font-size:clamp(22px,2.6vw,32px);
  color:var(--pink);line-height:1;}
.svp-ab-lb{font-size:13.5px;color:var(--wh-on-strong);margin-top:9px;line-height:1.4;}
.svp-ab-sub{font-size:11.5px;color:var(--wh-on-quiet);margin-top:6px;line-height:1.45;}
@media (max-width:820px){
  .svp-ab-grid{grid-template-columns:1fr;}
  .svp-ab-disc{grid-template-columns:1fr 1fr;}
}
@media (max-width:480px){.svp-ab-disc{grid-template-columns:1fr;}}
/* Variation 3: the paragraph over a photograph, disciplines counted on a rail. */
.svp-ab-spread{position:relative;min-height:min(520px,84vh);display:flex;align-items:center;
  overflow:hidden;background:transparent;}
.svp-ab-spread .svp-ab-img{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;border-radius:0;}
.svp-ab-spread .svp-wash{position:absolute;top:0;left:0;right:0;bottom:0;inset:0;}
.svp-ab-spread>.wrap{position:relative;z-index:2;width:100%;
  padding-top:clamp(34px,4vw,56px);padding-bottom:clamp(34px,4vw,56px);}
.svp-ab-rail{list-style:none;margin:0;padding:0;}
.svp-ab-rail li{padding:9px 0;border-top:1px solid var(--wh-on-line);font-size:13px;
  color:var(--wh-on-body);display:flex;justify-content:space-between;gap:12px;}
.svp-ab-rail li:first-child{border-top:0;}
.svp-ab-rail li b{font-weight:600;color:var(--wh-on-accent);font-family:var(--serif);}

/* ── outline-free cards, on the blocks that already draw them ─────────
   Dylan on "who signs what": "it's better for having no outlines — it looks
   much cleaner." The border and the shadow come off and the card is held by
   its photograph and by the rule above its note. */
.svp-profile--plain{background:transparent;box-shadow:none;border-radius:0;}
.svp-profile--plain .svp-profile-img{border-radius:2px;}
.svp-profile--plain .svp-profile-body{padding:16px 0 0;}
.svp--navy .svp-profile--plain h3{color:#fff;}
.svp--navy .svp-profile--plain .svp-profile-cred{color:rgba(255,255,255,.68);}
.svp--navy .svp-profile--plain .svp-profile-note{color:rgba(255,255,255,.85);
  border-top-color:rgba(255,255,255,.22);}
.svp-insight--plain{background:transparent;border:0;box-shadow:none;padding:0!important;}
.svp-insight--plain .svp-insight-foot{border-top-color:var(--wh-on-line);}
.svp--navy .svp-insight--plain h3{color:#fff;}
.svp--navy .svp-insight--plain .svp-insight-lede,
.svp--navy .svp-insight--plain .svp-insight-meta{color:rgba(255,255,255,.68);}
.svp--navy .svp-insight--plain .arwbtn{border-color:rgba(255,255,255,.4);color:#fff;}


/* ── MOBILE: THE HERO LINES UP WITH EVERY OTHER SECTION, AND ITS CARDS STACK ──
   Dylan, 26 September 2026, with two screenshots: the hero's side padding "is
   great on all screen sizes except mobile. Then it gets far too wide", and it
   should match the service-list block's gutter there; and the three cards
   "should display one beneath each other; there should not be any side by
   side."

   ⚠ THE GUTTER IS NOT A PADDING ON THE HERO — it is --hero-m, which is
   AUTHORED: compile.ts writes it from the hero's own margin setting (0-64px,
   default 12), and .hero-grid zeroes the .wrap padding it would otherwise sit
   inside. So --hero-m IS the hero's gutter, and a site that has turned it up
   insets the hero far past every section below it — on a phone, where .wrap is
   already at its 22px floor, with the least room to spare.

   ⚠ AND IT IS SET TO .wrap's OWN EXPRESSION, NOT TO 22px. Writing the number
   would be right today and wrong the first time somebody retunes the site
   gutter, and the two would drift apart silently. clamp(22px,4vw,48px) is
   exactly what .wrap uses, so "the same as the service list block" stays true
   by construction. Measured at 390px: .whatdo h2 sits 22 from each edge, and
   so does the hero.

   ⚠ ZEROING IT WAS THE FIRST ATTEMPT AND WAS WRONG — it put the cards hard
   against the screen edge (measured L=0, R=0), because there is no wrap
   padding underneath to fall back to.

   ⚠ It sets the VARIABLE rather than a padding, so everything derived from it
   moves together — the lead column's inset is calc(--hero-textm - --hero-m)
   and would otherwise grow by exactly what the margin gave up.

   ⚠ AND THE SELECTORS ARE DOUBLED ON PURPOSE. A media query adds NO
   specificity, so '.hero' inside one and '.hero' outside it are equal and the
   later declaration simply wins. This override lost that race TWICE while it
   was being written — first sitting beside .hero-mini in a sheet concatenated
   BEFORE this constant, then sitting at the top of this constant above the
   .hero block below. Both times the cards restacked and the gutter did not
   move, which is exactly the half-working symptom that reads as a different
   bug. Doubling the class makes it (0,2,0) and takes source order out of it,
   so this cannot come undone the next time somebody appends a sheet. */
@media(max-width:560px){
  .hero.hero{--hero-m:clamp(22px,4vw,48px);}
  .hero-mini.hero-mini{grid-template-columns:1fr;}
}
.hero{
  --hero-gap:12px; --hero-m:12px; --hero-mt:12px;
  --hero-textm:clamp(22px,4vw,48px);
  --hero-leadtop:12px; --hero-minirow:clamp(180px,26vh,340px);
  --hero-minh:360px; --hero-maxh:900px;
  --hero-sh:1; --hero-sh-up:80px; --hero-sh-mid:42%;
  --hero-ib:1.08; --hero-ic:1.04; --hero-is:1.06;
  padding:var(--hero-mt) 0 var(--hero-m);}
/* The grid carries .wrap, whose clamp() gutter WAS the side margin. */
.hero .hero-grid{padding-left:var(--hero-m);padding-right:var(--hero-m);
  gap:var(--hero-gap);}
/* THE WORDS KEEP THE OLD GUTTER; THE CARDS DO NOT.
   Narrowing --hero-m was for the cards, which want to reach out towards the
   edge of the screen. It moved the eyebrow, the headline and the paragraph
   with them, and set against nothing they read as pinned to the edge — a card
   has an edge of its own to sit inside, a line of text does not.
   So the lead column is inset back to --hero-textm, which defaults to exactly
   the clamp() gutter .wrap used to give the whole grid: unchanged text, the
   narrow margin the cards were narrowed for, and the two no longer one number.
   The inset is measured from the SECTION's edge, so it is what remains after
   the grid's own padding — and max(0px, …) so an outer margin already wider
   than the text margin simply wins rather than pulling the text back out. */
.hero .hero-lead{padding-left:max(0px,calc(var(--hero-textm) - var(--hero-m)));}
.hero .hero-mini{gap:var(--hero-gap);}
/* -- fill the screen --
   --hero-top is this section's distance from the top of the document, published
   by HEROFIT_JS. In the usual case (hero first, masthead above it) that IS the
   masthead's height, so the section's bottom edge lands on the bottom of the
   viewport. Both ends of that are site-wide: --hero-maxh so an unusually tall
   display cannot stretch the cards absurdly, and --hero-minh so a short laptop
   window — or a phone held landscape — cannot squash them into strips. Filling
   the screen is a look, and neither extreme of screen is what it was drawn for.
   NB clamp() resolves min over max if they ever cross, which is the safe way
   round: the floor wins and the cards stay readable. */
.hero.wh-hero-fill{
  min-height:clamp(var(--hero-minh), calc(100svh - var(--hero-top,0px)), var(--hero-maxh));
  display:flex;flex-direction:column;}
.hero.wh-hero-fill .hero-grid{flex:1;min-height:0;}
@supports not (height:100svh){
  .hero.wh-hero-fill{min-height:clamp(var(--hero-minh), calc(100vh - var(--hero-top,0px)), var(--hero-maxh));}
}
/* The mini cards own floor would otherwise fight the fill. The floor lives on
   the ROW instead (see below), so there is one of it rather than one per card. */
.hero.wh-hero-fill .hero-mini .card{min-height:0;}
/* -- THE WORDS SIT LEVEL WITH THE FEATURE CARD --
   Filling the screen hands the grid a height its content did not ask for, and
   grid-template-rows:auto auto splits that surplus BETWEEN the two rows. So
   the words got a deep empty band above them — the lead is align-self:end, so
   its share of the surplus lands on top — and the mini cards got the rest,
   ending up shorter than they should be. Two things looked wrong for one
   reason: the eyebrow floated a third of the way down the section while the
   feature card beside it started at the very top.
   Giving row 1 its content height and row 2 ALL the surplus fixes both at
   once: the words rise to the top of the grid, level with the top of the
   feature card, and the cards below grow to close the gap. --hero-leadtop is
   the breath between that top edge and the eyebrow — small, because the point
   is that they line up.
   minmax() rather than a bare 1fr: a long headline would otherwise take the
   whole grid and leave the cards at nothing. Past that floor the section grows
   instead — it is a min-height, so a hero slightly taller than the viewport is
   the honest failure, not two slivers.
   Scoped to the wide layout, because below 900px the grid is one column of
   three rows and this template does not describe it. */
@media(min-width:901px){
  .hero.wh-hero-fill .hero-grid{grid-template-rows:auto minmax(var(--hero-minirow),1fr);}
  .hero.wh-hero-fill .hero-lead{padding-top:var(--hero-leadtop);}
}
/* -- image tone --
   On .ph, which holds the background image and the optional <video>. The shadow
   is deliberately NOT inside .ph, so it is not brightened along with the photo. */
.hero .card .ph{filter:brightness(var(--hero-ib)) contrast(var(--hero-ic)) saturate(var(--hero-is));}
/* -- the shadow --
   Replaces the full-card .tintb::after wash, which was still 25% opaque at 55%
   of the card height and so dimmed the whole photograph to darken four lines of
   text. Scoped to .hero, so any other block using .tintb is untouched. Black
   rather than the old navy: navy over navy is what made the photographs read
   cold as well as dark. */
.hero .tintb::after{content:none;}
.hero-shade{position:absolute;left:0;right:0;bottom:0;z-index:1;pointer-events:none;
  height:var(--hero-shh,62%);
  background:linear-gradient(180deg,
    rgb(0 0 0 / 0) 0%,
    rgb(0 0 0 / calc(.30 * var(--hero-sh))) var(--hero-sh-mid),
    rgb(0 0 0 / calc(.86 * var(--hero-sh))) 100%);}
/* ── PER-CARD OVERLAY ──────────────────────────────────────────────────────
   A card names a style in data-g and its own strength in --sh-i. A card that
   names NEITHER falls through to the rule above, so this is purely additive:
   every hero that exists today renders as it does today.

   --sh-i falls back to --hero-sh, the block-wide strength, so setting only a
   STYLE on one card still respects the block's own shadow slider.

   Alphas are multiplied, never added, so strength scales the whole gradient
   instead of laying a flat grey over the photograph - a flat wash is what
   made the old hero read heavy. */
.hero-shade[data-g="fade"]{height:var(--hero-shh,62%);
  background:linear-gradient(180deg,rgb(0 0 0 / 0) 0%,
    rgb(0 0 0 / calc(.30 * var(--sh-i,var(--hero-sh,1)))) var(--hero-sh-mid,42%),
    rgb(0 0 0 / calc(.86 * var(--sh-i,var(--hero-sh,1)))) 100%);}
.hero-shade[data-g="soft"]{height:calc(var(--hero-shh,62%) + 14%);
  background:linear-gradient(180deg,rgb(0 0 0 / 0) 0%,
    rgb(0 0 0 / calc(.14 * var(--sh-i,var(--hero-sh,1)))) 58%,
    rgb(0 0 0 / calc(.62 * var(--sh-i,var(--hero-sh,1)))) 100%);}
.hero-shade[data-g="deep"]{height:calc(var(--hero-shh,62%) + 26%);
  background:linear-gradient(180deg,rgb(0 0 0 / 0) 0%,
    rgb(0 0 0 / calc(.42 * var(--sh-i,var(--hero-sh,1)))) 38%,
    rgb(0 0 0 / calc(.95 * var(--sh-i,var(--hero-sh,1)))) 100%);}
.hero-shade[data-g="veil"]{height:100%;
  background:linear-gradient(180deg,
    rgb(0 0 0 / calc(.16 * var(--sh-i,var(--hero-sh,1)))) 0%,
    rgb(0 0 0 / calc(.30 * var(--sh-i,var(--hero-sh,1)))) 58%,
    rgb(0 0 0 / calc(.74 * var(--sh-i,var(--hero-sh,1)))) 100%);}
.hero-shade[data-g="corner"]{height:100%;
  background:linear-gradient(26deg,
    rgb(0 0 0 / calc(.90 * var(--sh-i,var(--hero-sh,1)))) 0%,
    rgb(0 0 0 / calc(.34 * var(--sh-i,var(--hero-sh,1)))) 44%,
    rgb(0 0 0 / 0) 74%);}
.hero-shade[data-g="vignette"]{height:100%;
  background:
    radial-gradient(120% 92% at 50% 16%, rgb(0 0 0 / 0) 44%,
      rgb(0 0 0 / calc(.55 * var(--sh-i,var(--hero-sh,1)))) 100%),
    linear-gradient(180deg, rgb(0 0 0 / 0) 56%,
      rgb(0 0 0 / calc(.70 * var(--sh-i,var(--hero-sh,1)))) 100%);}
/* ⚠ "none" keeps the element in place. HEROSHADE_JS measures it to size the
   band per card, and a removed node would make that script a silent no-op. */
.hero-shade[data-g="none"]{background:none;}
/* .c-in is already z-index:2, so the text stays above this. */
@media(max-width:900px){
  /* One column: filling would make an unusable 3x-viewport stack. */
  .hero.wh-hero-fill{min-height:0;}
  .hero.wh-hero-fill .hero-mini .card{min-height:clamp(240px,46vh,520px);}
}
/* ── ROTATING CARD OPTIONS ────────────────────────────────────────────────
   Dylan, 15 October 2026: a card may have several options; on one site-wide
   clock each card with more than one slides to its next, right to left, with
   no gap, and a progress bar shows how long the current combination has been
   up. HEROROT_JS swaps the classes; everything visible is here.

   A rotating card's options are stacked in ONE grid cell of .hero-rot, so the
   wrapper is as tall as its tallest option and nothing is absolutely placed.
   Every option is still a whole .card, so the hover zoom, the shade sizing,
   the rule measurement and the link styles all apply to it unchanged. The
   hidden ones are visibility:hidden, not display:none, because HEROSHADE_JS
   and HERORULE_JS measure every card, and a card with no box measures as 0.
   ⚠ A hero whose cards each have one option emits none of this markup, so
   none of these rules reach it. */
.hero[data-hr]{--hr-ease:cubic-bezier(.77,0,.18,1);}
.hero-rot{position:relative;display:grid;overflow:hidden;isolation:isolate;border-radius:2px;}
.hero .hero-rot>.card{grid-area:1 / 1;}
.hero-rot>.card:not(.hr-on){visibility:hidden;}
.hero-rot>.card.hr-on{z-index:2;}
.hero-rot>.card.hr-out{visibility:visible;z-index:1;animation:hr-par-out var(--hr-dur,.9s) var(--hr-ease) forwards;}
.hero-rot>.card.hr-in{visibility:visible;z-index:3;animation:hr-slide-in var(--hr-dur,.9s) var(--hr-ease) forwards;box-shadow:-24px 0 40px rgb(0 0 0 / .35);}
.hero[data-hr-fx="push"] .hero-rot>.card.hr-out{animation-name:hr-push-out;}
.hero[data-hr-fx="push"] .hero-rot>.card.hr-in{box-shadow:none;}
.hero[data-hr-fx="wipe"] .hero-rot>.card.hr-out{animation:none;}
.hero[data-hr-fx="wipe"] .hero-rot>.card.hr-in{box-shadow:none;animation-name:hr-wipe-in;}
.hero[data-hr-fx="wipe"] .hero-rot>.card.hr-in .ph{animation:hr-zoom calc(var(--hr-dur,.9s) * 1.6) cubic-bezier(.22,1,.36,1) both;}
/* The words follow the picture a beat later, so the card arrives rather than jumps. */
.hero-rot>.card.hr-in .c-in{animation:hr-text-in calc(var(--hr-dur,.9s) * .9) cubic-bezier(.22,1,.36,1) calc(var(--hr-dur,.9s) * .35) both;}
@keyframes hr-slide-in{from{transform:translateX(100%);}to{transform:none;}}
@keyframes hr-par-out{from{transform:none;filter:brightness(1);}to{transform:translateX(-35%);filter:brightness(.45);}}
@keyframes hr-push-out{from{transform:none;}to{transform:translateX(-100%);}}
@keyframes hr-wipe-in{from{clip-path:inset(0 0 0 100%);}to{clip-path:inset(0 0 0 0);}}
@keyframes hr-zoom{from{transform:scale(1.15);}to{transform:scale(1);}}
@keyframes hr-text-in{from{opacity:0;transform:translateX(28px);}to{opacity:1;transform:none;}}
@keyframes hr-fade{from{opacity:0;}to{opacity:1;}}
/* Asked-for less motion: a short crossfade, nothing slides or zooms. */
@media (prefers-reduced-motion: reduce){
  .hero-rot>.card.hr-out{animation:none !important;filter:none;}
  .hero-rot>.card.hr-in{animation:hr-fade .4s ease both !important;box-shadow:none !important;}
  .hero-rot>.card.hr-in .ph,.hero-rot>.card.hr-in .c-in{animation:none !important;}
}
/* Where each card is in its own cycle. */
.hr-ticks{position:absolute;z-index:4;top:14px;left:16px;display:flex;gap:5px;pointer-events:none;}
.hr-ticks i{display:block;width:16px;height:3px;border-radius:2px;background:rgb(255 255 255 / .38);transition:background .3s;}
.hr-ticks i.on{background:#fff;}
/* The progress bar and its pause button. Without the script nothing rotates,
   so there is nothing for the bar to measure: it is not shown. */
html:not(.wh-js) .hr-bar{display:none;}
.hr-bar{display:flex;align-items:center;gap:12px;}
.hero>.hr-bar{margin:14px var(--hero-m,12px) 0;}
.hero.wh-hero-fill>.hr-bar{flex:none;}
.hero-lead>.hr-bar{width:75%;min-width:min(100%,32ch);margin:0 0 8px;order:5;}
.hr-track{position:relative;flex:1;height:3px;border-radius:2px;background:rgb(255 255 255 / .18);overflow:hidden;}
.hr-fill{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--pink);transform-origin:left;transform:scaleX(0);}
.hr-pause{flex:none;width:28px;height:28px;border-radius:50%;border:1px solid rgb(255 255 255 / .4);background:transparent;color:#fff;cursor:pointer;display:grid;place-items:center;padding:0;}
.hr-pause:hover{border-color:#fff;}
.hr-pause:focus-visible{outline:2px solid #fff;outline-offset:2px;}
.hr-pause svg{width:10px;height:10px;fill:currentColor;}
.hr-pause .hr-i-play{display:none;}
.hr-pause[aria-pressed="true"] .hr-i-play{display:inline;}
.hr-pause[aria-pressed="true"] .hr-i-pause{display:none;}
@media(max-width:900px){
  .hero-rot.hero-feature{grid-row:2;min-height:360px;}
}


/* THE INDEX IS AN ATTRIBUTE, NOT A STYLE ATTRIBUTE. A style="--rv-i:2" was the
   obvious way to write it and the wrong one: several of the elements that
   animate already carry a style attribute of their own (a block image sets its
   own background-image there), and the second one is simply dropped. Sixteen
   rules cover every sequence any block has, and compose with whatever else the
   element is already doing. */
.wh-rv [data-rv]{--rv-i:0;}
.wh-rv [data-rv="1"]{--rv-i:1;}.wh-rv [data-rv="2"]{--rv-i:2;}
.wh-rv [data-rv="3"]{--rv-i:3;}.wh-rv [data-rv="4"]{--rv-i:4;}
.wh-rv [data-rv="5"]{--rv-i:5;}.wh-rv [data-rv="6"]{--rv-i:6;}
.wh-rv [data-rv="7"]{--rv-i:7;}.wh-rv [data-rv="8"]{--rv-i:8;}
.wh-rv [data-rv="9"]{--rv-i:9;}.wh-rv [data-rv="10"]{--rv-i:10;}
.wh-rv [data-rv="11"]{--rv-i:11;}.wh-rv [data-rv="12"]{--rv-i:12;}
.wh-rv [data-rv="13"]{--rv-i:13;}.wh-rv [data-rv="14"]{--rv-i:14;}
.wh-rv [data-rv="15"]{--rv-i:15;}
.js-rv .wh-rv:not(.is-in) [data-rv]{opacity:0;transform:translateY(var(--rv-lift,18px));}
/* The one style pass in which the runtime hides everything (rv-arm, REVEAL_JS):
   whatever was drawn before it goes straight to hidden instead of fading out. */
.rv-arm .wh-rv,.rv-arm .wh-rv *{transition:none!important;}
/* NO will-change. IT WAS THE CAUSE OF THREE SEPARATE COMPLAINTS.
   It used to sit on every [data-rv] element on the page from first paint, and
   come off again on .is-settled. Each of those is a compositing layer being
   built and torn down, and all three symptoms are that churn:

     · JERKINESS. A long page promotes dozens of elements at once, before any
       of them is near the viewport — rasterising and holding layers for work
       that has not started. The hint is meant to be given sparingly and just
       before it is needed; given to everything it is not a hint, it is a bill.
     · A ONE-OR-TWO-PIXEL DRIFT AFTER THE ANIMATION HAD FINISHED. The settle
       timer removed will-change at a moment unrelated to any repaint, so the
       element left its layer and the next scroll painted it at its ordinary
       sub-pixel position rather than the layer's rounded one. Nothing moved;
       it was re-rounded, which looks exactly like moving.
     · A NEIGHBOUR'S SHADOW BEING CROPPED MID-ANIMATION. A promoted element
       composites above content that overflows the section before it — such as
       the services list's circle, whose shadow falls onto the next section on
       purpose. See the z-index note on .whatdo, which fixes that end of it.

   Browsers already composite an opacity/transform transition without being
   told to, and they demote at the end of the transition, which is the one
   moment a repaint is happening anyway. */
.js-rv .wh-rv [data-rv]{
  transition:opacity var(--rv-dur,620ms) ease, transform var(--rv-dur,620ms) cubic-bezier(.22,1,.36,1);
  transition-delay:calc(var(--rv-i,0) * var(--rv-step,160ms));}
.js-rv .wh-rv.is-in [data-rv]{opacity:1;transform:none;}
/* ⚠ THE ONE-FRAME REWIND (is-rewind) THAT STOOD HERE WAS RETIRED ON 16 OCTOBER
   2026. It snapped a half-faded section back to hidden so a replay kept its
   stagger, and that snap is the jump Dylan then asked to be rid of: "it always
   finishes a previous animation before starting a new one". The runtime now
   queues a change of direction until the parts have finished moving, so a
   replay starts from a section that really is hidden and needs no rewind. See
   afterThis() in REVEAL_JS. */
/* THE INSIGHTS SECTION KEEPS ONE RHYTHM, AND IT IS THE LIST'S.
   Dylan, first: "the items within the RHS should fade in in much quicker
   succession than the other bits... less stagger." Then, having seen it: "the
   RHS now fades in at a good speed — can you make it so that the stagger
   between the other animations in that section is the same as the RHS items
   stagger is?"

   So the tighter beat stops being the list's exception and becomes the whole
   section's rhythm. Every part — the heading, the featured piece, each post
   down the right, the link at the foot — follows the one before it at a THIRD
   of the site's step. One ladder, evenly spaced, from the top of the section to
   the bottom of the column.

   A third of the step rather than a fixed number of milliseconds because the
   RELATIONSHIP is what is being said: a site that slows its whole rhythm down
   slows this with it, and one that speeds it up cannot end up with a section
   that staggers more than the rest of the page.

   This also retires --rv-lead. It existed to let the list restart its count
   from nought while the parts above it kept the full step — two rhythms needed
   a join between them. With one rhythm there is nothing to join: the plain
   index does the whole job, and the two-file agreement that the lead depended
   on (the CSS's number having to equal the block's first list index) cannot go
   wrong any more because it no longer exists.

   Scoped by the section's own class rather than by :has(), which keeps it
   working on the older browsers the site still serves, and placed BEFORE the
   settled/turning rules below: those match at the same weight and must still
   win, because a section that has landed or is reversing has no stagger at
   all. */
.js-rv .wh-rv.insights [data-rv]{
  --rv-step-ins:calc(var(--rv-step,160ms) / 3);
  transition-delay:calc(var(--rv-i,0) * var(--rv-step-ins));}
/* is-settled still earns its place: it drops the entrance delay, so a hover
   lift or a link underline afterwards is instant rather than inheriting a
   900ms wait from the part's own arrival. */
.js-rv .wh-rv.is-settled [data-rv]{transition-delay:0ms;}
/* ⚠ INERT SINCE 16 OCTOBER 2026, AND KEPT: nothing adds is-turning any more,
   because a change of direction now waits for the parts to finish (afterThis() in
   REVEAL_JS). The navy masthead's own rules still name the class, so the rule
   costs nothing and removing it would only make those selectors look wrong.
   The original reasoning follows.
   CHANGING YOUR MIND IS ONE GESTURE, NOT A RE-DEAL.
   Scroll down past a section, back up so it starts to leave, then straight
   down again: the parts were still in flight, and the stagger was applied to
   them AGAIN on the way back. A part that was 60% gone sat frozen for its own
   delay before returning, so the section came back in pieces, from different
   places, at different times — which reads as broken rather than as a
   reversal. While a section is turning, every part turns together and at once;
   the stagger belongs to an arrival, and this is not one. Nothing is queued and
   nothing waits, so a fast reversal costs no more than a slow one. */
.js-rv .wh-rv.is-turning [data-rv]{transition-delay:0ms;}
/* THE HERO IS TWO GROUPS, NOT A LIST. Its words and its pictures each have a
   delay of their own, so "let the eye rest on the headline" is one number to
   change rather than a re-indexing of every part. */
.js-rv .wh-rv[data-rv-hero] [data-rv-part="text"]{transition-delay:var(--rv-hero-text,80ms);}
.js-rv .wh-rv[data-rv-hero] [data-rv-part="image"]{transition-delay:var(--rv-hero-img,460ms);}
@media (prefers-reduced-motion: reduce){
  /* Everything at once, and no movement — not the same animation played fast,
     which is what the setting is asking us not to do. */
  .js-rv .wh-rv [data-rv]{transition:none!important;transition-delay:0ms!important;
    opacity:1!important;transform:none!important;}
}

/* THE LOGO'S OWN EM, HELD STILL.
   Registered so it computes to a LENGTH at .wh-lg and inherits as that length,
   rather than as the token "1em" re-resolving against whatever font-size the
   descendant happens to have. Without it a gap between the strapline and the
   name came out in the STRAPLINE's em — a quarter of what the control said —
   and every space in the lockup meant something different depending on which
   element it landed on. A browser that does not know @property falls back to
   the token, which is exactly the old behaviour rather than a broken one. */
@property --lg-em{syntax:"<length>";inherits:true;initial-value:16px;}
/* vertical-align for the block contexts this may one day land in — inline-flex
   otherwise sits on the text baseline and collects the descender below it. */
.wh-lg{--lg-em:1em;display:inline-flex;align-items:center;vertical-align:middle;line-height:1;}
.wh-lg--block{background:var(--lg-bg,transparent);padding:calc(var(--lg-pad,0)*var(--lg-em));border-radius:var(--lg-rad,0);}
.wh-lg--rabove-l,.wh-lg--rabove-c,.wh-lg--rabove-r{flex-direction:column;}
.wh-lg--rabove-l{align-items:flex-start;}
.wh-lg--rabove-c{align-items:center;}
.wh-lg--rabove-r{align-items:flex-end;}
.wh-lg-t{display:inline-flex;flex-direction:column;min-width:0;}
.wh-lg--al .wh-lg-t{align-items:flex-start;text-align:left;}
.wh-lg--ac .wh-lg-t{align-items:center;text-align:center;}
.wh-lg--ar .wh-lg-t{align-items:flex-end;text-align:right;}
.wh-lg-n{display:inline-flex;flex-direction:row;align-items:baseline;min-width:0;}
.wh-lg--stack .wh-lg-n{flex-direction:column;align-items:inherit;}
.wh-lg-w1,.wh-lg-w2,.wh-lg-d{white-space:nowrap;font-style:normal;}
.wh-lg-w1{font-family:var(--lg-f1,var(--serif));font-weight:var(--lg-w1,600);font-style:var(--lg-i1,normal);font-size:calc(var(--lg-s1,1)*1em);letter-spacing:calc(var(--lg-t1,0)*.001em);line-height:var(--lg-l1,1);color:var(--lg-c1,inherit);text-transform:var(--lg-u1,none);}
.wh-lg-w2{font-family:var(--lg-f2,var(--lg-f1,var(--serif)));font-weight:var(--lg-w2,var(--lg-w1,600));font-style:var(--lg-i2,normal);font-size:calc(var(--lg-s2,var(--lg-s1,1))*1em);letter-spacing:calc(var(--lg-t2,var(--lg-t1,0))*.001em);line-height:var(--lg-l2,var(--lg-l1,1));color:var(--lg-c2,var(--lg-c1,inherit));text-transform:var(--lg-u2,none);}
.wh-lg-d{font-family:var(--lg-fd,var(--body));font-weight:var(--lg-wd,700);font-style:var(--lg-id,normal);font-size:calc(var(--lg-sd,.26)*1em);letter-spacing:calc(var(--lg-td,340)*.001em);line-height:var(--lg-ld,1);color:var(--lg-cd,inherit);text-transform:var(--lg-ud,uppercase);}
/* The two words are separated by a MARGIN, not a gap: the whole point of the
   control is that a lockup can be tighter than the space its face ships, and
   flex gap cannot go below zero. */
.wh-lg--row .wh-lg-w2,.wh-lg--row .wh-lg-sep{margin-left:calc(var(--lg-gap,.26)*var(--lg-em));}
.wh-lg--row .wh-lg-sep+.wh-lg-w2{margin-left:calc(var(--lg-gap,.26)*var(--lg-em));}
.wh-lg--stack .wh-lg-w2{margin-top:calc(var(--lg-gap,0)*var(--lg-em));}
/* The gap between the strapline and the name is put on the NAME, not on the
   strapline: .wh-lg-n has no font-size of its own, so a length on it is the
   logo's. :not(:first-child) is the strapline above; :not(:last-child) below.
   Both without :has(), which this does not need. */
.wh-lg-t>.wh-lg-n:not(:first-child){margin-top:calc(var(--lg-dgap,.5)*var(--lg-em));}
.wh-lg-t>.wh-lg-n:not(:last-child){margin-bottom:calc(var(--lg-dgap,.5)*var(--lg-em));}
.wh-lg-sep--rule{width:1px;align-self:stretch;background:currentColor;opacity:.5;}
/* Same reason as the words: the roundel wants to be able to overlap. */
.wh-lg--rleft .wh-lg-crest{margin-right:calc(var(--lg-rgap,.32)*var(--lg-em));}
.wh-lg--rright .wh-lg-crest{margin-left:calc(var(--lg-rgap,.32)*var(--lg-em));}
.wh-lg--rabove-l .wh-lg-crest,.wh-lg--rabove-c .wh-lg-crest,.wh-lg--rabove-r .wh-lg-crest{margin-bottom:calc(var(--lg-rgap,.32)*var(--lg-em));}
/* --lg-ry nudges the disc up or down from where align-items centred it, so it
   can be optically centred on the wordmark when a strapline pulls the block's
   mid-line above the name. Done with position, not transform, so a header's
   own hover transform on the disc never fights it. Positive is down. */
.wh-lg .wh-lg-crest{width:calc(var(--lg-rsize,1.15)*var(--lg-em));height:calc(var(--lg-rsize,1.15)*var(--lg-em));flex:0 0 auto;position:relative;top:calc(var(--lg-ry,0)*var(--lg-em));}
/* THE TITTLE, IN ITS OWN COLOUR.
   The tittle is recoloured IN PLACE by painting the letter with a two-stop
   gradient and clipping that paint to the glyph (background-clip:text): the top
   band up to --lg-dotcut is the dot colour, everything below is the letter's own
   colour. Because it is the SAME pixels being recoloured — not a second copy
   laid over the first — the dot can never float above the letter and no
   original-colour dot can peek out behind it. The cut is a control, not a
   constant, because where the tittle ends is a property of the FACE, and this
   studio offers twenty. */
.wh-lg-i{-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;background-image:linear-gradient(to bottom,var(--lg-dot,inherit) 0,var(--lg-dot,inherit) var(--lg-dotcut,30%),currentColor var(--lg-dotcut,30%));}

/* ── A LOGO INSIDE A HEADER ──────────────────────────────────────────────
   The logo's own size IS the size that header already draws its wordmark at,
   so a logo that changes nothing is the wordmark it replaced — which is what
   makes the two seeded logos honest rather than approximate. --lg-scale is the
   one multiplier the Logos card sets per header, and it defaults to 1. */
/* --lg-fs IS THE WHOLE VALUE AND --lg-scale LIVES IN ITS FALLBACK. An author
   who has typed an exact pixel height gets exactly it; everyone else, and every
   logo that has never been measured, keeps the multiplier they had. Nesting
   them the other way round would make the height a multiple of a multiple. */
.masthead .wh-lg{font-size:var(--lg-fs,calc(clamp(28px,3.4vw,40px)*var(--lg-scale,1)));color:#fff;}
.drophdr .wh-lg,.wh-fs-hdr .wh-lg{font-size:var(--lg-fs,calc(19px*var(--lg-scale,1)));color:var(--navy);}
/* Three classes, deliberately: .wh-fs-hdr .crest sizes the sheet's disc from
   MEGA_MENU_CSS, which is appended AFTER this stylesheet, so at two classes the
   logo's own size would lose there and only there. */
/* var() SUBSTITUTES TEXT, and --lg-rsize already carries its unit, so the
   calc() this used to be became calc(1.45em*1em) — a length times a length,
   which is invalid, so the whole declaration was dropped and the disc filled
   its container. Named straight, no arithmetic. */
.wh-lg .crest.wh-lg-crest{width:calc(var(--lg-rsize,1.15)*var(--lg-em));height:calc(var(--lg-rsize,1.15)*var(--lg-em));}
/* The masthead's brand cluster is a centred COLUMN of crest, strapline and
   wordmark. A logo is the whole cluster and arranges itself, so the column has
   to stand down — as a ROW, not as a block: a block would put the logo on a
   text line and give the header the body font's descender as 8px of dead space
   under the mark, which is what it did. The two other headers are already flex
   rows and are left alone. */
.masthead .brandtop.wh-brand-lg{flex-direction:row;align-items:center;justify-content:center;}
/* PHONES: A SMALLER LOGO IN THE SAME SPACE (Dylan, 17 October 2026, with a
   phone screenshot: "perhaps the logo could be slightly smaller at the top?
   Keep it so that the top bit as a whole still occupies the same amount of
   space, but … a bit more padding above and below it"). A transform, not a
   size, on purpose: it draws the logo at 82% while its layout box keeps its
   full height, so the header is exactly as tall as before and the room the
   logo gives up becomes equal space above and below it. That also keeps
   MASTFIT_JS's measured --wh-mast-h, which the hero's "room for a transparent
   header" pads by, unchanged. Any kind of logo (lockup, upload, flattened,
   the plain crest and wordmark) and any height setting shrinks by the same
   share. The masthead only; the sticky bar and the menu sheet are untouched.
   ⚠ A left-aligned masthead shrinks towards its left edge so it keeps its
   gutter. */
@media (max-width:700px){
  .masthead .brandtop{transform:scale(.82);transform-origin:50% 50%;}
  .masthead.wh-mast-left .brandtop{transform-origin:0 50%;}
}

/* ── THE MASTHEAD, LEFT-ALIGNED ──────────────────────────────────────────
   The arrangement the drop-in header has always used, offered at the top too.
   A grid rather than a row so the strapline still sits above the name with the
   roundel beside BOTH of them — in a flex row the three would land side by
   side, which is not the same design at a different alignment, it is a
   different design. */
/* THE HORIZONTAL PADDING HAS TO COME BACK. .masthead .inner sets its padding
   as 26px top and bottom, zero left and right, which overrides the .wrap padding it also carries — harmless while the
   content is centred, and hard against the viewport edge the moment it is not.
   The same clamp() the wrap and the drop-in header use, so the three headers
   share one gutter. */
.masthead.wh-mast-left .inner{flex-direction:row;align-items:center;padding:20px clamp(22px,4vw,48px);}
.masthead.wh-mast-left .brandtop{display:grid;grid-template-columns:auto 1fr;column-gap:14px;align-items:center;justify-items:start;text-align:left;}
.masthead.wh-mast-left .brandtop.wh-brand-lg{display:flex;align-items:center;justify-content:flex-start;}
.masthead.wh-mast-left .crest{grid-row:1/3;}
.masthead.wh-mast-left .cm{margin-top:0;}
.masthead.wh-mast-left .pe{margin-top:0;}
.masthead.wh-mast-left .nav{margin-left:auto;}.masthead.wh-mast-left .nav{margin-top:0;}
/* Clearance for the fixed corner, exactly as the drop-in header reserves it
   (see the note there). The CENTRED masthead needs none: its row is centred and
   wraps onto a second line well clear of the corner. A left-aligned one is on
   the same line as the corner and does not. */
/* THE SAME GAP, IN THE OTHER LAYOUT. The left-aligned masthead reserves its
   corner space the same way the drop header does, and carried the same
   hard-coded 14px — so fixing only the drop header would have left the top
   header still wrong, in the one layout that puts its row on the corner's
   line. --wh-navgap on .masthead resolves to that row's own 16–32px clamp. */
.masthead.wh-mast-left.wh-hdr-nav .inner{padding-right:calc(clamp(22px,4vw,48px) + var(--wh-corner-w) + var(--wh-navgap,14px));}

/* ── THE FRAME ───────────────────────────────────────────────────────────
   A wrapper round the lockup, not a property of it. Asked for by Dylan on 25
   September 2026: a white or coloured border, a chosen proportion, a chosen
   thickness, and padding on each of the four sides independently.

   Every value is in PIXELS and every one comes from a variable with a zero or
   transparent fallback, so a logo whose frame card has never been opened emits
   no variables and this whole block computes to nothing visible. That is what
   lets logoHtml emit the wrapper only when the frame is switched on without the
   two ever disagreeing.

   box-sizing is border-box because the proportion somebody asks for is the
   proportion of the PICTURE — border and padding included. A 1:1 frame whose
   square was only the content box would be visibly oblong the moment a border
   was added, which is the sort of wrongness nobody can name but everybody
   sees. */
/* THE FLATTENED LOGO. Sized from the font-size like the lockup it replaces, so
   every header control — the multiplier, the exact height, the overhang —
   keeps working without knowing which of the two it is looking at. The width
   follows the image's own proportions; the width/height attributes on the tag
   reserve the box before the file lands so the header does not jump. */
/* ⚠ THE PICTURE'S OWN ROUNDED CORNERS, FROM ONE SHORTHAND.
   Dylan, 27 September 2026, asked to round each corner of an uploaded photo logo
   separately in pixels, and for the cut corner to be TRANSPARENT rather than filled.
   Both are properties of putting the radius on the IMAGE: border-radius clips the
   image and paints nothing in the corner it removes, so what shows through is
   whatever was already behind the logo.

   ⚠ NOT ON .wh-lg-fr, WHICH ALREADY HAS A RADIUS. That one is the FRAME's, and the
   frame's values are all gated on the frame being switched on — so rounding a
   photograph through it would turn a border and four paddings on as well. It is also
   the wrong element: a radius on the frame rounds the frame while the image inside
   keeps its square corners and paints over the curve, which is the same mistake the
   masthead submenu's panel needed overflow:hidden for earlier today.

   The default is 0, so every logo that has never been rounded is unchanged. */
/* ⚠ HOW BIG THE PICTURE IS, AND WHY IT IS NOT A FONT-SIZE.
   Dylan, 28 September 2026: "when I set the logo sizes in the sticky header and in
   the top header in the header & footer settings, they don't seem to change the
   heights?" He was right, and this declaration is where it went wrong. The two size
   controls both end up as a font-size on .wh-lg — and an uploaded picture is not a
   .wh-lg, it is a bare .wh-lg-img. So 1em here resolved against whatever the link
   inherited from the body, 16px, and the image stood 5.375 x 16 = 86px tall at every
   setting: nothing, x2, 40px and 120px all measured 86px in Chromium, in both headers.

   --lg-ih IS THE HEIGHT THE AUTHOR TYPED, in pixels, straight onto the image, with
   width:auto beside it so the file's own proportions give the width — literally what
   he asked for next in the same message ("a px height with the width automatically
   set from that"). No measurement is in the loop, which matters: for an UPLOAD there
   is no lockup to measure and hEm is a stand-in (the file's proportion times six),
   so anything that divided by it would be only as accurate as that fiction.

   ⚠ THE FALLBACK IS THE OLD BEHAVIOUR TIMES THE MULTIPLIER, NOT A NEW DEFAULT. With
   no height typed, a picture keeps the em-derived size it has today, so no live page
   moves by a pixel on the day this ships — and --lg-scale is folded in so that the
   multiplier a site may already have stored finally does something here too, instead
   of being read by a rule this element never matched. A site with a picture and a
   stored multiple WILL therefore change size; that is the control it typed starting
   to work, and the Header & footer card says so on screen rather than leaving it to
   be discovered (AGENTS.md §2.7).

   ⚠ NO BACKTICKS IN THIS COMMENT, AND THE FIRST DRAFT HAD FOUR. LOGO_CSS is a
   template literal, so a backtick in prose ends the literal and the file stops
   parsing — which is the same class of mistake as the three html tags a comment
   in this file was carrying on 27 September, and it is why the names below are
   written bare.

   ⚠ WHAT THIS DOES NOT FIX: a FLATTENED lockup's image is trimmed to its painted ink
   while hEm measures the line box, so a built raster's height has never been quite
   the height of the lockup it replaced. A typed height is now exact for what is
   actually drawn, which is the number a reader sees; the em fallback keeps the old
   discrepancy. Said out loud rather than left as a surprise. */
.wh-lg-img{display:block;height:var(--lg-ih,calc(var(--lg-rh,1)*1em*var(--lg-scale,1)));width:auto;border-radius:var(--lg-irad,0);}
/* THE SHADOW ROUND A HEADER LOGO.
   One rule must cover the live lockup, a flattened WebP and an uploaded image
   alike — and a framed logo's shadow must trace the FRAME rather than the type
   inside it, which is what somebody asking for "a shadow all around the logo"
   means. filter is inherited by nothing and composites the whole subtree once,
   which is also why this cannot be done with box-shadow: that would draw a
   rectangle round the bounding box. Absent by default, so a header that has
   never set one is untouched.

   ⚠⚠ 29 SEPTEMBER 2026 — MOVED OFF THE LINK AND ONTO ITS DIRECT CHILDREN, AND
   THE REASON IS A REAL DEFECT DYLAN PHOTOGRAPHED. This said
   .wh-brand-lg{filter:...} — the LINK — which is also the element carrying the
   backlight's ::before. **A CSS filter applies to an element AND its
   pseudo-elements, and the result is composited into a filter region; anything
   reaching outside that region is clipped, at a hard straight edge.** The halo is
   deliberately a circle LARGER than the logo, so on any header with BOTH a shadow
   and a backlight switched on, the glow was sliced off. Dylan, with a screenshot:
   "the backlight behind the masthead logo seems to be rendering strangely, with
   the backlight light getting cut off on the LHS."

   Naming the three direct children instead keeps every word of the paragraph
   above true — .wh-lg-fr is the frame wrapper, so a framed logo's shadow still
   traces the frame; .wh-lg is the live lockup; .wh-lg-img is a flattened or
   uploaded picture, which is a direct child when there is no frame. The link
   itself is now unfiltered, so its ::before is no longer inside a filter region
   and the halo draws whole.

   ⚠ DIRECT CHILDREN (>) ON PURPOSE, NOT DESCENDANTS. A framed logo is
   .wh-lg-fr > .wh-lg-img; a descendant selector would match BOTH and apply the
   drop-shadow twice — once to the picture and again to the frame around it —
   which is visibly heavier and is exactly the doubling the stacked shape-glow in
   logoBacklight creates on purpose and this does not want. */
.wh-brand-lg > .wh-lg-gr > .wh-lg,
.wh-brand-lg > .wh-lg-gr > .wh-lg-fr,
.wh-brand-lg > .wh-lg-gr > .wh-lg-img{filter:var(--lg-shadow,none);}
/* ⚠ THE GROWTH WRAPPER CARRIES NOTHING BUT THE GROWTH. No filter, no halo, no
   background — that is the whole reason it exists; see 'grown()' in logos.ts for
   the three-element argument. inline-flex so a transform applies to it at all and
   so it wraps the logo without adding a line box of its own, and
   vertical-align:middle to match the lockup it now stands in for. */
.wh-lg-gr{display:inline-flex;vertical-align:middle;}
/* THE LIGHT BEHIND THE LOGO — a point source at the logo's centre, radiating out.
   Dylan asked for this on 28 September 2026 in preference to a shadow: lit from
   behind, subtly, rather than cast in front. The numbers below are his, chosen in
   the interactive study and adopted as LOGO_BACKLIGHT_DEFAULTS.

   ⚠ MATCHED ONLY BY .wh-bl, WHICH compile.ts EMITS ONLY WHEN THE LIGHT IS ON.
   The obvious alternative is one unconditional rule whose variables default to
   zero, and it looked cheaper until it was written out: every branded header in
   the estate would then carry a pseudo-element, a gradient and a blur filter
   whose only purpose is to be invisible. A class costs six bytes of markup on the
   sites that asked for it and nothing at all on the ones that did not.

   ⚠ isolation:isolate, NOT z-index:0. The halo sits at z-index:-1 so it paints
   behind the logo and in front of the bar it is standing on, and that needs a
   stacking context on the link. Setting z-index here would ALSO have entered a
   fight this rule cannot reliably win: .drophdr--lgtop .brand already sets
   z-index:2 at the same (0,2,0) specificity, so which one applied would depend on
   which sheet was concatenated last. isolation creates the context without
   claiming a layer, so both declarations are true at once.

   ⚠ THE BOX IS A FIXED DIAMETER, NOT AN INSET ROUND THE LOGO. Light from a point
   does not know the shape of what it is behind, and an inset halo grows with the
   logo, which is the one thing the study showed reading as fake. So the reach is
   an absolute number and a bigger logo simply sits further into its own light.

   ⚠ WHAT THIS DOES NOT DO: it cannot light the page BEHIND a masthead that is
   painted over the section below it, because the halo is inside the link. On a
   transparent masthead the light lands on the hero; on a solid one it lands on
   the navy. Both are wanted. But nothing here spills onto the page beneath a
   header that clips, and no header in this sheet clips today. If one ever does,
   the halo will be trimmed at the bar edge and this comment is where to start.

   ⚠⚠ 16 OCTOBER 2026 — THE BOX NOW CARRIES ITS OWN BLUR, AS PADDING. Dylan, with a
   screenshot of the home page's transparent masthead: "the backlight behind the
   top logo … seems to be getting cut off (at the bottom)". It was, in a straight
   line exactly the .inner's 30px bottom padding below the logo, which is the
   bottom of the masthead's own box. The glow is a blur filter on a box only
   --lg-bl-size across, so everything the blur spreads beyond that box exists
   ONLY as a filter outset. A browser that sizes the masthead's compositing layer
   from the boxes inside it, and not from their filter outsets, cuts the glow off
   at the masthead's edge. Chromium here draws it whole; Dylan's browser did not.
   So the box is padded by three blur radii (where a Gaussian has all but faded
   out) and the gradient is kept to the content box, so what is PAINTED is the
   same circle as before, but the light the blur spreads now lands inside a box
   any browser counts. box-sizing is stated because a reset elsewhere may make
   pseudo-elements border-box, which would shrink the circle by the padding.
   ⚠ NOT VERIFIED IN SAFARI: there is no WebKit in the sandbox. Checked in
   Chromium only, where the before and after renders differ by at most 2/255.

   ⚠ THE MASTHEAD ONLY, ON PURPOSE (review of #505). The padded box is a real box,
   so unlike a filter outset it counts towards the page's scrollable area. The
   footer reuses wh-bl, and softness can reach 400px, which pads by 1200px: on the
   footer that box would hang below the end of the page and leave a long blank
   scroll after it. The masthead sits at the top with the page below it, and the
   sticky bar is position:fixed, so neither adds scroll. The footer's glow is left
   on the plain rule, so it could still be cut at the footer's edge in the same
   browser; nobody has reported that, and the fix for it is not this one. */
.wh-brand-lg.wh-bl{position:relative;isolation:isolate;}
.wh-brand-lg.wh-bl::before{content:"";position:absolute;inset:50% auto auto 50%;width:var(--lg-bl-size,0px);height:var(--lg-bl-size,0px);transform:translate(-50%,-50%);border-radius:50%;pointer-events:none;z-index:-1;filter:blur(var(--lg-bl-blur,0px));background:radial-gradient(circle closest-side,rgb(var(--lg-bl-rgb,255 255 255)/var(--lg-bl-a,0)) 0%,rgb(var(--lg-bl-rgb,255 255 255)/calc(var(--lg-bl-a,0)*0.55)) var(--lg-bl-mid,50%),rgb(var(--lg-bl-rgb,255 255 255)/0) 100%);}
/* ⚠⚠ 19 OCTOBER 2026 — AND THEN NO FILTER AT ALL, ON THE MASTHEAD. The padding
   above shipped and Dylan still saw the cut "on page load", sometimes righting
   itself "after a while". Coming and going as the page settles is the
   compositor, and the blur filter is drawn on a surface the compositor sizes
   and clips for itself, so a bigger box did not reach it. The masthead's halo
   now draws haloGradient() (logos.ts): the same light with its softness already
   computed into the stops, as a plain background on the padded box, with
   filter:none. A background is painted wherever its box is. The padding is
   still three blur radii, which is where that gradient ends, and the origin is
   the padding box so closest-side is the whole of it. The breathe pulse is on
   opacity and is untouched.
   ⚠ The sticky bar and the footer keep the filter: nobody has seen either cut,
   and the scroll argument above still applies to the footer.
   ⚠ NOT VERIFIED IN DYLAN'S BROWSER: Chromium here never showed the cut. */
.masthead .wh-brand-lg.wh-bl::before{--lg-bl-diameter:calc(var(--lg-bl-size,0px) + 6*var(--lg-bl-blur,0px));box-sizing:content-box;width:min(var(--lg-bl-diameter),var(--lg-bl-fit-width,var(--lg-bl-diameter)));height:var(--lg-bl-diameter);padding:0;border-radius:0;filter:none;background:var(--lg-bl-grad,none);background-size:var(--lg-bl-diameter) var(--lg-bl-diameter);background-position:center;background-repeat:no-repeat;}
/* ⚠⚠ 25 OCTOBER 2026 — THE LIGHT GETS A LAYER OF ITS OWN. Dylan, iPad Safari,
   on the enquiry page and on "Learning for life": the glow "sometimes getting
   cut off", a hard straight edge where the masthead meets the hero. Neither the
   filter (gone, 19 Oct) nor the clip (gone, 22 Oct) was left to blame. What both
   heroes have is a compositing layer of their own: the enquiry hero's photo
   cards zoom on will-change:transform, the home hero's roundel parallaxes. The
   halo is painted with the masthead into whatever layer the masthead lands in,
   and when WebKit's overlap pass does not lift that layer above the hero's, the
   hero's ground is drawn over the half of the light that spills onto it. Which
   layer wins depends on when the hero's layers appear, so it comes and goes.
   will-change:transform puts the halo on its own layer, ordered by its z-index
   inside the masthead (z-index:60), above every hero; its box is the full
   padded diameter, so the layer holds all of the light. One layer, only on a
   site with the light switched on. Chromium draws it identically either way.
   ⚠ NOT VERIFIED IN SAFARI: there is no WebKit in the sandbox. */
.masthead .wh-brand-lg.wh-bl::before{will-change:transform;}
/* ⚠⚠ 18 OCTOBER 2026 — "NEITHER ADDS SCROLL" ABOVE WAS TRUE DOWNWARDS ONLY. Dylan,
   on his phone: "some of it seems to be wider than the screen, which means you can
   scroll right as well as up and down … a thin slither of space to the RHS". The
   padded halo is 170px plus six blur radii across, centred on the logo, so on a
   phone it reaches past the right edge of the screen. Measured in Chromium at
   384px with the live header settings: the masthead's scrollWidth was 404, and the
   page with it. The document's own overflow-x:clip does not stop this on a phone: Chrome for
   Android widens the layout viewport to the content and zooms the page out to fit,
   and that one widening was all four of his header reports that day: the gap on
   the right, the sticky bar's top sliding off when the toolbar hid, the burger
   sheet's first link under the logo tab, and Enquire and the burger moving when
   the sheet opened (the scroll lock took the overflow away, so the zoom changed).
   overflow-x:clip on the masthead trims the box at the masthead's own left and
   right edges, which are the screen's, so nothing that was visible is lost, and
   it leaves y visible, so the glow still spills down onto the hero.
   ⚠ clip, not hidden: hidden would make the masthead a scroll container and force
   overflow-y to auto, which would cut the glow at the bottom edge again (#505). */
.masthead{overflow-x:clip;}
/* 22 OCTOBER 2026 — THE LIT HEADER NO LONGER OWNS A CLIPPING EDGE.
   Dylan's Safari 26.6 screenshot has a hard cut exactly at the masthead bottom.
   The same live page did NOT reproduce it in bundled WebKit 26.5, so this is a
   structural removal of that boundary, not a claim to have reproduced Safari's
   compositor fault. Simply removing the clip made a 390px page 407px wide.

   Bound the halo's own paint box horizontally instead. Its background keeps the
   full diameter (including the three softness radii on either side), centred in
   that narrower box, so the circular light is neither resized nor stretched.
   No radius on the rectangle: 50% would clip that circle into a narrower ellipse.
   Inline-size containment supplies the HEADER width, including in a narrow
   editor preview or beside a classic scrollbar; it does not contain paint.

   The mobile brand is scaled to .82. Divide the available width by that same
   scale before its transform, or the light would stop short of the screen edge.
   For a left-aligned brand, twice the distance from its centre to the right edge
   is 2*(header width - gutter)/scale - brand width. The percentage is resolved
   on the pseudo-element against that brand's unscaled width. Its left overflow
   is outside the viewport; its right edge cannot widen the document.
   The inner wrapper is full width; its left gutter is the clamp used below.
   Unlit headers, sticky logos and footer logos retain their existing rules.
   Without container units, keep the original clip and the full-diameter halo. */
@supports(width:1cqw){
.masthead:has(.wh-brand-lg.wh-bl){overflow:visible;container-type:inline-size;--lg-bl-fit-scale:1;--lg-bl-fit-width:calc(100cqw / var(--lg-bl-fit-scale));}
.masthead.wh-mast-left:has(.wh-brand-lg.wh-bl){--lg-bl-fit-width:calc(200cqw / var(--lg-bl-fit-scale) - 2*clamp(22px,4vw,48px) / var(--lg-bl-fit-scale) - 100%);}
@media(max-width:700px){.masthead:has(.wh-brand-lg.wh-bl){--lg-bl-fit-scale:.82;}}
}
/* The optional slow pulse. On the pseudo-element's own opacity rather than on the
   alpha, so the gradient is interpolated once and the animation is a composited
   layer instead of a repaint per frame. Off unless asked for, and off for anybody
   who has asked their system for less motion. */
.wh-brand-lg.wh-bl-br::before{animation:wh-bl-breathe 5.5s ease-in-out infinite;}
@keyframes wh-bl-breathe{0%,100%{opacity:.72}50%{opacity:1}}
@media(prefers-reduced-motion:reduce){.wh-brand-lg.wh-bl-br::before{animation:none;}}
.wh-lg-fr{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:var(--lg-pt,0px) var(--lg-pr,0px) var(--lg-pb,0px) var(--lg-pl,0px);border:var(--lg-bw,0px) solid var(--lg-bc,transparent);border-radius:var(--lg-brad,0px);background:var(--lg-fbg,transparent);}
/* The shape, applied only for a chosen ratio. aspect-ratio is a SUGGESTION
   against content: a lockup wider than its square makes the box grow taller to
   stay square rather than crushing the type, which is the right way for it to
   fail. "auto" emits no class and no variable, so the frame is then exactly as
   wide as the lockup plus its padding. */
.wh-lg-fr--ar{aspect-ratio:var(--lg-ar,1);}

/* ── THE STICKY BAR: WHERE THE LOGO SITS, AND WHETHER IT HANGS BELOW ─────
   Both are classes on the BAR rather than styles on the brand link, because
   neither is a property of the link: the placement is done by the bar's own
   track layout, and the overhang needs the bar to stop clipping.

   ⚠ A GRID, NOT auto MARGINS, AND THAT IS THE WHOLE POINT OF THE CENTRE CASE.
   margin:auto on a flex child centres it in the space LEFT OVER by its
   siblings, so a wide nav pushes the brand off-centre and the bar is centred on
   nothing in particular — measured at brand-centre 437 against a bar centre of
   640 while this was being built. Three tracks with the brand in the middle one
   is centred on the BAR, which is what the control claims to do. */
.drophdr--lgc .inner{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
.drophdr--lgc .brand{grid-row:1;grid-column:2;justify-self:center;}
.drophdr--lgc .nav{grid-row:1;grid-column:3;justify-self:end;}
.drophdr--lgr .inner{display:grid;grid-template-columns:1fr auto;align-items:center;}
.drophdr--lgr .nav{grid-row:1;grid-column:1;justify-self:start;}
.drophdr--lgr .brand{grid-row:1;grid-column:2;justify-self:end;}
/* ⚠ THE OVERHANG IS A TRANSFORM, NOT A NEGATIVE MARGIN, and the first version
   was the margin and DID NOTHING. Rendered in Chromium it was pixel-identical
   to the bar without it.

   Two reasons it could never have worked. The bar's height comes from .inner's
   PADDING, not from the brand, so shrinking the brand's outer box does not move
   the bar's bottom edge. And the brand is a centred flex/grid item, so a
   negative bottom margin merely re-centres it half a step down rather than
   pushing it past anything.

   A translate moves the painted logo down without touching layout at all, which
   is exactly what "hang below the bar by N pixels" means: the bar keeps the
   height it always had and nothing else on the page moves. Both overflows still
   have to be opened — the bar clips its own box, and .inner carries the wrap
   padding that would clip it again — and the z-index keeps the overhanging part
   over the page rather than under the next section. */
.drophdr--lgover,.drophdr--lgover .inner{overflow:visible;}
/* ⚠ 90 for the same reason as .drophdr--lgtop .brand below: a logo that hangs
   past the bar must paint OVER the submenu, not under it. This is the legacy
   overhang control's twin of that rule. */
.drophdr--lgover .wh-brand-lg{transform:translateY(var(--lg-over,0px));position:relative;z-index:90;}
/* ⚠ AND THE MEGA MENU'S HEADER TAKES THE SAME TRANSLATE, so the two logos agree.
   Dylan, 27 September 2026: "the logo in the sticky menu has a position that may be
   slightly lower, due to how you set the 'hang below the bar' settings. You should
   ensure that they are the same; the mega menu logo should appear in exactly the same
   place as the one in the sticky menu, with the sticky menu one being the one that it
   takes its guide from."

   MEASURED before the fix, at 1440x900 with a 30px logo: with no overhang both logos
   sat 22.5px from their own bar's top edge and already agreed exactly. With
   overhang:7 the sticky one moved to 29.5px and this one stayed at 22.5 — a 7px
   mismatch, precisely the overhang. The cause is that .drophdr--lgover is a class on
   the BAR, and .wh-fs-hdr is a different element in a different subtree.

   ⚠ NO CLASS NEEDED HERE, and that is why this is one rule rather than a second
   compile-time flag: the mega header holds the SAME brand link as the sticky bar
   (it is literally dropBrand), so --lg-over is already on the element. The default
   of 0px makes this a no-op for every site that has never set an overhang. */
.wh-fs-hdr{overflow:visible;}
.wh-fs-hdr .wh-brand-lg{transform:translateY(var(--lg-over,0px));position:relative;z-index:2;}

/* ── THE LOGO IS HIDDEN WHILE THE BAR IS, AND ARRIVES AFTER IT ──────────────
   Dylan, 28 September 2026:

     "in the top of the page, the logo (if there is an overhang over the bottom of
     the sticky header) seems to peek in from the top of the page on the left.
     Please fix this. Perhaps it should be the case that when the sticky header
     pops in, the sticky header itself pops in and then the logo itself moves
     downwards from the top of the page immediately afterwards, slightly
     staggered? I.e. it's the case that usually, it would be hidden in the sticky
     header, which when the sticky header is out of view, should mean that it's
     hidden? And also, of course, doing the same in reverse."

   WHY IT PEEKED. The bar retracts by translateY(-100%) — it moves up by its OWN
   height and no further. A logo that overhangs the bar's bottom edge is therefore
   still inside the viewport when the bar has gone: it hangs below the line the bar
   retracted to. Both placement modes have to open the bar's overflow to let the
   logo out at all (see both blocks above), so nothing clips it either. The two
   facts are individually correct and together they put a logo at the top of a page
   that has no header on it.

   MEASURED IN CHROMIUM at 1440x900, at the top of the page with the bar fully
   retracted, as the brand box's bottom edge in viewport coordinates — before:

     bar 80px, padding-above 0, 19px logo     brand bottom  -62.0   hidden
     bar 80px, padding-above 70, 19px logo    brand bottom   +8.0   PEEKS 8px
     bar 64px, nothing set (the default)      brand bottom  -23.5   hidden

   His own site is a 102px picture logo in an 80px bar, which is ~22px of it.

   HOW IT IS FIXED, and it is his description rather than a shortcut to it. While
   the bar is not showing, the brand is translated up by its own height plus the
   padding above it — which puts its BOTTOM edge exactly on the bar's TOP edge,
   whatever the logo's height, whatever the padding and whatever the bar's height.
   Tucked there it is inside the bar's box, so when the bar leaves the viewport the
   logo leaves with it. Nothing is measured, nothing is hard-coded, and there is no
   height at which this stops being true, because -100% IS the element's height.

   THE STAGGER IS THE SAME TWO NUMBERS READ IN OPPOSITE ORDERS. On the way IN the
   move is delayed by the bar's own retract duration (--wh-drop-ms), so the bar
   arrives and the logo then drops in behind it. On the way OUT the delay is zero
   and the move is quicker than the bar's, so the logo tucks itself away first and
   the bar follows it up. That is "and also, of course, doing the same in reverse"
   stated as a cascade rather than as a script.

   ⚠ THE SELECTORS ARE .drophdr-SCOPED, AND THAT IS LOAD-BEARING, NOT INCIDENTAL.
   Dylan asked, before this was written: "ensure that whatever fix you implement
   for the logo peeking out from the top of the page, it doesn't break anything in
   the mega menu top left, where the logo also needs to appear (both when you've
   opened it from the sticky menu and when the sticky menu isn't open/you've opened
   it from the top right of the page)."

   The mega menu draws its OWN copy of the brand markup, in .wh-fs-hdr, which is a
   separate element in a separate subtree (compile.ts builds it from the same
   dropBrand string — the same HTML, not the same node). Nothing below can match
   it, so the sheet cannot hide it. That matters most in exactly the case he names:
   opening the menu from the top right of an UNSCROLLED page, where .drophdr has no
   .show class at all. A rule written against the brand without the .drophdr scope
   would have hidden the mega menu's logo precisely there and nowhere else — the
   hardest state to notice and the one he uses most.

   ⚠ AND THE TRANSITION IS DECLARED ON .brand WHILE --lgover's OFFSET LIVES ON
   .wh-brand-lg, deliberately. They are the same element when a logo is set (the
   link carries both classes), but --lgtop is emitted from padTop ALONE, so a site
   with a padding but no logo gets .drophdr--lgtop on a plain .brand with no
   wh-brand-lg on it. Keying the hide on .brand covers both; keying it on
   wh-brand-lg would have left that site still peeking, with a rule that looks
   right in the file.

   ⚠ WHAT THIS DOES NOT COVER, said out loud. It does nothing for a header with
   neither class — correctly, because a centred brand in a bar with hidden overflow
   cannot reach past the bar's bottom edge and has nothing to hide from. It does
   not stop the logo being visible DURING the retract; the bar is on its way out
   and its own bottom edge is still on screen, so that is the animation and not a
   leak. And it cannot help a logo so tall that it overhangs the TOP of the
   viewport once translated — there is no such logo, because -100% is measured from
   the element itself. */
/* ⚠ THE WAIT IS NOW A FRACTION OF THE BAR'S TRAVEL, NOT ALL OF IT.
   Dylan, 28 September 2026: "the sticky header appears and the logo seems to appear
   after the sticky header has appeared. I'd like it to be later than the sticky header,
   but possibly with a slight bit of overlap (i.e. the logo should appear slightly sooner
   than it does currently)."

   It used to wait the bar's ENTIRE duration — var(--wh-drop-ms,.38s) — so the logo
   could not begin until the bar had completely finished arriving. That is a stagger with
   no overlap at all, and it reads as two separate events rather than one movement.

   ⚠ A MULTIPLIER RATHER THAN A SUBTRACTION, AND THAT CHOICE IS THE LOAD-BEARING PART.
   The obvious way to write "start 140ms earlier" is calc(var(--wh-drop-ms,.38s) - .14s),
   and it is wrong for any site that shortens the bar: at --wh-drop-ms:.12s that calc is
   NEGATIVE, a negative transition-delay means "start already part-way through", and the
   logo would arrive WITH the bar — silently losing the ordering Dylan asked to keep, only
   on sites that had customised the bar. A fraction can never go negative and can never
   reorder the two, whatever the bar's duration.

   .62 leaves the logo starting with about 145ms of the bar's 380ms still to run: after it,
   overlapping it, which is what was asked for. The override (--wh-lg-wait) is untouched,
   so any site that had set its own timing keeps it exactly. */
/* ⚠ THE HOVER GROWTH RIDES ALONG IN THIS SHORTHAND, and it has to, because this
   rule would otherwise replace it. See the LOGO HOVER block at the end of this
   file for what it is and why it is 'scale' rather than 'transform'. The
   :not(.show) rule below overrides transition-duration and -delay as longhands,
   so the growth inherits the exit's timing while the bar is retracted — which is
   invisible, because a retracted header's logo cannot be hovered. */
.drophdr--lgover .brand,.drophdr--lgtop .brand{
  transition:transform var(--wh-lg-in,.30s) cubic-bezier(.4,0,.2,1) var(--wh-lg-wait,calc(var(--wh-drop-ms,.38s) * .62)),
             scale var(--wh-lg-hov,.22s) cubic-bezier(.22,1,.36,1);}
/* ⚠ THE EXIT, AND WHY IT USED TO WHIP — N1787, 30 SEPTEMBER 2026.
   Dylan: "the LHS logo in the sticky menu, on exit, exits less quickly. The
   sticky menu should go up at the same pace; it's just that the logo seems to
   whip upwards, which is not great and looks cheap."

   THE MECHANISM, WHICH IS AN ADDITION RATHER THAN A SPEED. The bar retracts by
   its own height over --wh-drop-ms (.38s). The brand inside it ALSO translates,
   because an overhanging logo sticks out beyond the bar's box and has to tuck
   itself in or it would still be visible after the bar had gone. The two
   transforms COMPOUND, so the logo's apparent travel is the sum of both — and
   it was doing that sum in .22s against the bar's .38s. It therefore left at
   roughly twice the bar's rate and was gone long before it. That is the whip:
   not one fast thing, but two movements that were never reconciled.

   THE FIX IS A FRACTION OF THE BAR, NOT A LITERAL, and it is the same reasoning
   the arrival delay above already uses. A literal .34s would be longer than the
   whole retract on any site that shortened the bar, leaving the logo hanging in
   mid-air after its header had gone. .86 can never outlast the bar whatever the
   bar's duration, and can never go negative.

   AND THE CURVE CHANGES, WHICH IS THE OTHER HALF. The shared
   cubic-bezier(.4,0,.2,1) is a decelerate — it leaves at full speed and eases
   into its destination. That is right for an ARRIVAL and is exactly wrong for a
   departure, where the fast first frames are the part the eye reads as cheap.
   The exit now uses a symmetric ease, so it gathers pace instead of starting at
   it.

   ⚠ The bar's own pace is UNTOUCHED, which is what Dylan asked for: nothing here
   reads or writes --wh-drop-ms, it only measures against it.

   ⚠ --wh-lg-out is unchanged as an override, so any site that had set its own
   exit keeps it exactly. */
/* ⚠ AND IT NOW WAITS FOR THE BAR INSTEAD OF RACING IT — 1 October 2026, which
   is the THIRD setting of this exit and the one that stops it being an animation
   at all for most of its length.

   Dylan: *"when I travel back up the page and the sticky header disappears, the
   logo seems to disappear first (rapidly) before the sticky header itself does …
   on exit, the sticky header should just move upwards with the logo attached to
   it/also moving upwards at the same pace (i.e. no separate animation on the
   logo at that stage). After the sticky header has reached the top, I'm mindful
   that the logo will still be poking slightly out of the bottom of it, so at
   THAT stage, not before, the logo should animate out upwards so that it leaves
   fully above the top of the sticky header itself."*

   So the two movements are SEQUENCED rather than reconciled. N1787 made them
   travel together at a shared pace — 86% of the bar's duration, so they finished
   at nearly the same moment — and that is still two things moving, which is what
   he is seeing. A delay of the bar's WHOLE duration means the logo does not move
   on its own at all while the bar travels: it is carried, at exactly the bar's
   pace, because the only transform running is the header's. Then, with the bar
   off the top edge, the logo tucks itself up past it.

   ⚠ A FRACTION AGAIN, FOR BOTH NUMBERS, AND FOR THE REASON THE ARRIVAL RULE
   ALREADY GIVES. A literal delay would be longer than the whole retract on any
   site that shortened the bar, so the logo would still be hanging in open air
   after its header had gone — the exact fault this is fixing, wearing the other
   hat. 1.0 of the bar's duration can never be early and can never be negative,
   whatever --wh-drop-ms is set to.

   ⚠ AND THE TRAVEL IS SHORTER NOW BECAUSE IT HAS LESS TO DO AND NOBODY IS
   WATCHING IT. Only the overhang is still on screen when it starts; the rest of
   the logo is already above the top edge. .45 of the bar's duration (170ms at
   the default) reads as a tuck rather than a journey.

   ⚠ --wh-lg-out and --wh-lg-out-wait are both still overrides, so a site that
   had set its own exit keeps it exactly.

   ⚠ WHAT THIS DOES NOT COVER, out loud: scrolling back DOWN inside the delay.
   The bar re-shows, .show returns, this rule stops applying and the arrival rule
   takes over mid-flight — which is correct, and is why the delay is on the exit
   rule rather than implemented with a timer in script. */
/* ⚠⚠ RE-TIMED 1 OCTOBER 2026, AND THE MEASUREMENT IS THE WHOLE ARGUMENT. Dylan:
   *"the bottom of the LHS icon, which was previously visible/poking out at the
   top of the page, just suddenly seems to disappear - no elegant move upwards
   out of view."*

   The sequencing above is right and is kept. What was wrong is that THE
   TRANSITION'S DISTANCE AND ITS VISIBLE DISTANCE ARE NOT THE SAME THING. The
   travel is the whole brand (102px on this site); the part anybody can SEE is
   only the overhang left below the bar once the bar has finished retracting —
   21px, about a fifth of it. The other four fifths animate an element that is
   already above the top edge.

   So with the old numbers, of 171ms declared only ~53ms happened on screen —
   three frames — and they began at the instant the bar had eased to a dead stop
   (cubic-bezier(.4,0,.2,1) ends with slope 0), finishing at ~777px/s. The bar
   itself averages 213px/s and peaks at 583px/s. A fifth of the logo crossing the
   edge at 1.3x the bar's peak speed, immediately after everything else stopped,
   is a velocity discontinuity: it reads as vanishing, not as leaving.

   ⚠ THE ASYMMETRY IS THE PROOF IT IS GEOMETRY RATHER THAN TASTE. Identical 21px,
   same element, same site: on ARRIVAL the visible part is the LAST fifth of the
   distance and the curve is slowest at the end, so it lasts ~145ms; on EXIT it
   was the FIRST fifth of a curve that eases out into a destination nobody can
   see, so it lasted ~53ms. 2.7x, which is exactly why the arrival reads as
   graceful and the exit did not.

   TWO NUMBERS CHANGE. .45 -> .95 of the bar's duration, and the curve becomes a
   pure accelerate: still slow off the mark, which was the point of carrying the
   logo with the bar, but it now KEEPS accelerating instead of easing out into an
   invisible end, so far more of the time lands on the visible fifth. Measured:
   53ms -> 169ms of visible travel, leaving the edge at 251px/s rather than
   777px/s — about the bar's own average pace.

   ⚠ WHAT THIS DOES NOT FIX, out loud: only ~21px is ever on screen, and this
   does not change that — it makes those 21px take three times as long. Making
   the declared travel equal the visible travel means computing the overhang from
   --drop-h and the border in CSS, which is four geometry assumptions and a
   second rule on .wh-brand-lg to keep the drop-shadow and backlight buried. That
   is a bigger change than the complaint warrants; if the tuck still reads as too
   short once this is live, that is the next step rather than a tweak to these
   numbers.

   ⚠ AND IT IS DELIBERATELY STILL BELOW 1.0 of the bar's duration. The logo is now
   (invisibly) in motion for 361ms after the bar has stopped, which is harmless —
   scrolling back down mid-flight hands over to the arrival rule, which is the
   documented behaviour and the reason the delay lives on the exit rule rather
   than in a timer. */
/* ⚠⚠ LINEAR SINCE 2 OCTOBER 2026, AND THE TWO COMPLAINTS IT SITS BETWEEN ARE THE
   WHOLE ARGUMENT — one is the mirror of the other and the old curve was the right
   answer to the first and the cause of the second.

   30 September, Dylan: *"the logo seems to whip upwards, which is not great and
   looks cheap."* The fix was to stop it leaving at full speed, and the curve went
   to cubic-bezier(.6,0,1,1), a pure ACCELERATE, chosen precisely so that "the
   visible fifth gets the slow part". That reasoning is sound and it is what put
   the slow part on screen.

   2 October, Dylan: *"it disappears at a regular pace and then the logo pulls up
   slightly afterwards, but there seems to be some kind of pause between the two
   … I just want it to appear as if the sticky menu pulls up out of sight, and
   once it is fully off-screen then the logo pulls off screen as a natural
   continuation, at the same pace and with no gap between the two effects."*

   THAT PAUSE IS THE ACCELERATE, and the delay is not the culprit: the bar's own
   curve is cubic-bezier(.4,0,.2,1), which DECELERATES into rest, so at the moment
   of hand-over the bar is barely moving — and the logo then starts from zero on a
   slow-in curve. Two near-zero velocities back to back read as a gap even though
   the arithmetic leaves none.

   ⚠ So linear, which is not a compromise between the two complaints but the
   literal answer to the second: "at the same pace" is a constant speed. It does
   not whip, because the speed is the travel over the whole duration rather than a
   fast first frame; and it does not pause, because it is at that speed from the
   first frame. The visible part is roughly a fifth of the declared travel (the
   note below explains why), so it clears in about 65ms of the 361 — crisp, and
   immediately after the bar rather than a beat later.

   ⚠ --wh-lg-out-ease is an override like its two neighbours, so a site that wants
   the old accelerate back can have it without editing this sheet. */
/* ⚠⚠ 17 OCTOBER 2026 — THE BAR NOW CARRIES THE LOGO ALL THE WAY OUT, AND THE TUCK
   HAPPENS WHERE NOBODY CAN SEE IT. Dylan: *"there is a brief pause between the
   sticky header having disappeared and the bottom of the logo which is still on
   screen disappearing; (2) the pace of the logo disappearing upwards doesn't seem
   to be the same once it does begin."*

   Both were still true after the linear change above, and the measurement says why.
   MEASURED IN CHROMIUM, 1440x900, 65px bar, a logo overhanging it by 2.5px, as the
   logo's bottom edge in viewport px against time since scrolling to the top:

     before   290ms 5.2   322ms 3.6   355ms 2.7   389ms 1.9   422ms -4.3
     after    the logo's bottom stays exactly 2.5px below the bar's and both reach
              the top edge on the same frame (receipt in the PR).

   (1) IS THE BAR'S DECELERATE, NOT THE DELAY. cubic-bezier(.4,0,.2,1) spends its
   last ~100ms moving a couple of pixels, so the overhang sat almost still at the
   top of the screen for that long, and THEN the logo's own transition began.
   (2) IS THAT SECOND TRANSITION: a linear tuck at a speed of its own, after a curve
   that had come to rest. No pair of timings fixes that, because two transitions on
   two elements are two movements whatever their numbers.

   SO THERE IS ONE MOVEMENT. The runtime (DROPHDR_JS, __whDropHang) measures how far
   the logo hangs below the bar at the moment the bar starts to leave and publishes
   it as --wh-lg-hang on the bar; the bar's retract travels its own height PLUS that,
   on its own curve, so the logo is carried out at exactly the bar's pace and its
   bottom edge reaches the top of the screen on the same frame the bar's motion ends.
   The tuck below is unchanged in DISTANCE (it still has to be tucked for the
   staggered arrival) but now happens in one instant after the bar has gone, when
   both are already off screen.

   ⚠ WHAT THIS DOES NOT COVER, out loud. A logo glow or drop-shadow that spreads
   below the logo's own box is not in the measurement (a filter has no box to read),
   so that soft edge can still be on screen when the tuck lands; it is a blur's fade,
   not a hard edge. Without the script --wh-lg-hang is unset, the bar retracts by its
   own height as before and the logo's overhang leaves in the instant tuck: no pause,
   but a snap. --wh-lg-out/-wait/-ease stay overrides. */
.drophdr--lgover:not(.show),.drophdr--lgtop:not(.show){
  transform:translateY(calc(-100% - var(--wh-lg-hang,0px))) translateZ(0);}
.drophdr--lgover:not(.show) .brand,.drophdr--lgtop:not(.show) .brand{
  transform:translateY(calc(-100% - var(--lg-padtop,0px)));
  transition-timing-function:var(--wh-lg-out-ease,linear);
  transition-duration:var(--wh-lg-out,.01ms);
  transition-delay:var(--wh-lg-out-wait,var(--wh-drop-ms,.38s));}
/* Reduced motion keeps the HIDING — which is a correctness fix, not a flourish —
   and drops only the travel. The logo is simply where it belongs at both ends. */
@media(prefers-reduced-motion:reduce){
  .drophdr--lgover .brand,.drophdr--lgtop .brand,
  .drophdr--lgover:not(.show) .brand,.drophdr--lgtop:not(.show) .brand{
    transition-duration:.01ms;transition-delay:0s;}}.drophdr--lgmid .brand{transition:transform var(--wh-lg-in,.30s) cubic-bezier(.4,0,.2,1) var(--wh-lg-wait,calc(var(--wh-drop-ms,.38s) * .62)),scale var(--wh-lg-hov,.22s) cubic-bezier(.22,1,.36,1);}.drophdr--lgmid:not(.show){transform:translateY(calc(-100% - var(--wh-lg-hang,0px))) translateZ(0);}.drophdr--lgmid:not(.show) .brand{transform:translateY(-100%);transition-timing-function:var(--wh-lg-out-ease,linear);transition-duration:var(--wh-lg-out,.01ms);transition-delay:var(--wh-lg-out-wait,var(--wh-drop-ms,.38s));}@media(prefers-reduced-motion:reduce){.drophdr--lgmid .brand,.drophdr--lgmid:not(.show) .brand{transition-duration:.01ms;transition-delay:0s;}}

/* ── N1768 · THE SHADOW THAT LOOKS HEAVIER FOR ITS FIRST FRAMES ──────────────
   Dylan, 28 September 2026: "the shadow behind it initially seems to be heavier, and
   then it corrects to its proper heaviness." And, asked the one question that separates
   the candidates, 29 September: "seems to happen on my ipad on every scroll, but not on
   other devices so far as I can see."

   THAT ANSWER IS WHAT MADE THIS DIAGNOSABLE, and it is worth saying why. Every scroll
   rules out a cold web font casting the shadow from heavier fallback glyphs, which can
   only happen once per load. iPad and not the desktops rules out anything in the
   cascade, which does not vary by device. What is left is COMPOSITING, and iPad is
   where it shows because the artefact scales with device pixel ratio.

   THE MECHANISM. .brand animates transform (the rules directly above). WebKit promotes
   an animating element to its own composited layer and rasterises the subtree into it
   ONCE for the animation. .wh-brand-lg sits inside that subtree and carries
   filter:var(--lg-shadow) -- see the rule earlier in this file. A drop-shadow's blur
   radius is specified in CSS pixels but applied in the layer's device space, so a layer
   rasterised at a coarser scale than the screen spreads the same blur over
   proportionally more device pixels and reads DARKER and HEAVIER. When the animation
   finishes the layer is re-rasterised at the display's real scale and the shadow becomes
   correct -- which is exactly "heavier, then corrects", and exactly why a 1x desktop
   display shows little or nothing while a 2x iPad shows it every time.

   THE FIX is to stop the filter being rasterised as part of the animating layer at all:
   will-change:filter gives .wh-brand-lg a compositing layer of its OWN, rasterised at
   device scale independently, after which the ancestor's transform merely composites a
   finished texture rather than re-filtering it.

   GATED, SO IT COSTS NOTHING WHERE THERE IS NO SHADOW. --lg-iso is emitted by
   logoStyle() in compile.ts in the SAME branch that emits --lg-shadow, so it exists only
   when a shadow or a backlight shape glow is actually set. Everywhere else this resolves
   to will-change:auto, which is the initial value and creates no layer. That matters: an
   unconditional will-change would hold a permanent extra layer on every site in the
   estate to fix something only some of them can exhibit.

   SCOPED TO .drophdr ON PURPOSE. The masthead's logo is never transform-animated, so it
   has no animating ancestor to be rasterised into and needs no layer of its own.

   ⚠ WHAT THIS DOES NOT COVER, AND THE NEXT READER SHOULD KNOW IT. One candidate
   survives that this cannot address and no test here can distinguish: iOS Safari also
   rasterises composited layers at reduced resolution WHILE A SCROLL IS IN FLIGHT, and a
   drop-shadow drawn that way looks heavier until the scroll settles. That would also
   present as iPad-only on every scroll. It was ranked second because Dylan tied the
   effect to the logo ARRIVING rather than to scrolling in general, but it is not ruled
   out, and if the heaviness survives this fix that is where to look -- the remedy there
   is different and is not a layer hint. Nothing in this repository can separate the two:
   jsdom performs no layout and no compositing (AGENTS.md section 3), there is no browser
   in the sandbox, and the only instrument that would settle it is an iPad. */
/* ⚠ AND THE SELECTOR IS .drophdr .wh-brand-lg, NOT .drophdr .brand .wh-brand-lg,
   WHICH IS WHAT I WROTE FIRST AND WHICH MATCHED NOTHING AT ALL. compile.ts emits the
   sticky brand as one element carrying BOTH classes -- a class="brand wh-brand-lg" --
   so a descendant combinator between them asks for a .wh-brand-lg INSIDE a .brand and
   there is no such element on any page. The rule parsed, shipped, and did nothing. Only
   compiling a real page and looking at the element caught it; reading the stylesheet
   could not, because a selector that matches nothing looks exactly like one that works.
   Left as a comment rather than tidied away because the same two classes sit on one
   element in the mega menu's header too, and the next person to write a rule for this
   logo will reach for the same wrong shape. */
/* ⚠⚠ 2 OCTOBER 2026 — THIS HINT HAD BEEN POINTING AT AN ELEMENT WITH NO FILTER
   ON IT SINCE 29 SEPTEMBER, which is to say it had been doing nothing. The note
   above is the diagnosis and it is still right; what went stale is WHERE. It was
   written when .wh-brand-lg itself carried filter:var(--lg-shadow). The halo fix
   LATER THE SAME DAY moved that filter onto the link's children — and nothing
   moved this with it, so the layer hint has been sitting on an unfiltered link
   ever since, which is a no-op dressed as a fix.
   It now names the elements that actually carry the filter, which since N1905 are
   the growth wrapper's children. Dylan reported the surviving heaviness on the
   masthead and the footer rather than the sticky bar, and N1905's wrapper is what
   fixes those; this is the sticky bar's own half of the same mechanism, finally
   applied to the right element. */
.drophdr .wh-brand-lg > .wh-lg-gr > .wh-lg,
.drophdr .wh-brand-lg > .wh-lg-gr > .wh-lg-fr,
.drophdr .wh-brand-lg > .wh-lg-gr > .wh-lg-img{will-change:var(--lg-iso,auto);}

/* ── PADDING ABOVE THE LOGO, WHICH REPLACES "HANG BELOW THE BAR" ─────────────
   Dylan, 27 September 2026: "you should replace the hang below the bar setting with a
   setting that just allows me to choose the size of the logo that goes in there, and
   an option to choose how much padding you want above it. If the logo size + the
   padding above happens to make it overhang the bottom, that's fine, but that's how
   it should be determined."

   So the logo is placed from the TOP of the bar instead of being centred in it, and
   where its bottom edge falls is then arithmetic: padding + height. An overhang is a
   consequence and no longer a number anybody types.

   ⚠ TWO DECLARATIONS, AND NEITHER IS A TRANSFORM — which is the opposite of the
   overhang above, so it is worth saying why. The overhang had to be a transform
   because it moved the logo out of a box it was centred in without disturbing
   anything; here the placement IS the point, so flex-start puts the logo's top at the
   bar's own top edge and the margin is the padding, literally.

   ⚠ AND IT CANNOT CHANGE THE BAR'S HEIGHT, which is a property of .drophdr .inner
   rather than a promise made here: that rule sets height:var(--drop-h,64px) with no
   vertical padding, so a child pushed past it OVERFLOWS rather than growing it.
   Nothing else on the page moves. This is also what makes "measured from the bar's
   top edge" true rather than approximately true: there is no padding in between.

   ⚠ THE OVERHANG'S CLASS AND THIS ONE ARE MUTUALLY EXCLUSIVE AT THE COMPILER, not
   here (see dropOver in compile.ts). Both applying would translate a logo that had
   already been placed, landing it below the number that was typed. */
/* ⚠ align-self ON THE BRAND, NOT align-items ON THE ROW — CORRECTED 28 SEPTEMBER
   2026, AND THE FIRST VERSION TOOK THE WHOLE MENU WITH IT.

   This used to be .drophdr--lgtop .inner{align-items:flex-start;}. .inner is the
   bar's flex row and it holds THREE things, not one: the brand, the menu row, and
   (in the classic design) whatever else the row draws. Top-aligning the container
   top-aligned all of them. So turning the padding-above control on — which is the
   only way to place a logo from the bar's top edge — silently sent HOME, OUR STORY
   and INSIGHTS to the top of the bar as well.

   Dylan, 28 September 2026, reporting it as part of the bar-height complaint: "the
   menu items themselves (in the screenshot, home, our story, insights) aren't
   vertically centred within the bar. They should be vertically centred within the
   bar, with the same amount of padding above and below."

   He is right, and note that his two symptoms had two DIFFERENT causes even though
   they looked like one fault: the Enquire pill and burger were off-centre because
   the corner carried a hard-coded 10px (see CORNER_CSS), and the menu row was
   off-centre because of this rule. Fixing either alone would have left him half a
   bug and the impression that it had not been fixed.

   align-self on the brand says the one true thing — THIS ITEM starts at the top —
   and leaves .inner's align-items:center to everything else. It is also strictly
   stronger than the class-doubling this block needed before: align-self on a child
   beats align-items on its parent by the cascade's own rules rather than by
   specificity, so there is no order or sheet-splitting question to lose. */
.drophdr--lgtop,.drophdr--lgtop .inner{overflow:visible;}
/* ⚠ z-index 90 HERE TOO, AND LEAVING IT AT 2 WOULD HAVE UNDONE THE FIX IN
   BASE_CSS SILENTLY. LOGO_CSS is appended AFTER BASE_CSS by siteStylesheet, and
   '.drophdr--lgtop .brand' and '.drophdr .brand' are both (0,2,0) — so on a site
   with a padding-above set, which is every site that can HAVE an overhanging
   logo, this rule is the later of the two equals and wins. The one case the fix
   exists for is the one case it would not have reached. */
.drophdr--lgtop .brand{align-self:flex-start;margin-top:var(--lg-padtop,0px);position:relative;z-index:90;}
/* The mega menu's header, from the same number and for the same reason as the
   translate above: .wh-fs-hdr is the same 64px flex row with the same brand.

   ⚠ THE SELECTOR IS DOUBLED — .wh-fs-hdr.wh-fs-hdr--lgtop — AND THE FIRST DRAFT WAS
   NOT, AND SILENTLY DID NOTHING. .wh-fs-hdr sets align-items:center and lives in
   MEGA_MENU_CSS; these rules live in LOGO_CSS. A single class ties it at (0,1,0), so
   which one wins depends on the order the two sheets end up in — and measured in
   Chromium it was the centring: with padTop:0 the mega logo sat at 22.5px while the
   sticky bar's sat at 0, and with padTop:40 it sat at 42.5 rather than 40, because
   align-items was still 'center' and flex centred the item's 59px OUTER box (19 + the
   40 margin) in 64px, putting its content top at 2.5 + 40.

   That is the same cross-rule tie that cost the About chip its pink hover for months
   (theAboutChipPaintsWhenHovered.test.ts), met again within the hour, and it is worse
   here because the two rules are in DIFFERENT stylesheets — there is no source order
   to read at all. (0,2,0) cannot lose to (0,1,0) whatever the order. */
/* ⚠ AND THE DOUBLED SELECTOR IS GONE FROM HERE TOO, for the better reason rather
   than because it stopped being needed. The note above explains why it existed:
   .wh-fs-hdr{align-items:center} lives in MEGA_MENU_CSS, this block lives in
   LOGO_CSS, and a single class tied at (0,1,0) across two sheets with no source
   order to appeal to. That was answered by writing the class twice.

   align-self answers it at the root instead: a child's align-self ALWAYS beats its
   parent's align-items, because they are different properties on different
   elements rather than two rules competing for one declaration. There is no tie
   left to break, so the doubling has nothing to do and the mega header's other
   children (there is only the brand today, but that is a fact about the markup and
   not a promise) keep the centring they were given. */
.wh-fs-hdr.wh-fs-hdr--lgtop .brand{align-self:flex-start;margin-top:var(--lg-padtop,0px);position:relative;z-index:2;}

/* ── THE LOGO GROWS 2% ON HOVER, AND DOES NOT UNDERLINE ──────────────────────
   Dylan, 1 October 2026: *"if you hover over the logo in the top left of the
   sticky header, or in the masthead, or in the footer, it has no underline
   effect but it does grow by 2%. It should grow by 2% from the centre in the
   masthead and the footer, and it should grow downwards in the sticky header (as
   there's no space between it and the top of the screen, so if it grew from the
   centre/equally on all sides, then some of it would be cut off at the top)."*

   ⚠ THIS REVERSES THE UNDERLINE HE ASKED FOR THAT MORNING. The three logos are
   back in the sweep's exclusion list in BASE_CSS; the note there has the two
   instructions side by side.

   ⚠ 'scale', NOT 'transform', AND THAT IS THE WHOLE OF WHY THIS WORKS. .brand
   already animates 'transform' in the sticky bar — the staggered arrival and the
   tuck-out above both write translateY into it — so a hover written as
   transform:scale(1.02) would REPLACE the translate and drop the logo back into
   place mid-retract. 'scale' is an independent property: the two compose, and
   neither rule has to know the other exists. (transform-origin governs both, so
   the origin below is shared — which is harmless, because a pure translation is
   origin-independent.)

   ⚠ THE ORIGIN IS THE REQUEST, not a detail. 50% 50% is the initial value and is
   what the masthead and the footer want. The sticky bar's logo is flush with the
   top of the window, so growing about its centre pushes 1% of its height past
   the top edge where it is simply cut off; 'top' keeps the top edge still and
   lets it grow down into the page, which is where there is room.

   ⚠ .brandtop IS NAMED AS WELL AS .brand because the masthead's logo is
   '<a class="brandcrest brandtop">' — it carries neither of the other two
   classes, and a rule that named only .brand would have left the one logo he
   looks at most without the effect.

   ⚠ WHAT THIS DOES NOT COVER, said out loud: a touch device latches :hover on
   tap, so a tapped logo stays 2% larger until something else is touched. That is
   true of every hover effect on this site and is not worth a media query of its
   own here; if it ever matters, (hover:hover) is the one-line change. */
/* ⚠⚠ 2 OCTOBER 2026 — THE GROWTH MOVED OFF THE LINK AND ONTO ITS CONTENT, AND
   THIS IS THE SECOND TIME THE SAME ELEMENT HAS TAUGHT THE SAME LESSON. Dylan:
   *"when I hover over the masthead logo, the backlight/shadow behind seem to
   flicker, becoming much brighter (and cropped in strange ways) before going
   back to what it should be. Same thing in the footer."*

   THE MECHANISM IS ALREADY WRITTEN DOWN IN THIS FILE, under the sticky bar's
   will-change:filter rule — it was diagnosed on 29 September for SCROLL and the
   hover-grow added on 1 October handed it a second trigger. An element with an
   animating transform is promoted to its own composited layer and its subtree is
   rasterised into that layer ONCE for the animation. Two things in this subtree
   hate that:

     · the halo, .wh-brand-lg.wh-bl::before — a blurred radial gradient on a box
       DELIBERATELY WIDER THAN THE LOGO. Rasterised at the animating layer's
       coarser scale and then scaled up it reads BRIGHTER, and anything outside
       the layer's bounds is cut at a hard straight edge. That is "much brighter"
       and "cropped in strange ways", in one sentence, from one cause.
     · the optional breathe animation on that same pseudo-element's opacity. An
       ancestor acquiring a layer mid-animation can restart or re-composite it,
       which shows as a jump from .72 to 1 — "much brighter" again, by a second
       route, on the sites that switched it on.

   ⚠ AND THE 29 SEPTEMBER FIX CANNOT HELP HERE, which is worth knowing before
   anybody reaches for it. Its own note says it is "SCOPED TO .drophdr ON PURPOSE.
   The masthead's logo is never transform-animated, so it has no animating
   ancestor to be rasterised into and needs no layer of its own." **That sentence
   was true when it was written and the hover-grow made it false two days later**,
   and nobody connected the two. Nor would widening its scope work: compile.ts
   emits one element carrying BOTH classes, so .wh-brand-lg IS the element being
   scaled — a will-change:filter on it cannot separate its own filter from its own
   transform.

   SO THE FIX IS STRUCTURAL RATHER THAN A LAYER HINT: scale the logo's CONTENT and
   leave the link alone. The link keeps the halo and is never transformed, so the
   ::before is not in an animating subtree, is not re-rasterised, is not clipped
   and does not have its animation disturbed.

   ⚠ AND IT IS MORE FAITHFUL TO THE BACKLIGHT ANYWAY, independently of the bug.
   The halo's own rule says: "THE BOX IS A FIXED DIAMETER, NOT AN INSET ROUND THE
   LOGO. Light from a point does not know the shape of what it is behind … a
   bigger logo simply sits further into its own light." A light that grew 2%
   whenever the logo did was already contradicting that; now it does not move at
   all, and the logo grows INTO it, which is what a backlight does.

   ⚠ :not(.wh-brand-lg) ON THE OLD RULE, SO THE LEGACY LOGO KEEPS ITS GROWTH.
   compile.ts has two brand shapes: the configured logo, which carries
   .wh-brand-lg and the three possible children below, and the fallback
   '<a class="brandcrest brandtop">' holding ROUND_STRIP and two spans, which
   carries neither. The fallback has no shadow and no halo, so scaling the link is
   harmless there and is the only thing that CAN be scaled — moving the rule
   wholesale onto the children would have silently removed the hover effect from
   every site in the estate that has not configured a logo. That is the failure
   mode this file records twice already: a selector that matches nothing looks
   exactly like one that works.

   ⚠ THE THREE CHILDREN ARE THE SAME THREE THE SHADOW RULE NAMES, and they are
   exhaustive rather than a guess: logoHtml() returns a .wh-lg-fr wrapper, a bare
   .wh-lg-img raster, or a .wh-lg lockup, and nothing else. All three compute to
   inline-flex or block, so 'scale' applies to each; on a plain inline box it
   would not, and the effect would vanish with no error anywhere.

   ⚠ WHAT THIS DOES NOT COVER, SAID OUT LOUD. The drop-shadow on those children
   is still inside the animating subtree, so the coarse-raster artefact remains
   POSSIBLE for the shadow alone on a high-DPR display — the same residue the
   29 September note describes, and it cannot be removed while the shadow has to
   trace a logo that is itself growing. It is a far weaker effect than the halo's,
   because a 6-18px blur close behind the mark has nothing like a wide gradient's
   area to redistribute, and neither symptom Dylan reported is specific to it. If
   any heaviness survives on an iPad, that is where to look, and the instrument
   needed is an iPad: jsdom performs no layout and no compositing (AGENTS.md §3)
   and there is no browser in this sandbox, so nothing here can measure it. */
.masthead .brand,.masthead .brandtop,footer .fbrand,
.masthead .wh-brand-lg > .wh-lg-gr,
footer .wh-brand-lg > .wh-lg-gr,
.drophdr .wh-brand-lg > .wh-lg-gr{
  transition:scale var(--wh-lg-hov,.34s) var(--wh-lg-hov-ease,cubic-bezier(.33,.9,.28,1));}
.masthead .brand:not(.wh-brand-lg):hover,
.masthead .brandtop:not(.wh-brand-lg):hover,
footer .fbrand:not(.wh-brand-lg):hover,
.drophdr .brand:not(.wh-brand-lg):hover{scale:var(--wh-lg-grow,1.045);}
.masthead .wh-brand-lg:hover > .wh-lg-gr,
footer .wh-brand-lg:hover > .wh-lg-gr,
.drophdr .wh-brand-lg:hover > .wh-lg-gr{scale:var(--wh-lg-grow,1.045);}
/* ⚠ 16 OCTOBER 2026 — THE RESIDUE THE NOTE ABOVE LEFT OPEN, MEASURED AND CLOSED.
   Dylan: *"with the shadow behind the logo in the masthead, and also in the
   footer, it still seems to adjust when I hover. It's like the shadow recalculates
   when I hover, and again when I stop hovering."* Measured in Chromium on a
   compiled page with a shadowed lockup: the growth wrapper had NO layer at rest,
   Chrome gave it one only for the length of the grow, and took it away again
   when the grow finished; the page went 6, 7, 6 painted layers on hover in and
   the same on hover out. The drop-shadow on the wrapper's children was drawn
   differently on and off that layer (up to 238 of 255 apart at the same size),
   so it visibly redrew at each swap: twice in, twice out.
   will-change:scale puts the wrapper on its layer BEFORE the grow and keeps it
   there, so there is no swap to see (8 layers, steady, through in and out). The
   grow itself is untouched. Cost: up to three small permanent layers, and only
   where a logo is configured; the legacy text logo scales its link and has no
   shadow, so it is left alone. Allowed by Dylan for this file, 16 October. */
.masthead .wh-brand-lg > .wh-lg-gr,
footer .wh-brand-lg > .wh-lg-gr,
.drophdr .wh-brand-lg > .wh-lg-gr{will-change:scale;}
/* The sticky bar's logo grows DOWNWARD, because it is flush with the top of the
   window — Dylan's own words on 1 October. The origin has to follow the scale
   onto whichever element now carries it, and the link keeps its copy for the
   legacy shape. */
.drophdr .brand,
.drophdr .wh-brand-lg > .wh-lg-gr{transform-origin:top center;}
@media(prefers-reduced-motion:reduce){
  /* The growth is the point rather than the travel, so it stays — instantly. */
  .masthead .brand,.masthead .brandtop,footer .fbrand,
  .masthead .wh-brand-lg > .wh-lg-gr,
  footer .wh-brand-lg > .wh-lg-gr,
  .drophdr .wh-brand-lg > .wh-lg-gr{transition-duration:.01ms;}}



/* ── shared by both ─────────────────────────────────────────────────────── */
/* font-style:normal is not a default: the site-wide h1 rule takes
   --h-title-style from Global typography, so a slanted page-title setting
   would otherwise slant both heroes (Codex review on #475).
   --h-hero-face is the Brand styles "Hero headings" font; unset, it is not
   emitted and both heroes stay on --serif as before (16 October 2026). */
.hm h1,.hs h1{font-family:var(--h-hero-face,var(--serif));font-weight:var(--wt-heading,300);font-style:normal;letter-spacing:var(--tracking-heading,-.045em);color:#fff;margin:0;width:auto;min-width:0;}
.hm-ph,.hs-ph{background-color:var(--navy-deep);background-size:cover;background-position:center;}
.hm-btn,.hs-btn{display:inline-flex;align-items:center;gap:10px;font-family:var(--serif);font-weight:600;font-size:15px;letter-spacing:-.01em;padding:15px 24px;line-height:1;border-radius:999px;text-decoration:none;transition:background-color .25s ease,color .25s ease,box-shadow .25s ease;}

/* ── Navy masthead ─────────────────────────────────────────────────────────
   A navy band holds the heading; the photos hang over its lower edge onto the
   paper. The overlap is the band's bottom padding meeting the row's negative
   top margin, so the two numbers move together. */
.hm{background:var(--paper);}
/* ROOM TO BREATHE (Dylan, 16 October 2026, from the masthead-breathing
   mockups, "Roundel set right"): "Learning for life" has the stage. More room
   above it (clamp(90px,16vh,180px), was clamp(28px,5vw,64px)) and below it
   (about 50px to clamp(80px,10vh,130px)), each following the screen's HEIGHT,
   so on a laptop the whole masthead sits on the first screen with space under
   the photos. The bottom padding is that gap plus the 120px the photos hang
   into the band (.hm-row's margin-top). */
.hm-band{background:var(--navy-ink);padding:clamp(90px,16vh,180px) 0 calc(clamp(80px,10vh,130px) + 120px);}
/* ONE fluid size, no breakpoint (Dylan, 16 October 2026: "have a minimum and a
   maximum size of that text, and it just scales according to the screen
   size"). The first version capped at 190px while the column stops at 1280px,
   so from about 1500px wide the heading outgrew its column and broke onto a
   second line; and a phone rule set 21vw, so at 700px it jumped from 87px to
   147px. 168px leaves a few per cent of room under the largest size at which
   "Learning for Life" fits the 1280px column on one line (about 178px); 12.4vw
   reaches it at about 1355px and it holds there. Measured in Chromium from 390 to 2560px wide. */
/* 16 October 2026, later the same morning: 148px at most, from 168px, with
   the minimum and the slope scaled to match (the mockup's "Balanced" sizes). */
.hm h1{font-size:clamp(58px,10.8vw,148px);line-height:.86;}
/* Words made bold in the editor arrive as <em>: pink, same weight, upright. The
   hex is Dylan's own (16 October 2026), not --pink, so a later brand-pink
   change does not move it without him. */
.hm h1 em{color:#d97195;font-style:normal;font-weight:inherit;}
/* The service hero takes the same pink words (Dylan, 17 October 2026: "similar
   to what you can do in the navy masthead"), the same hex and the same rule:
   colour only, the heading's own weight, upright. It lives here rather than in
   theme.ts beside .svp-hero h1 because this stylesheet is emitted after
   SERVICE_PAGES_CSS, and so the protected engine file is not touched. */
.svp-hero h1 em{color:#d97195;font-style:normal;font-weight:inherit;}
.hm-row{display:grid;grid-template-columns:minmax(250px,1.2fr) 3.2fr;gap:clamp(28px,4vw,56px);margin-top:-120px;padding-bottom:clamp(48px,6vw,80px);align-items:start;}
.hm-lead{padding-top:160px;}
/* The introduction is the services list's paragraph grey, the same token
   (.whatdo .lead .purpose: var(--muted)), so the two stay in step. Dylan, 16
   October 2026: "the paragraph text below that starts with an initial/drop cap
   … the same colour as the paragraph text within the services lists block".
   The initial keeps its own navy (.hm-t-initial below, --navy since 17
   October); the signature too. */
/* --hm-intro: the size set in Global → Block settings → Navy masthead →
   Introduction (heroWatermark.ts); unset, 19px as always. The initial and the
   white area below the band follow it on their own (both are in its em).
   Justified, as body paragraphs are site-wide (Dylan, 16 October 2026: "that
   text there should be justified"); it was left-aligned.
   Hyphenated (Dylan, 18 October 2026: "make it so that the justified text in
   the navy masthead is hyphenated. The slogan at the top should not be"): at
   34ch, justified without hyphens opened wide gaps between words. The browser
   hyphenates by the page's lang attribute, which compile.ts always writes
   (⚠ never spell the tag here: this comment ships in the page's CSS, and
   compile.test counts the html and body tags in the document).
   -webkit-hyphens is for Safari on iPhone and iPad, which ignores the plain
   property. The slogan is the h1, outside .hm-lead, so it never breaks; the
   small capitals signature below opts back out. */
.hm-lead p{font-size:var(--hm-intro,19px);line-height:1.38;max-width:34ch;margin:0 0 28px;color:var(--muted);text-align:justify;-webkit-hyphens:auto;hyphens:auto;}
.hm-btn{background:var(--navy-ink);color:#fff;}
.hm-btn:hover{background:var(--navy);}
/* The photos are sized from the screen's height, so they always fit the first
   screen: each about 29vh tall at most (four of them, 3:4, three 24px gaps,
   the 52px deepest step), never under 480px for the row, centred in their
   column. */
.hm-shots{display:grid;grid-template-columns:repeat(4,1fr);gap:24px;width:100%;max-width:max(480px,calc((29vh - 52px) * 3 + 72px));justify-self:center;}
.hm-shot{display:block;color:inherit;text-decoration:none;}
/* 2px corners (Dylan, 16 October 2026: "the image corners are all 2px radius"), was 4px. */
.hm-shot .hm-ph{aspect-ratio:3/4;border-radius:2px;transition:transform .8s cubic-bezier(.2,.7,.2,1);}
a.hm-shot:hover .hm-ph{transform:scale(1.03);}
/* The steps: second down most, fourth lowest, repeating every four. */
.hm-shot:nth-child(4n+2){margin-top:36px;}.hm-shot:nth-child(4n+3){margin-top:10px;}.hm-shot:nth-child(4n+4){margin-top:52px;}
/* THE ENTRANCE: the introduction first, then the photos left to right, 1, 2,
   3, 4 (Dylan, 17 October 2026: "when they first fade in … to fade in from left
   to right"). ⚠ Corrected: on 16 October this was 1, 3, 2, 4, the rotation's
   order; the rotation (HERO_MASTHEAD_ROTATE_JS, data-hm-step) keeps 1, 3, 2, 4
   and only the first appearance changed. It is still the site's reveal (reveal.ts): the
   same gate that waits for the curtain, the same is-in, the same replay. Only
   the timings are the masthead's own: --hmin-* from Global → Block settings →
   Navy masthead → Entrance, each falling back to the site's reveal rhythm, so
   an untouched site keeps today's numbers. --hmin-k is a photo's place in the
   sequence. Not once settled or turning: reveal.ts zeroes the delays then, and
   these must not win over that. Opacity and transform only, as before. */
.js-rv .hm.wh-rv:not(.is-settled):not(.is-turning) .hm-lead{transition-duration:var(--hmin-text,var(--rv-dur,620ms));}
.js-rv .hm.wh-rv:not(.is-settled):not(.is-turning) .hm-shot{transition-duration:var(--hmin-photo,var(--rv-dur,620ms));transition-delay:calc(var(--hmin-first,var(--rv-step,160ms)) + var(--hmin-k,0) * var(--hmin-stagger,var(--rv-step,160ms)));}
.hm-shot:nth-child(2){--hmin-k:1;}.hm-shot:nth-child(3){--hmin-k:2;}.hm-shot:nth-child(4){--hmin-k:3;}
/* Rotating alternates (heroMasthead.ts). Only a photo that HAS alternates is
   drawn as layers; the first layer carries .hm-on in the markup, so without
   the runtime (or under reduced motion) it simply shows the first photo. A
   layer losing .hm-on fades out over --hmr-out; the next gains it and fades in
   over --hmr-in, so the old one is gone completely before the new one comes. */
/* NO NAVY BEHIND A CHANGING PHOTO (Dylan, 16 October 2026: "when the images
   fade out … there's a navy blue box behind them. Please remove this navy
   blue. I just want the image to disappear completely"). The navy was .hm-ph's
   placeholder colour on the frame the layers sit in; between one layer going
   and the next arriving, the page behind now shows instead. */
.hm-rot{position:relative;overflow:hidden;}
.hm-ph.hm-rot{background-color:transparent;}
.hm-rot .hm-layer{position:absolute;inset:0;opacity:0;transition:opacity var(--hmr-out,0.8s) ease;}
.hm-rot .hm-layer.hm-on{opacity:1;transition-duration:var(--hmr-in,0.8s);}
.hm-cap{display:flex;flex-wrap:wrap;justify-content:space-between;gap:2px 10px;margin-top:12px;font-family:var(--serif);font-weight:500;font-size:14px;line-height:1.25;letter-spacing:-.02em;color:var(--navy-ink);}
.hm-cap small{font-family:var(--body);font-weight:400;font-size:13px;color:var(--muted);}

/* ── The P roundel watermark (Dylan, 16 October 2026) ──────────────────────
   The site's own roundel drawing, scaled up: ring r150 stroke 10 and the P at
   the same scale and offset as every small roundel, in white. It sits in the
   band, so it runs up behind a transparent header (which overlays the band)
   while the heading and photos are padded below it, and the band clips it.
   Size is a share of the band's height; "cut off on the right" pushes it right
   by a share of its own width; "up or down" moves its centre by a share of
   the band. The block writes those per block as --hmwm-* on the element;
   heroWatermark.ts writes the site-wide motion settings, only when they differ
   from the fallbacks below.
   ⚠ THE FALLBACKS HERE ARE HERO_WATERMARK_DEFAULTS (heroWatermark.ts), copied
   because this file imports nothing; heroWatermark.test.ts pins them equal,
   the spring curve included.
   ⚠ STACKING. The watermark is positioned, so it would paint over the photos
   that hang into the band unless they are positioned too and come later in the
   page: hence position:relative on the band's wrap and on .hm-row. */
.hm-band{position:relative;overflow:hidden;}
.hm-band>.wrap,.hm-row{position:relative;}
.hm-wm{position:absolute;right:0;top:calc(50% + var(--hmwm-y,4) * 1%);height:calc(var(--hmwm-size,160) * 1%);aspect-ratio:1;width:auto;
  transform:translate(calc(var(--hmwm-cut,31) * 1%),-50%);pointer-events:none;z-index:0;}
.hm-wm-pop,.hm-wm svg{display:block;width:100%;height:100%;transform-origin:50% 50%;}
.hm-wm-ink{opacity:calc(var(--hmwm-strength,3) / 100);}
.hm-wm-disc{display:none;}
/* THE ZOOM: one spring, not keyframes at the peak and troughs (see springEasing
   for why the mockup's version paused at its largest). The opacity is its own
   short fade, so the spring moves only the size and the angle. */
/* ⚠ linear() is Safari 17.2+ (Codex review of Release B, 16 Oct 2026): where it is
   missing the whole declaration would be dropped and the roundel would not zoom at
   all. So the plain rule springs once with a back-out cubic, and only a browser
   that understands linear() is given the real spring. The imprint's press-in is a
   plain cubic already and needs no fallback. */
.hm-wm.is-zoom .hm-wm-pop{animation:hmwm-pop var(--hmwm-dur,1.2s) cubic-bezier(.34,1.56,.64,1) var(--hmwm-wait,.1s) both,hmwm-fade calc(var(--hmwm-dur,1.2s) * .3) ease-out var(--hmwm-wait,.1s) both;}
@supports (animation-timing-function:linear(0,1)){.hm-wm.is-zoom .hm-wm-pop{animation-timing-function:var(--hmwm-ease,linear(0,0.1771,0.3851,0.5966,0.7907,0.9539,1.0789,1.1639,1.2115,1.2268,1.2169,1.1941,1.1672,1.1298,1.0866,1.042,1,0.9639,0.9358,0.9172,0.9081,0.9082,0.916,0.9299,0.9479,0.9678,0.9876,1.0058,1.0209,1.0322,1.0391,1.0418,1.0405,1.036,1.0289,1.02,1.0106,1.0023,0.9961,0.9923,0.9908,0.9913,0.9929,0.9951,0.9971,0.9987,0.9996,0.9999,1)),ease-out;}}
/* WAITS FOR THE CURTAIN. The site intro and the page transition cover the page
   at load (the roots that lock scrolling: html[data-wh-intro], [data-wh-hold],
   [data-wh-arriving], .wh-cover; reveal.ts waits on the same four). Without
   this the spring played behind the white panel and was simply there when it
   lifted, which is exactly what the reveal once did (Dylan, 25 September). Held
   at its start, it plays when the curtain's attribute comes off. */
html[data-wh-intro] .hm-wm *,html[data-wh-hold] .hm-wm *,html[data-wh-arriving] .hm-wm *,html.wh-cover .hm-wm *{animation-play-state:paused;}
@keyframes hmwm-pop{from{transform:scale(var(--hmwm-from,.6)) rotate(var(--hmwm-angle,0deg));}to{transform:none;}}
@keyframes hmwm-fade{from{opacity:0;}to{opacity:1;}}
/* THE DRIFT is not here: its rules and keyframes come with a masthead block
   whose roundel actually drifts (HERO_WATERMARK_DRIFT_CSS, heroWatermark.ts),
   so a page with the drift switched off carries none of it. Dylan, 16 October
   2026. */
/* Imprint (16 Oct 2026): it presses in rather than springing, as in the
   masthead-institutional mockup, and the disc is never drawn. */
.hm-wm.is-imprint.is-zoom .hm-wm-pop{animation:hmwm-press 1.6s cubic-bezier(.25,.8,.25,1) .15s both;}
@keyframes hmwm-press{from{transform:scale(1.035);opacity:0}to{transform:none;opacity:1}}
/* ── The institutional touches (Dylan, 16 October 2026, 05:09) ─────────────
   From the masthead-institutional mockups. Each is switched on for the whole
   site in Global → Block settings → Navy masthead (heroWatermark.ts,
   HERO_MASTHEAD_TOUCHES); the block emits a touch's markup or class only when
   it is on, so a site with every touch off carries none of this into the page.
   Literata is the sub-heading face, --h-sub-face, as everywhere else on the
   site that means "the serif accent". Nothing is slanted.
   ⚠ AT 600, NOT THE MOCKUP'S 400: the site fetches the sub-heading face at 600
   and 700 only (fontLoading.test.ts), so a 400 here would be drawn at 600
   anyway while claiming otherwise. */
/* Foundation stone: on desktop, in the left column on the navy above the
   introduction (the column starts 150px above the band's foot, so 34px down
   it is still navy); on phones the introduction follows the photos, so the
   copy in the band, under the heading, is the one shown. */
.hm-found{display:flex;flex-direction:column;gap:7px;border-top:1px solid rgba(255,255,255,.22);padding-top:13px;font-family:var(--serif);font-weight:500;font-size:11.5px;line-height:1.25;letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.7);}
.hm-found span:first-child{color:#fff;}
.hm-found-band{display:none;}
.hm-lead{position:relative;}
.hm-found-lead{position:absolute;left:0;right:0;top:34px;}
/* The initial: the introduction's first letter, three lines deep. Dylan,
   05:15: "ensure that the initial's height stretches fully to 3 lines high".
   ⚠ ONE FLOAT FOR EVERY BROWSER, NOT initial-letter (17 October 2026). This
   used initial-letter where the browser had it, with this float as the
   fallback. Dylan, 17 October, from his own screen: "the initial/dropcap seems
   to extend slightly above the top of the text to its right. Please make it so
   that its top edge is level with the top edge of the text on the right. Just
   reduce the height slightly." initial-letter sets the letter's cap height
   from the first line's cap height, and each engine reads that from the font
   differently: in Chromium the F sat 2px BELOW the first line's tallest
   letters, on his screen it stood above them. A float sized by arithmetic
   lands in the same place everywhere, so the numbers below are the only ones.
   The span it fills: from the top of the first line's tallest letters (the
   ascenders of Ysabeau, .70em above the baseline) to the third line's
   baseline, two line heights lower (1.38em each), over Literata's cap height
   as measured on the F (.69; the .73 used before overstated it) is the
   letter's size, 5.01em. With line-height 1 Literata's cap top sits about
   .22 of its size below the float's top, and the first line's tallest letters
   start 5px (.31em) below the line's top; -.157em lifts the one onto the
   other. Measured in Chromium at 1440 and 390 wide: the F's top is level
   with the first line's ascenders and its foot on the third line's baseline,
   to the pixel. ⚠ It holds for those two faces and this paragraph's 1.38 line
   height; a change to either needs the numbers again.
   ⚠ CORRECTED THE SAME EVENING. On Dylan's iPhone, after the republish, the
   P's foot hung about 1.3px below the third line's baseline and its top sat
   at the capitals' height ("still seems to be slightly too tall"): Safari
   sets Literata a touch lower and larger than Chromium. The span now runs
   from Ysabeau's capitals (.66em) to .04em above the third baseline,
   (2 × 1.38 + .62) / .69 = 4.90em, lifted -.153em; in Chromium that is a
   pixel inside both ends, which is where Safari's extra lands it on the
   lines. Measured in Chromium only (no Safari here).
   THE COLOUR IS --navy, NOT --navy-ink (17 October 2026, evening). Dylan,
   twice: "I also want the initial/drop cap to be navy" and "it doesn't seem
   to be navy". --navy-ink (#14283f) is the band's near-black; at this size
   beside grey type it reads as black. --navy (#284570) is the navy the
   Introduction block's drop cap uses, which he took as navy.
   The right margin is the room Dylan asked for in the same message ("it could
   do with slightly more space to the right ... it feels a little cramped"):
   .07 of the letter is .35em of the paragraph (5.6px at the live 16px), which
   with the F's own side bearing leaves about 9px of paper between its arm and
   the text, up from 4px.
   ⚠ THE NEGATIVE BOTTOM MARGIN (16 October 2026). Dylan: "the drop cap extends 3
   lines but the fourth line underneath it is full-width". Without it the float
   could end on the fourth line's top, and rounding would hang it over that
   line and indent it too. -.1em ends the float a few pixels above the third
   line's baseline: lines one to three wrap it and line four runs full width. */
.hm-t-initial .hm-intro::first-letter{float:left;font-family:var(--h-sub-face,var(--serif));font-weight:600;font-style:normal;color:var(--navy);letter-spacing:0;
  font-size:calc((2 * 1.38em + .62em) / .69);line-height:1;margin:-.153em .07em -.1em 0;}
/* Signature: under the introduction, like a signed letter. */
.hm-lead p.hm-sig{display:flex;flex-direction:column;gap:3px;margin:-6px 0 28px;font-family:var(--serif);font-weight:600;font-size:11.5px;line-height:1.5;letter-spacing:.18em;text-transform:uppercase;color:var(--navy-ink);-webkit-hyphens:manual;hyphens:manual;}
.hm-sig::before{content:"";width:28px;height:1px;background:var(--pink);margin-bottom:9px;}
.hm-sig span{font-family:var(--h-sub-face,var(--serif));font-weight:600;font-size:13px;letter-spacing:.04em;text-transform:none;color:var(--muted);}
/* Gilt edge: two pink hairlines at the band's foot; the photos hang over it. */
.hm-t-gilt .hm-band::after{content:"";position:absolute;left:0;right:0;bottom:7px;height:1px;background:var(--pink);opacity:.85;box-shadow:0 5px 0 rgba(217,113,149,.45);}
/* Rounded bottom (Dylan, 17 October 2026): the spotlight strip's curve, the
   same 28px and 22px as .hs-panel below. The band already clips
   (overflow:hidden), so the roundel and the gilt edge follow the curve. */
.hm-t-round .hm-band{border-radius:0 0 28px 28px;}
@media (max-width:700px){.hm-t-round .hm-band{border-radius:0 0 22px 22px;}}
/* Footnote: a pink reference after the heading, the note at the section's foot. */
.hm-fnr{font-family:var(--h-sub-face,var(--serif));font-weight:600;font-size:.16em;line-height:0;color:var(--pink);vertical-align:super;margin-left:.08em;letter-spacing:0;position:relative;top:-.9em;}
.hm-fnote p{margin:0 0 40px;border-top:1px solid var(--line);padding-top:12px;max-width:520px;font-family:var(--h-sub-face,var(--serif));font-size:13.5px;line-height:1.45;color:var(--muted);text-align:left;}
.hm-fnote p span{color:var(--pink);margin-right:8px;}
/* Inscription: a motto in widely spaced capitals, led by a short pink rule. */
.hm-motto{display:flex;align-items:center;gap:18px;margin:0 0 clamp(18px,2.2vw,30px);font-family:var(--h-sub-face,var(--serif));font-weight:600;font-size:clamp(12px,1.05vw,15px);line-height:1;letter-spacing:.32em;text-transform:uppercase;color:rgba(255,255,255,.72);text-align:left;}
.hm-motto::before{content:"";flex:none;width:42px;height:1px;background:var(--pink);}
/* Spine: the name running up the left margin, clear of the photos. */
.hm-spine{position:absolute;left:clamp(14px,1.6vw,26px);bottom:170px;writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap;display:flex;align-items:center;gap:14px;font-family:var(--serif);font-weight:500;font-size:11px;line-height:1;letter-spacing:.32em;text-transform:uppercase;color:rgba(255,255,255,.5);z-index:1;}
.hm-spine::before{content:"";width:1px;height:46px;background:var(--pink);}
/* Ribbon: a bookmark hanging from the top edge, notched at its end. */
.hm-ribbon{position:absolute;top:0;left:clamp(10px,1.4vw,20px);width:clamp(9px,.9vw,13px);height:clamp(110px,12vw,170px);background:var(--pink);z-index:1;clip-path:polygon(0 0,100% 0,100% 100%,50% calc(100% - 8px),0 100%);box-shadow:inset -2px 0 0 rgba(0,0,0,.12);}
/* Column rules: hairlines at each column's edge, on the same grid as the row. */
.hm-cols{position:absolute;inset:0;pointer-events:none;}
.hm-cols>.wrap{height:100%;display:grid;grid-template-columns:minmax(250px,1.2fr) 3.2fr;gap:clamp(28px,4vw,56px);}
.hm-cols>.wrap>div{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;}
.hm-cols i{display:block;height:100%;border-left:1px solid rgba(255,255,255,.07);}
.hm-cols>.wrap>div>i:last-child{border-right:1px solid rgba(255,255,255,.07);}
/* Laid paper: fine laid lines and wide chain lines, on the navy and below it. */
.hm-t-laid .hm-band{background-image:repeating-linear-gradient(90deg,rgba(255,255,255,.028) 0 1px,transparent 1px 118px),repeating-linear-gradient(0deg,rgba(255,255,255,.012) 0 1px,transparent 1px 4px);}
.hm-t-laid{background-image:repeating-linear-gradient(90deg,rgba(20,40,63,.035) 0 1px,transparent 1px 118px),repeating-linear-gradient(0deg,rgba(20,40,63,.018) 0 1px,transparent 1px 4px);}
/* Mounts and plaques: square photos with a keyline mount; numbered captions. */
/* 2px, not square: Dylan, 16 Oct 2026, "the image corners are all 2px radius". */
.hm-t-plaques .hm-shot .hm-ph{border-radius:2px;position:relative;box-shadow:0 0 0 1px rgba(20,40,63,.18);}
.hm-t-plaques .hm-ph::after{content:"";position:absolute;inset:8px;border:1px solid rgba(255,255,255,.6);}
.hm-t-plaques .hm-cap{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;border-top:1px solid var(--navy-ink);padding-top:10px;margin-top:14px;font-weight:600;font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;}
.hm-rn{font-family:var(--h-sub-face,var(--serif));font-weight:600;font-size:13px;letter-spacing:.06em;color:var(--pink);}
.hm-t-plaques .hm-cap small{grid-column:2;font-size:13px;letter-spacing:-.01em;text-transform:none;}
/* Ruled frame: a double hairline just inside the band, pink diamonds at the corners. */
.hm-frame{position:absolute;inset:clamp(10px,1.4vw,20px);border:1px solid rgba(255,255,255,.16);pointer-events:none;}
.hm-frame::after{content:"";position:absolute;inset:5px;border:1px solid rgba(255,255,255,.07);}
.hm-frame i{position:absolute;width:9px;height:9px;border:1px solid rgba(217,113,149,.7);transform:rotate(45deg);background:var(--navy-ink);}
.hm-frame i:nth-child(1){left:-5px;top:-5px;}.hm-frame i:nth-child(2){right:-5px;top:-5px;}.hm-frame i:nth-child(3){left:-5px;bottom:-5px;}.hm-frame i:nth-child(4){right:-5px;bottom:-5px;}
/* THE ROUNDEL SET RIGHT, CLEAR OF THE HEADING (Dylan, 16 October 2026: "is
   there a way to organise the roundel so that there's always a bit of space
   between it and the title to the left?", then "on smaller screen sizes, the
   roundel doesn't get cut off completely"). The block puts the roundel in a
   slot beside the heading (.hm-head), so the slot starts where the heading's
   words END, whatever they say and whatever font Brand styles gives them, and
   a gap of clamp(40px,6vw,120px) keeps it off them. The roundel is at most
   64vh across and shows 90% of itself; where the slot plus the margin beyond
   the column (--hm-x0) cannot hold that, it shrinks to fit (cqw is the slot's
   width), never under 170px; only below that is it pushed right and cropped.
   ⚠ --hm-x0 is the page column's outer margin, from .wrap in theme.ts
   (max-width var(--wrap), padding clamp(22px,4vw,48px)); a change there needs
   it here. 100vw counts a desktop scrollbar, so the gap can read ~15px less. */
.hm-head{display:flex;align-items:center;}
.hm-head>h1{flex:0 1 auto;}
.hm-wm-slot{flex:1 1 0;min-width:0;align-self:stretch;position:relative;margin-left:clamp(40px,6vw,120px);container-type:inline-size;
  --hm-x0:max(clamp(22px,4vw,48px),calc((100vw - var(--wrap,1280px)) / 2 + clamp(22px,4vw,48px)));}
.hm-wm.is-clear{right:auto;top:50%;--hmwm-s:clamp(170px,calc((100cqw + var(--hm-x0)) / .9),64vh);height:var(--hmwm-s);
  left:max(0px,calc(100% + var(--hm-x0) - var(--hmwm-s) * .9));transform:translateY(-50%);}
@supports not (width:1cqw){.hm-wm.is-clear{--hmwm-s:clamp(170px,30vw,64vh);}}
@media (max-width:700px){
  .hm-wm{height:calc(var(--hmwm-psize,67) * 1%);}
  /* Phones: the heading takes the width, so the roundel goes back behind the
     band's right edge exactly as "Behind the heading" draws it there. Every
     box between it and the band is static so the band places it; the words
     are lifted above it instead. */
  .hm-clear .hm-band>.wrap,.hm-clear .hm-head,.hm-clear .hm-wm-slot{position:static;}
  .hm-clear .hm-head{display:block;}
  .hm-clear .hm-wm-slot{margin:0;container-type:normal;}
  .hm-clear .hm-band>.wrap>:not(.hm-head),.hm-clear .hm-head>h1{position:relative;z-index:1;}
  .hm-wm.is-clear{left:auto;right:0;top:calc(50% + var(--hmwm-y,4) * 1%);height:calc(var(--hmwm-psize,67) * 1%);transform:translate(calc(var(--hmwm-cut,31) * 1%),-50%);}
  .hm-band{padding-top:clamp(28px,5vw,64px);}
  .hm-shots{max-width:none;}
  .hm-found-lead,.hm-cols,.hm-spine{display:none;}
  .hm-found-band{display:flex;margin-top:24px;font-size:9.5px;letter-spacing:.14em;}
  .hm-motto{letter-spacing:.15em;gap:10px;font-size:10.5px;white-space:nowrap;}.hm-motto::before{width:24px;}
  .hm-frame{inset:8px;}
  .hm-fnr{font-size:.2em;}
  .hm-band{padding-bottom:185px;}
  .hm h1{line-height:.88;}
  .hm-row{grid-template-columns:1fr;gap:28px;margin-top:-105px;padding-left:0;padding-right:0;}
  .hm-lead{order:2;padding:0 clamp(22px,4vw,48px);}
  .hm-shots{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding:0 clamp(22px,4vw,48px);gap:12px;padding:0 clamp(22px,4vw,48px);scrollbar-width:none;}
  .hm-shot{flex:0 0 62%;scroll-snap-align:start;margin-top:0!important;}
}
/* PHONES, RESPACED AND WITHOUT THE ROUNDEL (Dylan, 17 October 2026, with a
   phone screenshot: "Not enough spacing above the slogan, too much below the
   founded in oxford/bespoke education consultancy stuff and above the images
   … on the phone, remove the imprint from the background"). Written after the
   block above so it wins at the same specificity.
   - Above the heading: the band's top padding goes from clamp(28px,5vw,64px)
     (28px on a phone) to clamp(56px,14vw,80px), so the heading clears the
     transparent header's logo by about 28px more.
   - Below the founded lines: the band's bottom padding goes from 185px to
     145px. The photos still hang 105px into the navy (.hm-row's margin-top is
     untouched), so the navy gap between the founded lines and the photos falls
     from 80px to 40px.
   - The roundel, Watermark or Imprint, set right or behind, is not drawn at all
     on phones. ⚠ The block's "Phone size" setting (--hmwm-psize) therefore has
     no visible effect below 700px while this rule stands. Desktop and tablet
     wider than 700px are unchanged. */
@media (max-width:700px){
  .hm-band{padding-top:clamp(56px,14vw,80px);padding-bottom:145px;}
  .hm-wm{display:none;}
}

/* ── Spotlight strip ──────────────────────────────────────────────────────
   The words on a navy panel with rounded lower corners; the strip rises out
   of it, one photo wide, the rest narrow. The curve is 28px, not the mockup's
   first 36px, so it stays calm on a wide screen. Without the runtime the
   middle photo is the spotlit one, so the block is complete with scripts off.
   No captions and no clicking since 17 October 2026 (blocks/heroSpotlight.ts).

   ⚠ THE GAP UNDER THE HEADING is the panel's bottom padding less the strip's
   rise (margin-top), because the spotlit photo fills the strip's full height.
   It was 230 − 200 = 30px, and with the 140px heading's descenders hanging
   below its box the "p" of "Partnerships" nearly touched the wide photo
   (Dylan, 17 October 2026: "feels a little cramped"). It is now 48–80px on a
   wide screen and 40–56px on a phone, added to the panel, so the navy grows
   and the strip keeps its place against the panel's lower edge. */
.hs{background:var(--paper);}
.hs-panel{background:var(--navy-ink);border-radius:0 0 28px 28px;padding:clamp(36px,5vw,64px) 0 calc(200px + clamp(48px,6vw,80px));text-align:center;color:rgba(255,255,255,.86);}
.hs h1{font-size:clamp(56px,9vw,140px);line-height:.92;}
.hs-panel p{font-size:19px;line-height:1.38;max-width:46ch;margin:28px auto 26px;color:rgba(255,255,255,.86);-webkit-hyphens:manual;hyphens:manual;}
/* inherit, not center: body paragraphs are justified site-wide, and the
   centred-container guard (dylansChangesOn25September) recognises a carve-out
   by text-align:inherit on the container's p. */
.hs-panel p{text-align:inherit;}
.hs-acts{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
.hs-btn{background:var(--pink);color:#fff;}
.hs-btn:hover{background:var(--pink-deep);}
.hs-btn.hs-ghost{background:transparent;color:#fff;box-shadow:inset 0 0 0 1px rgba(255,255,255,.4);}
.hs-btn.hs-ghost:hover{box-shadow:inset 0 0 0 1px #fff;}
.hs-strip{display:flex;gap:14px;justify-content:center;align-items:flex-end;margin-top:-200px;padding:0 clamp(22px,4vw,56px);height:380px;overflow:hidden;}
.hs-it{position:relative;flex:0 1 120px;min-width:0;height:300px;border-radius:16px;overflow:hidden;transition:flex-basis .8s cubic-bezier(.2,.7,.2,1),height .8s cubic-bezier(.2,.7,.2,1);}
/* ⚠ WHY THE PHOTO DOES NOT SIMPLY FILL ITS BUTTON (17 October 2026, Dylan:
   the animation was laggy). It used to be width:100%;height:100%, so every
   frame of the 0.8s move resized all seven photo boxes and the browser had to
   rescale each 2400px picture to a new "cover" size and repaint it, on every
   frame. Now each photo is drawn once at the spotlit size and never changes
   size: the button only clips it (a narrow button shows its middle), and the
   narrow ones are shrunk with transform, which the graphics card does without
   repainting. The scale is the narrow height over the spotlit height
   (300/380 here, 250/330 on a phone), so a narrow photo frames exactly as it
   did when it was a cover image the narrow button's height.
   It does NOT cover a portrait photo framed by width: one of those now shows
   a slightly closer crop while narrow. */
.hs-it .hs-ph{display:block;position:absolute;left:50%;top:50%;width:560px;height:380px;transform:translate(-50%,-50%) scale(var(--hs-shrink,.7895));transition:transform .8s cubic-bezier(.2,.7,.2,1);will-change:transform;}
.hs-it.on .hs-ph{transform:translate(-50%,-50%);}
.hs-it.on{flex-basis:560px;height:380px;}
/* The progress dashes that sat here are gone (Dylan, 18 October 2026). Their
   row was 34px + 2px + clamp(40px,6vw,70px) tall; the section keeps exactly
   that under the photos, so nothing below moves. Only with a strip: the row
   was drawn only with photos (Codex on #571). */
.hs.hs-has-strip{padding-bottom:calc(36px + clamp(40px,6vw,70px));}
/* The P roundel imprint (Dylan, 18 October 2026; blocks/heroSpotlight.ts,
   version 1 of the partnerships-roundel mockup). Its width follows the screen
   between limits (at the default 50: 420px to 760px, half the width between);
   "cut off" pushes it past the panel's right edge and "below" sinks it under
   the panel's lower edge, each a share of its own width. The panel clips it.
   ⚠ STACKING, only with the imprint on: the panel is positioned to hold it, so
   it would paint over the photos that rise into it unless the strip is
   positioned too and comes later in the page; and the words sit above it. */
/* Room for a transparent header (headerRoomField): the panel's own top padding
   becomes the header's measured height plus the gap the category hero keeps
   under it. On the panel, not the section, so the navy runs up behind the
   header; the strip and the imprint are measured from the panel's foot and do
   not move against it. */
.wh-hdr-pad .hs-panel{padding-top:calc(var(--wh-mast-h,168px) + clamp(16px,2.5vw,40px));}
/* The service hero takes the same switch (18 October 2026). Its section is a
   centred "room" (.svp-room in theme.ts), so the padding goes on the content
   (.svp-wrap2), never the section: the ground, and the P roundel drawn on it,
   still run up behind the header, and the words can never start above the
   section's own top padding plus the header's height. */
.wh-hdr-pad.svp-hero .svp-wrap2{padding-top:var(--wh-mast-h,168px);}
@media (max-width:900px){
  .wh-hdr-pad.svp-hero .svp-wrap2{padding-top:var(--wh-mast-h,140px);}
}
/* "Partner with us" beside the service hero's "Enquire now" (19 October
   2026): the category hero's outlined second button (.hs-ghost above), in the
   service hero's chip so the two are the same size (BLOCK_CHIP_CSS sizes every
   .svp-enq--chip). The pair keeps the 26px .svp-enq had above it and wraps on
   a narrow phone. The outline is the ink at 40%, full on hover: white on navy
   as the category hero has it, navy on paper and white, where a white outline
   would vanish. Three classes so it outranks the pink chip's own fill.
   ⚠ THE ROW IS .svp-btnrow, NOT .svp-acts. It shipped on its branch as
   .svp-acts, which the process block already uses for its three-card grid
   (theme.ts); this sheet comes later, so every process block would have become
   a wrapping row with no phone collapse (Codex on #633). */
.svp-btnrow{display:flex;flex-wrap:wrap;gap:12px;margin-top:26px;}
.svp-btnrow .svp-enq{margin-top:0;}
.svp-enq.svp-enq--chip.svp-enq--ghost{background:transparent;color:var(--heading-deep,var(--navy-ink));box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 40%,transparent);}
.svp-enq.svp-enq--chip.svp-enq--ghost:hover{background:transparent;box-shadow:inset 0 0 0 1px currentColor;}
.svp--navy .svp-enq.svp-enq--chip.svp-enq--ghost{color:#fff;}
/* The standfirst carries the same pair under its words (19 October 2026),
   left by default, centred when its block says so. */
.svp-btnrow--centre{justify-content:center;}
.hs-imp .hs-panel{position:relative;overflow:hidden;isolation:isolate;}
.hs-imp .hs-panel>.wrap,.hs-imp .hs-strip{position:relative;z-index:1;}
.hs-wm{--hsim-w:clamp(calc(var(--hsim-size,50) * 8.4px),calc(var(--hsim-size,50) * 1vw),calc(var(--hsim-size,50) * 15.2px));
  position:absolute;z-index:0;width:var(--hsim-w);aspect-ratio:1;pointer-events:none;
  right:calc(var(--hsim-w) * var(--hsim-cut,30) / -100);bottom:calc(var(--hsim-w) * var(--hsim-down,22) / -100);}
.hs-wm-pop,.hs-wm svg{display:block;width:100%;height:100%;transform-origin:50% 50%;}
/* It presses in as the masthead's imprint does (hmwm-press), once the words
   arrive (the reveal's is-in), held behind the site's curtain as the
   masthead's is. Without Animate it is simply there. */
.js-rv .hs.wh-rv:not(.is-in) .hs-wm-pop{opacity:0;}
.js-rv .hs.wh-rv.is-in .hs-wm-pop{animation:hmwm-press 1.6s cubic-bezier(.25,.8,.25,1) .15s both;}
html[data-wh-intro] .hs-wm *,html[data-wh-hold] .hs-wm *,html[data-wh-arriving] .hs-wm *,html.wh-cover .hs-wm *{animation-play-state:paused;}
@media (max-width:700px){.hs-wm{display:none;}}
@media (max-width:700px){
  .hs-panel{padding-bottom:calc(150px + clamp(40px,10vw,56px));border-radius:0 0 22px 22px;}
  .hs h1{font-size:18vw;}
  .hs-panel p{font-size:18px;}
  .hs-strip{gap:8px;height:330px;margin-top:-150px;padding:0 12px;}
  .hs-it{flex-basis:34px;height:250px;}
  .hs-it.on{flex-basis:250px;height:330px;}
  .hs-it .hs-ph{width:250px;height:330px;--hs-shrink:.7576;}
}
/* ⚠ THE MOVE IS TRANSFORM AND CLIP ONLY, NEVER LAYOUT (17 October 2026, Dylan:
   the Partnerships hero "is also quite jittery/jerky in its animations on
   iPad"). The rules above animate each photo's flex-basis and height, so for
   0.8s every frame re-laid out the whole strip (justify-content:center moves
   every photo), re-centred each photo in a box of a new fractional width, and
   re-cut each box's rounded overflow:hidden corners around the photo's own
   graphics layer. Safari does all three on the main thread and snaps the boxes
   to whole pixels, so the photos stepped instead of gliding.
   Here every photo keeps one fixed box, the spotlit photo's size, for good.
   Where it sits and how large it shows is that box's transform, and what
   shows of it is a clip-path inset on the same box with 16px corners, so
   nothing is laid out again while it moves.
   The numbers reproduce the flex layout exactly: the strip's photos sit
   narrow-gap-narrow from a centred start; a spotlit photo is wider by S - n,
   so every photo after it (.on ~ .hs-it) moves right by that much; and when
   the strip is narrower than S + (N-1)(n + g) both widths shrink in
   proportion, as flex-shrink did. --hs-n (the template, on the strip) is the
   photo count and --hs-i (on each photo) its place.
   S, n and the heights are plain numbers, multiplied into px, because Safari
   cannot divide a length by a length.
   ⚠ Only where container units exist (Safari 16, Chrome 105); an older
   browser keeps the flex layout above, jitter and all. Measured in Chromium
   only (no WebKit here): every photo's visible edges were compared with the
   flex layout's at 1440, 1024, 820 and 390 wide. */
@supports (width:1cqw){
  .hs-strip{display:block;position:relative;container-type:inline-size;--hs-S:560;--hs-nw:120;--hs-g:14px;--hs-Hs:380;--hs-Hn:300;}
  .hs-it{
    --hs-k:calc(var(--hs-S) + (var(--hs-n,5) - 1) * var(--hs-nw));
    --hs-av:calc(100cqw - (var(--hs-n,5) - 1) * var(--hs-g));
    --hs-sw:min(calc(var(--hs-S) * 1px),calc(var(--hs-av) * var(--hs-S) / var(--hs-k)));
    --hs-nwd:min(calc(var(--hs-nw) * 1px),calc(var(--hs-av) * var(--hs-nw) / var(--hs-k)));
    --hs-x:calc(max(0px,(100cqw - var(--hs-sw) - (var(--hs-n,5) - 1) * (var(--hs-nwd) + var(--hs-g))) / 2) + var(--hs-i,0) * (var(--hs-nwd) + var(--hs-g)) - var(--hs-S) * .5px);
    --hs-r:calc(var(--hs-Hn) / var(--hs-Hs));
    position:absolute;top:0;width:calc(var(--hs-S) * 1px);height:calc(var(--hs-Hs) * 1px);border-radius:0;overflow:visible;
    transform:translateX(calc(var(--hs-x) + var(--hs-nwd) / 2)) translateY(calc((var(--hs-Hs) - var(--hs-Hn)) * .5px)) scale(var(--hs-r));
    clip-path:inset(0 calc((var(--hs-S) * 1px - var(--hs-nwd) / var(--hs-r)) / 2) round calc(16px / var(--hs-r)));
    transition:transform .8s cubic-bezier(.2,.7,.2,1),clip-path .8s cubic-bezier(.2,.7,.2,1);will-change:transform;
  }
  .hs-it.on{width:calc(var(--hs-S) * 1px);height:calc(var(--hs-Hs) * 1px);transform:translateX(calc(var(--hs-x) + var(--hs-sw) / 2)) translateY(0px) scale(1);clip-path:inset(0 calc((var(--hs-S) * 1px - var(--hs-sw)) / 2) round 16px);}
  .hs-it.on~.hs-it{transform:translateX(calc(var(--hs-x) + var(--hs-sw) - var(--hs-nwd) / 2)) translateY(calc((var(--hs-Hs) - var(--hs-Hn)) * .5px)) scale(var(--hs-r));}
  /* ⚠ THE PHOTO DOES NOT MOVE ON ITS OWN HERE, AND IS NOT ITS OWN LAYER
     (Codex on #549, citing roundBottom.ts: Safari does not hold an animated,
     composited layer inside an ancestor's clip-path, and masking a moving
     photo made the call to action jitter). The narrow photo's shrink, which
     the rules above put on .hs-ph with will-change, is folded into the
     photo box's own transform instead, so the photo paints into the one
     moving layer whose clip it is. The clip is in that box's own, unscaled
     space: a narrow box is scaled by Hn/Hs, so its photo already stands Hn
     tall (no top inset), its window is n / r wide there, and its corners are
     16px / r so they show as 16px. */
  .hs-it .hs-ph,.hs-it.on .hs-ph{left:0;top:0;transform:none;transition:none;will-change:auto;}
  /* ⚠ ONE CLOCK FOR THE PLACE, THE SIZE AND THE WINDOW (19 October 2026,
     Dylan: the Partnerships hero's expand and contract "feels a bit jerky …
     especially for the image that's getting smaller"). The rules above
     transition transform and clip-path separately, and the window's width on
     screen is the clip's width times the scale, so it is the product of two
     curves and not a curve itself: half way through a desktop move the
     shrinking photo showed about 318px where the old flex layout showed 340,
     while its centre moved as if it were 340. Its left edge, which should
     stand still, lurched about 11px right and came back, and each gap
     beside the two moving photos opened by about 11px and closed. On top of
     that the browser runs the transform on the compositor and the clip on
     the main thread, so the window can lag the photo by a frame.
     Here each photo transitions one number instead: --hs-p (0 narrow, 1
     spotlit) and --hs-a (1 when it sits after the spotlit photo). Its place,
     scale, visible width (n + (S - n) p on screen) and corners are all
     worked out from those every frame, so the visible width moves in a
     straight line between the two sizes, as the flex layout's did, and a
     shrinking photo's left edge stays where it is. The resting states are the rules above exactly.
     The template does not add .hs-pp: the runtime (HERO_SPOTLIGHT_JS) does,
     and only after CSS.registerProperty has made both numbers animatable
     (Safari 16.4, Chrome 85, Firefox 128). An older browser keeps the
     separate transitions above. The cost: the move is no longer on the
     compositor, so it is a style recalculation of the strip's photos each
     frame; the clip was already repainted each frame.
     ⚠ EACH LENGTH IS MULTIPLIED BY A PLAIN NUMBER, NEVER A SUM OF LENGTHS
     DIVIDED. The first draft wrote the clip as (S - v / s) / 2 with v a sum
     of two min()s, and Chromium simplified it wrongly once s came to exactly
     1: the spotlit photo's side inset came out 440px instead of 0, so its
     window was inside out. Written out term by term it measured right at
     every step. Do not tidy it back.
     ⚠ Measured in Chromium only, as before (no WebKit here). */
  .hs-it{--hs-p:0;--hs-a:0;}
  .hs-it.on{--hs-p:1;}
  .hs-it.on~.hs-it{--hs-a:1;}
  .hs-pp .hs-it,.hs-pp .hs-it.on,.hs-pp .hs-it.on~.hs-it{
    --hs-s:calc(var(--hs-r) + (1 - var(--hs-r)) * var(--hs-p));
    transform:translateX(calc(var(--hs-x) + var(--hs-nwd) * (.5 - var(--hs-p) / 2 - var(--hs-a)) + var(--hs-sw) * (var(--hs-p) / 2 + var(--hs-a)))) translateY(calc((var(--hs-Hs) - var(--hs-Hn)) * .5px * (1 - var(--hs-p)))) scale(var(--hs-s));
    clip-path:inset(0 calc(var(--hs-S) * .5px + var(--hs-nwd) * (var(--hs-p) - 1) / (2 * var(--hs-s)) - var(--hs-sw) * var(--hs-p) / (2 * var(--hs-s))) round calc(16px / var(--hs-s)));
    transition:--hs-p .8s cubic-bezier(.2,.7,.2,1),--hs-a .8s cubic-bezier(.2,.7,.2,1);
  }
}
@media (max-width:700px){
  @supports (width:1cqw){.hs-strip{--hs-S:250;--hs-nw:34;--hs-g:8px;--hs-Hs:330;--hs-Hn:250;}}
}
/* THE ENTRANCE: the spotlit photo rises and fades in first, then the others
   fan out from it, nearest first, left then right, one at a time (Dylan, 17
   October 2026; the order is arrivalOrder in blocks/heroSpotlight.ts, written
   as --hs-o on each photo). It is the site's reveal (reveal.ts): the same gate
   that waits for the curtain, the same is-in, the same replay. Not [data-rv]
   on the photos, because the reveal moves a part with transform and here
   transform is where a photo sits: the rise is the separate translate
   property, which adds to transform instead of replacing it. The strip itself
   no longer arrives as one block.
   Timings chosen, not asked for: the first photo starts 400ms after the
   section's words begin, each next one 180ms later, each over the site's
   reveal length (620ms unless Global changes it), rising 28px. Five photos
   are all in about 1.75s after the words start. Opacity and translate only,
   so nothing is laid out. The transition lists repeat the move's, because a
   later declaration replaces the whole list. */
.js-rv .hs.wh-rv:not(.is-in) .hs-it{opacity:0;translate:0 28px;}
.js-rv .hs.wh-rv .hs-it{--hs-in:calc(400ms + var(--hs-o,0) * 180ms);transition:flex-basis .8s cubic-bezier(.2,.7,.2,1),height .8s cubic-bezier(.2,.7,.2,1),opacity var(--rv-dur,620ms) ease var(--hs-in),translate var(--rv-dur,620ms) cubic-bezier(.22,1,.36,1) var(--hs-in);}
@supports (width:1cqw){
  .js-rv .hs.wh-rv .hs-it{transition:transform .8s cubic-bezier(.2,.7,.2,1),clip-path .8s cubic-bezier(.2,.7,.2,1),opacity var(--rv-dur,620ms) ease var(--hs-in),translate var(--rv-dur,620ms) cubic-bezier(.22,1,.36,1) var(--hs-in);}
  .js-rv .hs.wh-rv .hs-pp .hs-it{transition:--hs-p .8s cubic-bezier(.2,.7,.2,1),--hs-a .8s cubic-bezier(.2,.7,.2,1),opacity var(--rv-dur,620ms) ease var(--hs-in),translate var(--rv-dur,620ms) cubic-bezier(.22,1,.36,1) var(--hs-in);}
}
/* ── Room for a transparent header (headerRoomField in blocks/shared.ts) ──
   The padding goes on the CONTENT, never the section: the section's background,
   and anything drawn on it such as a watermark, still runs up behind the
   header. --wh-mast-h is the header's measured height at this width; the
   fallbacks are the ones the doors opener uses before that script has run. */
.wh-hdr-pad .hero-grid,.wh-hdr-pad .hm-band>.wrap{padding-top:var(--wh-mast-h,168px);}
@media (max-width:900px){
  .wh-hdr-pad .hero-grid,.wh-hdr-pad .hm-band>.wrap{padding-top:var(--wh-mast-h,140px);}
}
@media (prefers-reduced-motion:reduce){
  .hm-shot .hm-ph,.hs-it,.hs-it .hs-ph,.hm-rot .hm-layer{transition:none;}
  /* The photo strip's entrance too: there, all at once and still. */
  .js-rv .hs.wh-rv .hs-it{transition:none;opacity:1!important;translate:none!important;}
  .hs-pp .hs-it,.hs-pp .hs-it.on,.hs-pp .hs-it.on~.hs-it,.js-rv .hs.wh-rv .hs-pp .hs-it{transition:none;}
  /* The watermark is simply there: no zoom, no drift. */
  .hm-wm .hm-wm-pop,.hm-wm svg{animation:none!important;}
  .js-rv .hs.wh-rv .hs-wm-pop{animation:none!important;opacity:1!important;}
}

:root{--wh-rb:28px;}@media (max-width:700px){:root{--wh-rb:22px;}}.wh-rb{position:relative;clip-path:inset(-100vh 0 0 0 round 0 0 var(--wh-rb) var(--wh-rb));will-change:opacity;}:where(.wh-rb){background-color:var(--paper);}.wh-tuck{margin-top:calc(var(--wh-rb) * -1);border-top:var(--wh-rb) solid transparent;isolation:isolate;}.wh-tuck{overflow:visible;clip-path:inset(0);}.wh-rb.wh-tuck{clip-path:inset(-100vh 0 0 0 round 0 0 var(--wh-rb) var(--wh-rb));}.wh-tuck>.ph.tintn,.wh-tuck>.bg.ph,.wh-tuck>.svp-ph,.wh-tuck>.svp-wash,.wh-tuck>.svp-cover-scrim,.wh-tuck>.wh-fsv-scrim{top:calc(var(--wh-rb) * -1);bottom:0;height:auto;}.wh-tuck>.band.ph{margin-top:calc(var(--wh-rb) * -1);border-top:var(--wh-rb) solid transparent;box-sizing:content-box;background-origin:border-box;}.wh-rb>.ph.tintn,.wh-rb>.bg.ph,.wh-rb>.svp-ph,.wh-rb>.svp-wash,.wh-rb>.svp-cover-scrim,.wh-rb>.wh-fsv-scrim{bottom:-1px;height:auto;}.cta-band.wh-tuck,.cta-band.wh-rb{clip-path:none;}.cta-band.wh-tuck{overflow:hidden;border-top-width:0;padding-top:calc(clamp(180px,22vw,300px) + var(--wh-rb));}.cta-band.cta-snug.wh-tuck{padding-top:calc(clamp(90px,11vw,150px) + var(--wh-rb));}.cta-band.wh-tuck>.ph.tintn{top:0;}.cta-band.wh-rb{overflow:hidden;border-radius:0 0 var(--wh-rb) var(--wh-rb);}:where(.wh-tuck).svp-room{min-height:calc(min(560px,90vh) + var(--wh-rb));}@media (max-width:900px){:where(.wh-tuck).svp-room{min-height:0;}}:where(.wh-tuck).svp-car{min-height:calc(min(540px,80vh) + var(--wh-rb));}:where(.wh-tuck).svp-stats{min-height:calc(min(520px,84vh) + var(--wh-rb));}@media (max-width:900px){:where(.wh-tuck).svp-stats{min-height:calc(420px + var(--wh-rb));}}:where(.wh-tuck).svp-cover{min-height:calc(min(620px,92vh) + var(--wh-rb));}:where(.wh-tuck).wh-fsvideo{height:calc(100vh - var(--drop-h,64px) + var(--wh-rb));height:calc(100lvh - var(--drop-h,64px) + var(--wh-rb));}:where(.wh-tuck).wh-fsvideo.wh-fsv-34{height:calc((100vh - var(--drop-h,64px)) * .75 + var(--wh-rb));height:calc((100lvh - var(--drop-h,64px)) * .75 + var(--wh-rb));}.wh-tuck>.wh-fsvideo-video,.wh-tuck>.wh-fsvideo-poster,.wh-tuck>.wh-fsvideo-fallback{top:calc(50% - var(--wh-rb) / 2);height:calc(100vh - 62px + var(--wh-rb));height:calc(100lvh - 62px + var(--wh-rb));}:where(.wh-tuck).wh-pc{min-height:calc(100svh - var(--drop-h,64px) + var(--wh-rb));}@supports not (height:100svh){:where(.wh-tuck).wh-pc{min-height:calc(100vh - var(--drop-h,64px) + var(--wh-rb));}}@media (max-width:1000px){:where(.wh-tuck).wh-pc{min-height:0;}}

.hm-btn,.hs-btn,.wh-fsv-btn,.cta-band .inner>.btn,.enqf-btn,.enqf--wide .enqf-btn,.enqf--wide .enqf-back,.svp-enq.svp-enq--chip,.svp-more,.svp-sub-field,.svp-sub-row .btn{
  box-sizing:border-box!important;height:45px!important;padding:0 24px!important;
  font-family:var(--serif)!important;font-size:15px!important;font-weight:600!important;
  font-style:normal!important;letter-spacing:-.01em!important;text-transform:none!important;line-height:1!important;
  white-space:nowrap;}
.svp-enq--chip .t{font-family:inherit;font-size:inherit;font-weight:inherit;letter-spacing:inherit;}
/* The "Show more" count keeps its accent colour; its serif at 700 comes down to
   the chip's 600 so the chip reads as one piece (Codex on #522). */
.svp-more .svp-more-n{font-family:inherit;font-weight:inherit;}
/* The subscribe band's email field is a chip beside its button (18 Oct 2026):
   the same 45px and the same 15px, but its text is the reader's typing, not a
   label, so it stays in the body face at the regular weight and keeps 4px more
   side padding to stay out of the curve. */
.svp-sub-field{padding:0 28px!important;font-family:var(--body)!important;font-weight:400!important;letter-spacing:0!important;}
