/* =========================================================================
   FOLDS — header, and the stage that carries fold 1 into fold 2
   ========================================================================= */

/* -------------------------------------------------------------------------
   HEADER — pills on a white rail
   ------------------------------------------------------------------------- */
.hdr {
  position: fixed; inset: 0 0 auto 0; z-index: 120;
  padding: clamp(0.9rem, 1.6vw, 1.4rem) 0;
  transition: background-color var(--t-2) var(--ease),
              box-shadow var(--t-2) var(--ease);
}
.hdr.is-stuck {
  background: rgba(var(--white-rgb), 0.82);
  backdrop-filter: saturate(1.5) blur(14px);
  box-shadow: 0 1px 0 var(--rule-quiet);
}
/* justify-content:space-between pins brand left / hdr-right right on its
   own — desktop additionally relies on .pillnav's auto side-margins to
   center itself in the middle, but that trick needs a justified anchor at
   each end regardless, and it's the only anchor left once .pillnav is
   display:none below 1024px. */
/* Wider than every other .shell on purpose: at --page (1360px) the pill nav
   read as cramped against the logo and CTA on wide screens, well before
   .shell's own max-width was reached. Scoped to the header only — the rest
   of the site keeps --page, so this trades a few px of edge alignment with
   the hero/folds below for a header row with room to breathe. */
.hdr .shell { max-width: calc(var(--page) + 200px); display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }

/* min-width:0 lets this shrink below its text's intrinsic width instead of
   pushing .hdr-right off the row — flex items default to min-width:auto,
   which is the content's full unbroken width. Needed for .brand-name's
   ellipsis below to ever actually engage. */
.brand { display: flex; align-items: center; gap: 0.65rem; min-width: 0; }
.brand img { width: 34px; height: 34px; flex: none; }
.brand b { min-width: 0; font-size: var(--fs-md); font-weight: 700; letter-spacing: var(--track-tight); line-height: 1.1; }
/* The site title comes from Settings > General and isn't length-limited —
   a long one (or a site title that's a full domain) must not be able to
   push the header's CTA button off the row or into overlap with it. */
.brand-name {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.brand i {
  display: block; font-style: normal;
  font-size: var(--fs-nano); font-weight: 600; letter-spacing: var(--track-micro);
  text-transform: uppercase; color: var(--ink-quiet);
}

.pillnav {
  margin-inline: auto;
  display: flex; align-items: center; gap: 2px;
  padding: 4px;
  border-radius: var(--r-pill);
  background: var(--n-05);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet);
}
.pillnav a {
  padding: 0.55rem 1.05rem;
  border-radius: var(--r-pill);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ink-mid);
  transition: background-color var(--t-1) var(--ease), color var(--t-1) var(--ease);
}
.pillnav a:hover { background: var(--white); color: var(--ink); }
/* header.php's wp_nav_menu() call wraps each link in <li> once a real menu
   is assigned in Appearance > Menus (the fallback function it replaces
   outputs plain <a> tags with no <li> at all, which is why this never
   showed up before a menu existed). display:contents drops the <li>'s own
   box — including the browser's default list-item bullet — so its <a>
   becomes the flex item directly, exactly like the fallback's bare links. */
.pillnav li { display: contents; }

/* Items with children (Our Doctors, Patient Info, Our Services) get a
   nested <ul class="sub-menu"> from wp_nav_menu(). Left under the
   display:contents rule above, the parent <li>'s box disappears and that
   <ul> gets promoted straight into the pillnav's flex row — unstyled,
   permanently visible, not a dropdown. Opting has-children <li>s out of
   display:contents gives the dropdown a real box to anchor to. */
.pillnav li.menu-item-has-children {
  display: inline-flex;
  align-items: center;
  position: relative;
}
.pillnav li.menu-item-has-children > a::after {
  content: "";
  display: inline-block;
  width: 5px; height: 5px;
  margin-left: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  opacity: 0.55;
}
.pillnav .sub-menu {
  display: none;
  position: absolute;
  top: 100%; left: 0;
  z-index: 10;
  min-width: 210px;
  margin: 8px 0 0;
  padding: 6px;
  list-style: none;
  border-radius: var(--r-md);
  background: var(--white);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet),
              0 26px 50px -30px rgba(var(--deep-rgb), 0.45);
}
.pillnav li.menu-item-has-children:hover > .sub-menu,
.pillnav li.menu-item-has-children:focus-within > .sub-menu {
  display: block;
}
/* The 8px margin-top above bridges a real gap between the pill link and
   the dropdown box. Since the dropdown only shows on li:hover, that gap
   is dead space belonging to neither element — the cursor loses hover
   crossing it, closing the menu before a click can land. This invisible
   pseudo-element fills the gap so hover stays continuous from link to
   dropdown item. */
.pillnav .sub-menu::before {
  content: "";
  position: absolute;
  top: -8px; left: 0; right: 0;
  height: 8px;
}
.pillnav .sub-menu li { display: block; }
.pillnav .sub-menu a {
  display: block;
  padding: 0.55rem 0.75rem;
  border-radius: calc(var(--r-md) - 6px);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--ink-mid);
  white-space: nowrap;
}
.pillnav .sub-menu a:hover { background: var(--n-05); color: var(--ink); }

/* =========================================================================
   OUR SERVICES — mega menu
   The other has-children pills (Our Doctors, Patient Info) keep the plain
   dropdown above; this one branch alone gets a two-column panel: every
   service listed on the left, a preview image on the right that swaps to
   match whichever row is hovered/focused (site.js's megaMenu()). Markup
   comes from Millwaydental_Mega_Menu_Walker (inc/class-mega-menu-walker.php)
   — the <ul class="sub-menu mega-menu"> it outputs keeps the "sub-menu"
   class so the hover-open rule above and mobileNavAccordion()'s
   `:scope > .sub-menu` lookup still find it unchanged; only the styling
   below diverges for it.

   Every selector that touches an <a>, <li> or the <ul> itself is prefixed
   .pillnav .sub-menu.mega-menu (or deeper) rather than left bare — the
   generic .pillnav .sub-menu / .sub-menu a / .sub-menu a:hover rules above
   type-select every element inside ANY sub-menu, this one included, at
   specificity (0,2,1)/(0,3,1). A bare two-class selector like
   .mega-menu .mega-row sits at (0,2,0) and silently loses that fight
   regardless of where it's written in the file — matching or exceeding the
   class count is what actually wins it.

   Positioning stays anchored to the trigger <li> (position:relative, same
   as every other has-children item) rather than centred under the whole
   header — an earlier version centred it on .hdr instead, which put real
   dead space between the pill and the panel's top edge (the header's own
   padding, not covered by either the <li> or the panel), and a mouse
   moving down through that space lost :hover before ever reaching the
   panel. Anchoring to the <li> keeps that gap the same tight few px the
   working dropdowns already have. Right-aligned rather than left-aligned
   (right:0 vs the plain dropdown's left:0) because "Our Services" sits
   near the right end of the pill row, next to the CTA — a wide panel
   hung off the left edge there would run past the viewport. */
.pillnav .sub-menu.mega-menu {
  top: 100%; left: auto; right: 0;
  width: min(772px, calc(100vw - 2 * var(--gutter)));
  min-width: 0;
  padding: 0;
  overflow: hidden;   /* keeps the footer bar's fill inside the rounded corners */
  /* deeper than the plain dropdown's — this panel is six times the area and
     needs to read as sitting well above the page, not resting on it */
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet),
              0 32px 64px -28px rgba(var(--deep-rgb), 0.30),
              0 8px 20px -12px rgba(var(--deep-rgb), 0.14);
}

/* Open/close, once site.js has taken over.
   .mega-ready is added by megaMenu() at init — until then (and forever, if
   scripting is off) the generic :hover/:focus-within display rule above is
   in sole charge and the panel works exactly like the other two dropdowns,
   just without the entrance animation.

   The handover matters because you cannot tween from display:none: with
   .mega-ready the panel stays display:block permanently and visibility +
   pointer-events carry the open state instead, which gives GSAP a real box
   to animate opacity and transform on. */
.pillnav li.has-mega.mega-ready > .sub-menu.mega-menu {
  display: block;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}
/* opacity:1 here, not left to GSAP: with reduced-motion on (or GSAP absent)
   nothing ever sets an inline opacity, and an open panel still stuck at the
   base rule's 0 would be invisible rather than merely unanimated. GSAP's
   own inline tween overrides this while it runs and lands on the same
   value, so the two never disagree. */
.pillnav li.has-mega.mega-ready.is-open > .sub-menu.mega-menu {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
}

.mega-grid { display: flex; align-items: stretch; padding: 0.9rem; gap: 0.9rem; }

/* Two columns, not one. Eleven services in a single column ran ~570px tall
   against a preview card barely half that, so the panel was mostly a long
   thin list with dead space beside it. Paired up it is six rows deep, which
   is within a few px of the card's own height — the two sides now finish
   together instead of one trailing the other. */
.mega-list {
  flex: 1 1 auto; min-width: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* --mega-rows is set per-menu by the walker (see its end_lvl comment):
     with column flow it is the column break, so the services read top to
     bottom down column one and continue in column two, rather than
     alternating across the two. The 6 is only a fallback for markup that
     somehow arrives without it. */
  grid-template-rows: repeat(var(--mega-rows, 6), auto);
  grid-auto-flow: column;
  gap: 2px;
  align-content: start;
  /* the seam between the list and the preview card, instead of a bare gap —
     reads as one structured panel rather than two floating blocks */
  border-right: var(--hair) solid var(--rule-quiet);
  padding-right: 0.9rem;
}

/* Reset first: every <a> in here is also a .sub-menu a (see the note
   above), which sets display:block / padding / white-space:nowrap / color
   at (0,2,1). The per-row/card/foot rules below add back real values at
   equal-or-higher specificity, so they still win despite coming later. */
.pillnav .sub-menu.mega-menu a {
  padding: 0; white-space: normal; color: inherit; font-weight: inherit;
}

.pillnav .sub-menu.mega-menu .mega-row {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.42rem 1.3rem 0.42rem 0.42rem;
  border-radius: 10px;
  position: relative;
  transition: background-color var(--t-1) var(--ease);
}
.mega-row-thumb {
  flex: none; display: block;
  width: 38px; height: 38px;
  border-radius: 9px; overflow: hidden;
  background: var(--n-05);
}
.mega-row-thumb img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform var(--t-2) var(--ease);
}
.mega-row-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.mega-row-t {
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink);
  line-height: 1.25;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* the category the service page's own hero eyebrow already carries —
   uppercase micro, the same language every other eyebrow on the site uses */
/* 0.06em rather than --track-label's 0.15em: these are real category names
   ("Aesthetic & confidence", "Restorative dentistry"), not the two-word
   eyebrows that tracking was set for, and at full label tracking the longer
   ones ellipsised inside the column. */
.mega-row-cat {
  font-size: var(--fs-nano); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-quiet);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pillnav .sub-menu.mega-menu .mega-row:hover,
.pillnav .sub-menu.mega-menu .mega-row.is-active {
  background: var(--n-05);
}
.pillnav .sub-menu.mega-menu .mega-row:hover .mega-row-thumb img,
.pillnav .sub-menu.mega-menu .mega-row.is-active .mega-row-thumb img {
  transform: scale(1.08);
}
.mega-row-arrow {
  position: absolute; right: 0.5rem; top: 50%;
  width: 12px; height: 12px;
  transform: translateY(-50%) translateX(-4px);
  color: var(--ink-quiet);
  opacity: 0;
  transition: opacity var(--t-1) var(--ease), transform var(--t-1) var(--ease);
}
.pillnav .sub-menu.mega-menu .mega-row:hover .mega-row-arrow,
.pillnav .sub-menu.mega-menu .mega-row.is-active .mega-row-arrow {
  opacity: 1; transform: translateY(-50%) translateX(0);
}

/* --- the preview card ----------------------------------------------------
   Follows whichever row is hovered or focused: photo, category, name,
   description, explore link. Every service's photo is stacked in the frame
   at once and crossfaded (site.js), so no hovered row ever waits on a
   network round trip to show its image. */
.mega-visual {
  flex: 0 0 236px;
  display: flex; flex-direction: column; gap: 0.6rem;
}
.pillnav .sub-menu.mega-menu .mega-visual-frame {
  position: relative; display: block;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--n-05);
}
.mega-visual-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
}
.mega-visual-img.is-active { opacity: 1; }
.mega-visual-body { display: flex; flex-direction: column; gap: 3px; }
.mega-visual-cat {
  font-size: var(--fs-nano); font-weight: 600;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  color: var(--accent-text);
}
.mega-visual-cat:empty { display: none; }
.mega-visual-title {
  font-size: var(--fs-md); font-weight: 700;
  letter-spacing: var(--track-tight); line-height: 1.2;
  color: var(--ink);
}
/* --fs-xs, not --fs-nano: this is the one piece of running copy in the
   panel rather than a label, and at nano (9px) it was below the size the
   rest of the site sets for anything meant to actually be read. */
.mega-visual-desc {
  font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-mid);
  /* clamped rather than left to grow — the real lead paragraphs run from 20
     to 60 words, and the card's height is what keeps the two columns level */
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.pillnav .sub-menu.mega-menu .mega-visual-cta {
  display: inline-flex; align-items: center; gap: 0.4rem;
  margin-top: auto;
  font-size: var(--fs-xs); font-weight: 700; color: var(--accent-text);
}
.mega-visual-cta svg {
  width: 13px; height: 13px;
  transition: transform var(--t-1) var(--ease);
}
.mega-visual:hover .mega-visual-cta svg { transform: translateX(3px); }

/* --- the footer bar ------------------------------------------------------
   Full width across the panel's foot, on the quiet tint so it reads as a
   base the two columns sit on rather than a third block floating beside
   them. The preview card above is aria-hidden (it only mirrors a row that
   is already in the tab order), so these are the panel's own real links. */
.mega-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-2);
  padding: 0.7rem 0.9rem;
  border-top: var(--hair) solid var(--rule-quiet);
  background: var(--surface-quiet);
}
.pillnav .sub-menu.mega-menu .mega-foot-all {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--fs-xs); font-weight: 700; color: var(--ink);
}
.mega-foot-all svg {
  width: 13px; height: 13px;
  transition: transform var(--t-1) var(--ease);
}
.pillnav .sub-menu.mega-menu .mega-foot-all:hover svg { transform: translateX(3px); }
.mega-foot-aside { display: inline-flex; align-items: center; gap: 0.6rem; }
.mega-foot-note { font-size: var(--fs-nano); color: var(--ink-quiet); }
.pillnav .sub-menu.mega-menu .mega-foot-cta {
  display: inline-flex; align-items: center;
  padding: 0.42rem 0.85rem;
  border-radius: var(--r-pill);
  background: var(--deep); color: var(--on-deep);
  font-size: var(--fs-nano); font-weight: 700;
  transition: background-color var(--t-1) var(--ease);
}
.pillnav .sub-menu.mega-menu .mega-foot-cta:hover { background: var(--deep-soft); }

.hdr-right { display: flex; align-items: center; gap: var(--sp-3); flex: none; }
.hdr-tel { font-size: var(--fs-sm); font-weight: 600; white-space: nowrap; }
/* the header CTA is just .btn .btn-primary now — see base.css */

/* --- mobile menu trigger -------------------------------------------------
   Hidden above 1024px, where .pillnav already sits in the row and needs no
   trigger — see the narrow-build rules further down for where this turns
   on and .pillnav becomes the panel it opens. A plain 44px button rather
   than .btn: this is a utility control, not a call to action, and doesn't
   want the flood-on-hover or disc language those carry. */
.hdr-burger {
  display: none;
  position: relative;
  width: 44px; height: 44px; flex: none;
  padding: 0; border: 0; background: none;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.hdr-burger-bar {
  position: absolute; left: 50%; top: 50%;
  width: 20px; height: 2px;
  margin-left: -10px;
  background: var(--ink);
  border-radius: var(--r-pill);
  transition: transform var(--t-1) var(--ease), opacity var(--t-1) var(--ease);
}
/* three bars, 6px apart, centred as a set on the button's own middle —
   translateY off the shared 50% top rather than three separate top values,
   so the 6px gap is the one number that has to change to retune it */
.hdr-burger-bar:nth-child(1) { transform: translateY(-6px); }
.hdr-burger-bar:nth-child(2) { transform: translateY(0); }
.hdr-burger-bar:nth-child(3) { transform: translateY(6px); }
/* the X: bars 1 and 3 rotate onto the centre bar's line, which fades out
   rather than also rotating — a middle bar rotated to 0-length reads as a
   flicker, where a fade reads as the two remaining bars crossing over it */
.hdr-burger.is-open .hdr-burger-bar:nth-child(1) { transform: translateY(0) rotate(45deg); }
.hdr-burger.is-open .hdr-burger-bar:nth-child(2) { opacity: 0; }
.hdr-burger.is-open .hdr-burger-bar:nth-child(3) { transform: translateY(0) rotate(-45deg); }
.hdr-burger:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* site.js injects one of these next to every has-children link regardless
   of viewport (see mobileNavAccordion() — it doesn't check width, the CSS
   does). Hidden here so it never appears in the desktop pill row, where
   :hover/:focus-within already open the dropdown and a visible arrow
   button would just be a second, redundant control sitting beside the
   first. The narrow-build rules further down give it its real shape. */
.pillnav-toggle { display: none; }

/* The mobile panel's footer actions (header.php's items_wrap). Off in the
   wide build, where the header row already shows the phone number and the
   booking button. */
.pillnav-cta { display: none; }

/* the tap-to-close backdrop below the fixed header — see the narrow-build
   rules for its shown state (site.js drops the [hidden] attribute and adds
   .is-open together, so this never needs its own opacity transition to
   fight the attribute's display:none). */
.nav-scrim {
  display: none;
  position: fixed; inset: 0; z-index: 115;   /* under .hdr (120), over page content */
  background: rgba(var(--shade-rgb), 0.4);
}

/* =========================================================================
   THE STAGE
   Fold 1 and fold 2 are one pinned scene. The specimen never re-mounts: the
   tooth met in the hero is literally the tooth that develops a cavity on the
   way down and then gets repaired.
   ========================================================================= */
/* The pin is 100svh and the scene runs for 7.2 screens, so GSAP's spacer is
   8.2 screens tall — and the last screen of it is an empty pin scrolling out
   after the specimen has already faded. That was a full blank screen between
   the two folds. Pulling the next section up over that dead tail closes it,
   and fold 3 arrives just as the specimen finishes leaving. */
.stage {
  position: relative;
  /* One source of truth for the specimen's geometry. The band below is
     positioned FROM these rather than from the bottom of the screen, so the
     gap under the tooth is a constant instead of whatever happens to be left
     over — it measured 44px on one screen and -7px on another. */
  --tooth-rise: -44px;   /* matches RISE in site.js */
  /* the space between line 2's own rendered bottom and .hero-foot — not
     "under the tooth" any more now that line 2 sits below it; see .hero-foot */
  --tooth-gap: 20px;
  --tooth-h: min(62vh, 545px, calc(100svh - 460px));
  /* The vertical anchor everything in the hero hangs off — the specimen, both
     halves of the headline, both floating cards, .hero-foot. Plain 50svh grows
     without limit on a tall monitor: at 2560x1440 it pushed the whole
     composition 228px below the header, versus 13px at a common 900px-tall
     laptop screen, and the header itself does not grow to match. Capped so
     the anchor stops moving once the header-to-content gap is already
     comfortable — measured directly against the header's own (roughly fixed)
     height, not derived from it. */
  --stage-center: min(50svh, 540px);
  /* Line 1 sits at a fixed distance above the specimen's centre; line 2 hangs
     --word-gap below line 1's OWN bottom edge, not symmetrically around the
     centre — moving line 2 is a --word-gap edit, not a --line1-offset one,
     and the two are deliberately independent so one can move without
     dragging the other along with it.

     --word-gap is small (40px, not the ~140-220px tried earlier) on purpose:
     the tooth asset has a baked-in contact shadow low on its roots (see
     .hero-word-split below) that reads as damage once real letters sit
     behind it, so line 2 is kept close under line 1 and high against the
     tooth's solid crown/upper-root, well clear of that shadow. Line 1 keeps
     its own deliberate bite into "Smiles" — that part of the tooth has no
     shadow problem and was never in question. */
  /* 104, up from 80: this lifts BOTH lines (line 2's own formula subtracts it
     too), which is what opens the room under the headline without touching
     the captions. The ceiling is the header, not the tooth — at 1440x820, the
     tightest common case, the old value left 77px of clearance and this
     leaves 53. */
  --line1-offset: 112px;
  /* 24, down from 40: line 2 rides an extra 16px closer to line 1, so the two
     lines read as one headline rather than two stacked ones, and every pixel
     it gains is a pixel added below it. */
  --word-gap: 24px;
  /* The captions do NOT follow the two lines.

     They used to hang off line 2's own formula, so tightening --word-gap or
     lifting --line1-offset dragged them up by exactly as much as the headline
     gained and the 21px gap under line 2 never actually opened. This is their
     own anchor, and 40px is the (--line1-offset − --word-gap) the old formula
     resolved to — so the cards sit precisely where they always did while the
     headline moves off them. Raise it to lift the captions, lower it to drop
     them; neither touches the headline. */
  --caption-drop: 40px;
  /* The horizontal gap "Take"/"Shape." sit either side of, AND the same gap
     the two captions below them align to — one number, shared, so the words
     and their captions can never drift apart from each other. Sized off the
     tooth's own rendered width (--tooth-h times its 680:1209 aspect), not a
     flat px number, so it tracks the tooth at every viewport. 0.8x leaves
     the tooth's outer edges overlapping the innermost letters ("e" of Take,
     "S" of Shape) by a deliberate sliver. */
  --flank-gap: calc(var(--tooth-h) * 680 / 1209 * 0.8);
}
/* Fold 3 exists now, so the pull-up is back. It only applies to the pinned
   build: stacked, there is no dead tail and pulling the next section up would
   bury the procedure.

   82, not 70 and not 100. The spacer runs one viewport past the pin's end, so
   whatever this does not claw back is dead scroll: at 70 the reader crossed
   270px of empty white between fold 2 finishing and fold 3's panel reaching
   the top. 100 closes that gap exactly — and is too far, because fold 3's
   eyebrow then arrives while fold 2's "Polished & Checked" is still legible
   and the two collide, which is the failure the fade length was tuned around
   in the first place. 82 halves the dead band to ~160px and still lands fold
   3's panel edge below the last of fold 2. Measured, not guessed: the
   collision appears between 90 and 100.

   .is-pinned is set by site.js at the moment the pin is created, because the
   pull is measured against the spacer GSAP inserts and is nonsense without
   it. The media query alone still matched with scripting off, where no spacer
   exists, and fold 3 landed 738px up on top of the hero. */
@media (min-width: 1025px) and (prefers-reduced-motion: no-preference) {
  .stage.is-pinned { margin-bottom: -82svh; }

  /* ...and OFF again on the Treatments page, which is the stage on its own
     with nothing under it but the footer.

     Not a taste call — the pull-up has a hard ceiling there. GSAP's spacer is
     8.2 screens (the 100svh pin plus the 7.2 screens of scroll the scene
     runs for), and progress only reaches 1 once the reader has scrolled 7.2
     screens past the top of the section. Maximum scroll is
     (spacer − pull-up + everything below the stage) − one screen, so the
     scene can only finish while `pull-up ≤ height of everything below the
     stage`. On the homepage that is nine folds and the pull-up is free; on a
     standalone stage page it is the footer, roughly half a screen — so 82svh
     would silently steal the last third of the procedure and the tooth would
     never be repaired.

     The cost is the dead tail the pull-up exists to hide: about one screen of
     white between the scene fading out and the footer arriving. Visible, but
     it is white space rather than a scene that cannot finish. */
  .stage.stage--standalone.is-pinned { margin-bottom: 0; }
}
.stage.is-static { margin-bottom: 0; }
/* GSAP injects .pin-spacer as a full-height block around the pin. It spans the
   whole scene, so where fold 3 is pulled up underneath it, it swallowed the
   first register row's hover and links. Nothing but the pin itself ever needs
   to take events, so the spacer is opted out and the pin opted back in. */
.stage .pin-spacer { pointer-events: none; }

.stage-pin {
  height: 100svh;
  position: relative;
  z-index: 2;              /* stays over fold 3 while the scene is still pinned */
  pointer-events: auto;
  overflow: hidden;
  isolation: isolate;
  /* no background: the body is already white, and an opaque pin would hide
     fold 3 where the two now overlap */
}

/* --- the shared specimen ------------------------------------------------ */
.spec {
  position: absolute;
  left: 50%; top: var(--stage-center);
  margin-left: -0.5px;
  z-index: 3;
  display: grid; place-items: center;
  will-change: transform;
  /* No blend mode: the frames are matted and carry a real alpha channel, so
     the tooth is genuinely opaque and simply sits in front of the word. This
     is what the multiply trick could never do — under multiply, dark type
     behind a light subject reads straight through it. */
}
/* v9 puts two cards in the bottom band, and the specimen's roots landed on
   top of them — 620px against a card top of 619px at 1440x820. The third term
   is the fix: reserve the band's height instead of stepping at one arbitrary
   breakpoint, so the tooth is only ever as tall as the space actually left
   above the cards. 420 is measured against the real band: the cards lost
   their icon discs and dropped to 153, so the reserve came down with them and
   the specimen got the space back. */
.spec img, .spec canvas { height: var(--tooth-h); width: auto; }
/* the compacted band measures ~135 without the icon discs, so the reserve and
   the caps both relax. The card overrides that go with this live below the
   .hcard rules — put here they lost to them, since a media query adds no
   specificity. */
@media (min-width: 1025px) and (max-height: 900px) {
  .stage {
    --tooth-h: min(56vh, 470px, calc(100svh - 420px));
    /* Same reasoning as --tooth-h above, applied to the two headline lines:
       a shorter viewport leaves less room between the header and the
       specimen's own top edge for line 1's portion above the bite (and,
       symmetrically, between the specimen and .hero-foot for line 2's). */
    --fs-hero: clamp(2.2rem, 10.3vw, 7.95rem);
  }
}
.spec canvas { position: absolute; opacity: 0; }
.stage.live .spec canvas { opacity: 1; }
.stage.live .spec img { opacity: 0; }

.spec-halo {
  position: absolute; left: 50%; top: var(--stage-center);
  width: min(70vh, 600px); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 50% 46%,
    rgba(var(--sage-rgb), 0.30), rgba(var(--sage-rgb), 0.07) 58%, transparent 72%);
  z-index: 2;
  will-change: transform, opacity;
  pointer-events: none;
}

/* --- fold 1 layer -------------------------------------------------------
   No z-index here (used to be 1) — a positioned element WITH a z-index
   creates a stacking context, which traps every descendant's own z-index
   inside it. That is fine for .hero-word-line--top (still meant to run
   behind the specimen) but was silently overriding --bottom's own z-index
   below too: "Shape" needs to float clear of the tooth the same way the two
   floating cards do, and as long as this element created a context there was
   no z-index high enough on --bottom to make that happen — the whole
   hero-layer subtree painted as one unit, behind .spec, regardless. Removing
   it lets --top and --bottom set genuinely different stacking without
   restructuring the DOM (both still have to live inside the one <h1> for the
   accessible name to include the whole phrase). */
.hero-layer { position: absolute; inset: 0; }

/* The two lines sit behind the specimen at the edges where they meet it, and
   are tonal rather than black for the same reason the old single line was:
   the frames carry no alpha and composite with multiply, so a light subject
   over dark type would disappear. Decorative — the accessible heading is the
   h1 itself, id="hero-h". */
.hero-word { position: static; margin: 0; }
/* Display:none at every width. This started life as "hidden on desktop,
   released on a phone" — the wide hero has the specimen and the two cards
   to say what this is, and a standfirst there would be a third thing
   competing with the headline — but the phone release was removed on
   request, to give the mobile hero the space back (see the narrow-build
   block further down, where the override used to live). Left as a real
   rule rather than deleted outright: the markup still carries the
   paragraph in both front-page.php and template-treatments.php, so this is
   what keeps it off every screen rather than relying on nobody ever adding
   a release for it again by accident. */
.hero-lede { display: none; }

.hero-word-line {
  position: absolute; left: 0; right: 0;
  margin: 0;
  text-align: center;
  font-size: var(--fs-hero);
  font-weight: 800;
  letter-spacing: var(--track-hero);
  line-height: 0.9;
  color: var(--ink);
  white-space: nowrap;
  will-change: transform, opacity;
}
/* Line 1 hangs --line1-offset above the specimen's centre; line 2 hangs
   --word-gap below line 1's OWN bottom edge — see the comment on
   --line1-offset above for why they are independent rather than a
   symmetric pair.

   Both use `top`, never `bottom`, and that is load-bearing, not a style
   choice: .stage-pin is 100svh while the scene is pinned, but grows to `auto`
   (height enough for the whole procedure, laid out in normal flow) the
   moment .is-static applies — reduced motion, or JS never having run at all.
   `bottom: X` measures up from the CONTAINING BLOCK's bottom edge, so the
   same X lands in a wildly different place once that block goes from ~900px
   to several thousand — this element ended up rendered a full 1700px down
   the page, off the bottom of the visible hero entirely. `top: X` measures
   down from the top edge instead, which never moves. */
.hero-word-line--top {
  top: calc(var(--stage-center) + var(--tooth-rise) - var(--line1-offset) - 0.9 * var(--fs-hero));
  z-index: 1; /* behind .spec (3) — the deliberate "little part comes on text" look */
}
.hero-word-line--bottom {
  /* line 1's bottom edge (stage-center + tooth-rise - line1-offset) + the gap */
  top: calc(var(--stage-center) + var(--tooth-rise) - var(--line1-offset) + var(--word-gap));
  /* Back behind .spec (3), matching line 1 — briefly moved to z-index:6 (in
     front of the tooth) to keep "Shape" legible after the tight --word-gap
     hid its "Sh" entirely, now moved back on request: both lines take the
     same occluded, "tooth cuts through the type" treatment. */
  z-index: 1;
}

/* "Take" and "Shape." flank the tooth instead of running as one centred
   phrase through the middle of it. A two-column GRID, not flex with
   justify-content:center on a single run — that distinction matters here,
   not just as a implementation detail. "Shape." is visibly wider than "Take"
   (six letters and a period against four), so centering the pair as ONE
   flex group put the gap's own midpoint off the tooth's centre by half that
   width difference: "Take" cleared the tooth with 52px to spare while the
   tooth ate 105px into "Shape." A two-column grid gives each word its OWN
   equal-width half — "Take" end-aligned in the left one, "Shape." start-
   aligned in the right — so the gap between them sits at exactly 50% of the
   line, matching the tooth's own centre (also 50%), regardless of how the
   two words' widths compare.

   The gap is --flank-gap, the same one the two captions below align to
   (see .hero-caption) — one shared number, so a word and its caption can
   never drift apart from each other. */
.hero-word-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: var(--flank-gap);
  align-items: baseline;
}
.hero-word-take {
  justify-self: end;
  font-weight: 300;
  color: var(--sage-deep);
}
.hero-word-shape { justify-self: start; }
/* .hero-word-shape needs no rule of its own — it inherits color: var(--ink)
   from .hero-word-line, same as "Shape." did as plain trailing text before. */

/* -------------------------------------------------------------------------
   CAPTIONS — the rating and the location, sitting plainly under "Take" and
   "Shape." Normal placement, on request: no rotation, no idle bob, no
   scatter — the opposite of how these two used to behave (floating above
   "Where Great Smiles", tilted, bobbing). Both still sit above .spec
   (z-index 3), because unlike the word, a trust signal has to stay legible
   rather than lose a letter to the specimen.

   Each is a positioned ZONE (.hero-caption--rating / --loc, one per half of
   the line) containing a naturally-sized CARD (.hero-caption-card) pushed to
   the zone's inner edge — the same two-piece structure .hero-word-split
   uses for "Take"/"Shape.", and for the same reason: a caption centred in
   its own half would not line up with a word that is end/start-aligned
   rather than centred in its half. */
.hero-caption {
  position: absolute;
  /* Anchored to the specimen, NOT to line 2 — see --caption-drop for why.
     The line-box term and the 1.3rem stay so the cards still scale with the
     headline's own size at every viewport. */
  top: calc(
    var(--stage-center) + var(--tooth-rise) - var(--caption-drop)
    + 0.9 * var(--fs-hero) + 1.3rem
  );
  z-index: 5;
  display: flex;
  will-change: transform, opacity;
}
.hero-caption--rating {
  left: 0;
  right: calc(50% + var(--flank-gap) / 2);
  justify-content: flex-end; /* card's inner edge meets "Take"'s inner edge */
}
.hero-caption--loc {
  left: calc(50% + var(--flank-gap) / 2);
  right: 0;
  justify-content: flex-start; /* card's inner edge meets "Shape."'s inner edge */
}

.hero-caption-card {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  background: var(--white);
  border-radius: var(--r-md);
  box-shadow: 0 4px 10px rgba(var(--deep-rgb), 0.08), 0 22px 46px -18px rgba(var(--deep-rgb), 0.38);
  /* white-space lives on the individual text lines below, not here — both
     cards now carry two lines and nowrap on the flex container itself would
     say nothing about them (it only ever governed this element's own text
     content, and neither card has any left directly inside it any more). */
}
.hero-caption-card--rating { padding: 0.5rem 1.05rem 0.5rem 0.5rem; }
.hero-caption-card--loc    { padding: 0.5rem 1.15rem 0.5rem 0.5rem; }

/* the shared two-line text column both cards now use, sitting beside the
   avatar stack on one and the pin badge on the other */
.hero-caption-copy { display: flex; flex-direction: column; gap: 0.18rem; }

/* --- the rating card: a stack of the site's own reviewers, not stock faces
   -------------------------------------------------------------------------
   Five overlapping initials, not photographs — every set of stock "happy
   customer" headshots the web already has too many of, and these have to
   read as this practice's own 240+, not a template's. The five pairs are
   the five reviewers quoted verbatim in the Reviews fold further down this
   page (Roger Clark, Dahlia Wright, Chris Renton, Roxanne Labeda, Linda
   Ferguson) — real people who left real reviews, in the same order they
   appear there, so the claim the stack makes is one the page can back up a
   few folds later rather than an invented crowd. */
.hero-avatars { display: flex; flex: none; }
.hero-avatar {
  width: 27px; height: 27px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.01em;
  box-shadow: 0 0 0 2px var(--white);
}
.hero-avatar + .hero-avatar { margin-left: -8px; }
/* Two accessible pairings, alternating — not five different hues. The
   palette reserves colour-as-text to a short list already checked against
   white (tokens.css), so variety here comes from alternating those two
   rather than reaching for shades the palette never cleared. */
.hero-avatar:nth-child(odd)  { background: var(--tint-open); color: var(--leaf); }
.hero-avatar:nth-child(even) { background: var(--n-10); color: var(--ink-mid); }

.hero-caption-stars { display: inline-flex; gap: 2px; color: var(--star); }
.hero-caption-stars svg { width: 13px; height: 13px; fill: currentColor; }
.hero-caption-t { font-size: var(--fs-sm); font-weight: 500; color: var(--ink); white-space: nowrap; }
.hero-caption-t b { font-weight: 700; }

/* --- the location card: a live badge, not a static pill -------------------
   The pin sits in its own disc, same size and shadow language as the
   avatars beside it on the other card, and the open/closed dot rides its
   corner the way a presence dot rides an avatar elsewhere on the web — one
   glance answers "is anyone there right now" without adding a third line of
   text next to the two-line address. It reuses the exact open/closed
   arithmetic the Visit fold's own hours table already runs (see hours() in
   site.js), not a second guess at the same schedule. */
.hero-loc-badge {
  position: relative;
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--tint-open);
  color: var(--leaf);
}
.hero-loc-pin { width: 15px; height: 15px; }
/* Hidden until hours() confirms it can read the clock — same rule
   .visit-status follows below, for the same reason: a wrong colour reads as
   confidently wrong, where staying blank just reads as not-yet-loaded. */
.hero-loc-dot {
  position: absolute; right: -1px; bottom: -1px;
  width: 10px; height: 10px;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--white);
}
.hero-loc-dot.is-open { background: var(--leaf); }
.hero-loc-dot.is-shut { background: var(--n-40); }
/* A soft pulse ring, open hours only — closed gets a plain steady dot, since
   pulsing "closed" reads as an alert rather than as a status. */
.hero-loc-dot.is-open::after {
  content: '';
  position: absolute; inset: -3px;
  border-radius: 50%;
  border: 1.5px solid var(--leaf);
  opacity: 0.55;
  animation: hero-loc-pulse 2.2s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .hero-loc-dot.is-open::after { animation: none; display: none; }
}
@keyframes hero-loc-pulse {
  from { transform: scale(0.7); opacity: 0.55; }
  to   { transform: scale(1.9); opacity: 0; }
}

.hero-caption-name { font-size: var(--fs-sm); font-weight: 700; color: var(--ink); white-space: nowrap; }
.hero-caption-sub  { font-size: var(--fs-xs); font-weight: 500; color: var(--ink-mid); white-space: nowrap; }

.hero-foot {
  /* hung off whichever sits lower: the specimen's own bottom edge, or line
     2's rendered bottom (its own top — see .hero-word-line--bottom — plus
     its line-box height). Either one can be the taller of the two depending
     on --tooth-h and --fs-hero at a given viewport, so this needs the real
     max — anchoring to just one of them left .hero-foot overlapping
     whichever it ignored. */
  position: absolute; left: 0; right: 0;
  top: calc(max(
    var(--stage-center) + var(--tooth-rise) + var(--tooth-h) / 2,
    var(--stage-center) + var(--tooth-rise) - var(--line1-offset) + var(--word-gap) + 0.9 * var(--fs-hero)
  ) + var(--tooth-gap));
  max-width: none;
  /* A centred stack under the specimen: the two cards, then the two actions
     on one line. The three-column band is gone with the copy. */
  display: grid; justify-items: center;
  gap: clamp(0.9rem, 1.6vw, 1.35rem);
  z-index: 4;
}
/* both actions on one line, centred under the cards */
.hero-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.7rem; }

/* -------------------------------------------------------------------------
   HERO CARDS — CDCP and Invisalign, carried over from v7

   v7 drew these as glassmorphism panels. That is on the banned list and would
   not survive on a white ground anyway, so only the content came across: flat
   white card, hairline edge, sage disc for the icon, and the same arrow link
   the rest of the site uses.
   ------------------------------------------------------------------------- */
/* wider than before — the band is no longer sharing the row with two other
   columns, so the cards get the space and the photographs get bigger */
.hero-cards {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(0.9rem, 1.5vw, 1.3rem);
  width: min(100%, 940px);
}
.hcard {
  /* photo beside the copy, as on the old site */
  display: grid; grid-template-columns: 104px minmax(0, 1fr);
  gap: 1.2rem; align-items: center;
  will-change: transform;
  padding: 1.15rem 1.3rem 1.15rem 1.15rem;
  background: var(--white);
  border-radius: var(--r-sm);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet),
              0 26px 50px -40px rgba(var(--deep-rgb), 0.5);
  transition: box-shadow var(--t-2) var(--ease), transform var(--t-2) var(--ease);
}
/* a circle, cropped by object-position rather than by cutting new files */
/* The sources are 1024-square and the hole is square, so object-fit had
   nothing to crop — it just shrank the whole room into a thumbnail. The zoom
   and focal point are set per image, inline. */
.hcard-pic {
  width: 104px; height: 104px; border-radius: 50%;
  overflow: hidden; flex: none;
  background: var(--n-10);
  box-shadow: 0 0 0 3px var(--white), 0 0 0 4px var(--rule-quiet);
}
.hcard-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.hcard-t {
  font-size: var(--fs-md); font-weight: 700; line-height: 1.35;
  letter-spacing: var(--track-tight); color: var(--ink);
}
/* the one word the old site set in its accent colour */
.hcard-t em { font-style: normal; color: var(--accent-text); }
.hcard-s {
  margin-top: 0.38rem;
  font-size: var(--fs-xs); line-height: 1.55; color: var(--ink-mid);
}
.hcard-a {
  display: inline-flex; align-items: center; gap: 0.42rem;
  margin-top: 0.6rem;
  font-size: var(--fs-xs); font-weight: 600; color: var(--accent-text);
  transition: gap var(--t-1) var(--ease);
}
/* an SVG arrow, not a text one — the glyph "→" sits on a different baseline
   in every fallback face and was landing a pixel low next to the label */
.hcard-a svg { width: 13px; height: 13px; flex: none; }
.hcard:hover {
  box-shadow: inset 0 0 0 var(--hair) var(--rule),
              0 30px 56px -38px rgba(var(--deep-rgb), 0.6);
}
@media (prefers-reduced-motion: no-preference) {
  .hcard:hover { transform: translateY(-3px); }
}
.hcard:hover .hcard-a { gap: 0.7rem; }

/* Compact cards on a laptop-height screen — trimmed proportionally, not
   crushed, so the icons stay legible and the band still reads as roomy.
   Must come after the .hcard block above: same specificity, later wins. */
@media (min-width: 1025px) and (max-height: 900px) {
  .hcard { padding: 0.95rem 1.05rem; grid-template-columns: 78px minmax(0, 1fr); gap: 0.95rem; }
  .hcard-pic { width: 78px; height: 78px; }
  .hcard-t { font-size: var(--fs-sm); }
  .hcard-s { font-size: var(--fs-xs); }
}

/* -------------------------------------------------------------------------
   THE OFFER CARD — the right-hand hero card, turning over between the two
   complimentary gifts.

   It has to hold its own beside a card that never moves, in a box half the
   hero wide, without becoming the loudest thing on a screen whose subject is
   the headline. So the movement is small and it is the SAME movement in three
   places at once — the disc turns over, the second line of the title turns
   with it, and the ring around the disc fills toward the next turn. One
   gesture read three ways, rather than three things animating.

   Nothing here is decoration hung on the existing card: .hcard's own grid,
   padding, shadow, hover and compact-screen rules all still apply untouched,
   and the two cards keep exactly the same silhouette side by side.
   ------------------------------------------------------------------------- */

/* The ring lives outside .hcard-pic, which clips to a circle — so the pic
   needs a positioning parent that does not. This takes no size of its own:
   the grid column (104px, 78px compact) sizes it, and .hcard-pic fills it. */
.hcard-media { position: relative; line-height: 0; }

/* One hairline fewer than the sibling card's disc: the progress ring is the
   outer ring here, and two concentric rings 4px apart read as a mistake. */
.hcard-pic--swap {
  /* display:block is load-bearing, not tidiness. .hcard-pic is a <span> and
     gets its box only from being a direct child of .hcard, which is a grid —
     grid items are blockified. Here it sits one level down inside
     .hcard-media, so it stays inline, and an inline box ignores the
     width/height/overflow that make the disc a disc: the slides collapsed to
     nothing and the well rendered empty. */
  display: block;
  position: relative;
  box-shadow: 0 0 0 3px var(--white);
  /* the product slide blends with the wash behind it and must not reach
     through to the card underneath — see .swap-slide--product */
  isolation: isolate;
}

.swap-slide {
  position: absolute; inset: 0;
  opacity: 0;
  transform: scale(1.07);
  transition: opacity var(--t-3) var(--ease), transform 1.1s var(--ease);
}
.swap-slide.is-on { opacity: 1; transform: none; }
.swap-slide img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The smile is a face, not a product. The source is already cropped to the
   mouth, but a square image in a round hole loses its corners — so this
   pushes in a little further and anchors on the teeth, which puts the
   before/after split in the middle of the disc instead of high in it. */
.swap-slide--face img { transform: scale(1.12); transform-origin: 50% 44%; }

/* The toothbrush is a product cutout on a near-white ground, not a
   photograph of a scene, so it is framed on its own terms rather than
   cropped like one:

     · multiply drops that near-white ground onto the wash below it, so the
       disc reads as a tinted studio card instead of a white square inside a
       white card. The frames on the specimen deliberately avoid blend modes
       (see .spec) — that is about dark type behind a light subject, which is
       not the situation here.
     · rotated, because a toothbrush is a long thin object and a circle's
       longest line is its diagonal. Upright it occupied a quarter of the
       disc's width and looked like a mistake at 104px; on the diagonal it
       fills the frame and reads as styled.
     · 1.12, not more: the brush is 84% of its own frame's height, so
       0.84 x 1.12 = 0.94 of the diagonal — it grows to fill the disc and
       still keeps both ends inside it. */
.swap-slide--product { background: rgba(var(--sage-rgb), 0.16); }
.swap-slide--product img {
  object-fit: contain;
  transform: rotate(-18deg) scale(1.12);
  mix-blend-mode: multiply;
}

/* --- the buzz ------------------------------------------------------------
   Asked for: the toothbrush should vibrate so it catches the eye.

   In bursts, not continuously. A brush that shakes without stopping is a
   thing the eye has to keep dismissing for as long as the hero is on
   screen, and at 104px it reads less like a working toothbrush than like a
   rendering fault. A short buzz that starts, decays and stops is what an
   electric toothbrush actually does when you switch it on, and the eye
   catches the ONSET — so half a second every three and a half is more
   attention-getting than five seconds of continuous shake, not less.

   Scoped to .is-on, so it runs only while the toothbrush is the slide being
   shown — and restarts each time the card turns back to it, which puts a
   fresh buzz right where a reader has just been given something new to look
   at. The other half of the cycle costs nothing at all.

   Every frame restates `rotate(-18deg) scale(1.12)`: transform is one
   property, so a keyframe that set only the rotation would silently drop
   the scale the framing depends on (see the note above it). The amplitude
   decays 3.2deg -> 0.8deg across the burst, and the scale lifts a hair
   while it runs so the brush reads as coming forward rather than only
   twitching in place. */
@media (prefers-reduced-motion: no-preference) {
  .swap-slide--product.is-on img { animation: brush-buzz 2.8s linear infinite; }
}
/* --- the germs -----------------------------------------------------------
   Asked for: dots coming off the disc while the brush buzzes, for germs
   being driven away.

   Five, not a burst. This is a 104px disc inside a hero card, and a particle
   spray at that size stops reading as anything and just looks noisy — five
   specks leaving the rim on their own trajectories read as "flicked off"
   with room to see each one.

   --n-40, not a brand green. Every green on this site means clean or means
   Millway; germs are neither, and a grey speck reads as debris the moment it
   appears. It is a graphic-only token (1.97:1) which is exactly what this is.

   Geometry: each dot is parked ON the rim in percentages of .hcard-media, so
   the ring of them tracks the disc through all three of its sizes (104 /
   78 / 48px), and then travels a small FIXED number of pixels outward. Fixed
   because the travel has to clear the ring — which is 7px past the disc at
   every width — and land inside the card's own padding, and both of those
   are constants rather than fractions of the photograph.

   Directions are chosen so nothing flies out of the card: the disc sits at
   the card's left edge, so the dots go up, right, and up-left into the
   1.15rem of padding — never left or down-left, which is off the card
   entirely, and never far enough right to reach the copy column. */
.swap-germs { position: absolute; inset: 0; pointer-events: none; }
.swap-germs i {
  position: absolute;
  width: 5px; height: 5px;
  margin: -2.5px 0 0 -2.5px;   /* centres the dot on its own left/top point */
  border-radius: 50%;
  /* --n-50, up from --n-40. The lighter one measures 1.97:1 on white, which
     is right for a rule and wrong for something that is supposed to catch
     the eye — at 5px it read as a smudge on the card. --n-50 is 5.21:1 and
     is the quietest token that still carries small TEXT, so a dot in it is
     unambiguously visible while staying grey rather than turning into a
     brand mark. */
  background: var(--n-50);
  opacity: 0;
}
/* Nine, evenly around the rim except for the bottom-left quadrant, which is
   off the card: the disc sits against the card's left edge, so anything
   heading down-left leaves the .hcard entirely (it has no overflow:hidden to
   stop it). Everything here goes up, right, or up-left into the padding.

   Positive delays, not negative. Both shift the phase of an infinite
   animation, but a negative one pulls the dot's flight EARLIER — several
   would have left before the shake that is supposed to be throwing them.
   Spread over 0.18s they stream out across the buzz instead of leaving as
   one puff. */
.swap-germs i:nth-child(1) { left:  50%; top:  0%; --gx:   0px; --gy: -22px; }
.swap-germs i:nth-child(2) { left:  75%; top:  7%; --gx:  11px; --gy: -19px; animation-delay: 0.03s; }
.swap-germs i:nth-child(3) { left:  93%; top: 25%; --gx:  19px; --gy: -11px; animation-delay: 0.06s; }
.swap-germs i:nth-child(4) { left: 100%; top: 50%; --gx:  20px; --gy:   0px; animation-delay: 0.09s; }
.swap-germs i:nth-child(5) { left:  93%; top: 75%; --gx:  19px; --gy:  11px; animation-delay: 0.12s; }
.swap-germs i:nth-child(6) { left:  75%; top: 93%; --gx:  11px; --gy:  19px; animation-delay: 0.15s; }
.swap-germs i:nth-child(7) { left:  25%; top:  7%; --gx: -11px; --gy: -19px; animation-delay: 0.05s; }
.swap-germs i:nth-child(8) { left:   7%; top: 25%; --gx: -17px; --gy: -10px; animation-delay: 0.11s; }
.swap-germs i:nth-child(9) { left:  38%; top:  2%; --gx:  -5px; --gy: -21px; animation-delay: 0.18s; }

/* Only while the toothbrush is the slide being shown. data-swap-on is
   written by swap() in site.js from the active slide's own name, so this
   follows the brush rather than a position in the deck — and with no
   scripting, or on the whitening slide, the dots are simply never animated
   and sit at opacity 0. */
@media (prefers-reduced-motion: no-preference) {
  .hcard--offer[data-swap-on="product"] .swap-germs i {
    animation: germ-fly 2.8s linear infinite;
  }
}
/* Same 2.8s period as the buzz and the same window inside it, so the dots
   leave WITH the shake rather than on a rhythm of their own. They fade out
   before they stop, which is what keeps them from piling up at the end of
   their travel waiting for the cycle to come round — and it is also what
   lets the travel be longer than the space available: only the first half
   of each flight is ever visible, so a dot aimed 20px into a 19px gap
   disappears before it gets there. */
@keyframes germ-fly {
  0%, 49% { opacity: 0; transform: translate3d(0, 0, 0) scale(0.3); }
  55%     { opacity: 1; transform: translate3d(calc(var(--gx) * 0.3), calc(var(--gy) * 0.3), 0) scale(1.15); }
  68%     { opacity: 0.8; transform: translate3d(calc(var(--gx) * 0.7), calc(var(--gy) * 0.7), 0) scale(1); }
  84%     { opacity: 0; transform: translate3d(var(--gx), var(--gy), 0) scale(0.6); }
  100%    { opacity: 0; transform: translate3d(var(--gx), var(--gy), 0) scale(0.6); }
}

/* Reported as not visible enough, and it was not. Three things changed and
   all three matter:

     amplitude  +/-3.2deg -> +/-7deg at the peak. At 3.2 the brush moved
                about a pixel and a half at its tip inside a 104px circle,
                which is a shimmer rather than a shake.
     duration   0.5s of buzz -> 0.9s (50% to 82% of the cycle), long enough
                for a reader to notice it has started, look at it, and see
                it settle.
     frequency  3.4s cycle -> 2.8s, so it comes round while the reader is
                still on the card rather than after they have moved on.

   The translate is new too, and it is what actually sells it. Rotation alone
   pivots the brush about its own centre, where the middle of the object
   barely moves at all; a pixel and a half of position on top of that makes
   the whole thing jump, which is what an electric toothbrush against a hard
   surface does.

   Order is `rotate() scale() translate()` on purpose — the translate resolves
   in the rotated frame, so it swings rather than slides, and the two read as
   one object being driven instead of two effects stacked.

   Every frame restates the whole transform: it is one property, so a keyframe
   setting only the rotation would silently drop the scale the framing depends
   on (see .swap-slide--product img above). Amplitude decays 7deg -> 1.8deg
   across the burst so it dies away rather than stopping dead. */
@keyframes brush-buzz {
  0%, 50%, 100% { transform: rotate(-18deg)   scale(1.12) translate(0, 0); }
  52%           { transform: rotate(-11deg)   scale(1.16) translate(1.5px, -1.5px); }
  54.5%         { transform: rotate(-25deg)   scale(1.16) translate(-1.5px, 1.5px); }
  57%           { transform: rotate(-11.8deg) scale(1.16) translate(1.4px, -1.2px); }
  59.5%         { transform: rotate(-24.2deg) scale(1.15) translate(-1.4px, 1.2px); }
  62%           { transform: rotate(-12.6deg) scale(1.15) translate(1.2px, -1px); }
  64.5%         { transform: rotate(-23.4deg) scale(1.15) translate(-1.2px, 1px); }
  67%           { transform: rotate(-13.6deg) scale(1.14) translate(1px, -0.8px); }
  69.5%         { transform: rotate(-22.4deg) scale(1.14) translate(-1px, 0.8px); }
  72%           { transform: rotate(-14.8deg) scale(1.14) translate(0.7px, -0.6px); }
  74.5%         { transform: rotate(-21.2deg) scale(1.13) translate(-0.7px, 0.6px); }
  77%           { transform: rotate(-16.2deg) scale(1.13) translate(0.4px, -0.3px); }
  79.5%         { transform: rotate(-19.8deg) scale(1.12) translate(-0.4px, 0.3px); }
  82%           { transform: rotate(-18deg)   scale(1.12) translate(0, 0); }
}

/* =========================================================================
   MAKING THE OFFER READ AS AN OFFER

   Asked for by the practice: the complimentary gift card should be "a bit
   more inviting". It was not, and the reason was structural rather than a
   matter of degree — it was the same object as the CDCP card sitting next
   to it. Same flat white, same hairline, same 12px grey-green text link.
   Two identical cards, one of which happens to be giving something away,
   and nothing on the page said which.

   Three changes, in the order they do the work:

     1. The card gets a surface of its own — a wash of brand light pooled
        behind the gift, fading to white before it reaches the copy.
     2. A seal on the disc. Absolutely positioned, so it costs no height.
     3. The link becomes something you press.

   Deliberately NOT done: an eyebrow line ("Our gift to you"), a corner
   ribbon, or a second colour. Every one of those costs vertical space in a
   hero whose phone build is tuned to the pixel, and the card is already
   carrying a photograph that turns over on a timer. The offer needs to feel
   generous, not loud.

   Contrast was measured, not assumed. The wash peaks at roughly --tint-open
   (#EDF3E4, the open-now badge's own surface), where --ink-mid measures
   5.04:1 and --accent-text 5.71:1 — both legal, and both improving as the
   wash fades out under the copy that uses them.
   ========================================================================= */
.hcard--offer {
  /* Two layers, white underneath: the wash is a radial pooled at the top
     left, which is where the disc is, so the light appears to come off the
     gift itself. A flat tint over the whole card would have read as a
     different card stock rather than as something lit. */
  background:
    radial-gradient(115% 88% at 10% 4%,
      rgba(var(--sage-rgb), 0.32) 0%,
      rgba(var(--sage-rgb), 0.10) 46%,
      rgba(var(--sage-rgb), 0) 72%),
    var(--white);
  /* The edge picks up the same green rather than staying --rule-quiet. It
     is the quietest possible way to say the two cards are not the same
     kind of thing. */
  box-shadow: inset 0 0 0 var(--hair) rgba(var(--sage-deep-rgb), 0.30),
              0 26px 50px -40px rgba(var(--deep-rgb), 0.5);
  /* The whole card switches gifts on click now (see swap() in site.js), so
     the pointer says so everywhere on it — not just over the disc, which is
     where this used to sit before the handler widened to the full card.
     The dots and the "Choose your gift" link keep their own cursor:pointer
     from being an actual <button>/<a>, which this does not disturb: same
     value, and a more specific rule on each wins regardless. */
  cursor: pointer;
}
.hcard--offer:hover {
  box-shadow: inset 0 0 0 var(--hair) rgba(var(--sage-deep-rgb), 0.5),
              0 30px 56px -38px rgba(var(--deep-rgb), 0.6);
}

/* --- the gift name, on a strip ------------------------------------------
   Asked for: the turning gift name on a green strip, in white.

   It has to be --leaf, and that is not a preference. The logo sage is
   2.04:1 on white and tokens.css bans it from ever sitting under type; a
   white label on a sage strip would be the single worst contrast pair on
   the site. --leaf is the green that exists precisely so an accent can
   carry text, and white on it measures 6.13:1.

   THE STRIP HOLDS STILL
   ---------------------
   Reported as "vibration on the text", and fair. The animation was never on
   the words — it is scoped to .swap-slide--product.is-on img and always has
   been — but .swap-word has always risen 0.34em into place as it turns over,
   and that movement was invisible while these were bare words and obvious
   the moment they became a solid green block. A coloured block that jumps
   every five seconds reads as a glitch even when a word doing the same reads
   as a transition. So the strip crossfades and does not move.

   It also does not RESIZE, which is the other half of holding still and the
   reason there is no `justify-self` here. Both words share one grid cell (see
   .swap-words above) sized to the longer of them; left to stretch — the
   default — both strips are that same width and the label inside is centred,
   so what a reader sees is one steady green strip whose text changes. Made to
   hug each word instead, the strip would grow and shrink on every turn, and
   two different-width strips would cross-dissolve through each other's edges.
   Steady beats snug here. */
.hcard-t--offer .swap-word {
  text-align: center;
  padding: 0.18em 0.5em;
  border-radius: var(--r-pill);
  background: var(--leaf);
  color: var(--ink-invert);
  font-weight: 700;
  /* Beats `.hcard-t em` at (0,1,1) on class count, which is what was
     colouring these --accent-text. */
  transform: none;
  transition: opacity var(--t-2) var(--ease);
}

/* --- the link becomes a button -------------------------------------------
   "Ask about the offer" asked the reader to do the work; "Choose your gift"
   offers them something. The shape follows the words: a filled pill in
   --leaf (white on it measures 6.13:1) rather than the same quiet arrow
   link the card beside it carries, because this is the one card on the page
   with something to give away.

   SAME COLOURS AS "BOOK APPOINTMENT", ON PURPOSE
   ----------------------------------------------
   Asked for, and right: this is the second thing on the page a reader can
   act on, and it should look like it belongs to the first. So it takes
   .btn-primary's exact combination — a --deep pill, a white label, and a
   --sage disc carrying a --deep glyph — and its hover end state too: the
   pill goes sage, the label goes deep (8.03:1), the disc trades back. The
   only thing not copied is .btn's flood animation, which needs
   `overflow: hidden` on the anchor, and that would clip the 44px thumb
   target .hcard-a::after paints outside the pill at 640px down. The target
   is worth more than the flourish.

   Which is also why this stays a styled .hcard-a rather than becoming a
   .btn .btn-primary outright: it keeps that target for free.

   SMALL, at both widths
   ---------------------
   --fs-xs and a 20px disc against .btn's --fs-sm and 27px, and the padding
   asymmetric (more on the label side than the disc side) exactly as .btn
   has it. It comes to ~29px tall, which is what the plain text link it
   replaced already occupied — so a filled button with an icon costs this
   card nothing. .hcard-a's own mobile rule pushes to --fs-sm at 560px; the
   two-class specificity here holds it at --fs-xs, which is what "small on
   mobile as well" needs.

   The gap above goes on .hcard-foot, NOT on the link's own margin-top:
   `.hcard-foot .hcard-a` further down this file already sets that to 0 at
   (0,2,0), the same specificity this rule carries and later in the source,
   so a margin-top here would silently lose. */
.hcard--offer .hcard-foot { margin-top: 0.1rem; }
.hcard--offer .hcard-a {
  gap: 0.34rem;
  padding: 0.22rem 0.24rem 0.22rem 0.62rem;
  border-radius: var(--r-pill);
  background: var(--deep);
  color: var(--ink-invert);
  /* --fs-micro, down from --fs-xs. Weight goes UP as the size comes down:
     600 at 10.9px is thinner on the page than 600 at 12.2px was, and a
     button label is the last thing that should get harder to read for being
     smaller. White on --deep is 16.36:1, which is the headroom that makes a
     size this small legible rather than merely small.

     Declared, not inherited. .hcard-a's own rule inside the 560px block
     raises every card link to --fs-sm, and that is (0,1,0) inside a media
     query — media queries carry no specificity, so without this line the
     button would quietly grow on exactly the width it was asked to stay
     small on. The two classes here outrank it at every width. */
  font-size: var(--fs-micro);
  font-weight: 700;
  box-shadow: 0 10px 22px -16px rgba(var(--deep-rgb), 0.8);
  transition: background-color var(--t-2) var(--ease), color var(--t-2) var(--ease);
}
.hcard--offer .hcard-a i {
  width: 17px; height: 17px; border-radius: 50%;
  display: grid; place-items: center; flex: none;
  background: var(--sage); color: var(--deep);
  font-style: normal; line-height: 1;
  transition: background-color var(--t-2) var(--ease), color var(--t-2) var(--ease);
}
/* Beats `.hcard-a svg` at (0,1,1), which sizes the other cards' arrows. */
.hcard--offer .hcard-a i svg { width: 10px; height: 10px; display: block; }

/* The card as a whole is the hover target, same as it is for the other
   card's arrow — and the gap is pinned back to its own value, because
   `.hcard:hover .hcard-a` widens it to 0.7rem to slide an arrow that is no
   longer there. */
.hcard--offer .hcard-a:hover,
.hcard--offer:hover .hcard-a {
  gap: 0.42rem;
  background: var(--sage); color: var(--deep);
}
.hcard--offer .hcard-a:hover i,
.hcard--offer:hover .hcard-a i { background: var(--deep); color: var(--sage); }

/* --- the progress ring --------------------------------------------------- */
/* Sits in the band just outside the disc's white halo. Rotated so the sweep
   starts at twelve o'clock rather than at three. */
/* Sized with explicit width/height rather than `inset: -7px`. An <svg> is a
   replaced element, so `width: auto` resolves to its INTRINSIC size and the
   four insets are quietly dropped — the ring rendered at the browser's
   default 300x150 box instead of around the disc. */
.swap-ring {
  position: absolute;
  top: -7px; left: -7px;
  width: calc(100% + 14px); height: calc(100% + 14px);
  /* base.css gives every svg max-width:100%, which is exactly the wrong
     thing for the one svg on the site that is deliberately WIDER than its
     parent: it clamped the ring back to the disc's own width, and a viewBox
     that then has to fit a 92-tall by 78-wide box scaled the circle down and
     hid it behind the disc. */
  max-width: none;
  transform: rotate(-90deg);
  overflow: visible;
  pointer-events: none;
}
.swap-ring circle { fill: none; stroke-width: 2.4; }
.swap-ring-track { stroke: var(--rule-quiet); }
.swap-ring-arc {
  stroke: var(--graphic); stroke-linecap: round;
  /* dasharray/dashoffset are written by site.js — it owns the timing, so it
     owns the sweep. Until it runs, the arc is simply not drawn. */
  stroke-dasharray: 0 999;
}
/* Once the reader has taken control with the dots, the countdown is no
   longer telling the truth, so it stops being drawn. */
.hcard--offer.is-manual .swap-ring-arc { opacity: 0; }
.hcard--offer.is-manual .swap-ring-track { opacity: 0.6; }

/* --- the turning title --------------------------------------------------- */
/* One line: "Complimentary" and the turning gift name sit side by side,
   wrapping together if the card ever gets too narrow for both — rather than
   the gift permanently owning a line of its own underneath. */
.hcard-t--offer {
  display: flex; flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.38em;
}
.offer-lead { display: inline; }
/* Both words in ONE grid cell, so the box is as wide as the LONGER of them
   and as tall as one line — the card cannot change size as they turn over,
   and nothing below it moves. inline-grid, not grid: this now sits beside
   "Complimentary" in the flex row rather than claiming the whole line. */
.swap-words { display: inline-grid; }
.swap-word {
  grid-area: 1 / 1;
  font-style: normal;
  white-space: nowrap;
  opacity: 0;
  transform: translate3d(0, 0.34em, 0);
  transition: opacity var(--t-2) var(--ease), transform var(--t-3) var(--ease);
}
.swap-word.is-on { opacity: 1; transform: none; }

/* --- the dots ------------------------------------------------------------ */
/* On the card's bottom line, opposite the link, so they cost no height at
   all — which matters on a laptop screen where .hcard is already compact. */
.hcard-foot {
  display: flex; align-items: center; justify-content: space-between;
  /* Wraps rather than squeezing. .hcard's text column is narrow on a phone —
     a 104px disc and 1.2rem of gap come out of a card that is already only a
     gutter short of the screen — and against the dots the link was compressed
     to its longest word: "Ask / about / the / offer", one word per line. Now
     the dots drop to their own line when the two no longer fit, and the link
     never breaks at all. */
  flex-wrap: wrap;
  gap: 0.5rem 0.8rem; margin-top: 0.6rem;
}
.hcard-foot .hcard-a { margin-top: 0; white-space: nowrap; }

.swap-dots { display: flex; align-items: center; gap: 2px; flex: none; }
/* 22px of target around a 7px dot: the dot is the affordance, the button is
   the thing you can actually hit. */
.swap-dot {
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  background: none; border: 0; border-radius: 50%;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.swap-dot::before {
  content: '';
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--n-20);
  transition: background-color var(--t-1) var(--ease), transform var(--t-2) var(--ease);
}
.swap-dot:hover::before { background: var(--n-40); }
.swap-dot.is-on::before { background: var(--graphic); transform: scale(1.28); }
.swap-dot:focus-visible { outline: 2px solid var(--sage-deep); outline-offset: 1px; }

/* Reduced motion: the card still turns over — by hand, on the dots — but
   nothing moves on its own and nothing counts down toward moving. */
@media (prefers-reduced-motion: reduce) {
  .swap-slide, .swap-word { transition: none; }
  .swap-slide { transform: none; }
  .swap-ring { display: none; }
}

.hero-meta {
  grid-column: 3; justify-self: end; text-align: right;
  display: grid; gap: 3px;
}
.hero-meta b { font-size: var(--fs-lg); font-weight: 700; font-variant-numeric: tabular-nums; }
.hero-meta span {
  font-size: var(--fs-nano); letter-spacing: var(--track-micro);
  text-transform: uppercase; color: var(--ink-quiet);
}

/* =========================================================================
   THE FLAT HERO  (.stage--flat — the homepage)

   The homepage keeps fold 1 and nothing else: the specimen and the five-step
   procedure moved to the Treatments page as one scene (see
   template-treatments.php), because the whole premise of that scene is that
   the tooth met in the hero is the tooth being repaired — it cannot be split
   across two pages. front-page.php's section therefore ships no [data-stage],
   so site.js's stage() never finds it and nothing on this page is pinned.

   That leaves a layout problem this block exists to answer. Every piece of
   the wide hero above is positioned FROM the specimen: both headline lines
   off --tooth-rise, both captions off --caption-drop, .hero-foot off the
   tooth's own bottom edge, and the gap in line 2 off --flank-gap. Take the
   tooth away and those offsets do not merely shift — they collide. Solving
   the two formulas at a common 900px-tall screen puts the captions' top edge
   about 20px BELOW .hero-foot's, so the rating card and the hero cards
   render on top of one another, and line 2 keeps a ~290px hole in the middle
   of it for an object that is no longer there.

   Rather than re-tune four sets of offsets around an absent object, the flat
   hero drops the absolute positioning entirely and lays fold 1 out as a
   centred column. Every piece keeps its own styling — type, cards, pills,
   buttons are all untouched — and only placement changes. Nothing here is
   scoped to motion preference or to whether scripting ran, because none of
   it depends on either any more.

   Wide only. Below 1025px the stacked build in the NARROW section further
   down already lays this exact markup out as a column, and it has to keep
   winning there — which it does, because this block never applies at that
   width rather than because of any specificity tie-break.
   ========================================================================= */
@media (min-width: 1025px) {
  .stage--flat .stage-pin {
    /* auto height with a one-screen floor, not the pinned build's flat
       100svh: without the procedure under it the hero is the whole section,
       and a hard height would clip it on a short laptop screen instead of
       letting the page scroll. */
    height: auto; min-height: 100svh; overflow: visible;
    /* Two columns, so the rating card and the location pill stay side by
       side. They are two separate elements in the markup (each aligned to
       its own half of line 2 on the pinned build — see the note by them in
       front-page.php), and in a single-column stack one would drop under the
       other. Everything else spans both columns. */
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-content: center;
    justify-items: center;
    column-gap: clamp(0.7rem, 1.5vw, 1.1rem);
    row-gap: clamp(1rem, 1.9vw, 1.6rem);
    /* top clears the fixed header (~90px) with room to spare */
    padding: clamp(7.5rem, 13vh, 10rem) var(--gutter) var(--fold);
  }

  /* Dissolved so their children become items of the column above — the same
     trick the narrow build uses, and for the same reason: the headline, the
     two captions, the cards and the actions live at three different depths
     in the markup, and a single `order` sequence has to reach all of them.
     .hero-foot carries .shell too; display:contents drops that box as well,
     which is why .stage-pin takes the gutter itself above. */
  .stage--flat .hero-layer,
  .stage--flat .hero-foot { position: static; display: contents; }

  .stage--flat .hero-word            { grid-column: 1 / -1; order: 1; justify-self: stretch; }
  .stage--flat .hero-caption--rating { grid-column: 1; order: 2; justify-self: end; }
  .stage--flat .hero-caption--loc    { grid-column: 2; order: 2; justify-self: start; }
  .stage--flat .hero-cards           { grid-column: 1 / -1; order: 3; }
  .stage--flat .hero-actions         { grid-column: 1 / -1; order: 4; }

  /* display:block matters: these are <span>s that were only block-level by
     virtue of being absolutely positioned, and static they would run inline
     into one another. */
  .stage--flat .hero-word-line {
    position: static; display: block;
    top: auto; left: auto; right: auto;
  }
  .stage--flat .hero-word-line--top { margin-bottom: 0.06em; }

  /* No tooth to flank, so no gap to leave for one. display:block drops the
     two-column grid (grid-template-columns/justify-self become no-ops), so
     "Take"/"Shape." fall back to plain inline spans reading left to right and
     line 2 centres as one phrase — exactly what the narrow build has always
     done with this same markup, for this same reason. */
  .stage--flat .hero-word-split { display: block; }
  .stage--flat .hero-word-take  { justify-self: auto; }
  /* margin-left:0, unlike the narrow build's 0.28em. The two spans sit on
     separate lines in the markup, so once the grid is gone the collapsed
     newline between them is already rendering a word space — and at
     --fs-hero that space is ~28px on its own. Adding the phone build's
     margin on top of it doubled the gap and line 2 read as two words with a
     hole between them rather than as one phrase. */
  .stage--flat .hero-word-shape { justify-self: auto; margin-left: 0; }

  /* A little more air under the headline than between the rows below it: the
     captions are a footnote to the h1 and should read as attached to it. */
  .stage--flat .hero-caption {
    position: static; inset: auto;
    margin-top: clamp(0.3rem, 0.9vw, 0.8rem);
  }
}

/* --- fold 2 layer ------------------------------------------------------- */
.proc-layer {
  position: absolute; inset: 0; z-index: 4;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 30vw) minmax(0, 1fr);
  align-items: center;
  gap: 0 clamp(20px, 3vw, 52px);
  padding: 6rem var(--gutter) 4rem;
  /* boxed: the procedure stays inside the page measure so the specimen reads
     as sitting in a container, against fold 3 which runs full bleed */
  max-width: var(--page);
  margin-inline: auto;
  opacity: 0;
  pointer-events: none;
}

.proc-index { grid-column: 1; justify-self: end; width: min(100%, 320px); }
.proc-steps { position: relative; }
.proc-steps::before {
  content: '';
  position: absolute; right: 20px; top: 12px; bottom: 12px;
  width: var(--hair);
  background: linear-gradient(to bottom, transparent, var(--rule) 14%, var(--rule) 86%, transparent);
}
.pstep { position: relative; min-height: 4.9rem; padding-right: 46px; text-align: right; }
.pstep-n {
  font-size: var(--fs-micro); letter-spacing: var(--track-micro);
  color: var(--ink-quiet); font-variant-numeric: tabular-nums;
  transition: color var(--t-2) var(--ease);
}
.pstep-t {
  font-size: var(--fs-lg); font-weight: 600; line-height: 1.25; margin-top: 5px;
  letter-spacing: var(--track-tight);
  color: var(--ink-quiet);
  transition: color var(--t-2) var(--ease);
}
.pstep.done .pstep-t { color: var(--ink-mid); }
.pstep.now .pstep-n { color: var(--accent-text); }
.pstep.now .pstep-t { color: var(--ink); }

.pstep::after {
  content: '';
  position: absolute; right: 15px; top: 1.3rem;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--white);
  box-shadow: inset 0 0 0 var(--hair) var(--n-40);
  transition: transform var(--t-2) var(--ease),
              background-color var(--t-2) var(--ease),
              box-shadow var(--t-2) var(--ease);
}
.pstep.done::after { background: var(--n-40); }
.pstep.now::after {
  background: var(--graphic);
  box-shadow: inset 0 0 0 var(--hair) var(--sage-deep), 0 0 0 7px rgba(var(--sage-rgb), 0.24);
  transform: scale(1.24);
}
.pstep::before {
  content: '';
  position: absolute; left: calc(100% - 14px); top: calc(1.3rem + 5px);
  height: var(--hair); width: clamp(20px, 2.6vw, 44px);
  background: linear-gradient(to right, var(--sage-deep), rgba(var(--sage-deep-rgb), 0));
  transform: scaleX(0); transform-origin: left center; opacity: 0;
  transition: transform var(--t-3) var(--ease), opacity var(--t-2) var(--ease);
}
.pstep.now::before { transform: scaleX(1); opacity: 1; }

.proc-detail { grid-column: 3; display: grid; justify-self: start; width: min(100%, 350px); }
.pcard {
  grid-area: 1 / 1;
  position: relative;
  padding: 24px 26px 20px;
  background: var(--white);
  border: var(--hair) solid var(--rule-quiet);
  border-radius: var(--r-sm);
  box-shadow: 0 34px 70px -50px rgba(var(--deep-rgb), 0.55);
  opacity: 0;
  transform: translate3d(0, 14px, 0);
  transition: opacity var(--t-2) var(--ease), transform var(--t-3) var(--ease);
}
.pcard.now { opacity: 1; transform: none; }
.pcard::before {
  content: '';
  position: absolute; left: 0; top: 0; height: 2px; width: 0;
  background: linear-gradient(to right, var(--graphic), rgba(var(--sage-rgb), 0));
  transition: width var(--t-3) var(--ease);
}
.pcard.now::before { width: 70px; }
.pcard-n {
  display: flex; align-items: baseline; gap: 7px;
  font-size: var(--fs-micro); letter-spacing: var(--track-micro);
  text-transform: uppercase; color: var(--ink-quiet);
  font-variant-numeric: tabular-nums; margin-bottom: 9px;
}
.pcard-n b { font-size: var(--fs-xs); font-weight: 700; color: var(--accent-text); }
.pcard h3 { font-size: var(--fs-xl); font-weight: 700; letter-spacing: var(--track-tight); line-height: 1.18; margin-bottom: 9px; }
.pcard p { font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-mid); }
.pcard dl { margin-top: 16px; padding-top: 4px; border-top: var(--hair) solid var(--rule-quiet); }
.pcard dl div {
  display: grid; grid-template-columns: 7rem minmax(0, 1fr); gap: 10px;
  padding: 6px 0; border-bottom: var(--hair) solid var(--rule-quiet);
}
.pcard dl div:last-child { border-bottom: 0; }
.pcard dt { font-size: var(--fs-nano); letter-spacing: var(--track-label); text-transform: uppercase; color: var(--ink-quiet); padding-top: 0.32em; }
.pcard dd { margin: 0; font-size: var(--fs-xs); font-weight: 500; color: var(--ink); font-variant-numeric: tabular-nums; }

/* =========================================================================
   NARROW — no pin, no scrub. A plain stack: hero, specimen, then the five
   steps each with their own copy.
   ========================================================================= */
@media (max-width: 1024px) {
  .hdr-tel { display: none; }
  .hdr-burger { display: block; }
  /* Book appointment drops out of the row on a phone — the burger sits
     alone in .hdr-right now, and .brand's own min-width:0 + ellipsis (see
     up top) takes back whatever width the button used to hold. Booking
     stays one tap away regardless: the floating "Book online" tab
     (footer.php, every template) is already visible on every page. */
  .hdr-right .btn { display: none; }

  /* --- the mobile panel ---------------------------------------------------
     Same markup as the desktop pill row (wp_nav_menu only renders once —
     see the note by it in header.php), restyled here from a horizontal row
     of pills into a full-width dropdown sheet. Positioned absolute against
     .hdr itself (the nearest positioned ancestor — .shell sets no position
     of its own), so `top: 100%` tracks the header's real height at every
     width and clamp() step instead of a guessed px number.

     Closed state is opacity/visibility/transform, not display:none: a
     hard display toggle would cut off the slide-down transition, and
     visibility:hidden (rather than just opacity) keeps the closed panel's
     links out of tab order and out of find-in-page. */
  .pillnav {
    position: absolute; top: calc(100% + 6px);
    /* Inset from both edges rather than bleeding to them. A sheet that
       runs edge to edge reads as part of the browser chrome; held off the
       sides by the same --gutter every other section on the site uses, it
       reads as a card belonging to the page, and the rounded corners and
       shadow actually show. */
    left: var(--gutter); right: var(--gutter);
    z-index: 116;   /* above .nav-scrim (115), under .hdr itself (120) */
    margin-inline: 0;
    /* Reset, not inherited: the desktop rule above sets border-radius to
       var(--r-pill) (999px) for the compact pill row, and an unset property
       does not fall back to a default — it keeps whatever the cascade last
       gave it. On a panel this tall a 999px radius rounds into a near-circle
       and clips the menu text at the sides. */
    border-radius: var(--r-md);
    /* Was min(56svh, 400px), which on a 844px-tall phone left a 400px
       window for a list that runs past 1000px once Our Services is open —
       two services visible at a time. This uses the room actually
       available: everything below the header, less a margin so the card
       never reaches the bottom edge. */
    max-height: calc(100svh - 100px - var(--sp-3));
    padding: 0.4rem;
    display: flex; flex-direction: column; align-items: stretch; gap: 0;
    background: var(--white);
    box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet),
                0 24px 48px -20px rgba(var(--deep-rgb), 0.28),
                0 6px 16px -10px rgba(var(--deep-rgb), 0.16);
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;   /* scrolling the list must not scroll the page behind it */
    opacity: 0; visibility: hidden;
    transform: translateY(-8px);
    transition: opacity var(--t-1) var(--ease), transform var(--t-1) var(--ease),
                visibility 0s linear var(--t-1);
  }
  .pillnav.is-open {
    opacity: 1; visibility: visible;
    transform: none;
    transition: opacity var(--t-1) var(--ease), transform var(--t-1) var(--ease);
  }
  .nav-scrim {
    display: block;
    opacity: 0;
    transition: opacity var(--t-1) var(--ease);
  }
  .nav-scrim.is-open { opacity: 1; }
  /* [hidden] still wins the display fight even after .is-open lands (site.js
     drops the attribute in the same tick it adds the class, but the CSS
     needs to be correct independent of that ordering) */
  .nav-scrim:not(.is-open) { display: none; }
  /* site.js adds this to <html> for exactly as long as the panel is open,
     so the page behind the scrim can't scroll out from under a reader
     mid-tap */
  html.nav-open { overflow: hidden; }

  /* Back to a real list — display:contents on desktop was what let the
     wide build's flex row skip straight to the <a>; a vertical stack of
     rows wants the <li>'s own box back for the divider and the tap
     target's full width. */
  .pillnav li { display: block; }
  /* Hairlines are inset by the row's own padding rather than running the
     full card width, so they read as separators between items instead of
     as a table's grid lines. The last row has nothing below it to divide. */
  .pillnav > li + li { border-top: var(--hair) solid var(--rule-quiet); }
  .pillnav > li:last-child { border-bottom: 0; }
  .pillnav a {
    display: block;
    padding: 0.8rem 0.75rem;
    border-radius: 10px;
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--ink);
  }
  .pillnav a:hover { background: none; }
  /* the pressed state a phone actually shows — there is no hover here */
  .pillnav a:active { background: var(--surface-quiet); }

  /* Has-children rows stop being a hover target (there is no hover on a
     phone) and go back to a plain block row — but the row itself is now a
     flex pair: the link keeps navigating to its own page on tap, and
     site.js's mobileNavAccordion() adds a separate .pillnav-toggle button
     alongside it (see there) so the submenu only opens on a deliberate
     tap of the arrow, not on every tap of the row. */
  .pillnav li.menu-item-has-children {
    display: flex; flex-wrap: wrap; align-items: center;
    position: static;
  }
  .pillnav li.menu-item-has-children > a {
    flex: 1 1 auto;
  }
  .pillnav li.menu-item-has-children > a::after { display: none; }   /* the desktop chevron; .pillnav-toggle replaces it here */

  /* The toggle button site.js inserts next to the link. A real element,
     not the desktop chevron's ::after: a pseudo-element on the <a> can't
     take its own click handler independent of the link it decorates, and
     that independence is the entire point — tapping the row's text must
     still follow the link, only the arrow should open the list. */
  /* A tinted disc rather than a bare chevron: this is the one control on
     the row that does something OTHER than follow the link, and at 44px of
     invisible target next to a full-width link there was nothing to show
     that. The disc makes the tappable area visible. */
  .pillnav-toggle {
    display: grid; place-items: center;
    width: 38px; height: 38px; flex: none;
    margin-right: 0.35rem;
    padding: 0; border: 0;
    border-radius: 50%;
    background: var(--surface-quiet);
    box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet);
    cursor: pointer;
    transition: background-color var(--t-1) var(--ease);
    -webkit-appearance: none; appearance: none;
  }
  .pillnav-toggle-chev {
    width: 7px; height: 7px;
    margin-top: -3px;   /* optical centring: a rotated square sits low in its box */
    border-right: 1.6px solid var(--ink);
    border-bottom: 1.6px solid var(--ink);
    transform: rotate(45deg);
    transition: transform var(--t-1) var(--ease);
  }
  /* points up once the list under it is open, same "flip to mean the
     opposite" language .swap-dot and the review card's chevrons use
     elsewhere on the site */
  .pillnav-toggle[aria-expanded="true"] { background: var(--deep); box-shadow: none; }
  .pillnav-toggle[aria-expanded="true"] .pillnav-toggle-chev {
    margin-top: 3px;
    border-color: var(--on-deep);
    transform: rotate(-135deg);
  }
  .pillnav-toggle:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

  /* Collapsed by default — display:none rather than a height animation,
     which needs a real (not `auto`) end value to transition against and
     every one of these lists is a different length. Scoped to this media
     query only, so it can never leak into the desktop hover rules further
     up the file, which stay in sole control there regardless of whether
     site.js has run. */
  .pillnav .sub-menu { display: none; }
  /* The desktop open rule (:hover / :focus-within, up near .pillnav
     .sub-menu) has to be switched off here, not just out-ranked. Tapping
     the toggle leaves focus ON the button, so the <li> matches
     :focus-within for as long as the reader stays there — and that rule
     is (0,4,1), the same weight as the .is-open rule below. The result
     was a submenu that opened on the first tap and then refused to close
     on the second: the class came off and the chevron rotated back, but
     :focus-within kept display:block. Measured, not guessed — is-open
     false, aria-expanded false, computed display still block.

     Below 1024px .is-open is the only thing that may open a submenu. */
  .pillnav li.menu-item-has-children:hover > .sub-menu,
  .pillnav li.menu-item-has-children:focus-within > .sub-menu { display: none; }
  .pillnav li.menu-item-has-children.is-open > .sub-menu { display: block; }
  .pillnav .sub-menu {
    position: static;
    /* spans the full row on its own line below the link+toggle pair —
       .menu-item-has-children is a flex row now, and without this the
       list tried to sit beside the link as a second flex item instead of
       underneath both of them */
    flex-basis: 100%;
    /* The open list is a tinted, rounded well set inside the parent row,
       not more full-width rows. Nesting has to be visible at a glance on a
       phone: without it an expanded "Our Services" just looks like eleven
       more top-level items and the reader loses track of what level they
       are on. */
    margin: 0 0 0.35rem;
    padding: 0.3rem;
    min-width: 0;
    border-radius: 10px;
    background: var(--surface-quiet);
    box-shadow: none;
  }
  .pillnav .sub-menu::before { display: none; }   /* the hover-gap bridge; nothing to bridge on tap */
  .pillnav .sub-menu li { border-top: 0; }
  .pillnav .sub-menu li + li { border-top: var(--hair) solid var(--rule-quiet); }
  .pillnav .sub-menu a {
    padding: 0.6rem 0.6rem;
    border-radius: 8px;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--ink-mid);
  }
  .pillnav .sub-menu a:active { background: rgba(var(--white-rgb), 0.9); }

  /* --- the panel's footer actions ----------------------------------------
     Sticky to the card's bottom edge so the booking action stays reachable
     while the reader scrolls a long service list, rather than sitting
     hundreds of pixels below the fold of a scrolling panel. */
  .pillnav-cta {
    display: grid;
    gap: 0.5rem;
    position: sticky; bottom: -0.4rem;   /* -0.4rem cancels .pillnav's own padding */
    margin-top: 0.3rem;
    padding: 0.6rem 0.35rem calc(0.4rem + env(safe-area-inset-bottom, 0px));
    background: var(--white);
    border-top: var(--hair) solid var(--rule-quiet);
  }
  /* Both scoped under .pillnav on purpose. The panel's own `.pillnav a`
     rule is (0,1,1) — display:block, its own colour — and a bare
     `.pillnav-call` at (0,1,0) loses to it: the phone row lost its flex
     layout (icon stacked above the number) and the booking button
     inherited --ink, painting dark text on its dark fill. Two classes
     each puts these at (0,2,0), which wins. */
  .pillnav .pillnav-call {
    display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
    padding: 0.7rem 1rem;
    border-radius: var(--r-pill);
    background: var(--surface-quiet);
    box-shadow: inset 0 0 0 var(--hair) var(--rule);
    font-size: var(--fs-sm); font-weight: 700; color: var(--ink);
  }
  .pillnav .pillnav-call svg { width: 16px; height: 16px; flex: none; color: var(--leaf); }
  .pillnav .pillnav-book {
    display: inline-flex; width: 100%; justify-content: center;
    font-size: var(--fs-sm); font-weight: 700;
    color: var(--on-deep);
  }

  /* Our Services mega variant: no hover on a phone, so the whole
     preview-card half of the panel has nothing to follow and is dropped.
     What is left collapses to the same flat accordion list as Our
     Doctors/Patient Info above — one service per row, its thumbnail and
     category kept (they carry real information a bare text list doesn't),
     with the footer bar's two links as the list's last rows.

     The visibility/opacity handover to GSAP is desktop-only: unset here so
     an open accordion is never invisible on a phone, where .is-open lives
     on the <li> for the accordion rather than for this panel. site.js
     clears any inline styles GSAP left behind when the breakpoint is
     crossed — inline always beats these, so CSS alone cannot undo them. */
  .pillnav li.has-mega.mega-ready > .sub-menu.mega-menu {
    visibility: visible; opacity: 1; pointer-events: auto;
  }
  /* Both of these are (0,6,1), so the open one wins on source order alone —
     written as :not() rather than as a plain pair because the accordion's
     own generic rule is only (0,5,1) and loses to the desktop handover
     outright, which is what stopped the panel opening on a phone at all. */
  .pillnav li.has-mega.mega-ready:not(.is-open) > .sub-menu.mega-menu { display: none; }
  .pillnav li.has-mega.mega-ready.is-open > .sub-menu.mega-menu { display: block; }

  .pillnav .sub-menu.mega-menu {
    width: auto;
    overflow: visible;
    box-shadow: none;
  }
  .mega-grid { flex-direction: column; padding: 0; gap: 0; }
  .mega-list {
    display: block;
    border-right: 0; padding-right: 0;
  }
  .mega-visual { display: none; }   /* photo, description and explore link all need hover */
  .pillnav .sub-menu.mega-menu .mega-row {
    padding: 0.5rem 0.6rem;
    border-radius: 8px;
    gap: 0.65rem;
  }
  .mega-row-thumb { width: 34px; height: 34px; border-radius: 8px; }
  .mega-row + .mega-row { border-top: var(--hair) solid var(--rule-quiet); }
  .mega-row-arrow { display: none; }
  .mega-row-t { font-size: var(--fs-sm); font-weight: 600; color: var(--ink); }
  /* The category was rendering at nearly the weight of the service name —
     uppercase, letter-spaced and only a step smaller, so each row read as
     two competing lines. It is a label under a title, so it is set like
     one: smaller, lighter, and tracked normally. */
  .mega-row-cat {
    font-size: var(--fs-nano); font-weight: 500;
    letter-spacing: 0.02em; text-transform: none;
    color: var(--ink-quiet);
  }

  /* the footer bar unstacks into two ordinary rows in the accordion */
  .mega-foot {
    flex-direction: column; align-items: stretch; gap: 0;
    padding: 0; background: none;
    border-top: var(--hair) solid var(--rule-quiet);
    margin-top: 0.3rem;
  }
  .mega-foot-aside {
    flex-direction: column; align-items: stretch; gap: 0;
  }
  .mega-foot-note { display: none; }
  .pillnav .sub-menu.mega-menu .mega-foot-all {
    padding: 0.65rem 0.6rem; font-size: var(--fs-sm);
    justify-content: space-between;
  }
  /* The services list carries its own "Book a consultation" button on
     desktop. In the panel it would sit directly above .pillnav-cta's
     sticky "Book appointment" — two dark buttons, same action, stacked.
     The sticky one wins because it is reachable from anywhere in the
     list; this one is dropped rather than duplicated. */
  .pillnav .sub-menu.mega-menu .mega-foot-aside { display: none; }
  .pillnav .sub-menu.mega-menu .mega-foot-cta {
    justify-content: center;
    margin: 0.35rem 0.6rem 0.5rem;
    padding: 0.6rem 0.85rem;
    font-size: var(--fs-xs);
  }

  .stage-pin { height: auto; overflow: visible; }
  .spec {
    position: relative; left: auto; top: auto;
    transform: none !important; margin: 0 auto var(--sp-4);
  }
  .spec img, .spec canvas { height: min(42vh, 320px); }
  .spec-halo { display: none; }

  /* --- the stacked hero is a real column now ----------------------------
     It used to be a pile of absolutely-positioned pieces held together by a
     15.6rem padding reservation on .hero-layer, with the two badges parked
     on top of that reserved space. It read as loose parts rather than a
     section, and every one of those numbers had to be re-measured by hand
     whenever a font size moved.

     .stage-pin becomes the column and orders its contents outright.
     display:contents on the two wrappers dissolves them so the headline,
     the lede, both badges, the actions and the cards are all siblings of
     that column — which is what makes a single `order` sequence possible
     across elements that live at three different depths in the markup. */
  .stage-pin {
    display: flex; flex-direction: column;
    /* Clears the fixed header, then centres the hero in the first screen.
       ---------------------------------------------------------------------
       The floor (6rem) is the original value and is still the only thing
       that matters on a short handset: it clears the header — 72px on a
       phone, 79px at the 1024 end — plus breathing room.

       It clears the header and nothing more. There was a 50svh centring
       calculation here while the two hero cards were meant to fall BELOW
       the first screen — the padding claimed half the leftover viewport so
       the fold would end on the buttons. The brief has changed: both cards
       now belong inside the first screen, so there is no surplus to share
       out and the padding goes back to its original job. */
    padding-top: clamp(6rem, 14vw, 8rem);
  }

  /* THE HOMEPAGE HERO IS EXACTLY ONE SCREEN TALL — NEVER MORE
     -----------------------------------------------------------------------
     Reported, with a screenshot: fold 2's eyebrow and heading were visible
     under the hero on first load, with a stretch of unclaimed white space
     above them. Every earlier pass at this problem (see the removed
     margin-top note above, and the trimmed margins below) tried to solve it
     by getting the CONTENT's own height right by hand — reading font-size,
     line-height and padding off the stylesheet and adding them up. That is
     fragile by construction: it has to be re-proven every time a margin, a
     font size or the content itself changes, there is no browser available
     in this environment to check the arithmetic against, and the
     screenshot is the proof it was not enough.

     So the fix moves from "make the content the right height" to "make the
     BOX the right height, and let the content sit inside whatever that
     leaves" — which cannot be wrong by construction, only by how much
     spare room it leaves:

       height: 100svh    not min-height. The pinned WIDE build already uses
                          exactly this pattern (100svh, overflow hidden) for
                          the same reason — one screen, no more — so this
                          brings the phone build in line with a pattern this
                          file already trusts elsewhere, rather than
                          inventing a second way to do the same job.

       overflow: hidden   the backstop. If content ever runs long on a
                          genuinely short device, this crops it rather than
                          growing the section past 100svh and letting fold 2
                          show through the gap underneath — the exact defect
                          reported. Cropping a card is a worse look than
                          showing it, which is why every trim below this
                          rule exists: to make the crop something that
                          should now never actually have to fire, not a
                          plan B relied on in the common case.

       justify-content:
       center             "keep spacing on top side… center align
                          vertically", asked for directly. Content shorter
                          than the box (the ordinary case once the trims
                          below are in) gets equal breathing room above and
                          below rather than being jammed under the header —
                          and the space that opens up ABOVE the headline is
                          exactly the "more of the photo visible" effect the
                          removed margin-top was trying to fake with a
                          guessed number. This produces it correctly instead,
                          different on an SE than on a Pro Max because it is
                          measured by the browser at paint time rather than
                          picked off a clamp() in this file.

     .hero-layer and .hero-foot stay display:contents (declared right below,
     unchanged) specifically so this flex column can see every child at once
     — the headline, the badges, the actions and the cards all live at three
     different depths in the markup, and centring the WRAPPERS instead of
     their contents would have centred three separate empty boxes rather
     than one column of visible things. */
  .stage--flat .stage-pin {
    height: 100svh;
    overflow: hidden;
    /* Reported back: the section stopped showing fold 2 (the height cap
       worked) but the content sat flush against the header with the whole
       gap dumped at the bottom instead of splitting around it — on
       whatever device that screenshot came from, `center` was not doing
       its job. The most likely reason is the max-height:680px rule further
       down this file, which force-switched this to flex-start as a
       crop-safety measure on short screens — this file has no way to know
       the real height of the device that screenshot was taken on, only
       whether it happened to fall under a guessed 680px line.

       `safe center` is the fix for exactly that uncertainty: it centres
       normally, and ONLY falls back to start-alignment on its own, in the
       browser, at paint time, if centring would actually push content past
       the box's edge. That is the real "is there enough room" question
       this file was trying to answer in advance with a fixed number; asking
       the browser to answer it live is strictly more correct, so the
       hand-written max-height override for this property is removed below
       rather than tuned again.

       Support: Safari 16.4+, Chrome/Edge 115+, Firefox 89+ (2023 and
       after on every engine) — current enough that an unsupported browser
       simply drops the whole declaration and falls back to CSS's own
       default alignment (effectively top-anchored), which is no worse than
       the bug just reported. */
    justify-content: safe center;
    /* "A little extra gap on top side, so the background photo gets more
       visibility" — asked for directly, and layered ON TOP of the shared
       padding-top (still clamp(6rem,14vw,8rem) above, still doing its own
       job of clearing the header) via calc() rather than replacing it.

       That is not a style choice, it is the only way this can work at all:
       padding-top is one scalar property, and this selector is MORE
       specific than the shared one above (two classes against one), so a
       second plain `padding-top: <value>` here would not add to the
       original — it would overwrite it outright, dropping the header
       clearance from ~96-128px to whatever this second value alone came
       to. That would have sat the headline under the fixed header, a
       correctness bug worse than the visual one this whole pass exists to
       fix. calc() reaches back into the SAME clamp() the shared rule uses
       and adds to it, so both jobs — clearing the header, and opening a
       little extra photo above the headline — stay exactly what they say
       they are.

       The added amount is modest on purpose — "a little extra", not a
       second pass at re-opening the overflow risk the rest of this change
       exists to close. */
    padding-top: calc(clamp(6rem, 14vw, 8rem) + clamp(1rem, 3.5vh, 1.75rem));
  }
  .hero-layer { position: static; padding: 0; display: contents; }
  .hero-foot { position: static; display: contents; }

  /* .hero-lede carried an order (2, between the headline and the badges)
     back when the narrow build released it to display:block below. Removed
     with that override — an order on an element nobody ever sees is just a
     number for the next person to wonder about. */
  .hero-word            { order: 1; }
  .hero-caption--rating { order: 3; }
  .hero-caption--loc    { order: 4; }
  .hero-actions         { order: 5; }
  .hero-cards           { order: 6; }
  /* 7 and 8 only apply to the un-enhanced stack — once the phone scrub runs,
     the specimen is moved inside .proc-layer and .proc-scrub becomes the item
     that carries the whole procedure. The order still reads the same either
     way: cards, then the tooth, then the steps. */
  .spec                 { order: 7; }
  .proc-layer           { order: 8; }
  .proc-scrub           { order: 8; }

  /* THE PHONE STANDFIRST — REMOVED, ON REQUEST
     -------------------------------------------------------------------
     This block used to release .hero-lede from the desktop `display: none`
     (see the un-conditional rule up near .hero-word) back to display:block,
     with its own measure, colour and centring. Asked to give that space
     back to the rest of the fold and keep the phone hero cleaner, so the
     override is gone and nothing replaces it — .hero-lede falls through to
     the same display:none it already carries above 1024px, and now carries
     at every width.

     Worth knowing rather than silently losing: this paragraph — "Restorative
     and cosmetic dentistry on Erin Mills Parkway — gentle care, modern
     tools, and every step explained plainly." — was the only thing on a
     phone that stated what the practice DOES. Its own comment in
     front-page.php said so directly: the headline lands straight on the
     trust badges with nothing between them now. That trade was made on
     purpose here, not overlooked; if a reader ever asks "wait, what kind of
     practice is this" scrolling past the hero, this paragraph going is the
     first place to look. */

  /* Both word lines and both captions render as plain, calm, stacked content
     on a phone. The specimen doesn't overlap anything here (it is
     separately in-flow, position: relative, further down), so there is
     nothing for the lines to bite into. */
  .hero-word-line,
  .hero-word-line--top,
  .hero-word-line--bottom {
    position: static;
    /* display:block for the same reason .stage--flat's desktop rule needs
       it: these are <span>s that were only block-level by virtue of being
       absolutely positioned, and going static drops them back to inline.
       It went unnoticed while this text was left-aligned — an inline box
       starts at the left edge either way — but text-align cannot centre an
       inline box, so line 1 stayed left while line 2 centred (line 2 is
       blockified by .hero-word-split's own display:block just below). */
    display: block;
    font-size: var(--fs-hero-sm);
    /* --ink, not --n-20. This is the page's h1 and the label for the whole
       stage, and --n-20 is a rule colour: 1.29:1 on white. On a phone the
       headline read as "tooth." with the first half a ghost, while the same
       element one pixel wider is nearly black. Same element, same colour. */
    white-space: normal; color: var(--ink);
    /* it is not inside .shell, so it needs the gutter itself — and a
       line-height of 0.9 put its descenders straight into the sub-line */
    padding-inline: var(--gutter);
    text-align: center;
  }

  /* NOTE: a fixed `margin-top` used to sit here, pushing the headline down
     by a guessed svh-based amount so more of the background photo showed
     above it. Removed in favour of real vertical centring — see
     .stage--flat .stage-pin below, further up this block, where
     `justify-content: center` now does the same job for every device
     automatically instead of one hand-picked clamp() trying to guess it.
     Centring a shorter-than-viewport column already opens a gap above the
     headline for exactly the same reason this margin did; it just does it
     correctly, self-adjusting to how much room a given phone actually has
     rather than a number read off a spec sheet. */

  .hero-word-line--top { margin-bottom: 0.15em; }
  /* One rhythm through the stacked hero, so the opening screen reads as
     composed rather than as a stack of things that happen to follow each
     other. Every block below is separated by --sp-4 (32px); the only
     tighter gap is the one INSIDE the badge pair, which is a pair and
     should read as one unit. See .hero-cards at the foot of this block for
     the deliberate larger break that ends the fold. */
  .hero-word-line--bottom { margin-bottom: var(--sp-3); }

  /* The flanking split is a wide-viewport trick — the specimen sits separately
     in flow here, not behind the text, so there is no tooth to flank and a
     forced gap would just leave a hole between the two words. display:block
     drops the grid entirely (grid-template-columns/justify-self are no-ops
     once this isn't a grid), so "Take"/"Shape." fall back to plain inline
     spans reading left to right, same as line 1. */
  .hero-word-split { display: block; }
  .hero-word-take { justify-self: auto; }
  .hero-word-shape { justify-self: auto; margin-left: 0.28em; }

  /* Still absolute, not static: both captions live in the DOM as siblings of
     .hero-layer (moved there for their desktop z-index — see the comment by
     the markup), so on this stacked, DOM-order-is-visual-order layout, static
     would drop them after the cards and actions instead of above the
     headline. .stage-pin stays position:relative at every width, so
     anchoring here against the top padding .hero-layer reserves for exactly
     this works without needing a flex re-order of the whole stage.

     left/right/bottom are all reset here — the desktop rule gives each
     caption an asymmetric HALF of the line (e.g. `left:0; right:calc(50% +
     …)`) so it can align to its word's inner edge; on a single stacked
     column there is no "half" to align to, so both go back to spanning the
     full width. */
  /* Static, not absolute. They were positioned off .hero-layer's reserved
     padding purely because DOM order would otherwise have dropped them below
     the cards; the column above orders them explicitly instead, so they can
     just be flow content — no top offsets to re-measure. */
  .hero-caption {
    position: static; inset: auto;
    padding-inline: var(--gutter);
    justify-content: center;
  }

  /* The two overrides above used to carry `transform: none !important;
     opacity: 1 !important`, and they are still needed — but only where
     stage() actually writes inline opacity/transform onto these elements,
     which is the pinned/scrub build on the Treatments page. The homepage's
     flat hero ships no [data-stage] at all, so stage() returns before it
     touches anything and there is nothing there to override.

     Scoping matters because !important outranks CSS ANIMATIONS too, not
     just inline styles — left blanket, it silently pinned the flat hero at
     opacity 1 and the entrance below could never have run. */
  .stage:not(.stage--flat) .hero-word-line,
  .stage:not(.stage--flat) .hero-caption {
    transform: none !important;
    opacity: 1 !important;
  }
  /* The two badges still read as a pair, but no longer as a stack: at
     0.75rem they touched, and the second badge looked like a second row
     of the first rather than its own card. The step up to the buttons
     grows with them, so the run reads review / address / actions as three
     things rather than a block and a button. */
  /* The location badge goes. It repeats what the visit fold states in
     full further down (and what the LocalBusiness JSON-LD in inc/seo.php
     already gives a search engine), and on a phone the screen it costs is
     wanted for the two cards below. display:none rather than .sr-only
     here precisely BECAUSE the same NAP is marked up properly elsewhere on
     the page — an off-screen copy would be a second, competing one. */
  .hero-caption--loc { display: none; }
  /* now the last thing before the buttons, so it carries the gap the
     location badge used to */
  .hero-caption--rating { margin-bottom: var(--sp-3); }

  /* The break that ends the opening screen.
     ---------------------------------------------------------------------
     The fold is meant to close on the two calls to action; the cards are
     the next thing, not part of it. At --sp-4 they sat 32px under the
     buttons and read as a fourth row of the hero, which is what made the
     opening screen feel crowded.

     This is the lower half of the centring .stage-pin sets up above. The
     two subtrahends sum to the hero column's own height (14rem here plus
     17.25rem there = 31.25rem = the measured 494-512px), which is what
     makes the cards begin as the first screen ends. The larger share sits
     up top, so this gap stays the tighter of the two.

     A flat percentage cannot do this. The hero column is near-constant at
     every phone width (nothing in it scales with the screen) while the
     screen itself runs 480svh to 833svh, so the surplus to be split is a
     difference, not a ratio: 14svh would be 67px on an SE and 117px on a
     Pro Max where the real shortfalls are -152px and 329px.

     svh, not vh: svh is the viewport with the browser's own chrome
     showing, which is the height a reader actually opens the page at.

     Floor and ceiling keep it sane outside that range — a short screen
     never gets less than --sp-4, and a tall tablet never gets a canyon. */
  .hero-cards {
    width: 100%; padding-inline: var(--gutter);
    margin-top: var(--sp-3);
    /* The stage ends on these cards, and the tinted .office band starts on
       the very next pixel — the second card was sitting ON the seam, with
       its shadow falling across the change of background. This is the
       white run-out that closes the hero before the colour changes; the
       section below still adds its own --fold padding underneath on top
       of whatever is left here.

       --sp-4, trimmed from --sp-5: this is the one piece of PURE
       whitespace in the whole column, so it is the first place to take
       height back from — paid here to buy the new margin-top on .hero-word
       above (which pushes the headline down for more of the background
       photo to show) without pushing the total column any taller than it
       already was. See the max-height block below this one for the rest
       of that budget on a genuinely short phone. */
    margin-bottom: var(--sp-4);
  }

  /* Stacked, at full size.
     ---------------------------------------------------------------------
     These two used to share one line, which cost them their size: fitting
     "Book appointment" and "Call (905) 607-4333" side by side inside a
     350px gutter meant --fs-xs type, a clipped 0.5rem right padding and a
     24px icon — a pill noticeably smaller than the same button everywhere
     else on the site, and the primary action of the page reading as the
     smallest thing in the fold.

     One per row removes that constraint entirely, so both go back to the
     base .btn's own type and padding and the icon returns to its 27px.

     Capped and centred rather than edge to edge: at the full gutter width
     a pill on a tablet would run 700px wide with two words in the middle
     of it. The cap has come down twice — 20rem read as a full-bleed bar,
     then 17rem left ~65px stranded between the label and its disc, which
     read as two things at opposite ends of a pill rather than one
     control. 14rem is the longer label ("Call (905) 607-4333") plus its
     disc plus roughly 16px, so the split still puts the disc flush right
     without opening a corridor across the middle.

     Both pills keep the SAME cap rather than sizing to their own text:
     stacked one above the other, two different widths would read as a
     mistake, and the shorter label simply carries a slightly wider gap. */

  /* PRE-EXISTING SYNTAX BUG, FOUND AND FIXED IN PASSING
     -----------------------------------------------------------------------
     The comment below this one was sitting outside any comment block until
     this edit — raw prose in the middle of a stylesheet, straight after the
     block above's own comment-end marker, with no comment-start marker of
     its own. Depending on the browser that is either a hard parse failure
     on this rule or worse: CSS's error recovery treats unrecognised text
     before a brace as a garbled selector and drops the ENTIRE rule
     silently, which means .hero-actions and everything below it in this
     file may not have been styled on any narrow screen. Found while
     verifying an unrelated edit to .hero-lede a few dozen lines up — worth
     a full read of this file for the same pattern elsewhere if it surfaces
     again, since it means an earlier edit here silently orphaned a
     comment's tail the same way one just did to .hero-deco-bg's.

     The prose itself is untouched — only its own opening marker was added
     — since it also does not match the rule below (.hero-actions .btn sets
     justify-content to CENTRE here, not space-between), which suggests it
     documents a narrower, --fs-md breakpoint the file may have moved or
     removed since this was written rather than the block immediately under
     it. Left as-is rather than guessing at a rewrite. */
  /* space-between, not the base .btn's centre: the label goes to the left
     edge and the icon disc to the right, so the two pills agree on where
     their parts sit however differently their labels measure. Centred,
     "Book appointment" and "Call (905) 607-4333" put their discs in two
     different places down the column.

     The label also comes up one step, --fs-sm to --fs-md. It is the size
     the rest of the site sets a card title at, and on the page's primary
     action, on a phone, at arm's length, --fs-sm was small. */
  .hero-actions {
    flex-direction: row;
    justify-content: center;
    flex-wrap: nowrap;
    padding-inline: var(--gutter);
    gap: 0.5rem;
  }
  .hero-actions .btn {
    /* auto/none, not inherited from the stacked build above: these two
       share a 350px row now, so each takes only the width of its own
       label. flex:none stops the longer of the pair being squeezed by the
       shorter one.

       The padding, the inner gap and the disc below are all a notch
       tighter than they need to be at 390px. That is deliberate: it is
       what lets both discs survive down to a 360px handset instead of
       being dropped there — see the note in the 340px block at the foot of
       this file. The pair measures ~307px against the ~320px a 360px
       screen has once its gutters are taken. */
    width: auto; max-width: none; flex: none;
    justify-content: center;
    font-size: var(--fs-xs);
    padding: 0.62rem 0.45rem 0.62rem 0.75rem;
    gap: 0.45rem;
    white-space: nowrap;
    transform: none !important;
  }
  .hero-actions .btn i { width: 20px; height: 20px; }
  .hero-actions .btn i svg { width: 10px; height: 10px; }
  /* the cards no longer travel; on the stacked build there is nothing to
     make way for */
  .hcard { transform: none !important; }

  .proc-layer {
    position: static; opacity: 1; pointer-events: auto;
    grid-template-columns: minmax(0, 1fr);
    padding: 0 var(--gutter) var(--fold);
  }
  .proc-index, .proc-detail { grid-column: 1; justify-self: stretch; width: auto; }
  .proc-steps::before { display: none; }
  .pstep {
    min-height: 0; padding: 0 0 var(--sp-3) 20px; text-align: left;
    border-left: 2px solid var(--rule-quiet);
  }
  .pstep.now { border-left-color: var(--graphic); }
  .pstep::after, .pstep::before { display: none; }
  .proc-detail { display: block; margin-top: var(--sp-4); }
  .pcard {
    opacity: 1; transform: none; box-shadow: none;
    border: 0; border-top: var(--hair) solid var(--rule-quiet);
    border-radius: 0; padding: var(--sp-3) 0 var(--sp-4);
  }
  .pcard::before { display: none; }

  /* =======================================================================
     THE PHONE SCRUB  (.is-scrub — added by site.js only once the stage has
     been rebuilt and the frames are loading, so everything above stays the
     fallback for reduced motion, no JS, or a failed frame set)

     One sticky screen: the five steps as a horizontal rail across the top
     with the live one centred, the specimen under it, and that step's card
     beneath. A runway below supplies the scroll distance the rail, the frame
     sequence and the card all read from.

     Every selector here is .stage.is-static.is-scrub, not .stage.is-scrub.
     .is-scrub is only ever added on top of .is-static, and the
     `.stage.is-static .proc-layer { position: static }` / `.pcard { opacity:1 }`
     block further down this file is the same two-class weight and later in
     source — at .stage.is-scrub it won, the sticky silently degraded to
     static and all five cards stayed lit. The extra class settles it on
     specificity so source order stops mattering.
     ======================================================================= */
  .stage.is-static.is-scrub .proc-scrub { position: relative; }
  /* 5 steps over ~3 screens — roughly half a screen of scroll per step, which
     is slow enough to read the card before the next one takes over. */
  .stage.is-static.is-scrub .proc-runway { height: 300svh; }

  /* No side gutter on the stage itself — the specimen and the rail both want
     the whole width, and inset by --gutter the tooth read as a small picture
     in a wide margin rather than as the subject. Only the card takes an
     inset, and a tighter one than the page gutter, because it is the one
     thing here that is a line of text needing a measure. */
  .stage.is-static.is-scrub .proc-layer {
    position: sticky; top: 0;
    height: 100svh;
    display: flex; flex-direction: column; gap: 0.7rem;
    /* the header is fixed and overlays this */
    padding: calc(73px + 0.6rem) 0 var(--sp-3);
  }

  /* .proc-layer is a flex column too now, so the order values set on .spec
     for the OUTER column still apply to it in here — and order:7 against the
     other two children's default 0 dropped the specimen below the card. Each
     child of this stage states its own place. */
  .stage.is-static.is-scrub .proc-index  { order: 1; }
  .stage.is-static.is-scrub .spec        { order: 2; }
  .stage.is-static.is-scrub .proc-detail { order: 3; }

  /* --- the rail --------------------------------------------------------- */
  /* width:100% and min-width:0, not `auto`. The rail's five slots come to
     200% of it by design, and on `auto` the element sized to that content
     instead of to the column — 459px inside a 390px screen, which put a real
     horizontal scrollbar on the page. It has to be exactly the container's
     width for overflow:hidden to have the right box to clip against. */
  .stage.is-static.is-scrub .proc-index {
    flex: none; overflow: hidden;
    width: 100%; min-width: 0; max-width: 100%;
    box-sizing: border-box;
  }
  .stage.is-static.is-scrub .proc-steps { min-width: 0; }
  /* Each step is exactly the content-box width (70% of the rail once the 15%
     padding either side is taken), so ONE step is centred and its neighbours
     sit half-visible at each edge. Stepping is therefore a flat -70% of the
     rail per index — no per-item measuring, and it holds at any width. */
  /* 25% either side, so a slot is half the rail: the live step sits centred
     and its neighbours land exactly on the two edges, half in frame. At the
     15%/70% first cut a slot was wide enough that the neighbouring labels
     fell outside the rail entirely and the "faded either side" read was lost
     — the rail just looked like one word. */
  .stage.is-static.is-scrub .proc-steps {
    display: flex; padding-inline: 30%;
    transform: translateX(calc(var(--step, 0) * -40%));
    transition: transform var(--t-3) var(--ease);
  }
  /* Fades the ends rather than cutting them, so the rail reads as running on
     past the frame. Only the outer tenth: each neighbour's label is centred
     in its own slot and those centres land ~40% out from the middle, so a
     deeper fade erased the very text it was meant to be softening. */
  .stage.is-static.is-scrub .proc-index {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
  }
  .stage.is-static.is-scrub .pstep {
    flex: 0 0 100%;
    min-height: 0; padding: 0; border: 0; text-align: center;
    opacity: 0.25;
    transition: opacity var(--t-2) var(--ease);
  }
  .stage.is-static.is-scrub .pstep.now { opacity: 1; }
  .stage.is-static.is-scrub .pstep-n {
    font-size: var(--fs-nano); letter-spacing: var(--track-micro);
    color: var(--ink-quiet); margin-bottom: 0.15rem;
  }
  .stage.is-static.is-scrub .pstep.now .pstep-n { color: var(--accent-text); }
  /* nowrap in a 40%-wide slot, so the longest label ("Polished & Checked")
     sets the ceiling for all five */
  .stage.is-static.is-scrub .pstep-t {
    font-size: var(--fs-sm); font-weight: 600;
    letter-spacing: var(--track-tight); white-space: nowrap;
    color: var(--ink);
  }
  /* the live step is the screen's heading, so it carries a size the four
     ghosts either side do not */
  .stage.is-static.is-scrub .pstep.now .pstep-t { font-size: var(--fs-lg); }

  /* --- the specimen ----------------------------------------------------- */
  .stage.is-static.is-scrub .spec { flex: 1 1 auto; min-height: 0; margin: 0; }
  .stage.is-static.is-scrub .spec img,
  .stage.is-static.is-scrub .spec canvas {
    height: 100%; width: 100%; max-height: 100%;
    object-fit: contain;
  }

  /* --- the card --------------------------------------------------------- */
  /* The active card is in flow so the box takes its height; the other four
     are stacked on top of it at zero opacity, which is what lets them cross
     over rather than jump. */
  .stage.is-static.is-scrub .proc-detail {
    position: relative; flex: none; margin-top: 0;
    padding-inline: 1.15rem;
    max-height: 44svh; overflow: hidden;
  }
  .stage.is-static.is-scrub .pcard {
    position: absolute; inset: 0 1.15rem auto 1.15rem;
    opacity: 0; pointer-events: none;
    margin: 0; padding: 0; border: 0;
    transition: opacity var(--t-2) var(--ease);
  }
  .stage.is-static.is-scrub .pcard.now { position: relative; inset: auto; opacity: 1; pointer-events: auto; }

  /* --- type ------------------------------------------------------------
     The card is the only sustained read on this screen and it was set two
     steps down from the page's body size to make room. Sizes here are
     picked against that: the heading drops so the paragraph can come up,
     which is the right way round — the step is already named twice above,
     in the rail and in the "03 — of 05" line. */
  .stage.is-static.is-scrub .pcard h3 {
    font-size: var(--fs-lg); line-height: 1.22; margin-bottom: 6px;
  }
  .stage.is-static.is-scrub .pcard p { font-size: var(--fs-sm); line-height: 1.62; }
  .stage.is-static.is-scrub .pcard dl { margin-top: 0.7rem; }
  .stage.is-static.is-scrub .pcard dl div { padding: 0.34rem 0; }
  .stage.is-static.is-scrub .pcard dd { font-size: var(--fs-sm); }
}

/* =========================================================================
   THE FLAT HERO'S ENTRANCE — narrow widths only

   The rest of the page enters on the .r/IntersectionObserver reveal, and
   that is the wrong tool here: the hero is above the fold on load, so an
   observer fires it the same instant either way, and .r's own opacity
   would then have to be reconciled with the JS-driven hero styles. A plain
   keyframe on a delay does the same job with nothing to reconcile — and it
   runs on first paint, which is the point of a hero entrance.

   The order is the reading order: headline, then what the practice does,
   then the proof, then where it is, then the two ways to act, then the
   cards. Roughly 90ms apart, so the column assembles as one movement
   rather than counting itself out.

   `both` fill is doing real work: without it every element would flash at
   full opacity before its delay elapsed, which is the exact thing this is
   meant to avoid. Backwards fill holds the from-state through the delay.

   Wide screens are untouched — .stage--flat's desktop layout is a centred
   column that has never had an entrance, and adding one there would fight
   the reveal running on the folds directly below it. */
@media (max-width: 1024px) and (prefers-reduced-motion: no-preference) {
  .stage--flat .hero-word-line,
  .stage--flat .hero-caption,
  .stage--flat .hero-actions,
  .stage--flat .hero-cards {
    animation: hero-rise 0.8s var(--ease) both;
  }
  /* .hero-lede is display:none at every width now, so its own entry here
     (and its 0.24s delay, which the badges below have absorbed rather than
     leaving a gap in the rhythm) went with it — see the removed override in
     the narrow-build block above. */
  .stage--flat .hero-word-line--top    { animation-delay: 0.06s; }
  .stage--flat .hero-word-line--bottom { animation-delay: 0.15s; }
  .stage--flat .hero-caption--rating   { animation-delay: 0.24s; }
  .stage--flat .hero-caption--loc      { animation-delay: 0.33s; }
  .stage--flat .hero-actions           { animation-delay: 0.42s; }
  .stage--flat .hero-cards             { animation-delay: 0.51s; }
}
@keyframes hero-rise {
  from { opacity: 0; transform: translate3d(0, 16px, 0); }
  to   { opacity: 1; transform: none; }
}

/* On a phone two card columns leave ~150px each and the copy shreds into
   six-line paragraphs, so the pair drops to one column, full width. That
   alone does not make either card feel like it belongs on a phone, though:
   everything below it is still the 104px disc, the roomy padding and the
   card-title size that were sized for sitting beside a twin at 460px+ wide,
   now just stretched across a narrow screen with nothing to balance it —
   the photo reads as oversized and the gaps as loose rather than airy.

   The offer card gets this for free rather than needing its own pass: its
   disc and title are .hcard-pic/.hcard-t with a --swap/--offer modifier
   class alongside, not a separate size of their own, so shrinking the
   shared rule shrinks both cards identically and the two keep the same
   silhouette they have at every other width. */
@media (max-width: 560px) {
  /* Phone breathing room in the hero cards.
     ---------------------------------------------------------------------
     Reported from the live site: on a phone the two cards read as one
     cramped block. They were: 11px apart, 6px between a title and its
     sub-line, 7px before the action link, and 14px of padding holding
     three lines of copy off the card edge — every gap in the pair smaller
     than the smallest step on the site's own spacing scale.

     The values below are one rhythm rather than per-element nudges, so
     the spacing reads as deliberate at every point in the card:

       8px  (--sp-1)  between lines inside the copy column
       12px           before the action link — the one break that separates
                      a paragraph from something clickable, so it is the
                      one step larger than the others
       16px  (--sp-2) photo to copy, AND card to card: the two gaps a
                      reader sees as "between things" get the same number
       18.4px         card padding, matching the desktop card's own

     Line-heights come up with it — the gaps were only half the problem,
     the lines inside each block were closed up too.

     Desktop is untouched: every declaration here is inside this query and
     overrides the base .hcard rules only below 560px. */
  .hero-cards { grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); }

  /* The photo sets the card's height — the copy column beside it is only a
     title and a link now that the sub-lines are off — so every pixel off
     the disc is a pixel off both cards. 72px to 60px to 48px, with both
     cards needing to sit inside the first screen alongside the headline,
     lede, badge and buttons: it costs a picture nobody reads detail in,
     not a word.

     At 48px the photo is no longer what sets the height — the title, its
     gap and the link now come to slightly more — so this is about as far
     as shrinking the disc alone can usefully go. The offer card's progress
     ring follows on its own: it is absolutely positioned at
     calc(100% + 14px) of .hcard-media, so it tracks whatever the disc
     measures rather than carrying a size of its own. */
  .hcard {
    grid-template-columns: 48px minmax(0, 1fr);
    gap: var(--sp-2);
    padding: 1.05rem 1.15rem;
  }
  .hcard-pic { width: 48px; height: 48px; }

  /* "Proudly supporting the" comes off the first card, leaving the title
     as "Canadian Dental Care Plan" — which fits the ~240px copy column on
     one line where the full sentence took three. The words are hidden, not
     deleted (see the span in front-page.php): the card still reads as a
     full sentence to a search engine and to a screen reader. */
  .hcard-t-lead {
    position: absolute; width: 1px; height: 1px;
    margin: 0; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }

  /* The offer card's second line is the one that animates between "Teeth
     Whitening" and "Electric Toothbrush". The longer of the two wrapped,
     so the card changed height every time the swap ran — a card that
     resizes on a timer while you are reading it. nowrap holds both to one
     line and the height stops moving. */
  .swap-word { white-space: nowrap; }
  /* 1.32 was chosen for a two-line title in a half-width desktop card. The
     same title wraps to two or three lines at this width, where 1.32 sets
     them almost touching. */
  .hcard-t { font-size: var(--fs-sm); line-height: 1.45; }

  /* The card sub-lines come off the phone layout — the space they took is
     wanted above, for the badges and the buttons, and the title plus the
     link already say what each card is ("Proudly supporting the Canadian
     Dental Care Plan → Coverage info").

     Hidden the .sr-only way, NOT display:none. The copy has to stay
     crawlable — that is the whole point of taking it off the screen
     rather than out of the template — and the clip technique leaves the
     text in the DOM, in the accessibility tree and in the rendered
     markup Google indexes, where display:none removes it from the
     accessibility tree entirely and is the pattern search engines
     discount. Screen-reader users on a phone still hear it.

     position:absolute also takes it out of flow, so the title-to-link gap
     falls to .hcard-a's own margin rather than leaving a hole. */
  .hcard-s {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  /* --fs-xs (12.16px) was this card's own base size at every width, never a
     deliberate mobile choice — and the other three "explore/read more"
     arrow-links on this page (.doc-link, .tx-link, .rev-src) all read at
     15.04px on a phone. --fs-sm rather than matching them exactly: this
     card is the one genuinely tighter context of the four (a 72px disc and
     two lines of copy already share the same row this link sits under), so
     it gets a real improvement toward the others rather than the full jump
     — going all the way to 15.04 risked wrapping the shorter of the two
     labels onto its own second line where the others have the width to
     spare. */
  /* 0.75rem was the gap under a two-line sub-paragraph; with that
     paragraph off the phone layout the link sits directly under a single
     line of title, and the same space read as a hole between two things
     that belong together. The 44px thumb target is unaffected — it is
     painted by .hcard-a::after outside the layout, so closing this gap
     moves the text without shrinking what a finger can hit. */
  .hcard-a { margin-top: 0.4rem; font-size: var(--fs-sm); }

  /* "Small on mobile as well." The button keeps --fs-xs here rather than
     following .hcard-a up to --fs-sm — the two-class rule in the offer block
     already holds the size, and this only trims the padding and the disc a
     little further so the pill stays proportionate to a card whose photo is
     down to 48px.

     The 44px thumb target is untouched: it is painted by .hcard-a::after
     outside the layout (see the 640px block further down), which is exactly
     why this stayed a styled .hcard-a instead of becoming a real .btn — a
     .btn has overflow:hidden and would have clipped it away. */

  /* Icon only on a phone: the gift disc, and nothing else.
     ---------------------------------------------------------------------
     The words go the .sr-only way and NOT via display:none, and the reason
     is not tidiness. The <i> beside them is aria-hidden — it is a picture of
     a present, it says nothing — so "Choose your gift" is the entire
     accessible name of this link. Removed, the card would ship a call to
     action that a screen reader announces as "link" and nothing more, which
     is the worst thing that can happen to a button. Clipped, it stays in the
     accessibility tree, in the rendered markup Google indexes, and in the
     DOM — exactly as .hcard-s and .hcard-t-lead already do a few rules up on
     this same card. */
  .hcard--offer .hcard-a-label {
    position: absolute; width: 1px; height: 1px;
    margin: 0; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  /* With the label out of flow the anchor shrinks to its disc, so the pill
     becomes a circle on its own: equal padding all round, and --r-pill on a
     square box IS a circle. The gap needs no resetting either — an
     absolutely positioned child is not a flex item, so there is nothing left
     for it to sit between. 16px of disc in 4px of padding comes to 24px,
     against the ~110px-wide pill it replaces.

     Tapping is unaffected. The 44px target is painted by .hcard-a::after
     outside the layout in the 640px block below, so a 24px button is still a
     full-size thumb target — which is the whole reason this stayed a styled
     .hcard-a rather than becoming a real .btn, whose overflow:hidden would
     have clipped that target away. */
  /* The disc IS the button here.
     ---------------------------------------------------------------------
     What read as a black border was the button itself: .hcard--offer's
     .hcard-a is a --deep pill with a --sage disc inside it, and once the
     label was clipped away all that survived of the pill was a 4px ring of
     near-black around the disc. A pill with nothing in it but its own icon
     is not a pill, so on a phone the fill, the padding and the lift all go
     and the sage disc stands on its own.

     Which is also what pays for the bigger icon: dropping the ring frees
     the corner, so the disc nearly doubles (16px -> 30px) while the whole
     button grows by six. The glyph goes 10px -> 17px, well clear of the
     size where its bow and box were mushing together.

     --deep on --sage is 8.03:1 — the one place tokens.css allows a sage
     fill under a mark, precisely because what sits on it is near-black
     rather than white. */
  .hcard--offer .hcard-a {
    padding: 0;
    background: none;
    box-shadow: none;
  }
  .hcard--offer .hcard-a i {
    width: 30px; height: 30px;
    box-shadow: 0 6px 14px -8px rgba(var(--deep-rgb), 0.5);
  }
  .hcard--offer .hcard-a i svg { width: 17px; height: 17px; }

  /* The desktop hover pair, switched off rather than inherited. There is no
     hover on a phone, but :hover still latches on tap in most mobile
     browsers and can stay latched after the finger has gone — which would
     put the --deep ring back on the button a reader had just pressed, and
     leave it there. */
  .hcard--offer .hcard-a:hover,
  .hcard--offer:hover .hcard-a { background: none; }
  .hcard--offer .hcard-a:hover i,
  .hcard--offer:hover .hcard-a i { background: var(--sage); color: var(--deep); }

  /* --- and the phone layout of the card itself ------------------------
     Phone only. Every rule from here to the end of this comment's block is
     inside the 560px query and the desktop card is untouched by all of it.

     Now that the button is a 24px disc rather than a 110px pill, it has no
     business taking a row of its own at the bottom of a card this tight. It
     goes to the top-right corner, and the title gets the width back.

     Absolute, against .hcard rather than against the foot — so it leaves
     the grid entirely and the body column keeps its full measure. It costs
     the button nothing to move: .hcard-a::after, the 44px thumb target, is
     positioned against this element wherever it sits, so a 24px disc in the
     corner is still a full-size tap area. */
  .hcard--offer { position: relative; }
  .hcard--offer .hcard-a {
    position: absolute;
    top: 0.85rem; right: 0.9rem;
    margin: 0;
  }

  /* "Complimentary" and the gift strip stack instead of sharing a line.
     column, not a wrap: at --fs-sm the pair came to more than the body
     column and broke wherever it happened to run out, which put the strip
     on line two some of the time and hanging off line one the rest. This
     makes the break the design rather than an accident of measure.

     align-items: flex-start matters — the strip is the flex item, and left
     to stretch it would run the full width of the body with the label
     stranded in the middle of it. Its INTERNAL width is unchanged: both
     labels still share one grid cell sized to the longer of them, so the
     strip is a constant and only its text turns over.

     The padding keeps the longest line clear of the button now sitting in
     that corner. */
  .hcard--offer .hcard-t--offer {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
    /* Clearance for the disc in the corner, and it grew with it. The button
       is 30px wide sitting 0.9rem in, against .hcard's own 1.15rem of
       padding — so it reaches about 26px into the body column, and this is
       that plus a gap. */
    padding-right: 2.3rem;
  }

  /* The two dots are all that is left in the foot row, so it needs a little
     air above it rather than the hairline gap it had when it was carrying
     the button as well. */
  .hcard--offer .hcard-foot { margin-top: 0.45rem; }

  /* The strip comes in a touch tighter with everything else on the card. */
  .hcard-t--offer .swap-word { padding: 0.16em 0.45em; }

  /* The germs come down with the disc, but not as far as the disc does. It
     goes 104px -> 48px; the dots go 5px -> 4px and their travel to roughly
     two thirds. Scaling them by the disc's own ratio would have put 2px
     specks on a phone, which is where "more visible" fails first — the
     smaller the screen, the less a proportionally-correct dot survives.

     What the travel is really limited by is the card, not the photograph:
     .hcard's padding is 1.05rem here and the gap to the copy 1rem, so the
     numbers below are what fits inside those rather than a fraction of
     anything. */
  .swap-germs i { width: 4px; height: 4px; margin: -2px 0 0 -2px; }
  .swap-germs i:nth-child(1) { --gx:   0px; --gy: -14px; }
  .swap-germs i:nth-child(2) { --gx:   7px; --gy: -12px; }
  .swap-germs i:nth-child(3) { --gx:  12px; --gy:  -7px; }
  .swap-germs i:nth-child(4) { --gx:  14px; --gy:   0px; }
  .swap-germs i:nth-child(5) { --gx:  12px; --gy:   7px; }
  .swap-germs i:nth-child(6) { --gx:   7px; --gy:  12px; }
  .swap-germs i:nth-child(7) { --gx:  -7px; --gy: -12px; }
  .swap-germs i:nth-child(8) { --gx: -11px; --gy:  -6px; }
  .swap-germs i:nth-child(9) { --gx:  -3px; --gy: -13px; }
}

/* A short phone (a 568pt SE, or any handset with the browser chrome showing)
   leaves ~250px under the specimen and the step cards want ~330. The
   definition list is the part that can go: it is supplementary detail, and
   every one of its rows restates something the paragraph above has already
   said in a sentence. Clipping it silently — which is what max-height alone
   did — was the alternative. */
@media (max-width: 1024px) and (max-height: 680px) {
  .stage.is-static.is-scrub .pcard dl { display: none; }
  .stage.is-static.is-scrub .pcard p { font-size: var(--fs-xs); line-height: 1.55; }
}

/* The homepage's own short-phone budget.
   ---------------------------------------------------------------------
   What this block defends against changed once .stage--flat .stage-pin
   picked up `height: 100svh; overflow: hidden;` above. Fold 2 showing on
   load is no longer possible AT ALL — the section is a hard-capped box now,
   not content that grows until it happens to end. What is still at stake
   here is not "does fold 2 show", it is "does anything inside the hero get
   CROPPED by that cap on the shortest phones" — the headline, a caption, or
   the bottom of a card disappearing behind the hidden overflow instead of
   simply being followed by fold 2 a little sooner than on a taller device.

   This USED to also force justify-content to flex-start here, on the
   reasoning that centred content overflowing a hidden box crops from both
   ends at once — the headline and the cards vanishing together, which is
   the worst failure since it damages the page's own h1. That line is gone:
   .stage--flat .stage-pin now sets `justify-content: safe center`, which
   is the browser-native version of the exact same idea — it centres
   normally and only falls back to start-alignment on its own, at paint
   time, if centring would truly push content past the edge. A media query
   pinned to a guessed 680px could be wrong about any given device (and
   was, in practice — the flex-start line quietly won on a screen this file
   had no way to know was short-by-its-definition, cutting the top gap this
   pass exists to add rather than only guarding against overflow); `safe`
   cannot be wrong, because it is not guessing.

   What is left below is real height still worth freeing on a short phone,
   independent of that question: three margins trimmed one step, so a crop
   — should `safe center`'s own fallback ever need to happen — has as
   little as possible left to take. */
@media (max-width: 1024px) and (max-height: 680px) {
  .stage--flat .hero-word-line--bottom { margin-bottom: var(--sp-2); }
  .stage--flat .hero-caption--rating { margin-bottom: var(--sp-2); }
  .stage--flat .hero-cards { margin-top: var(--sp-2); margin-bottom: var(--sp-3); }
}

/* Static: reduced motion on a wide screen. Without this the four inactive
   cards sit at opacity 0 and nothing will ever advance the step. */
.stage.is-static .stage-pin { height: auto; overflow: visible; }
.stage.is-static .hero-layer { position: static; padding-top: 7rem; display: block; }
.stage.is-static .hero-word-line { transform: none !important; opacity: 1 !important; }
.stage.is-static .hero-caption { transform: none !important; opacity: 1 !important; }

/* On a narrow viewport .is-static is always present too (wideQ fails
   regardless of motion preference), and .stage.is-static .hero-layer above
   — same specificity, declared first — would otherwise win over the
   @media (max-width: 1024px) rule and silently put the padding back to 7rem.
   This has to out-order it, not just out-specify it: same two-class
   specificity, later in the source.

   It now carries display too, for the same reason: the narrow build turns
   .stage-pin into an ordered flex column and needs this wrapper dissolved so
   the headline, lede, badges and actions become items of it. The plain
   `.hero-layer { display: contents }` in the media query above is a single
   class and loses to the is-static rule every time. */
@media (max-width: 1024px) {
  .stage.is-static .hero-layer { padding-top: 0; display: contents; }
}
.stage.is-static .spec {
  position: relative; left: auto; top: auto;
  transform: none !important; margin-inline: auto;
}
.stage.is-static .spec-halo { display: none; }
.stage.is-static .proc-layer {
  position: static; opacity: 1; pointer-events: auto;
  grid-template-columns: minmax(0, 1fr); padding-top: var(--fold);
}
.stage.is-static .proc-index,
.stage.is-static .proc-detail { grid-column: 1; justify-self: stretch; width: auto; }
.stage.is-static .proc-detail { display: block; }
.stage.is-static .pcard { opacity: 1; transform: none; }

/* =========================================================================
   FOLD 3 — WHY WE STAND APART

   Rebuilt and compacted. The nested white card is gone — everything sits on
   the green panel directly, which is where most of the old height went. The
   composition is a connected 1-2-3 flow, then text | image | text.
   ========================================================================= */
/* =========================================================================
   FOLD 2 — WHERE WE ARE

   Copy on the left, a layered map composition on the right: the map panel
   itself, the practice's own reception photograph inset over one corner,
   and an address card lifted off the opposite one. Three planes rather than
   one flat picture, which is what makes it read as composed instead of as a
   screenshot dropped into a column.

   --surface-quiet, so the fold separates from the white hero above it
   without needing a rule drawn between them.
   ========================================================================= */
.office {
  position: relative; z-index: 1;
  background: var(--surface-quiet);
  scroll-margin-top: 88px;
}
.office-grid {
  display: grid;
  /* the copy takes the wider column: it carries a d3 display heading and a
     four-line paragraph, where the map only has to stay square enough to
     read as a map */
  grid-template-columns: minmax(0, 1.02fr) minmax(0, 0.98fr);
  gap: clamp(2.2rem, 5vw, 4.5rem);
  align-items: center;
}
.office-copy .lead { margin-top: var(--sp-3); max-width: 46ch; }
.office-actions {
  margin-top: var(--sp-4);
  display: flex; flex-wrap: wrap; gap: 0.7rem;
}

/* --- the layered visual -------------------------------------------------- */
/* The containing block for the two floating pieces. It is NOT the map
   figure itself: the map clips to its own rounded corners (overflow:hidden,
   below) so anything positioned against it would be clipped by it, and both
   the photo and the card are meant to hang over its edges. */
.office-visual { position: relative; }

.office-map {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-md);
  /* square-ish rather than a strip: a map needs its surroundings visible to
     be worth showing at all, and a letterbox crop of one road is not that */
  aspect-ratio: 1 / 1;
  background: var(--n-10);   /* holds the shape while the iframe loads */
  box-shadow: 0 30px 60px -34px rgba(var(--deep-rgb), 0.45),
              inset 0 0 0 var(--hair) var(--rule);
}
/* border:0 because a bare iframe carries a 2px inset frame in most browsers,
   and it lands inside the rounded corners as a grey square */
.office-map iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0; display: block;
}

/* The reception photograph, over the map's top-right corner — the same
   inset the old site had, kept because it is the one thing on this fold
   that shows the practice rather than describing it. */
.office-inset {
  position: absolute;
  top: clamp(-1.4rem, -1.2vw, -0.8rem);
  right: clamp(-1.2rem, -1vw, -0.6rem);
  width: clamp(96px, 26%, 168px);
  aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  box-shadow: 0 0 0 7px var(--surface-quiet),
              0 22px 44px -22px rgba(var(--deep-rgb), 0.5);
}
.office-inset img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The address card, lifted off the bottom-left corner. */
.office-card {
  position: absolute;
  left: clamp(-1rem, -1vw, -0.5rem);
  bottom: clamp(-1.1rem, -1vw, -0.6rem);
  display: flex; align-items: flex-start; gap: 0.7rem;
  padding: 0.9rem 1.15rem;
  max-width: min(84%, 300px);
  background: var(--white);
  border-radius: var(--r-md);
  box-shadow: 0 4px 10px rgba(var(--deep-rgb), 0.06),
              0 26px 52px -22px rgba(var(--deep-rgb), 0.42);
}
.office-pin {
  width: 34px; height: 34px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--tint-open); color: var(--leaf);   /* 5.71:1 */
}
.office-pin svg { width: 16px; height: 16px; }
.office-card-body { min-width: 0; }
.office-card-name { font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }
.office-card-addr {
  margin-top: 0.1rem;
  font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-mid);
}
/* Written by hours() once it has read the Toronto clock; stays hidden if it
   cannot, rather than guessing — same rule .visit-status follows. */
.office-status {
  display: inline-flex; align-items: center; gap: 0.4rem;
  margin-top: 0.5rem; padding: 0.24rem 0.6rem;
  font-size: var(--fs-nano); font-weight: 700;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  border-radius: var(--r-pill);
}
.office-status::before {
  content: ''; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; flex: none;
}
.office-status.is-open { color: var(--leaf);  background: var(--tint-open); }
.office-status.is-shut { color: var(--n-80);  background: var(--n-10); }

@media (max-width: 900px) {
  /* One column, copy first. The map keeps its own width rather than running
     the full rail — a full-bleed square map on a phone is most of a
     screen's height for something the reader is only glancing at. */
  .office-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); }
  .office-visual { width: min(100%, 460px); margin-inline: auto; }
  .office-copy .lead { max-width: none; }
}
/* --- office fold, phone-only line fixes ----------------------------------
   Two things in this fold wrapped on a phone that read better on one line,
   and both are fixed by taking back width rather than by shrinking type.

   1. The eyebrow. "Under the vision of Dr. Nadeen Shaker:" ran to two
      lines at 360 and 390px (one line by 414px). The cause is the tracking,
      not the font size: --track-micro is 0.19em, so at this length roughly
      a quarter of the line is letter-spacing. Tightening it to 0.08em here
      is enough on its own, and an eyebrow set this small still reads as
      one at the narrower tracking.

   2. The two buttons. They measured 210px + 163px against 350px of
      available width, so the pair could not share a row and "Get
      directions" dropped underneath. Dropping the icon disc on a phone is
      what buys the room back — the same trade template-doctor.php already
      makes for its hero actions at this breakpoint — and nowrap then
      guarantees neither label breaks internally. flex:1 lets the two split
      the row evenly instead of leaving a ragged gap.
   ------------------------------------------------------------------------- */
@media (max-width: 640px) {
  /* Tracking only — the size is deliberately left alone. Stepping down to
     --fs-nano looked like the obvious next move and does the opposite:
     the narrow build re-declares --fs-nano as 0.72rem (see the legibility
     block near the foot of this file, "11.5px, from 9.3px"), which is
     BIGGER than --fs-micro's 0.68rem, so the line grew instead of
     shrinking. That bump exists so nothing on a phone renders at 9px, and
     it is not worth undoing for one eyebrow.

     At 0.08em this fits one line from 360px up. On a 320px screen the
     column is 280px against ~317px of text and it still takes two lines;
     closing that last 37px would need either negative tracking or a size
     below the floor that legibility rule sets, so it is left to wrap
     there — 320px is the 2016 SE, and the wrap is tidy rather than broken. */
  /* The eyebrow sat directly on the heading — h2 is margin:0 from the
     reset and nothing put anything between them, so "Under the vision of
     Dr. Nadeen Shaker:" and the sentence under it read as one block.

     margin-bottom rather than a margin-top on the h2: .eyebrow is
     inline-flex (it carries the green rule as a ::before), so it is an
     atomic inline-level box and its margin box is what sets the height of
     the line box it sits on — the 12px lands under it and pushes the
     heading down, which is the same result the .rev-head / .visit-head
     eyebrows get from their own `+ h2` rules. */
  .office-copy .eyebrow { letter-spacing: 0.08em; margin-bottom: 0.75rem; }

  .office-actions { flex-wrap: nowrap; gap: 0.5rem; }
  .office-actions .btn {
    flex: 1 1 0; min-width: 0;
    padding-inline: 0.9rem;
    white-space: nowrap;
    font-size: var(--fs-xs);
  }
  .office-actions .btn i { display: none; }

  /* Room around the map composition.
     ---------------------------------------------------------------------
     The map is not the only thing here: the reception photo hangs 9.6px
     ABOVE its top edge and the address card 9.6px BELOW its bottom one,
     and both are position:absolute — so neither adds any height, and the
     space actually visible above and below the composition is ~10px less
     than the numbers that produce it. Above, the 48px grid gap was really
     38px to the top of the photo; below, the fold's 54.4px was really
     45px under the card.

     So both get the overhang back plus a step: the grid gap goes up one
     rung, and .office-visual takes a bottom margin the fold padding then
     adds to. The margin is on the visual rather than on the fold so the
     copy column above is unaffected — only the map moves.

     Net: ~54px clear above the photo, and ~69px under the card before
     .apart's own 48px of top padding starts the next section. */
  .office-grid { gap: var(--sp-6); }
  .office-visual { margin-bottom: var(--sp-3); }
}

@media (max-width: 560px) {
  /* the two floating pieces pull in so neither hangs off the gutter */
  .office-inset { width: 84px; top: -0.6rem; right: -0.3rem; }
  .office-card { left: -0.3rem; bottom: -0.6rem; max-width: 90%; padding: 0.8rem 1rem; }

  /* #office-h's own 1.4rem override lived here. It is gone because the
     global section-heading collapse near the foot of this file now sets
     .display.d2 (which this heading is) to exactly the same 1.4rem/1.28 —
     so this rule had become a duplicate that could only ever drift out of
     step with the one that matters. */
}

.apart {
  position: relative; z-index: 1;   /* sits under the pin while it is still pinned */
  background: var(--white);
  scroll-margin-top: 88px;
  padding-block: clamp(3rem, 6vw, 6rem);
}
/* Wider than the page rail, which is the point — this panel is meant to run
   broader than the boxed procedure above it. But "gutter only" meant that on
   a 1600 screen the panel's own type started at 115 while every other fold on
   the page started at 184, and the eye reads that as a mistake rather than as
   a device.

   Capping the shell at the page plus the panel's own padding puts the panel
   edge exactly one padding outside the rail, so the type inside it lands on
   the rail with the rest of the page and only the surface runs wide. Below
   ~1450 the cap is inactive and the panel is gutter-to-gutter as before. */
.apart { --apanel-pad: clamp(1.5rem, 3.2vw, 3.2rem); }
.apart > .shell { max-width: calc(var(--page) + var(--apanel-pad) * 2); }
.apart-panel {
  position: relative;
  /* two sage washes under 10% alpha — tonal depth, not a colour ramp */
  background:
    radial-gradient(120% 90% at 85% -10%, rgba(var(--sage-rgb), 0.10), transparent 55%),
    radial-gradient(90% 70% at 0% 110%, rgba(var(--sage-rgb), 0.06), transparent 60%),
    var(--surface-deep);
  border-radius: var(--r-md);
  padding: clamp(1.8rem, 3.2vw, 3.2rem) var(--apanel-pad);
  box-shadow: inset 0 0 0 1px var(--rule-invert),
              0 40px 80px -50px rgba(var(--deep-rgb), 0.8);
}

/* --- the 1-2-3 flow ------------------------------------------------------ */
/* --- top row: white feature card + quote card, v7's ~55/45 split -------- */
.apart-top {
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: clamp(1.2rem, 2.4vw, 2rem);
  align-items: stretch;
}

.apart-card {
  background: var(--white);
  border-radius: var(--r-md);
  padding: clamp(1.7rem, 3vw, 2.6rem);
  box-shadow: 0 30px 60px -40px rgba(var(--deep-rgb), 0.5);
}
.apart-card .apart-eyebrow { color: var(--ink-quiet); }
.apart-card .apart-h {
  color: var(--ink);
  margin-top: 0.6rem;
  margin-bottom: clamp(1.3rem, 2.2vw, 1.9rem);
}
.apart-card .apart-h em { color: var(--accent-text); }

.apart-check { display: grid; gap: clamp(1rem, 1.8vw, 1.4rem); }
.apart-check li { display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 0.85rem; align-items: start; }
.apart-tick {
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; margin-top: 0.1rem;
  background: var(--n-05); color: var(--sage-deep);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet);
}
.apart-tick svg { width: 13px; height: 13px; display: block; }
.apart-check h3 { font-size: var(--fs-md); font-weight: 700; letter-spacing: var(--track-tight); color: var(--ink); line-height: 1.3; }
.apart-check p { margin-top: 0.3rem; line-height: 1.65; color: var(--ink-mid); } /* font-size: type system, foot of this file */

.apart-quote {
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(1.1rem, 2vw, 1.6rem);
  background: var(--deep-soft);
  border-radius: var(--r-md);
  padding: clamp(1.7rem, 3vw, 2.4rem);
  box-shadow: inset 0 0 0 1px var(--rule-invert);
}
/* Absorbs whatever height the quote and button leave, rather than carrying a
   fixed aspect ratio. The column runs 407-669px wide across viewports while
   the spare height barely moves, so any single ratio would fill the gap at
   one width and overshoot the card at another. */
.apart-shot {
  position: relative;
  flex: 1 1 auto; min-height: 130px;
  margin: 0; border-radius: var(--r-md); overflow: hidden;
  background: var(--deep);
  box-shadow: inset 0 0 0 1px var(--rule-invert);
}
/* Zoomed and pushed right, deliberately. The practice only has two
   photographs, and this one also fills fold 9's Visit panel at close to its
   native framing — shown wide here too, the reader meets the same room shot
   twice in one scroll and it reads as a stock library running thin. Enlarged
   to 190% and offset, the frame lands on the chair and window instead of the
   whole room, so the two uses read as two pictures.

   Sized rather than transformed: GSAP owns the transform on this figure's
   parent for the entrance, and nesting a second transform here would fight
   the crop as the card slides in. */
.apart-shot img {
  position: absolute;
  top: -46%; left: -58%;
  width: 190%; height: 190%; max-width: none;
  object-fit: cover; object-position: 50% 50%; display: block;
}

.apart-quote p {
  /* font-size: type system, foot of this file */
  line-height: 1.8; color: var(--on-deep-mid);
  padding-left: 1rem; border-left: 2px solid var(--sage);
}
.apart-quote .btn { align-self: flex-start; }

/* --- bottom row: video + vertical timeline, ~62/38 -------------------------
   38% sits inside the 30-40% asked for. */
.apart-bottom {
  display: grid;
  grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr);
  gap: clamp(1.2rem, 2.4vw, 2rem);
  margin-top: clamp(1.2rem, 2.4vw, 2rem);
  align-items: stretch;
}

.apart-video {
  /* centre, not stretch: the iframe carries its own 16:9 ratio, so stretching
     the figure only added dead background under the player. Where the step
     stack runs taller, centring splits the difference above and below rather
     than pooling it all at the bottom. */
  align-self: center;
  margin: 0; border-radius: var(--r-md); overflow: hidden;
  background: var(--deep-soft);
  box-shadow: inset 0 0 0 1px var(--rule-invert),
              0 34px 70px -34px rgba(var(--shade-rgb), 0.72);
}
/* border-radius does not clip an iframe's own content — only the parent's
   overflow:hidden does that; inherit keeps the corners in step regardless */
.apart-video iframe {
  width: 100%; height: auto; aspect-ratio: 16 / 9;
  display: block; border: 0; border-radius: inherit;
}

/* Each step is its own card now, so the outer element is only a column —
   a panel inside a panel inside a panel read as far too much boxing. */
.vstep-card { position: relative; display: block; }

/* One gap value drives both the space between steps and the height of the
   connector that fills it — they have to be the same number or the line
   floats short of the next capsule. */
.vstep-list {
  --vgap: clamp(0.7rem, 1.4vw, 1.05rem);
  display: grid;
  /* 1fr rows off a full-height list: the three cards split the column
     equally, so they are the same height whatever the copy wraps to, and
     the stack always lands exactly on the video's height beside it */
  grid-auto-rows: minmax(0, 1fr);
  gap: var(--vgap);
  height: 100%;
}

.vstep {
  position: relative;
  display: grid; align-content: center; justify-items: center; gap: 0.32rem;
  text-align: center;
  padding: clamp(0.75rem, 1.5vw, 1.05rem) clamp(0.9rem, 1.8vw, 1.35rem);
  background: var(--deep-soft);
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 1px var(--rule-invert);
}
/* the capsule, then the name beneath it */
.vstep-cap {
  display: inline-flex; align-items: center;
  padding: 0.42rem 1rem;
  border-radius: var(--r-pill);
  background: var(--sage); color: var(--deep);
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  box-shadow: 0 10px 22px -12px rgba(var(--sage-rgb), 0.55);
}
.vstep-name {
  font-size: var(--fs-md); font-weight: 700; letter-spacing: var(--track-tight);
  color: var(--on-deep); line-height: 1.35; max-width: 24ch;
}
.vstep-txt {
  font-size: var(--fs-xs); line-height: 1.45;
  color: var(--on-deep-mid); max-width: 34ch;
}

/* The connector fills the gap below each step. Absolute, so it occupies no
   layout space and cannot push the gap open wider than --vgap. */
.vstep-link {
  position: absolute; z-index: 1;
  top: 100%; left: 50%;
  width: 2px; height: var(--vgap);
  margin-left: -1px;
  background: var(--rule-invert);
  border-radius: var(--r-sm);
  overflow: hidden;
  transform-origin: top center;
}
.vstep-link::after {
  content: '';
  position: absolute; left: 0; right: 0; top: 0; height: 70%;
  border-radius: var(--r-sm);
  background: linear-gradient(180deg, transparent, var(--sage) 50%, transparent);
  transform: translateY(-140%);
}
@media (prefers-reduced-motion: no-preference) {
  .vstep-link::after { animation: vstep-run 2.6s cubic-bezier(0.45, 0, 0.55, 1) infinite; }
  /* half a cycle behind, so the pulse reads as one thing travelling the whole
     timeline rather than two firing in unison */
  .vstep:nth-child(2) .vstep-link::after { animation-delay: 1.3s; }
}
@keyframes vstep-run {
  from { transform: translateY(-140%); }
  to   { transform: translateY(280%); }
}

@media (max-width: 1024px) {
  .apart { scroll-margin-top: 112px; }
  .apart-top, .apart-bottom { grid-template-columns: minmax(0, 1fr); }
  /* nothing to absorb once the cards are their natural height */
  .apart-shot { flex: none; aspect-ratio: 16 / 9; }
  /* no video beside it to match, so the cards take their natural height */
  .vstep-list { height: auto; grid-auto-rows: auto; }
  .apart-bottom { margin-top: var(--sp-4); }

  /* --- room between the panel's three blocks ----------------------------
     Stacked, this panel reads as three things: the white card, the green
     quote card, then the video and its three steps as one unit. The gaps
     that separate them are written `clamp(1.2rem, 2.4vw, 2rem)`, and 2.4vw
     is 34px at 1440 but only 9px at 390 — so a desktop gets the 2rem
     ceiling (32px) while a phone drops to the 1.2rem floor (19px). The
     narrow screen, which needs the separation most because the blocks are
     full-width and touching, was getting the least of it.

     Both come up to the 32px the wide build already uses, so the phone is
     brought in line with the design rather than given a new number. That
     also matches .apart-bottom's own margin-top just above, which means
     all three joins between the blocks are now the same 32px.

     The step cards inside the list keep a smaller gap of their own — they
     are one unit, not three more blocks, and spacing them like the blocks
     would break that reading. 16px is what the wide build gives them. */
  .apart-top,
  .apart-bottom { gap: var(--sp-4); }
  .vstep-list { --vgap: var(--sp-2); }
  /* the rail is a vertical device between two discs at a known gap; stacked
     under a narrower card the gap is still known, so it is kept rather than
     hidden — only the horizontal flow-rail depended on a row layout */

  /* Stacking the two columns hands each card the panel's full width, and the
     copy inside had nothing holding its measure: 115 characters a line at
     1024, 92 at 834. The cap only bites on tablets — a phone is already well
     under it — and a measure is what this token exists for. */
  .apart-check p, .apart-quote p { max-width: 66ch; }
}

/* =========================================================================
   FOLD 3 ON A PHONE — full bleed

   640, not 1024. The stacked build starts at 1024, but a full-width dark
   panel only works while the line length works: at 1024 the list paragraph
   measured 110 characters and at 834 it was 87, both far past the ~75 a
   reader tracks comfortably. Tablets keep the inset, rounded panel and the
   column measure that comes with it; phones, where full width buys real
   space, get the bleed.
   ========================================================================= */
@media (max-width: 640px) {
  /* --- fold 5 gets the same bleed ---------------------------------------
     The life-stages panel is the page's other dark surface and was carrying
     the same three-deep boxing: gutter, panel radius and padding, then each
     stripe's own radius. Edge to edge it reads as a band the page passes
     through rather than as a wide card, and the stripes gain the ~40px the
     gutter was holding. Its overflow is already hidden — the photos scale on
     hover — so the corners had nothing else to do.

     Scoped `.stages .stages-panel`, not `.stages-panel`: fold 5's own CSS is
     further down this file than this block, so at equal weight the base
     rule's border-radius won on source order and the corners stayed
     rounded. */
  .stages > .shell { padding-inline: 0; max-width: none; }
  .stages .stages-panel {
    border-radius: 0;
    padding: clamp(1.8rem, 6vw, 2.6rem) clamp(0.7rem, 2.6vw, 1rem) clamp(0.7rem, 2.6vw, 1rem);
  }
  /* the head is centred inside the panel, so it needs its own inset once the
     panel stops providing one */
  .stages .stages-head { padding-inline: 0.6rem; }
  /* --fs-lead is a standfirst size and set the stripe copy to 24 characters
     a line at 390 (19 at 320). At the page's body size the same column runs
     27 — still tight, but a readable measure rather than a column of
     three-word lines. */
  .stages .lband-copy p { line-height: 1.6; }   /* size: type system */
  /* "Kids" / "Adults" / "Seniors" ran at --fs-d2 — 27.2px on a phone, which
     was LARGER than the section heading above them once that came down to
     22.4px. A stripe title is a sub-heading; it cannot outrank the head of
     the section it sits inside. 1.2rem sits between the body copy under it
     (16px) and that section head, which is the order the page actually
     means. */
  .stages .lband h3 { font-size: 1.2rem; }

  /* This section was three boxes deep on a phone: page gutter, then the dark
     panel's radius and padding, then a white card's radius and padding. The
     copy started ~60px into a 390px screen and the panel read as a card
     rather than as the ground.

     The gutter and the radius both go, so the dark surface runs edge to edge
     and its own padding — reduced, since it is now the only inset before the
     cards — does the spacing. The white cards inside keep their radius: they
     are meant to read as cards ON this surface, and that is the one level of
     boxing worth keeping. */
  .apart > .shell { padding-inline: 0; max-width: none; }
  .apart-panel {
    border-radius: 0;
    padding: clamp(1.6rem, 5vw, 2.4rem) clamp(0.9rem, 3.4vw, 1.4rem);
    /* the inset hairline drew a line down the screen edge once square */
    box-shadow: 0 40px 80px -50px rgba(var(--deep-rgb), 0.8);
    /* The entrance in site.js starts the two top cards at x:-28 and x:+28,
       and GSAP applies that "from" state at load. With a gutter either side
       the offset had somewhere to go; full-bleed it put the quote card's
       right edge 14px past the viewport and gave the whole page a horizontal
       scrollbar. The panel is the natural place to contain its own children's
       entrance — .stages-panel already clips for the same reason. */
    overflow: hidden;
  }

  /* The list is the substance of this panel and it was set at --fs-sm, two
     steps under the page's body size, because on the wide build it shares a
     row with a second card. Stacked full-width there is no such constraint,
     so the copy comes back up to a reading size — 63 characters at 640px,
     33 at 390 — and the headings move with it. */
  .apart-check h3 { font-size: var(--fs-lg); }
  .apart-check p  { line-height: 1.6; }         /* size: type system */
  .apart-quote p  { line-height: 1.62; }        /* size: type system */
  .vstep h3       { font-size: var(--fs-lg); }
  .vstep p        { line-height: 1.6; }         /* size: type system */
  /* .btn's own base font-size (--fs-sm, 13.44px) is a desktop value nothing
     here had ever revisited — unlike the header pill and the hero's two
     CTAs, which both needed a smaller size just to fit their labels on one
     line and got it years ago. This button has no such constraint (it is
     alone on its own row), so nothing forced the question, and it quietly
     stayed the one primary button on the page rendering a hair larger than
     every other on a phone. --fs-xs matches the four that already made the
     trip, for consistency's own sake rather than a fit problem. */
  .apart-quote .btn { font-size: var(--fs-xs); }
}

/* =========================================================================
   FOLD 4 — THE DENTISTS

   Alternating spreads, so it reads differently again from the register above
   it and the stage above that. Both portraits were shot against clashing
   backdrops — one vivid cobalt, one grey studio sweep — and were keyed onto a
   single palette neutral so the two can sit on the same page.
   ========================================================================= */
/* =========================================================================
   FOLD 3b — THE CLINICIANS

   A dark full-bleed band introducing the two dentists below it. The sage
   washes are the same two-radial device .apart-panel uses — tonal depth
   under 10% alpha rather than a colour ramp — so the band belongs to the
   same family as the page's other dark surfaces without repeating their
   rounded-panel shape.
   ========================================================================= */
.clin {
  position: relative; z-index: 1;
  background:
    radial-gradient(110% 120% at 88% -20%, rgba(var(--sage-rgb), 0.12), transparent 58%),
    radial-gradient(80% 90% at -5% 115%, rgba(var(--sage-rgb), 0.07), transparent 62%),
    var(--surface-deep);
  padding-block: clamp(2.6rem, 5vw, 4.6rem);
}
/* Heading left, paragraph right, both starting on the same line — the
   paragraph is a standfirst to the heading, not a column beside it, so it
   aligns to the top rather than centring against a much taller block. */
.clin-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: clamp(1.4rem, 4vw, 3.6rem);
  align-items: start;
}
.clin-h { color: var(--on-deep); }          /* 16.36:1 */
.clin-h em { color: var(--sage); }          /*  8.03:1 — same as .stages-h */
.clin-lead {
  /* nudged down so its first line sits with the heading's second, rather
     than level with the eyebrow — the heading is the taller element and
     leading with its cap-line is what makes the two columns read as one
     sentence broken across the band */
  margin-top: clamp(0.2rem, 1.4vw, 1.5rem);
  font-size: var(--fs-lead); line-height: 1.72;
  color: var(--on-deep-mid);                /* 7.66:1 */
}

@media (max-width: 900px) {
  /* one column, and the paragraph loses the offset that only made sense
     while it had a heading beside it to align against */
  .clin-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
  .clin-lead { margin-top: 0; }
}

.docs { position: relative; z-index: 1; background: var(--white); scroll-margin-top: 88px; }
/* Title left, standfirst right, baselines aligned. Stacked, this header ran
   262px tall on its own — a third of the screen budget for a fold that has to
   hold two people. */
.docs-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  gap: clamp(1.2rem, 3vw, 3.5rem);
  align-items: end;
  padding-bottom: clamp(1.6rem, 3vw, 2.6rem);
}
.docs-head .lead { max-width: 44ch; padding-bottom: 0.35rem; }

/* The two sit side by side rather than as full-width alternating rows. That
   alternation was the whole reason this section ran to 1919px — two rows of a
   0.58/1 grid at 4.6rem of vertical padding each — and the mirrored second row
   bought nothing that the pairing does not say more plainly. */
.docs-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.75rem);
}

/* Each dentist is now a card, and the card is laid out landscape — portrait in
   a fixed column on the left, everything else beside it. A portrait ABOVE the
   text would have been the obvious card, and would have put the two frames'
   full height back into the fold; side by side, the tallest thing in the row
   is the text, and the photo fills to match it. */
.doc {
  display: grid;
  grid-template-columns: clamp(120px, 13vw, 178px) minmax(0, 1fr);
  gap: clamp(1rem, 1.8vw, 1.6rem);
  /* stretch, not start: the portrait fills whatever height the copy beside it
     needs, so the two cards read as one band rather than two photos floating
     above their own whitespace */
  align-items: stretch;
  padding: clamp(1.1rem, 1.8vw, 1.5rem);
  background: var(--white);
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 var(--hair) var(--rule),
              0 26px 62px -46px rgba(var(--deep-rgb), 0.5);
  transition: box-shadow var(--t-2) var(--ease);
}
.doc:hover {
  box-shadow: inset 0 0 0 var(--hair) var(--rule),
              0 30px 66px -40px rgba(var(--deep-rgb), 0.55);
}
.doc-shot { margin: 0; position: relative; }
/* so the link can be pushed to the card's floor and both cards' links share a
   baseline however unevenly the two bios run */
.doc-body { display: flex; flex-direction: column; }

/* The frame is the clipping mask GSAP wipes open; the image scales down inside
   it, so the portrait settles rather than simply appearing.

   4:5 rather than the source's square. Both files are 900x900 with the sitter
   centred, so cover crops 10% off each side and nothing off the top or bottom
   — the crop cannot cut a head however the column resizes, which is why no
   focal point is declared. The declared ratio is also what stops the two rows
   drifting apart again if a future portrait arrives at another size.

   The inset hairline survives the wipe: GSAP leaves clip-path at inset(0%),
   which clips at the border box, and an inset shadow is drawn inside it. */
/* Height comes from the row, not from a ratio: the column is a fixed 120-178px
   and the card's height is set by the copy, so a declared 4/5 left the frame
   short of the card's bottom edge by ~80px. The floor keeps it portrait if a
   bio is ever trimmed to nothing. cover means no ratio can crop a head. */
.doc-frame {
  display: block; position: relative;
  height: 100%; min-height: 11rem;
  overflow: hidden;
  border-radius: var(--r-md);
  background: var(--n-10);       /* the plate the portraits were keyed onto */
  box-shadow: inset 0 0 0 var(--hair) var(--rule);
}
.doc-shot img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
/* Dr. Lotfi was photographed further back than Dr. Shaker: same 900x900 plate,
   but her head measures about a quarter smaller in frame, which is what still
   made the matched pair look unmatched once the two frames were the same size.
   Her image box is grown past the frame instead — mostly downward, so the head
   stays put and the extra scale falls off the bottom. Done with the box rather
   than a transform because GSAP owns transform on this element for the wipe. */
.doc--lotfi .doc-shot img {
  inset: -3% auto auto -11%;
  /* sized, not inset-stretched: an absolutely positioned replaced element with
     auto width takes its intrinsic size, and the reset's max-width:100% then
     pulls it back inside the frame — which letterboxed her instead of zooming
     her. Hence the explicit 122% and the max-width release. */
  width: 122%; height: 122%; max-width: none;
}

/* A real element, not a pseudo — GSAP cannot animate ::after, and this rule is
   now drawn on entry rather than on hover. Same sage device as the register
   rows and the procedure cards.

   Inside the card it sits ON the frame's lower edge rather than 0.9rem below
   it: hanging under the portrait it fell into the gap between the photo and
   the card's padding and read as a stray mark rather than an underline. */
.doc-rule {
  position: absolute; left: 0; bottom: 0;
  height: 3px; width: 62%;
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: linear-gradient(to right, var(--graphic), rgba(var(--sage-rgb), 0));
  transform-origin: left center;
}

/* the number carries the same sage tick as .eyebrow, so the two label devices
   on the page are one device */
.doc-n {
  display: inline-flex; align-items: center; gap: 0.7rem;
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-micro); color: var(--ink-quiet);
}
.doc-n::after { content: ''; width: 28px; height: 2px; background: var(--graphic); flex: none; }
/* d2 down to d3's range: at the old size two names side by side in half-width
   columns wrapped to two lines each and set the row's height by themselves. */
.doc-name {
  margin-top: 0.5rem;
  font-size: var(--fs-d1);
  font-weight: 700; letter-spacing: var(--track-display); line-height: 1.12;
}
.doc-cred {
  margin-top: 0.3rem;
  font-size: var(--fs-sm); font-weight: 600;
  color: var(--accent-text);
}
.doc-bio {
  margin-top: 0.7rem;
  /* font-size: type system, foot of this file */
  line-height: 1.68;
  color: var(--ink-mid);
}
/* Was a three-row definition list — 8rem of label column, a rule under every
   row, ~150px tall. The labels were doing no work the values did not already
   carry ("Degree: BDS, Baghdad University"), so the values became chips and the
   labels went. Same three facts, a third of the height. */
.doc-facts {
  margin-top: 0.85rem;
  display: flex; flex-wrap: wrap; gap: 0.32rem;
}
.doc-facts li {
  padding: 0.24rem 0.6rem;
  font-size: var(--fs-nano); font-weight: 600;
  color: var(--ink-mid);
  background: var(--surface-quiet);
  border-radius: var(--r-pill);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-quiet);
}

.doc-link {
  display: inline-flex; align-items: center; gap: 0.55rem;
  margin-top: var(--sp-3);
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink);
  box-shadow: 0 1px 0 var(--rule);
  transition: color var(--t-1) var(--ease), box-shadow var(--t-1) var(--ease);
}
.doc-link i { font-style: normal; transition: transform var(--t-1) var(--ease); }
.doc-link:hover { color: var(--accent-text); box-shadow: 0 1px 0 var(--accent-text); }
.doc-link:hover i { transform: translateX(4px); }
/* scoped, because .doc-link is also fold 6's "All treatments" and that one
   sits in a row of --fs-micro labels rather than under a bio */
.doc-body .doc-link {
  margin-top: auto; padding-top: 0.9rem;
  align-self: start;   /* the underline stops at the text, not the column */
  font-size: var(--fs-sm);
}

/* Two cards side by side stop fitting well before the grid does: below ~1080
   each column is too narrow for a name plus a portrait beside it. One column
   of two landscape cards is still far shorter than the old alternating rows. */
@media (max-width: 1080px) {
  .docs-grid { grid-template-columns: minmax(0, 1fr); }
  .doc { max-width: 34rem; }
}

@media (max-width: 1024px) {
  /* the header wraps to two lines on a phone, so an anchor jump needs more
     clearance or the eyebrow lands underneath it */
  .reg, .docs { scroll-margin-top: 112px; }
  .docs-head { grid-template-columns: minmax(0, 1fr); align-items: start; }
  .docs-head .lead { max-width: none; padding-bottom: 0; }
}

/* Only here does the portrait go above the text: under ~520px a 120px column
   beside the copy leaves the bio setting at about 20 characters a line. */
/* Under ~520 the portrait goes above the copy and takes the card's full
   width — at the old 220px cap it sat in a 350px card with a third of the
   row left empty beside it, which read as an avatar rather than as the
   photograph of the person the card is about.

   Full width means the card's own edges, not its padding box: the padding
   moves off .doc and onto .doc-body so the image can meet the card's sides,
   and .doc clips so the top two corners follow its radius. */
@media (max-width: 520px) {
  .doc {
    grid-template-columns: minmax(0, 1fr);
    gap: 0; padding: 0; overflow: hidden;
  }
  .doc-shot { max-width: none; }
  /* 1/1, not the 4/5 used elsewhere: the sources are 900x900, so a square
     frame at full width crops nothing at all — and 4/5 across a 350px card
     is a 437px-tall photograph before the name is even reached. */
  .doc-frame { height: auto; aspect-ratio: 1 / 1; border-radius: 0; }
  .doc-body { padding: clamp(1.1rem, 4.5vw, 1.5rem); }

  /* Type matched to the phone treatment used on fold 3: the bio is the
     card's substance and was set at --fs-sm because on the wide build it
     shares a row with a 178px portrait. Stacked full-width that constraint
     is gone, so it comes up to a reading size and the supporting lines
     move with it. */
  /* 1.2rem, not --fs-d2 (27.2px here): a dentist's name is the title of a
     card inside the Dentists fold, and at --fs-d2 it outran the fold's own
     heading once that came down to 22.4px — the same inverted hierarchy the
     life-stage stripe titles had, and fixed to the same value so the page's
     two card-title sizes agree with each other. */
  .doc-name  { font-size: 1.2rem; }
  .doc-cred  { font-size: var(--fs-md); margin-top: 0.35rem; }
  .doc-bio   { line-height: 1.6; margin-top: 0.8rem; }  /* size: type system */
  .doc-facts li { font-size: var(--fs-xs); padding: 0.3rem 0.7rem; }
  .doc-body .doc-link { font-size: var(--fs-md); }
}

/* =========================================================================
   FOLD 5 — CARE FOR EVERY STAGE

   One dark rounded panel on white — v7's container — holding three 240px
   stripes. Each photo has one side the subject does not occupy; the wash goes
   there and the copy sits on the wash — left, right, left — so type never
   lands on a face. Measured before choosing: the boy leaves the left clear,
   the woman leaves the right clear (that side is pure backdrop), and the
   couple's frame is even enough to take either.

   The stripes used to run full bleed against the viewport. Boxed, they get a
   declared width instead of the screen's, so the photo crops to the same
   aspect at every size — and the fold stops reading as three black bars laid
   across a white page.
   ========================================================================= */
.stages {
  position: relative; z-index: 1;
  background: var(--white);
  scroll-margin-top: 88px;
  --spanel-pad: clamp(1rem, 2.4vw, 2.4rem);
}
/* Same device as .apart-panel above: cap the shell at the page rail plus the
   panel's own padding, so the panel edge sits exactly one padding outside the
   rail and the type inside it still lands on the rail with the rest of the
   page. Below ~1450 the cap is inactive and the panel is gutter to gutter. */
.stages > .shell { max-width: calc(var(--page) + var(--spanel-pad) * 2); }

.stages-panel {
  position: relative;
  /* the same two sub-10% sage washes the other dark box uses, mirrored — its
     light comes from the top right, this one's from the top left, which is
     where the header sits */
  background:
    radial-gradient(120% 90% at 22% -12%, rgba(var(--sage-rgb), 0.10), transparent 58%),
    radial-gradient(90% 70% at 100% 108%, rgba(var(--sage-rgb), 0.05), transparent 60%),
    var(--surface-deep);
  border-radius: var(--r-md);
  padding: clamp(2.4rem, 4.4vw, 4rem) var(--spanel-pad) var(--spanel-pad);
  box-shadow: inset 0 0 0 1px var(--rule-invert),
              0 40px 80px -50px rgba(var(--deep-rgb), 0.8);
  /* the stripes carry their own radius, but this catches the photo panels'
     1.07 hover scale at the panel corners */
  overflow: hidden;
}

/* Centred inside the box, unlike every other head on the page. The panel is a
   self-contained object with its own edges, so the copy answers to those
   rather than to the page rail. */
.stages-head {
  max-width: 62ch; margin-inline: auto; text-align: center;
  padding-bottom: clamp(2rem, 4vw, 3.2rem);
}
.stages-head .stages-h { color: var(--on-deep); }
/* sage is 8.03:1 on --deep — on this surface it is finally legal under type */
.stages-head .stages-h em { color: var(--sage); }
.stages-head .lead { margin-top: var(--sp-3); margin-inline: auto; color: var(--on-deep-mid); }

/* Three stripes, not one block: the gap is what makes them read as three. */
.stages-band { display: grid; gap: clamp(0.5rem, 1vw, 0.85rem); }

.lband {
  position: relative;
  /* Declared here rather than on the two children that need them, because the
     copy has to know exactly how wide the photo is to keep clear of it. */
  --media-w: clamp(300px, 46%, 680px);
  --lb-pad: clamp(1.25rem, 2.6vw, 2.6rem);
  /* 240, back down from 360. The band is a strip, not a hero — at 360 it took
     a third of a screen each and three of them read as three heroes. */
  height: 240px;
  overflow: hidden;
  background: var(--deep);
  isolation: isolate;
  border-radius: var(--r-md);
  /* Sage at 16%, not --rule-invert: the grey outline read as a box drawn
     around the stripe, this one reads as the brand green catching the light.
     border-box sizing means the 1px comes out of the declared height, and the
     photo panel positions against the padding box, so it sits inside the line
     rather than under it. */
  border: var(--hair) solid rgba(var(--sage-rgb), 0.16);
}

/* THE PHOTO GETS A BOX.

   It used to be `position:absolute; inset:0` — the photograph was the band,
   cropped by whatever the viewport happened to be. That is why the subjects
   came out cut through the hairline: at 1440x360 the crop is 4:1, at 1280x240
   it is 5.3:1, and the same file loses a different slice of head at every
   width. A panel with a declared width crops to a known aspect instead, so a
   photo that reads correctly once reads correctly everywhere.

   Anything dropped in here is cropped to roughly 2.7:1 at desktop. Supply
   images with the subject clear of the top and bottom edges. */
.lband-media {
  position: absolute;
  top: 0; bottom: 0; right: 0;
  width: var(--media-w);
  display: block;
  /* The photo is scaled 1.02 (and 1.07 on hover) so it has somewhere to move.
     Without this it spills past the panel, and the spill lands outside the
     wash that ::after paints — which showed as a hard 6px stripe of raw
     photograph down the seam: white on kids, pink on adults, green on
     seniors. The band's own overflow:hidden never caught it, because the
     spill is inside the band and only outside the panel. */
  overflow: hidden;
}
.lband--right .lband-media { right: auto; left: 0; }

.lband-img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* per band, so the subject stays inside the panel */
  object-position: var(--fx, 50%) var(--fy, 50%);
  display: block;
  transform: scale(1.02);
  transition: transform var(--t-3) var(--ease);
}

/* The seam. The photo dissolves into the copy panel rather than butting
   against it, which is what keeps the band reading as one strip instead of
   two boxes side by side — and it is the same wash the section had before,
   only now it belongs to the picture rather than to the whole band. */
.lband-media::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to left,
    rgba(var(--shade-rgb), 0) 34%, rgba(var(--shade-rgb), 0.4) 62%,
    rgba(var(--shade-rgb), 0.85) 86%, var(--deep) 100%);
}
.lband--right .lband-media::after {
  background: linear-gradient(to right,
    rgba(var(--shade-rgb), 0) 34%, rgba(var(--shade-rgb), 0.4) 62%,
    rgba(var(--shade-rgb), 0.85) 86%, var(--deep) 100%);
}

/* Where the subject sits in each file, expressed against the panel rather
   than against the band. Kids is hard right, the woman is hard left, and the
   couple sits centre-left so its panel pulls slightly back from the edge. */
.lband--kids    { --fx: 100%; }
.lband--adults  { --fx: 0%; }
.lband--seniors { --fx: 42%; }

/* The copy sits on the stripe's own inset now that the stripe has edges. It
   used to carry .shell's geometry — max-width plus gutter — because the band
   ran full bleed and the type had to be pulled back onto the page rail by
   hand. Inside the panel the stripe is already on the rail, so the same
   measurement applied twice would inset the copy twice. */
.lband-body {
  position: relative; z-index: 2;
  height: 100%;
  width: 100%;
  padding-inline: var(--lb-pad);
  display: flex; align-items: center;
}
/* The photo's own width is reserved on its side of the body, so the copy can
   never reach it. Bounding the copy by max-width alone does not hold: at 900
   the 40ch cap was wider than the space left beside the photo and "Adults" ran
   onto the woman's hair. One padding of clear air between type and picture. */
.lband--left  .lband-body { padding-right: calc(var(--media-w) + var(--lb-pad)); }
.lband--right .lband-body { padding-left:  calc(var(--media-w) + var(--lb-pad)); }
/* the copy width is now the inner block's job, since the body spans the rail */
.lband-copy {
  max-width: 44ch;
  /* the whole block moves as one on hover, so the three lines stay locked to
     each other instead of drifting apart */
  transition: transform var(--t-2) var(--ease);
}
.lband--right .lband-body { justify-content: flex-end; }

/* The title carries the stripe now that the icon disc is gone — one step under
   .d2 rather than a card title, because with nothing beside it the word is the
   only thing anchoring the left edge. */
.lband h3 {
  font-size: var(--fs-d2); font-weight: 700;
  letter-spacing: var(--track-display); line-height: var(--lh-tight);
  color: var(--on-deep);
  transition: color var(--t-2) var(--ease);
}
.lband p {
  margin-top: 0.5rem;
  /* font-size: type system, foot of this file */
  line-height: 1.55; color: var(--on-deep-mid);
  max-width: 44ch;
}
.lband-link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  margin-top: 0.85rem;
  font-size: var(--fs-md); font-weight: 600; color: var(--sage);
  transition: gap var(--t-1) var(--ease);
}
.lband-link svg { width: 16px; height: 16px; flex: none; }

@media (prefers-reduced-motion: no-preference) {
  .lband:hover .lband-img { transform: scale(1.07); }
  /* the copy drifts a few pixels away from the photo — mirrored, so both
     variants read as the stripe opening rather than as two different moves */
  .lband--left:hover  .lband-copy { transform: translateX(-6px); }
  .lband--right:hover .lband-copy { transform: translateX(6px); }
}
/* sage is 8.03:1 on --deep, so the title is allowed to land on it */
.lband:hover h3 { color: var(--sage); }
.lband:hover .lband-link { gap: 0.85rem; }

@media (max-width: 1024px) {
  .stages { scroll-margin-top: 112px; }
  /* 220 was sized for the old small type. The title is --fs-d2 now and the
     description a lead, which is ~190px of copy at this width — the strip
     needs the room or the two ends touch the edges. */
  .lband { height: 260px; }
  /* The photo panel narrows with the stripe rather than eating it. The floor
     drops from 300 because the stripe is now the panel's width, not the
     screen's — at 760 the old floor left the copy under 330px. */
  .lband { --media-w: clamp(240px, 42%, 520px); }
  .lband-copy { max-width: 40ch; }
}
@media (max-width: 700px) {
  /* No room for two panels side by side, so the split turns vertical rather
     than collapsing. The alternative — photo across the whole band with the
     copy washed over it — was measured and fails: at 390px the paragraph
     reaches the weak end of the wash and lands on the kids photo's white
     ground at 3.24:1. Stacked, the copy is back on solid --deep at 7.67:1 and
     the photograph is fully visible instead of 20% visible under a heavy
     wash. The panel keeps roughly the desktop crop, so a photo that works on
     one works on the other. */
  .lband { height: auto; display: block; }
  .lband-media, .lband--right .lband-media {
    position: relative; inset: auto;
    width: 100%; height: 150px;
  }
  /* the seam runs along the bottom edge now, not the side */
  .lband-media::after,
  .lband--right .lband-media::after {
    background: linear-gradient(to top,
      var(--deep) 0%, rgba(var(--shade-rgb), 0.72) 15%, rgba(var(--shade-rgb), 0) 48%);
  }
  /* the photo is above the copy now, so neither side is reserved */
  .lband-body,
  .lband--left .lband-body,
  .lband--right .lband-body {
    height: auto; justify-content: flex-start;
    padding-inline: var(--lb-pad);
    padding-block: var(--sp-3) var(--sp-4);
  }
  .lband-copy { max-width: none; }
}

/* =========================================================================
   FOLD 6 — TREATMENTS

   Four columns divided by hairlines rather than four boxes: the fold above
   is a photographic band and the one below is a card rail, so this one earns
   its difference by being the flattest thing on the page. The sage rule on
   the top edge is the only thing that moves.
   ========================================================================= */
.tx { position: relative; z-index: 1; scroll-margin-top: 88px; }
.tx-head { max-width: 64ch; padding-bottom: clamp(2.4rem, 4.5vw, 3.6rem); }
.tx-head .lead { margin-top: var(--sp-3); }

.tx-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: var(--hair) solid var(--rule);
}
.tx-card {
  position: relative;
  display: flex; flex-direction: column;
  padding: clamp(1.5rem, 2.2vw, 2.1rem) clamp(1rem, 1.6vw, 1.6rem);
  background: var(--white);
  border-right: var(--hair) solid var(--rule-quiet);
  transition: background-color var(--t-2) var(--ease),
              border-color var(--t-2) var(--ease),
              box-shadow var(--t-2) var(--ease);
}
.tx-card:last-child { border-right: 0; }
/* sits on the shared top rule, so hovering a column claims that segment of it */
.tx-card::before {
  content: '';
  position: absolute; left: 0; top: -1px;
  width: 100%; height: 3px;
  background: var(--graphic);
  transform: scaleX(0); transform-origin: left center;
  transition: transform var(--t-2) var(--ease);
}
/* White at rest, deep green under the pointer — the column inverts rather
   than just brightening, which is a far clearer "this one" than the old
   quiet-grey-to-white step.

   Raised by shadow, not by a translate: the sage bar above sits ON the grid's
   shared top rule, so lifting the card would peel that bar off the line it is
   meant to be claiming. z-index puts the shadow over the neighbouring columns
   rather than under them, and drops the hairline that would otherwise cut
   across the dark fill. */
.tx-card:hover {
  z-index: 1;
  background: var(--deep);
  border-right-color: transparent;
  box-shadow: 0 26px 54px -34px rgba(var(--deep-rgb), 0.55);
}
.tx-card:hover::before { transform: scaleX(1); }


/* Every piece of type re-tunes for the dark fill. These are not stylistic
   swaps — on --deep the resting colours measure between 1.4:1 and 1.9:1,
   i.e. unreadable — so each one moves to the token built for that surface:
     title  --on-deep      white     16.36:1
     body   --on-deep-mid  #A9B4B0    7.66:1
     tag    --on-deep-key  sage       8.03:1
   The tag stays green, as asked; it just becomes the light green, because
   --leaf is the green that works on white and sage is the one that works on
   deep. There is no green that does both. */
.tx-card:hover .tx-n   { color: var(--on-deep-mid); }
.tx-card:hover .tx-tag { color: var(--on-deep-key); }
.tx-card:hover .tx-t   { color: var(--on-deep); }
.tx-card:hover .tx-d   { color: var(--on-deep-mid); }

/* --- how the four arrive ------------------------------------------------
   The grid used to carry one .r for all four columns, so they faded up as a
   single slab — the least interesting way four things can enter, and the
   reason this fold read as a block dropping in rather than a row building.

   Each card reveals itself now. They all trip together (reveal() takes the
   section as the unit, deliberately), and the cascade is pure
   transition-delay, continuing from where .r-4 left the grid: 0.64s, then a
   card every 110ms. Small enough that the row still reads as one gesture,
   long enough that the eye follows it left to right.

   .r's own base delay would otherwise win, so these have to be at least as
   specific — hence .reveal-on on the front of each. */
@media (prefers-reduced-motion: no-preference) {
  /* a shorter rise than the page default: these are wide, short cards in a
     row, and 28px on all four at once reads as the whole grid sliding.

     :not(.in) is load-bearing, not tidiness. base.css resets the arrived
     state with `.reveal-on .r.in { transform: none }` — three classes, same
     as this rule — and folds.css is the later stylesheet, so an unqualified
     selector here wins the tie and pins every card 20px low forever. */
  .reveal-on .tx-card.r:not(.in) { transform: translate3d(0, 20px, 0); }
  .reveal-on .tx-card.r.in:nth-child(1) { transition-delay: 0.64s; }
  .reveal-on .tx-card.r.in:nth-child(2) { transition-delay: 0.75s; }
  .reveal-on .tx-card.r.in:nth-child(3) { transition-delay: 0.86s; }
  .reveal-on .tx-card.r.in:nth-child(4) { transition-delay: 0.97s; }

  /* The nine tiles run on the same idea at a quicker pace — 45ms apart, so
     the grid ripples rather than counting itself out. Starts after the label
     (.r-5, 0.78s) has landed. */
  .reveal-on .tx-chips li.r:not(.in) { transform: translate3d(0, 12px, 0); }
  .reveal-on .tx-chips li.r.in:nth-child(1) { transition-delay: 0.86s; }
  .reveal-on .tx-chips li.r.in:nth-child(2) { transition-delay: 0.905s; }
  .reveal-on .tx-chips li.r.in:nth-child(3) { transition-delay: 0.95s; }
  .reveal-on .tx-chips li.r.in:nth-child(4) { transition-delay: 0.995s; }
  .reveal-on .tx-chips li.r.in:nth-child(5) { transition-delay: 1.04s; }
  .reveal-on .tx-chips li.r.in:nth-child(6) { transition-delay: 1.085s; }
  .reveal-on .tx-chips li.r.in:nth-child(7) { transition-delay: 1.13s; }
  .reveal-on .tx-chips li.r.in:nth-child(8) { transition-delay: 1.175s; }
  .reveal-on .tx-chips li.r.in:nth-child(9) { transition-delay: 1.22s; }
  /* the button lands last, on its own beat, as the full stop */
  .reveal-on .tx-more-cta.r.in { transition-delay: 1.34s; }
}

/* All five carry a colour transition on the same curve as the card's own
   fill, so the column inverts as one move rather than the background
   changing and the type catching up after it. */
.tx-n {
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-micro); color: var(--ink-quiet);
  transition: color var(--t-2) var(--ease);
}
.tx-tag {
  margin-top: 1.15rem;
  font-size: var(--fs-micro); font-weight: 600;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--accent-text);
  transition: color var(--t-2) var(--ease);
}
.tx-t {
  margin-top: 0.45rem;
  font-size: var(--fs-card-t);
  font-weight: 700; letter-spacing: var(--track-display); line-height: 1.14;
  transition: color var(--t-2) var(--ease);
}
.tx-d {
  margin-top: 0.7rem;
  /* font-size: type system, foot of this file */
  line-height: 1.72; color: var(--ink-mid);
  transition: color var(--t-2) var(--ease);
}
.tx-link {
  margin-top: auto; padding-top: var(--sp-4);
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: var(--fs-xs); font-weight: 600; color: var(--ink);
  transition: color var(--t-2) var(--ease);
}
.tx-link svg { width: 13px; height: 13px; flex: none; transition: transform var(--t-1) var(--ease); }
/* sage, not --leaf: this is now sitting on --deep (8.03:1 against 1.9:1) */
.tx-card:hover .tx-link { color: var(--on-deep-key); }
.tx-card:hover .tx-link svg { transform: translateX(4px); }

/* The remaining nine treatments — fold 3 claims twelve under one roof and
   this is where that becomes checkable. Each one used to be a bare name in a
   pill; now it is a small two-line tile (name, then what it actually does),
   the same idea as the four featured cards above at a smaller scale, so
   "also offered" reads as nine real answers rather than a tag cloud. */
/* Centred, and read top to bottom: label, the nine tiles, then one button
   that closes the fold. The label and link used to share a justified line
   above a left-ragged block of pills, which gave the eye three different
   left edges to track and ended on a ragged right edge with nothing to
   resolve it. Centring the block makes it symmetrical, and moving the link
   to the end turns a stray text link into the fold's full stop. */
.tx-more {
  margin-top: clamp(2rem, 3.4vw, 2.8rem);
  padding-top: clamp(1.3rem, 2.2vw, 1.9rem);
  /* the grid above opens with a rule and never closed; this is that rule */
  border-top: var(--hair) solid var(--rule);
  display: grid;
  gap: clamp(1rem, 1.8vw, 1.4rem);
  justify-items: center;
  text-align: center;
}
.tx-more-lab {
  font-size: var(--fs-micro); letter-spacing: var(--track-micro);
  text-transform: uppercase; color: var(--ink-quiet);
}
/* Three columns of three, not a wrapped row of pills — a name-plus-line tile
   needs a column to sit in, not just a width to fit. 58rem keeps each column
   a comfortable ~300px for a six-to-seven-word second line at this size. */
.tx-chips {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.7rem;
  width: 100%;
  max-width: 58rem;
  /* .tx-more centres text for the label and the button either side of this;
     two lines of real copy read as a ransom note centred, so this is the one
     child in the block that goes back to reading left to right. */
  text-align: left;
}
.tx-chips li {
  padding: 0.85rem 1rem;
  display: flex; flex-direction: column; gap: 0.28rem;
  background: var(--white);
  border-radius: var(--r-sm);
  box-shadow: inset 0 0 0 var(--hair) var(--rule);
  transition: box-shadow var(--t-1) var(--ease), transform var(--t-1) var(--ease);
}
.tx-chip-name { font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }
.tx-chip-d { font-size: var(--fs-xs); line-height: 1.5; color: var(--ink-mid); }
/* These are names with a line under them, not links — the hover is only an
   acknowledgement that the pointer is on one, which is why it lifts and does
   not colour up like something clickable. */
.tx-chips li:hover {
  box-shadow: inset 0 0 0 var(--hair) var(--sage-deep);
}
@media (prefers-reduced-motion: no-preference) {
  .tx-chips li:hover { transform: translateY(-2px); }
}
.tx-more-cta { margin-top: clamp(0.3rem, 0.8vw, 0.7rem); }

@media (max-width: 1024px) {
  .tx { scroll-margin-top: 112px; }
  .tx-head { max-width: none; }
  .tx-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tx-card { border-bottom: var(--hair) solid var(--rule-quiet); }
  .tx-card:nth-child(2n), .tx-card:last-child { border-right: 0; }
  /* three even columns of tiles at 1024 is tight for a two-line tile; two
     wide (five rows, the last one holding one tile alone) reads more
     comfortably than three cramped columns do */
  .tx-chips { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 40rem; }
}
@media (max-width: 640px) {
  .tx-grid { grid-template-columns: minmax(0, 1fr); }
  .tx-card, .tx-card:nth-child(2n) { border-right: 0; }
  .tx-chips { grid-template-columns: minmax(0, 1fr); max-width: 26rem; }
}

/* =========================================================================
   FOLD 7 — BEGIN YOUR SMILE JOURNEY  (v7's composition, v9's tokens)

   The photograph is the whole band and the copy sits ON it, bottom-left,
   rather than beside it in a washed column. That means the scrim has to be
   bottom-weighted (v7's shape) instead of left-weighted (v9's earlier one):
   the type needs contrast exactly where it sits, and the top of the frame is
   left clear so the photograph reads as a photograph.
   ========================================================================= */
.cta {
  position: relative; z-index: 1;
  background: var(--white);
  scroll-margin-top: 88px;
}
/* Matches .docpg-cta in doctor.css — the homepage's own version of this
   section sits directly under the stage/procedure scene above it with no
   fold padding of its own, so it needs the same top space back. */
.home-cta { padding-top: 80px; }
/* The band itself is the "dark version" panel — same device as .apart-panel
   and .stages-panel: gutter on the left/right from .shell, var(--r-md) on
   the panel so the photo's corners round off instead of running to the
   viewport edge. */
.cta-panel {
  position: relative;
  isolation: isolate; overflow: hidden;
  background: var(--deep);
  border-radius: var(--r-md);
  min-height: clamp(420px, 46vw, 560px);   /* v7's 560px ceiling */
  display: grid; align-items: end;
}
/* The frame clips; the image inside it is deliberately taller than the frame.
   That over-height is v7's crop (it used inset: -18% 0 on the picture layer)
   and it is what pulls the two figures in close — at a plain inset:0 the same
   source reads as a wide room shot with the window and the overhead lamp
   dominating, and the subjects too small to carry the band. Kept as an
   over-height image rather than an over-sized .cta-media so the scrim, which
   lives in here too, still measures against the real section box. */
.cta-media { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.cta-media img {
  position: absolute; left: 0; top: -18%;
  width: 100%; height: 136%;
  object-fit: cover; object-position: 50% 42%;
}
/* Bottom-weighted, per v7 — transparent through the top half so the room is
   visible, ramping to near-solid under the bottom bar where the type lands.
   The faint top stop keeps the join with the fold above from reading as a
   hard seam. */
.cta-wash {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg,
      rgba(var(--shade-rgb), 0)    0%,
      rgba(var(--shade-rgb), 0.08) 45%,
      rgba(var(--shade-rgb), 0.5)  78%,
      rgba(var(--shade-rgb), 0.88) 100%),
    linear-gradient(180deg, rgba(var(--shade-rgb), 0.32) 0%, rgba(var(--shade-rgb), 0) 18%);
}

/* --- floating annotation callout ----------------------------------------- */
/* Sits over the photograph's clear upper half, left of the subject. Percent
   placement (not a fixed offset) so it tracks the crop as the band reflows. */
.cta-callout {
  position: absolute; top: 34%; left: 46%;
  z-index: 1;
  animation: cta-callout-float 5s ease-in-out infinite;
}
@keyframes cta-callout-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-10px); }
}
.cta-callout-card {
  display: inline-flex; align-items: center; gap: 0.6rem;
  padding: 0.75rem 1rem;
  border-radius: var(--r-md);
  /* Opaque, not v7's glassmorphic panel: backdrop-filter is on the banned
     list here, and over a busy photograph a translucent card loses the
     contrast this text needs anyway. */
  background: var(--white);
  color: var(--ink);
  font-size: var(--fs-xs); font-weight: 700; line-height: 1.3;
  letter-spacing: var(--track-tight);
  white-space: nowrap;
  box-shadow: 0 20px 40px rgba(var(--shade-rgb), 0.34);
}
.cta-callout-card svg { width: 20px; height: 20px; flex: none; color: var(--leaf); }

/* --- bottom bar: headline + proof on the left, CTA on the right ---------- */
.cta-inner {
  padding: clamp(2rem, 4vw, 2.5rem) clamp(1.5rem, 3.2vw, 3.2rem);
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: var(--sp-4) var(--sp-5); flex-wrap: wrap;
}
.cta-copy { max-width: 44ch; }
/* No margin-top: the eyebrow that used to sit above this is gone, so the
   headline is the first thing in the column now. */
.cta-h {
  color: var(--on-deep);
  text-shadow: 0 4px 20px rgba(var(--shade-rgb), 0.45);
}
.cta-lead {
  margin-top: var(--sp-3);
  color: var(--on-deep);
  text-shadow: 0 2px 10px rgba(var(--shade-rgb), 0.5);
}

.cta-proof { margin-top: var(--sp-3); display: flex; align-items: center; gap: 0.9rem; }
.cta-faces { display: flex; flex: none; }
.cta-faces img {
  width: 38px; height: 38px; border-radius: 50%;   /* v7's 38px */
  object-fit: cover; object-position: 50% 20%;
  background: var(--n-10);
  box-shadow: 0 0 0 2px var(--white), 0 4px 10px rgba(var(--shade-rgb), 0.3);
}
.cta-faces img + img { margin-left: -12px; }
.cta-stars { display: flex; gap: 2px; color: var(--star); }
.cta-stars svg { width: 13px; height: 13px; fill: currentColor; }
.cta-proof-t {
  margin-top: 0.28rem;
  font-size: var(--fs-sm); font-weight: 500; color: var(--on-deep-mid);
  text-shadow: 0 2px 8px rgba(var(--shade-rgb), 0.5);
}
.cta-btn { flex: none; }

@media (max-width: 1024px) {
  /* The callout is placed against the wide crop; once the band narrows it
     collides with both the headline and the subject, so it goes. Its claim
     already appears verbatim in fold 4. */
  .cta-callout { display: none; }
}

@media (max-width: 720px) {
  .cta-panel { min-height: clamp(360px, 64vw, 440px); }
  .cta-media img { object-position: 74% 34%; }
  /* Stacked, the bar spans the band, so the scrim carries further up. */
  .cta-wash {
    background: linear-gradient(180deg,
      rgba(var(--shade-rgb), 0.12) 0%, rgba(var(--shade-rgb), 0.55) 52%, rgba(var(--shade-rgb), 0.92) 100%);
  }
  .cta-inner { align-items: flex-start; }
  .cta-copy { max-width: none; }
  /* Same correction as .apart-quote .btn just above — .btn's un-overridden
     13.44px next to the header/hero pair's 12.16px. */
  .cta-btn { font-size: var(--fs-xs); }
}

/* =========================================================================
   FOLD 8 — WHAT PATIENTS SAY

   A scroll-snap rail, not a scripted carousel: five real reviews stay
   readable and reachable with the keyboard whether or not site.js runs. The
   arrows are an enhancement on top, and appear only when it does.
   ========================================================================= */
.rev { position: relative; z-index: 1; background: var(--white); scroll-margin-top: 88px; }
.rev.fold { padding-block: max(1.75rem, calc(var(--fold) - 50px)); }
.rev-head {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(2rem, 4vw, 4rem); align-items: center;
  padding-bottom: clamp(2.2rem, 4vw, 3.2rem);
}
.rev-head .lead { margin-top: var(--sp-3); }

.rev-cluster { position: relative; width: 268px; height: 208px; flex: none; }
.rev-circle {
  position: absolute; display: block;
  border-radius: 50%; overflow: hidden;
  background: var(--n-10);
  box-shadow: 0 20px 44px -24px rgba(var(--deep-rgb), 0.55);
}
/* Both of these are decorative vignettes beside a header about people, and
   both source files are already on the page at full size — the consultation
   shot fills fold 7's band, the operatory fills fold 9. At native framing the
   circles are simply those two pictures a third time. Zoomed hard, the large
   one holds faces and the small one holds the chair, which is what a vignette
   should have been showing anyway. */
.rev-circle img {
  position: absolute;
  width: 100%; height: 100%; max-width: none; object-fit: cover;
}
.rev-circle--lg img { width: 215%; height: 215%; top: -42%; left: -66%; }
.rev-circle--sm img { width: 265%; height: 265%; top: -58%; left: -78%; }
.rev-circle--lg { width: 176px; height: 176px; right: 0; top: 0; }
.rev-circle--sm {
  width: 108px; height: 108px; left: 0; bottom: 0;
  box-shadow: 0 0 0 7px var(--white), 0 20px 44px -24px rgba(var(--deep-rgb), 0.55);
}

.rev-rail {
  display: flex; gap: var(--sp-3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* A hovered card's shadow is painted outside the card's own box, and
     setting overflow-x makes overflow-y compute to auto rather than stay
     visible — so the rail was slicing that shadow off at its padding edge
     and the card read as cut away from the page.

     The room is made with padding and then taken back off the layout with a
     matching negative margin, so the rail occupies exactly the space it did
     before (4px above, --sp-2 below) while giving the shadow somewhere to
     land. The inline bleed is 16px, comfortably inside --gutter's 20px floor,
     so widening the rail can never push the page into a sideways scroll. */
  padding: 30px 16px 40px;
  margin: -26px -16px -24px;
  /* the arrows are the affordance; a native bar under five cards is noise */
  scrollbar-width: none;
}
.rev-rail::-webkit-scrollbar { height: 0; }
/* The SLOT. Holds the place in the rail and nothing else — every visual
   belongs to .rev-card-panel inside it, so the panel can outgrow this box
   on hover without the box (and therefore the rail, and therefore the rest
   of the page) changing size. */
.rev-card {
  flex: 0 0 clamp(262px, 31%, 392px);
  scroll-snap-align: start;
  position: relative;
  display: flex;
}
/* raised so an expanded panel covers its neighbours rather than sliding
   under the next card along */
.rev-card:hover { z-index: 2; }

/* White, not --surface-quiet. A Google review card is a white card with a
   hairline; the old off-white fill on a white section read as a generic
   testimonial box, and it also left the hover invert with two steps to
   travel instead of one clean white-to-green. */
.rev-card-panel {
  flex: 1;
  display: flex; flex-direction: column;
  padding: clamp(1.3rem, 2vw, 1.7rem);
  background: var(--white);
  border-radius: var(--r-md);
  box-shadow: inset 0 0 0 var(--hair) var(--rule);
  transition: background-color var(--t-2) var(--ease),
              box-shadow var(--t-2) var(--ease),
              max-height var(--t-3) var(--ease),
              transform var(--t-2) var(--ease);
}
/* The card inverts under the pointer — the same white-to-deep-green move
   .tx-card makes in fold 6, so the two read as one idea rather than two
   different hover treatments on one page. */
.rev-card:hover .rev-card-panel {
  background: var(--deep);
  box-shadow: 0 26px 54px -32px rgba(var(--deep-rgb), 0.6);
}
@media (prefers-reduced-motion: no-preference) {
  .rev-card:hover .rev-card-panel { transform: translateY(-3px); }
}

/* the "Read more" cue, shown only on cards site.js has measured as
   genuinely cut off — see .is-clamped below */
.rev-more {
  display: none;
  margin-top: 0.5rem;
  /* site.js swaps this span for a real <button> so it can be tapped on a
     phone (see reviewClamp()). These four strip the UA button chrome —
     border, grey fill, padding, native appearance — so it still renders as
     the plain label the desktop hover treatment was written against.
     font-family only, not the `font` shorthand, which would wipe the
     size/weight/tracking declared just below it. */
  border: 0; padding: 0; background: none;
  font-family: inherit; text-align: left; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  font-size: var(--fs-micro); font-weight: 600;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  color: var(--accent-text);
  transition: color var(--t-2) var(--ease), opacity var(--t-2) var(--ease);
}
.rev-card:hover .rev-more { color: var(--on-deep-key); opacity: 0; }

/* Every piece of type re-tunes for the dark fill, and none of these are
   stylistic swaps — at rest these colours measure 1.4:1 to 1.9:1 on
   --deep, i.e. unreadable. Each moves to the token built for that surface:
     name   --on-deep  white  16.36:1
     quote  --on-deep  white  16.36:1
   The quote is white rather than the quieter --on-deep-mid (#A9B4B0,
   7.66:1) it started on: the mid tone is the right weight for a supporting
   line, but the quote is the thing being read here, sometimes six lines of
   it, and it should not be the dimmest text on its own card.
   The avatar goes the other way — a solid sage disc carrying near-black
   type, which is the one pairing sage is legal in (8.03:1). */
.rev-card:hover .rev-name  { color: var(--on-deep); }
.rev-card:hover .rev-txt   { color: var(--on-deep); }
.rev-card:hover .rev-avatar { background: var(--sage); color: var(--deep); }

/* --- the card head: who left it ---------------------------------------- */
.rev-top { display: flex; align-items: center; gap: 0.7rem; }
.rev-avatar {
  width: 38px; height: 38px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--tint-open); color: var(--leaf);   /* 5.71:1 */
  font-size: var(--fs-sm); font-weight: 700; line-height: 1;
  transition: background-color var(--t-2) var(--ease), color var(--t-2) var(--ease);
}
/* min-width:0 so a long name can actually shrink and ellipsis rather than
   shoving the Google mark off the end of the row. It sits on the name
   itself now that the name is a direct child of the row — the column that
   used to wrap it went with the "Google review" line under it. */
.rev-name {
  min-width: 0;
  font-size: var(--fs-md); font-weight: 600; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: color var(--t-2) var(--ease);
}
.rev-g { width: 20px; height: 20px; flex: none; margin-left: auto; }

/* --- the rating -------------------------------------------------------- */
/* colour sits on each star, not on the row, because the row cannot stagger
   — the per-star delay below is what makes them turn one after another
   rather than all at once */
.rev-stars { display: flex; gap: 3px; margin-top: var(--sp-3); }
.rev-stars svg {
  width: 15px; height: 15px;
  color: var(--star); fill: currentColor;
  transition: color var(--t-2) var(--ease), transform var(--t-2) var(--ease);
}
/* gold on white, sage on deep green (8.03:1) — gold would hold up on the
   dark fill too, but the sage is the brand's own and ties the row to the
   avatar disc that just turned sage beside it */
.rev-card:hover .rev-stars svg { color: var(--on-deep-key); }
@media (prefers-reduced-motion: no-preference) {
  .rev-card:hover .rev-stars svg { transform: scale(1.1); }
  /* 55ms apart: enough that the eye follows the row left to right, short
     enough that all five have turned before the card finishes inverting */
  .rev-stars svg:nth-child(1) { transition-delay: 0s; }
  .rev-stars svg:nth-child(2) { transition-delay: 0.055s; }
  .rev-stars svg:nth-child(3) { transition-delay: 0.11s; }
  .rev-stars svg:nth-child(4) { transition-delay: 0.165s; }
  .rev-stars svg:nth-child(5) { transition-delay: 0.22s; }
}

/* --fs-lead, matching .lead and .svc1-panel-d/.svc-body-fold .body — this
   quote was the smallest body text on the page at --fs-sm, out of step with
   the paragraph copy everywhere else a service page shows a chunk of text. */
.rev-txt {
  margin-top: 0.7rem;
  /* font-size comes from the type system at the foot of this file */
  line-height: 1.78; color: var(--ink-mid);
  transition: color var(--t-2) var(--ease);
}

/* =========================================================================
   EQUAL-HEIGHT CARDS, AND THE HOVER EXPAND

   The rail stretches every card to match the tallest, and one long review
   was setting that height for all five — a three-line review rendered in a
   card with 200px of empty white under it. So the quote is clamped to a
   fixed number of lines and every card settles at one constant height,
   with the rest of the review reachable by hovering.

   Scoped to a real pointer on purpose, not just as a fallback. Hover
   cannot reveal anything on a touchscreen, so a "Read more" that only
   answers to hover would be a dead promise there — and it is not needed
   anyway: the narrow rail shows roughly one card at a time, where a card
   as tall as its own review reads fine. Touch keeps the natural-height
   behaviour this fold has always had.
   ========================================================================= */
@media (hover: hover) and (pointer: fine) {
  /* Lift the entire reviews section above the next fold while any card is
     expanded, so the grown panel is never hidden behind the section below. */
  .rev:has(.rev-card:hover) { z-index: 2; }

  .rev-card {
    /* the constant. ~6 lines of quote plus the head row, stars and padding */
    --rev-h: 310px;
    height: var(--rev-h);
    display: block;
  }

  /* Absolute, so its height is its own business and never the slot's.
     This is the whole mechanism: the panel can grow past var(--rev-h) on
     hover and the slot — hence the rail, hence the page below it — does
     not move a pixel.

     top/left/right but NOT bottom, with min-height taking the slot's
     height instead: pinning all four edges would fix the panel AT the
     slot's height, which is exactly the growth being asked for here. */
  .rev-card-panel {
    position: absolute; top: 0; left: 0; right: 0;
    min-height: 100%;
    max-height: var(--rev-h);
    overflow: hidden;
  }

  .rev-txt {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 6;
    overflow: hidden;
  }

  /* .is-clamped is added by site.js only where the text really is cut off
     (reviewClamp()), so a short review shows no fade, no "Read more", and
     does not move on hover — it has nothing more to give. */
  .rev-card.is-clamped .rev-txt {
    -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent);
    mask-image: linear-gradient(to bottom, #000 62%, transparent);
  }
  .rev-card.is-clamped .rev-more { display: block; }

  /* The expand. The clamp is released outright while the panel's own
     max-height animates, so the extra lines are uncovered by the panel
     growing rather than appearing all at once underneath it.

     A number and not `none`: the panel has to stay inside the rail's
     bottom padding or the rail's overflow clips it (see the note on
     .rev-rail below — overflow-x:auto forces overflow-y to auto). A review
     too long even for this keeps its fade, and "Read all reviews on
     Google" underneath is the honest way to the rest.

     580, up from 500. The longest of the five needed exactly 500, which
     sounds like it fit and did not: a scroll container's scrollHeight
     leaves out its own bottom padding, so "needed 500, capped at 500" was
     really the content plus a padding edge that had nowhere to go, and the
     last line lost its descenders to it. The ceiling now clears the
     tallest card with room to spare rather than landing exactly on it. */
  .rev-card.is-clamped:hover .rev-card-panel { max-height: 580px; }
  .rev-card.is-clamped:hover .rev-txt {
    -webkit-line-clamp: 40;
    -webkit-mask-image: none;
    mask-image: none;
  }

  /* Room for that growth to land in without being clipped, taken straight
     back off the layout by the matching negative margin — the same trick,
     and the same reason, as the shadow room this rule already made.

     z-index keeps the rail, and with it any expanded card, ABOVE the
     arrows and the Google link below: a card that grows over them should
     cover them, not be painted through by them. Nobody hovers a card and
     reaches for those controls in the same instant, and moving toward them
     leaves the card, which collapses it. */
  .rev-rail {
    position: relative; z-index: 1;
    padding-bottom: 310px;
    margin-bottom: -294px;

    /* The catch that comes with the two lines above, and it is not
       cosmetic. That padding leaves the rail's own box lying across the
       row beneath it, an element's padding still answers the pointer, and
       the rail is now on top — so the arrows and the Google link stop
       being clickable at all, at rest, hovered or not. Opting the box out
       of hit-testing and the cards back in keeps the stacking order
       without eating the clicks.

       Scrolling survives it: a wheel or a drag over a card targets the
       card, which is still hit-testable, and scrolling chains from there
       up to this element regardless of its own pointer-events. Only the
       strip of empty padding above and below the cards stops being
       draggable, and the keyboard path (the rail is tabindex="0") never
       used hit-testing to begin with. */
    pointer-events: none;
  }
  .rev-card { pointer-events: auto; }
}

.rev-nav { margin-top: var(--sp-3); display: flex; align-items: center; gap: 0.6rem; }
.rev-btn {
  /* flex:none is load-bearing, not tidiness. These sit in .rev-nav's flex
     row next to .rev-src, and a flex item's default min-width is auto —
     with a wide link beside them the two buttons were being compressed to
     39x46, i.e. ovals with the arrow squeezed inside. They are circles at a
     fixed size; they must never be a flex-shrink candidate. */
  width: 46px; height: 46px; flex: none; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--white); color: var(--ink);
  box-shadow: inset 0 0 0 var(--hair) var(--rule);
  transition: background-color var(--t-1) var(--ease), color var(--t-1) var(--ease),
              box-shadow var(--t-1) var(--ease), opacity var(--t-1) var(--ease);
}
.rev-btn svg { width: 16px; height: 16px; }
.rev-btn:hover { background: var(--deep); color: var(--ink-invert); box-shadow: none; }
.rev-btn[disabled] { opacity: 0.32; pointer-events: none; }
.rev-src {
  margin-left: auto;
  font-size: var(--fs-micro); font-weight: 700; letter-spacing: var(--track-micro);
  text-transform: uppercase; color: var(--ink-quiet);
  box-shadow: 0 1px 0 var(--rule-quiet);
  transition: color var(--t-1) var(--ease), box-shadow var(--t-1) var(--ease);
}
.rev-src:hover { color: var(--ink); box-shadow: 0 1px 0 var(--rule); }

@media (max-width: 1024px) {
  .rev { scroll-margin-top: 112px; }
  /* Same fix as .visit-head: h2 has margin:0 from the reset and --lh-display's
     1.04 leaves almost no natural leading, so eyebrow-to-heading was flush
     while heading-to-lead had a full var(--sp-3) — matching that here makes
     both gaps equal in mobile view. */
  .rev-head .eyebrow + h2 { margin-top: var(--sp-3); }
  .rev-head { grid-template-columns: minmax(0, 1fr); }
  .rev-cluster { display: none; }   /* decorative, and the header is taller here */
  /* 80, not 84: at 84 the second card started exactly at the shell's right
     edge, so the rail read as a single card with nothing after it. */
  .rev-card { flex-basis: min(80vw, 400px); }
}

/* =========================================================================
   FOLD 9 — VISIT US

   Photograph and card, equal height. The hours table is the live element:
   site.js marks today and states whether the practice is open, read in
   America/Toronto rather than on the reader's clock.
   ========================================================================= */
.visit { position: relative; z-index: 1; scroll-margin-top: 88px; }
.visit.fold { padding-block: max(1.75rem, calc(var(--fold) - 50px)); }
/* The headline breaks explicitly into two lines, and "Visit our welcoming"
   is wider than a 64ch rail at the display size — so the cap moves off the
   header and onto the paragraph, which is the only part that needs a
   reading measure. */
.visit-head { padding-bottom: clamp(2.2rem, 4vw, 3.2rem); }
.visit-head .lead { max-width: 64ch; margin-top: var(--sp-3); }

.visit-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.86fr);
  gap: clamp(1.5rem, 2.8vw, 2.8rem);
  align-items: stretch;
}
.visit-shot {
  margin: 0; border-radius: var(--r-md); overflow: hidden;
  background: var(--n-10); min-height: 340px;
}
.visit-shot img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 58%; }

.visit-card {
  display: flex; flex-direction: column;
  padding: clamp(1.5rem, 2.4vw, 2.2rem);
  background: var(--white); border-radius: var(--r-md);
  box-shadow: inset 0 0 0 var(--hair) var(--rule),
              0 26px 62px -42px rgba(var(--deep-rgb), 0.5);
}
/* Both blocks carry their own bottom rule — same device v7 used (each info
   group draws the line below it), which puts one divider after Location and
   a second after the hours table, ahead of the action row. */
.visit-block {
  padding-bottom: var(--sp-4); margin-bottom: var(--sp-4);
  border-bottom: var(--hair) solid var(--rule-quiet);
}
.visit-block h3 {
  font-size: var(--fs-micro); font-weight: 600;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  color: var(--ink-quiet);
}
.visit-tag { margin-top: 0.4rem; font-size: var(--fs-xs); font-weight: 700; color: var(--accent-text); }
.visit-addr {
  margin-top: 0.6rem; display: flex; align-items: flex-start; gap: 0.55rem;
  /* font-size: type system, foot of this file */
  font-weight: 500; line-height: 1.5;
}
.visit-pin { width: 15px; height: 15px; flex: none; margin-top: 0.25rem; color: var(--graphic); }
/* the address itself is now a link to the practice's Google Maps place
   (every template's visit fold) — underlined on hover only, so the card
   still reads as an address block rather than a row of link text */
.visit-addr a { color: inherit; text-decoration: none; }
.visit-addr a:hover { color: var(--accent-text); text-decoration: underline; text-underline-offset: 3px; }

/* --- the hours table: a bordered box of its own, nested in the card ------ */
.visit-hours-table {
  margin-top: 0.85rem;
  border: var(--hair) solid var(--rule);
  border-radius: var(--r-sm);
  overflow: hidden;
}
.visit-hours-head {
  display: flex; justify-content: space-between;
  padding: 0.5rem 0.85rem;
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--ink-quiet); background: var(--n-05);
}
.visit-hours li {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.55rem 0.85rem;
  border-bottom: var(--hair) solid var(--rule-quiet);
  font-size: var(--fs-sm); color: var(--ink-mid);
  font-variant-numeric: tabular-nums;
}
.visit-hours li:last-child { border-bottom: none; }
.visit-hours li:nth-child(even) { background: var(--n-05); }
.visit-hours li span:first-child { font-weight: 500; color: var(--ink); }
.visit-hours li.shut span:last-child { color: var(--ink-quiet); font-weight: 500; }
/* today wins over the zebra stripe: same specificity, later in source */
.visit-hours li.today {
  background: var(--tint-open);
}
.visit-hours li.today span:first-child { color: var(--leaf); font-weight: 700; }
.visit-hours li.today span:first-child::after {
  content: 'Today'; margin-left: 0.5rem;
  font-size: var(--fs-nano); font-weight: 700;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--white); background: var(--leaf);
  border-radius: var(--r-pill); padding: 0.14rem 0.5rem;
  vertical-align: middle;
}

.visit-act { padding-top: var(--sp-1); display: flex; gap: var(--sp-3); flex-wrap: wrap; }
/* These two stretch to share the row, unlike every other button on the page,
   which sizes to its own label. .btn centres its contents — right for a
   content-width pill, wrong once the pill is wider than what is in it: the
   label and the disc ended up floating together in the middle with dead
   space either side. .btn's padding is deliberately asymmetric (1.35rem
   leading, 0.8rem trailing) so the disc sits ON the trailing edge, and
   space-between is what restores that reading at any width. */
.visit-act .btn { flex: 1 1 auto; justify-content: space-between; }

/* --- dashed callout: new-patient note + phone, v7's banner --------------- */
.visit-banner {
  margin-top: var(--sp-3);
  display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
  gap: 0.6rem 1rem;
  padding: 0.85rem 1.1rem;
  background: var(--tint-open);
  border: 1px dashed rgba(var(--sage-deep-rgb), 0.4);
  border-radius: var(--r-md);
}
.visit-note { font-size: var(--fs-xs); font-weight: 600; color: var(--ink); }
.visit-tel {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--fs-xs); font-weight: 700; color: var(--accent-text);
  transition: color var(--t-1) var(--ease);
}
.visit-tel svg { width: 14px; height: 14px; flex: none; }
.visit-tel:hover { color: var(--leaf); box-shadow: 0 1px 0 var(--leaf); }

@media (max-width: 1024px) {
  .visit { scroll-margin-top: 112px; }
  /* h2 inherits margin:0 from the reset, and --lh-display's 1.04 leaves
     almost no natural leading above its glyphs, so eyebrow-to-heading was
     flush while heading-to-lead had a full var(--sp-3) — matching that
     here makes both gaps equal in mobile view. */
  .visit-head .eyebrow + h2 { margin-top: var(--sp-3); }
  .visit-head .lead { max-width: none; }
  .visit-grid { grid-template-columns: minmax(0, 1fr); }
  .visit-shot { min-height: 260px; max-height: 380px; }
}

/* =========================================================================
   FOOTER

   The ratios invert here: sage finally carries text at 8.03:1 on deep, and
   it labels the columns. Everything else is the two opaque invert tiers —
   no translucent whites, whose contrast would depend on what sat behind.
   ========================================================================= */
/* v7's footer is a panel, not a band: it sits inset from both edges on a white
   ground with the page's corner radius, the same object .apart-panel,
   .stages-panel and .cta-panel already are here. So the dark fill moves off
   the <footer> and onto .ftr-panel, and the footer element becomes the white
   ground the panel sits on — including the strip below it, which is what
   stops the radius reading as a notch cut out of a full-bleed band. */
.ftr {
  position: relative; z-index: 1;
  background: var(--white);
  /* Top space is the footer's own now, not borrowed from whatever section
     happens to close a given template — every page's last fold already
     carries its own bottom padding, but that padding is that section's,
     not the footer's, so the two boundaries could still read as touching. */
  padding-top: clamp(0.75rem, 2.5vw, 2.25rem);
  padding-bottom: clamp(1.25rem, 2.4vw, 2rem);
  --fpanel-pad: clamp(1.5rem, 3.2vw, 3.2rem);
}
/* Same device as the other two dark panels: cap the shell at the page rail
   plus the panel's own padding, so the panel edge sits exactly one padding
   outside the rail and the type inside it still lands on the rail with the
   rest of the page. */
.ftr > .shell { max-width: calc(var(--page) + var(--fpanel-pad) * 2); }

.ftr-panel {
  position: relative;
  overflow: hidden;
  color: var(--on-deep-mid);
  /* the same two sub-10% sage washes the other dark boxes carry, lit from the
     top left like the life-stages panel */
  background:
    radial-gradient(120% 90% at 18% -10%, rgba(var(--sage-rgb), 0.10), transparent 58%),
    radial-gradient(90% 70% at 100% 108%, rgba(var(--sage-rgb), 0.05), transparent 60%),
    var(--surface-deep);
  border-radius: var(--r-md);
  padding: clamp(2.4rem, 4.4vw, 3.8rem) var(--fpanel-pad) clamp(1.4rem, 2.4vw, 2rem);
  box-shadow: inset 0 0 0 1px var(--rule-invert),
              0 40px 80px -50px rgba(var(--deep-rgb), 0.8);
}
.ftr-top {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 0.85fr) minmax(0, 0.95fr) minmax(0, 1.1fr);
  gap: clamp(1.8rem, 3vw, 3.2rem);
}

.ftr-mark {
  display: inline-flex; align-items: center; gap: 0.6rem;
  font-size: var(--fs-lg); font-weight: 700; letter-spacing: var(--track-tight);
  color: var(--on-deep);
}
.ftr-mark img { width: 30px; height: 30px; }
.ftr-about {
  margin-top: var(--sp-3);
  font-size: var(--fs-sm); line-height: 1.75; max-width: 38ch;
}
/* Now an <a> to the practice's Google Maps place rather than a static <p>
   (see footer.php) — inline-flex so the pin sits on the first line of a
   two-line address instead of being pushed onto its own row. */
.ftr-addr {
  display: inline-flex; align-items: flex-start; gap: 0.5rem;
  margin-top: var(--sp-2);
  font-size: var(--fs-sm); color: var(--on-deep);
  transition: color var(--t-1) var(--ease);
}
.ftr-addr svg { width: 15px; height: 15px; flex: none; margin-top: 0.15em; color: var(--on-deep-key); }
.ftr-addr:hover { color: var(--on-deep-key); }

.ftr-social {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  margin-top: var(--sp-3);
  list-style: none; padding: 0;
}
.ftr-social a {
  display: grid; place-items: center;
  width: 38px; height: 38px;
  border-radius: 50%;
  color: var(--on-deep);
  background: rgba(var(--white-rgb), 0.08);
  box-shadow: inset 0 0 0 var(--hair) var(--rule-invert);
  transition: background-color var(--t-1) var(--ease), color var(--t-1) var(--ease);
}
.ftr-social a:hover { background: var(--on-deep-key); color: var(--deep); }
.ftr-social svg { width: 17px; height: 17px; }

.ftr-col h2 {
  font-size: var(--fs-micro); font-weight: 700;
  letter-spacing: var(--track-micro); text-transform: uppercase;
  color: var(--on-deep-key);
}
.ftr-list {
  margin-top: var(--sp-3);
  display: flex; flex-direction: column; gap: 0.6rem;
  font-size: var(--fs-sm);
}
.ftr-list a { transition: color var(--t-1) var(--ease); }
.ftr-list a:hover { color: var(--on-deep); }
.ftr-quiet { color: var(--on-deep-mid); font-variant-numeric: tabular-nums; }

.ftr-cta p { margin-top: var(--sp-3); font-size: var(--fs-sm); line-height: 1.7; max-width: 32ch; }
.ftr-cta .btn { margin-top: var(--sp-3); }

.ftr-rule {
  margin-top: clamp(2.4rem, 4vw, 3.4rem);
  border: 0; border-top: var(--hair) solid var(--rule-invert);
}
.ftr-bottom {
  margin-top: var(--sp-3);
  display: flex; justify-content: space-between; align-items: center;
  gap: var(--sp-2) var(--sp-4); flex-wrap: wrap;
  font-size: var(--fs-micro);
}
.ftr-legal { display: flex; flex-wrap: wrap; gap: 0.6rem var(--sp-3); }
/* The scroll-to-top disc is fixed in this corner, and at the very foot of the
   page that is exactly where the legal links sit. Reserve its width here
   rather than hiding the control, which is still useful from down here. */
.ftr-bottom { padding-right: 120px; }
.ftr-legal a { transition: color var(--t-1) var(--ease); }
.ftr-legal a:hover { color: var(--on-deep); }

@media (max-width: 1024px) {
  .ftr-bottom { padding-right: 96px; }
  .ftr-top { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ftr-brand { grid-column: 1 / -1; }
  .ftr-about { max-width: none; }
  /* Third and last of the three primary buttons still sitting at .btn's
     un-overridden 13.44px on mobile — see the matching note on
     .apart-quote .btn. All seven of the page's pill buttons converge on
     12.16px below this point now, not just the four with a fit problem to
     blame it on. */
  .ftr-cta .btn { font-size: var(--fs-xs); }
}
@media (max-width: 560px) {
  .ftr-top { grid-template-columns: minmax(0, 1fr); }
  /* stacked, the disc sits under the bar rather than beside it. This adds
     to .ftr-panel's OWN bottom padding (clamp(1.4rem, 2.4vw, 2rem),
     ~22-32px at this width) — combined they now total less than the fixed
     54px totop disc's own footprint (0.75rem off the viewport edge — see
     .totop's own phone rule), so the disc can sit over the legal row
     instead of below it. */
  .ftr-bottom { justify-content: flex-start; padding-right: 0; padding-bottom: 5px; }
}

/* =========================================================================
   SCROLL TO TOP — the specimen, again

   It arrives as the tooth finishes fading off the stage, so the same object
   the page opened with ends up parked in the corner. White plate, hairline
   edge, the label set around the rim rather than under it.
   ========================================================================= */
.totop {
  position: fixed;
  right: clamp(1rem, 2.2vw, 2.1rem);
  bottom: clamp(1rem, 2.2vw, 2.1rem);
  z-index: 110;
  width: 92px; height: 92px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 20px 44px -20px rgba(var(--deep-rgb), 0.45),
              inset 0 0 0 var(--hair) var(--rule-quiet);
  opacity: 0;
  transform: scale(0.55) translateY(16px);
  pointer-events: none;
  transition: opacity var(--t-2) var(--ease),
              transform var(--t-3) var(--ease),
              box-shadow var(--t-2) var(--ease);
}
.totop.on { opacity: 1; transform: none; pointer-events: auto; }
.totop:hover {
  box-shadow: 0 26px 54px -20px rgba(var(--deep-rgb), 0.55),
              inset 0 0 0 var(--hair) var(--rule);
}

.totop > * { grid-area: 1 / 1; }
.totop img { width: 25px; height: auto; }

.totop-ring { width: 100%; height: 100%; overflow: visible; }
.totop-ring text {
  font-family: var(--font);
  font-size: 8.6px;
  font-weight: 600;
  letter-spacing: var(--track-micro);
  text-transform: uppercase;
  fill: var(--ink-quiet);            /* 5.21:1 on white */
}
@media (prefers-reduced-motion: no-preference) {
  .totop.on .totop-ring {
    animation: totop-spin 22s linear infinite;
    transform-origin: 50% 50%;
  }
  .totop:hover .totop-ring { animation-duration: 9s; }
}
@keyframes totop-spin { to { transform: rotate(360deg); } }

/* No arrow under the tooth: it landed on the roots and the ring already says
   which way this goes. */

@media (max-width: 1024px) {
  .totop { width: 74px; height: 74px; }
  .totop img { width: 21px; }
  .totop-ring text { font-size: 9.6px; letter-spacing: var(--track-label); }
}

/* =========================================================================
   THE FLOATING BOOK-ONLINE CTA

   Right edge, vertically centred — deliberately not the bottom-right
   corner .totop already owns (above); the two are both fixed and both
   z-indexed near the top of the stack, and stacking a second circle behind
   or beside the first the moment .totop turns "on" would read as clutter,
   not two separate tools. Centring this one vertically keeps them apart at
   every viewport height without either needing to know the other exists.

   Site-wide: the markup lives once in footer.php, which every template
   calls through get_footer() — not duplicated per template. See site.js's
   bookFloat() for the "appears on first scroll, then stays" behaviour.
   ========================================================================= */
.book-float-wrap {
  position: fixed;
  /* 0, not a gutter: this is a tab fixed TO the edge, so the edge is where
     it sits. The squared-off corners on that side below only read as
     intentional if there is genuinely no gap behind them. */
  right: 0;
  top: 50%;
  z-index: 111; /* one above .totop's 110 — if a future layout ever does
                   bring them close, the conversion action wins the stack */
  transform: translateY(-50%) translateX(100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-3) var(--ease), transform var(--t-3) var(--ease);
}
.book-float-wrap.on {
  opacity: 1;
  transform: translateY(-50%) translateX(0);
  pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
  /* still enters and stays fixed, it just does not slide to get there */
  .book-float-wrap { transform: translateY(-50%); }
  .book-float-wrap.on { transform: translateY(-50%); }
}

/* Sits above the ring below it — both are positioned elements in the same
   stacking context, so DOM order alone would already settle this, but
   spelling it out means the next edit here does not have to rediscover
   that by trial and error. */
/* Turned on its side by writing-mode, NOT by transform: rotate(). Both put
   the label vertical, but rotate() only turns the PAINT — the element's own
   box stays the wide, short one it was laid out as, so the clickable area,
   the fixed positioning and the pulse ring below would all still be
   horizontal while the button looked vertical, and every one of them would
   need its own correction. writing-mode changes the layout itself: the box
   really is tall and narrow, so `right: 0` lands it flush, the hit area
   matches what is drawn, and inset:0 on the ring still traces the button.

   vertical-rl (not vertical-lr) sets Latin type reading top-to-bottom,
   which is the right way round for a tab on the RIGHT edge — the reader
   tilts their head toward the tab, not away from it.

   .btn's own inline-flex survives this: with the writing mode turned, its
   main axis turns with it, so the label and the icon disc stack down the
   tab instead of across it — no flex-direction override needed. */
.book-float {
  position: relative; z-index: 1;
  writing-mode: vertical-rl;
  /* Rounded on the exposed side, square where it meets the viewport edge:
     TL and BL are the two exposed corners, TR and BR the two sitting
     against the screen.

     --r-md (14px), not --r-pill. On a box this narrow a 999px radius does
     not read as "rounded", it rounds the entire left side into a half-
     capsule and the tab stops looking like a tab. 14px is also the radius
     every other rounded surface on the site already uses — the cards, the
     panels, the hero captions — so the tab belongs to the same family
     instead of being the one shape with a radius of its own. */
  border-radius: var(--r-md) 0 0 var(--r-md);
  /* .btn's padding is physical (0.72 top/bottom, 1.35 left for the label,
     0.8 right for the disc). The axis turned, so those have to be restated
     against the new one: the roomy end is now the TOP, where the label
     starts, and the tight end the BOTTOM, where the disc sits. */
  padding: 1.35rem 0.72rem 0.8rem;
  /* falls leftward, into the page, rather than straight down — a tab
     against the right edge has nothing below it to cast onto */
  box-shadow: -10px 0 36px -14px rgba(var(--deep-rgb), 0.5);
}
.book-float:hover { box-shadow: -14px 0 44px -14px rgba(var(--deep-rgb), 0.68); }
/* The one thing deliberately NOT turned. text-orientation leaves replaced
   content upright in a vertical writing mode, which is what should happen
   here: a calendar glyph laid on its side does not read as a rotated tab,
   it reads as a broken icon. */
.book-float i svg { writing-mode: horizontal-tb; }

/* The attention pulse — a soft ring expanding out of the pill and fading,
   three times, then still. A SEPARATE element from .book-float itself, not
   a pseudo-element on it: .btn sets overflow:hidden (to clip its own
   hover-flood to the pill shape — see the note on .btn::before in
   base.css), which would silently clip a ring meant to grow BEYOND the
   button's own edges down to nothing visible. As a sibling inside this
   wrapper, which sets no overflow, the ring is free to scale past the
   button it surrounds.

   Three iterations, not infinite: a tab left open all afternoon should not
   still be pulsing at 4pm. Delayed half a second so it starts once the
   slide-in entrance has actually landed, not underneath it. */
.book-float-ring {
  position: absolute; inset: 0;
  /* traces the tab it is pulsing out of, squared side included */
  border-radius: var(--r-md) 0 0 var(--r-md);
  /* sage, not --deep. A dark fill at low opacity over a white page just
     desaturates to grey, and read as a smudge behind the tab rather than
     as anything deliberate; sage is the brand's own light green and comes
     out as a halo. This is one of the few places a solid sage is safe —
     it is a shape with nothing sitting on it, never type (see the note on
     sage in tokens.css). */
  background: var(--sage);
  opacity: 0;
  pointer-events: none;
  /* Grows out of the stuck edge, into the page. Scaling from the centre
     (the default) would send half the ripple off the right of the viewport
     to be clipped away unseen, and the visible half would look lopsided
     for no reason a reader could name. */
  transform-origin: 100% 50%;
}
@media (prefers-reduced-motion: no-preference) {
  .book-float-wrap.on .book-float-ring {
    animation: book-float-pulse 1.7s ease-out 3;
    animation-delay: 0.5s;
  }
}
@keyframes book-float-pulse {
  0%   { transform: scale(1); opacity: 0.4; }
  100% { transform: scale(1.5); opacity: 0; }
}

/* Trimmed on a phone, but the words stay.

   This block used to hide the label and collapse the whole thing to a bare
   54px disc, because the tab was horizontal then and ran ~150px across a
   360px screen. Turned vertical it claims about 40px of the EDGE instead,
   which is no longer a trade worth making: an icon alone has to be
   interpreted, where "Book online" simply says what it is, and this is the
   one control on the site whose entire job is to be understood and
   clicked. */
@media (max-width: 640px) {
  .book-float { padding: 1.05rem 0.5rem 0.6rem; font-size: var(--fs-xs); }
  .book-float i { width: 23px; height: 23px; }
  .book-float i svg { width: 11px; height: 11px; }
}

/* =========================================================================
   PHONES — a pass over what the desktop layout leaves behind at 640px and
   under. Everything here is inside a max-width query, so nothing above it
   changes: this file's desktop rules are untouched by all of it.

   Four things were actually wrong on a phone, each measured rather than
   eyeballed. They are grouped by cause, not by selector.
   ========================================================================= */
@media (max-width: 640px) {

  /* Mobile-only unification: every reference page's CTA lands on the same
     top space here, though each keeps its own desktop value above.

     Top only — the bottom is deliberately left at zero.
     ---------------------------------------------------------------------
     What a reader sees between two sections is the WHITE, not the sum of
     the padding. At a join where the colour changes — .clin into .docs —
     the upper section's padding is painted in, so 64px above and 64px
     below still shows 64px of white, and that is the join that reads
     right.

     .cta and .rev are both white, so nothing is painted in and the two
     helpings simply add up: giving .cta a bottom padding to match the
     others (which is what this rule did a moment ago) put 128px of
     unbroken white under the panel — double every other gap on the page,
     and the only one that looked wrong.

     So this join contributes from one side only. .rev's own 64px does the
     work, and the visible white matches the colour-change joins exactly. */
  .home-cta { padding-top: var(--fold); padding-bottom: 0; }

  /* --- 1. the header ate 11% of the screen ------------------------------
     "Book appointment" wrapped to two lines inside the pill, which took the
     header from ~62px to 97px on every phone — a tenth of the viewport,
     permanently, because the header is sticky. The label is short enough to
     fit on one line at every width once it is allowed to stay on one. */
  .hdr .btn {
    white-space: nowrap;
    padding: 0.6rem 0.55rem 0.6rem 1rem;
    font-size: var(--fs-xs);
  }
  .hdr .btn i { width: 24px; height: 24px; }
  .hdr .btn i svg { width: 12px; height: 12px; }

  /* --- 2. type below the legibility floor -------------------------------
     --fs-nano resolves to 9.3px, which is a caption size on a desktop card
     and simply too small held at arm's length. It is only ever used for
     in-card meta, so lifting it on phones cannot disturb a heading. */
  :root { --fs-nano: 0.72rem; }   /* 11.5px, from 9.3px */

  /* --- 3. touch targets under 44px --------------------------------------
     Every one of these is an inline text link that rendered 18-25px tall —
     fine for a cursor, too small for a thumb. min-height rather than padding
     so the text stays where it is and only the hit area grows around it. */
  .lband-link, .doc-link, .tx-link, .rev-src, .visit-tel, .visit-map {
    min-height: 44px;
  }
  /* .hcard-a is deliberately NOT in that list. Inside the two hero cards
     the 44px box is taller than the link's own 22.6px line, and because the
     link is inline-flex/align-items:center the surplus splits evenly above
     and below — ~21px of dead space that read as a gap over "Coverage info"
     and pushed each card from ~140px to 161px. The thumb still gets its
     44px here; it just comes from a pseudo-element that is painted outside
     the layout instead of from the box itself, so the card keeps its
     natural height. Clicks land on the anchor either way — the pseudo is
     its own child, not an overlay above it. */
  .hcard-a { position: relative; }
  .hcard-a::after {
    content: "";
    position: absolute; left: 0; right: 0;
    top: 50%; height: 44px;
    transform: translateY(-50%);
  }
  /* The footer is the densest tap area on the page — two stacked columns of
     nav links at 17px and a legal row at 13px. 40px with the list's own gap
     pulled in almost to nothing: each target more than doubles while the
     column as a whole grows by far less than the targets do. */
  .ftr-list { gap: 0.1rem; }
  .ftr-list a, .ftr-legal a, .ftr-mark {
    min-height: 40px;
    display: inline-flex; align-items: center;
  }
  /* the wordmark is a link home, and drops to a single 34px line once the
     sub-line is hidden below 380 */
  .brand { min-height: 44px; align-items: center; }

  /* --- 5. and the length ------------------------------------------------
     The fold rhythm is a desktop measure: --fold bottoms out at 5rem, so
     every section carried 160px of vertical padding on a 390px-wide screen.
     Tightened here, which returns roughly the height the touch targets above
     just claimed — the page ends up shorter than it started, with none of
     the cramping. */
  /* Eased from 3.4rem to 4rem. The tightening above was right — 5rem was a
     desktop measure and 160px between sections on a 390px screen was too
     much — but 3.4rem overshot in the other direction once the sections
     that DON'T simply take var(--fold) are accounted for (see the block
     below). 4rem is still half the original: 64px a side, 128px between
     two stacked sections. */
  :root { --fold: 4rem; --fold-wide: 5rem; }

  /* --- one rhythm between every section ---------------------------------
     Four sections opted out of --fold and each landed somewhere different,
     so the page had no consistent gap at all. Measured on a 390px screen
     before this rule:

       .office .docs .stages .tx   54.4px   (var(--fold), the intended one)
       .apart                      48px     (clamp floor)
       .clin                       41.6px   (clamp floor)
       .rev .visit                 28px     (!)

     The last two are the interesting failure. Their padding is written
     `max(1.75rem, calc(var(--fold) - 50px))`, which is a DESKTOP formula:
     there --fold is ~130px and the subtraction leaves a sensible ~80px.
     On a phone --fold is 54.4px, so `- 50px` leaves 4.4px and the 1.75rem
     floor takes over — the two closing sections of the page ended up with
     barely half the padding of the sections above them.

     None of those opt-outs mean anything at this width, so all four come
     back onto the token and every section now breathes the same 64px. */
  .clin,
  .apart,
  .rev.fold,
  .visit.fold { padding-block: var(--fold); }

  /* --- the two junctions that need more than the token ------------------
     Equal padding does not buy equal separation, because it is not always
     the same colour. Two kinds of join happen on this page:

       OPEN    a coloured section ends and a white one begins, and the
               white simply carries on down into the next section's
               content — .clin into .docs. One edge. 64px reads fine, and
               this is the join that already looks right.

       CLOSED  a dark PANEL ends, and a coloured section starts right
               underneath it — .apart's step cards into .clin, and
               .stages' panel into .tx's tint. Here the 64px of white is
               boxed in by colour above AND below, so the same number that
               reads as breathing room in the open case reads as a stripe
               between two dark blocks.

     A closed join therefore needs more white than an open one to say the
     same thing. These two get half a fold extra, which is the only place
     on the page the number moves off the token — and it moves for a
     reason a reader can see, rather than to taste.

     Bottom padding rather than the next section's top: .clin and .tx both
     paint their own background, so THEIR padding is coloured in and adds
     nothing to the white gap. Only the white section above can widen it. */
  .apart,
  .stages.fold { padding-bottom: calc(var(--fold) + var(--sp-4)); }

  /* --- 4. the scroll-to-top disc sat on the copy -------------------------
     At 74px on a 390px screen the fixed disc covered live paragraph text
     while the reader was in the middle of it. Smaller, and tucked further
     into the corner, it clears the measure at every width down to 320. */
  .totop { width: 54px; height: 54px; right: 0.75rem; bottom: 0.75rem; }
  .totop img { width: 16px; }
  .totop-ring { display: none; }   /* the ring's text is unreadable at 54px */
}

/* Under ~380 the brand's second line ("Erin Mills · Mississauga") wraps and
   pushes the header taller again. The wordmark alone still identifies the
   practice, and the location is in the footer, the Visit fold and the page
   title. */
@media (max-width: 380px) {
  .brand b i { display: none; }

  /* .hdr-right is flex:none (never shrinks) and the header .btn text is
     nowrap, so even after .brand has shrunk to its own ellipsis minimum
     (and lost its second line just above) the still-labelled "Book
     appointment" button pushes past the viewport edge on the narrowest
     phones. Collapsing the label to sr-only rather than display:none keeps
     the link's accessible name intact for screen readers; only the icon
     shows visually. */
  .hdr .btn-primary .hdr-cta-label {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .hdr .btn-primary { padding: 0.6rem; gap: 0; }

}

/* Last resort for the hero CTAs: the discs come off, and only here.
   -------------------------------------------------------------------------
   These rules used to live in the 380px block above, which meant every 360
   and 375px handset — most phones in use — lost its icons when the
   arithmetic only ever required it of the very narrowest. They are their
   own query now, at 340px, because they answer a different question from
   the header rules above: those are about the header overflowing, this is
   about two pills sharing one row.

   With the pills tightened in the main mobile rule the pair measures
   ~307px, which clears a 360px screen (~320px usable) and a 375px one
   (~335px) comfortably. 320px is the one it cannot: ~280px usable against
   ~307px needed. The discs are the only part of the pair that is
   decoration rather than words, so they are what gives — dropping them
   buys ~40px and keeps both labels, at a readable size, on one row. */
@media (max-width: 340px) {
  .hero-actions .btn i { display: none; }
  .hero-actions .btn { padding-inline: 0.85rem; gap: 0; }
}

/* =========================================================================
   FOLD 6 ON A PHONE — type only

   The treatments grid is four narrow columns on a wide screen, and its copy
   was sized for that column: --fs-sm for the description, --fs-xs for the
   link. Stacked, each card is the full width of the page and the same copy
   was running 33 characters a line at 390 in a box with room for far more —
   set small for a constraint that is no longer there.

   Same sizes fold 3, 4 and 5 now use on a phone, so the four sections read
   as one page rather than four decisions. Declared at the end of the file
   on purpose: fold 6's own rules are ~1000 lines above and would otherwise
   win on source order at equal weight.
   ========================================================================= */
@media (max-width: 640px) {
  /* the card title has to stay clear of a 16px body, and --fs-card-t was
     tuned against a 308px column */
  .tx-card .tx-t { font-size: var(--fs-xl); line-height: 1.2; }
  .tx-card .tx-d { line-height: 1.6; }          /* size: type system */
  /* 15px, matching .doc-link — these two are the same control in two folds */
  .tx-card .tx-link { font-size: var(--fs-md); }
  /* the nine tiles are read, not tapped; at --fs-xs the name was the
     smallest type on the fold bar the uppercase eyebrows */
  .tx-chips li { padding: 0.9rem 1rem; }
  .tx-chip-name { font-size: var(--fs-sm); }
  .tx-chip-d { font-size: var(--fs-sm); }
}

/* =========================================================================
   FOLDS 8 AND 9 ON A PHONE — type only

   Same pass as folds 3-6: copy that was sized against a narrow desktop
   column comes up to a reading size once the phone gives it the full width.
   Declared at the end of the file for the same reason as fold 6's block —
   both sections' own rules are far above and win on source order at equal
   weight.
   ========================================================================= */
@media (max-width: 640px) {

  /* --- fold 8, the reviews rail ----------------------------------------
     .rev-txt is --fs-lead at every width now (see folds.css above), so the
     card only needs to widen to give that size back the characters it takes
     — 86vw still leaves the next card peeking at the edge, which is what
     tells the reader the rail scrolls. */
  .rev-rail .rev-card { flex-basis: min(86vw, 400px); }
  .rev-card .rev-name { font-size: var(--fs-lg); }
  /* --fs-md (15px) was too much here, and the size was only half of why.
     This label is uppercase AND carries --track-micro's 0.19em, so at 15px
     "READ ALL REVIEWS ON GOOGLE" measured 252px and wrapped to two lines
     inside a 350px row — the two arrow buttons next to it were being
     squeezed to 39px wide to make room (see .rev-btn's flex:none). Back to
     --fs-xs with the tracking pulled in: one line, proportionate to the
     buttons, and still a 44px tap target via the min-height rule above. */
  .rev-src { font-size: var(--fs-xs); letter-spacing: 0.08em; }

  /* --- review cards: short by default, with a "Read more" that works ----
     The clamp, the fade, the "Read more" cue and the expansion are one
     mechanism, and on desktop every part of it is driven by :hover — it
     lives behind @media (hover: hover) and (pointer: fine). A phone has no
     hover, so on touch that left a card clamped to six lines with a cue
     that could never be triggered, or (once the query mis-reported) no cue
     at all. Either way there was no way to read the rest.

     Here the same behaviour is rebuilt for tap: still short by default —
     six lines, same as desktop — but the cue is a real <button> that
     site.js upgrades from the decorative span and wires to .is-open. The
     card sizes to its own content, so opening one grows it in place.

     The rail's 310px bottom padding (and its -294px margin) exist to catch
     a panel growing on hover out of an absolutely positioned box. Nothing
     is absolutely positioned here, so that reservation comes back out. */
  .rev-card { height: auto; }
  .rev-card-panel {
    position: static;
    max-height: none;
    overflow: visible;
    /* min-height:100% is the other half of the absolute-panel mechanism and
       has to come off with it. Left in, it resolves against a card that is
       itself being stretched by the rail, and the two inflate each other —
       measured at 987px tall for a card whose quote is 80px. */
    min-height: 0;
  }
  .rev-txt {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 6;
    overflow: hidden;
  }
  /* .is-clamped comes from reviewClamp() and marks only the cards whose
     text is genuinely cut off, so a short review shows no fade and no
     button — it has nothing more to give. */
  .rev-card.is-clamped .rev-txt {
    -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent);
    mask-image: linear-gradient(to bottom, #000 62%, transparent);
  }
  .rev-card.is-open .rev-txt {
    -webkit-line-clamp: unset;
    -webkit-mask-image: none;
    mask-image: none;
  }

  /* the cue, as a real control. Hidden until reviewClamp() has decided
     this card actually needs one. */
  .rev-more { display: none; }
  .rev-card.is-clamped .rev-more {
    display: inline-flex; align-items: center; gap: 0.35rem;
    margin-top: 0.55rem;
    padding: 0.5rem 0.9rem;
    min-height: 40px;
    border: 0; border-radius: var(--r-pill);
    background: rgba(var(--white-rgb), 0.12);
    color: var(--on-deep-key);
    font: inherit;
    font-size: var(--fs-nano); font-weight: 700;
    letter-spacing: var(--track-label); text-transform: uppercase;
    cursor: pointer;
    -webkit-appearance: none; appearance: none;
  }
  .rev-card.is-clamped .rev-more:focus-visible {
    outline: 2px solid var(--on-deep-key); outline-offset: 2px;
  }

  .rev-rail {
    padding-bottom: var(--sp-2);
    margin-bottom: 0;
    pointer-events: auto;
  }
  /* Equal-height cards. This is only safe because the quote is clamped to
     six lines: uncapped, one 1259-character review made a 987px card and
     stretch dragged every other card to match it. Bounded, the tallest is
     ~339px, so stretching gives a tidy uniform row instead of ragged
     bottoms with gaps under the shorter quotes. The panel fills the card
     it is given so the surface reaches the full height. */
  .rev-rail { align-items: stretch; }
  .rev-card-panel { height: 100%; }

  /* --- fold 9, the visit card ------------------------------------------
     The hours table is the one thing on this page a reader is likely to be
     checking on a phone, in a hurry, and it was set two steps under the
     address directly above it. The phone number is both a link and a tap
     target and was smaller still. */
  .visit-hours li { font-size: var(--fs-md); }
  /* The hours never wrap. At the larger size the "today" row could not hold
     the day, the TODAY pill and the times on one line, and "9:00am – 6:00pm"
     broke across two — which made one row of a seven-row table taller than
     the rest and read as a rendering fault. The pill tightens to pay for it. */
  .visit-hours li span:last-child { white-space: nowrap; }
  .visit-hours li.today span:first-child::after {
    margin-left: 0.35rem; padding: 0.1rem 0.4rem; letter-spacing: 0.06em;
  }
  /* --- the hours table, opened up -------------------------------------
     The row padding (0.55rem, 8.8px) was set against the table's original
     --fs-sm text. The rule a few lines above raises that text to --fs-md
     for phone legibility but left the padding where it was, so seven rows
     of 15px type sat in 8.8px of space — the type grew and the box did
     not, which is what makes it read as cramped.

     0.7rem plus a 1.5 line-height puts each row at ~45px, which is both a
     comfortable reading rhythm and about the size a thumb expects, without
     touching the font that is already correct.

     padding-block only: the base rule's 0.85rem left/right still applies,
     so the columns stay where they are and nothing reflows horizontally —
     which matters, because the "today" row is deliberately held to one
     line (see the nowrap note above) and any change to the inline padding
     risks breaking it. */
  .visit-hours-table { margin-top: var(--sp-2); }
  .visit-hours li { padding-block: 0.7rem; line-height: 1.5; }
  .visit-hours-head { font-size: var(--fs-nano); padding-block: 0.62rem; }

  /* --- "today" stops competing for the row's width ----------------------
     The today row has to carry three things on one line — the day, the
     TODAY pill and the times — and at this width it does not fit:

       ~273px   available inside the card, the table border and the row
       ~ 88px   "Wednesday", the longest day, at --fs-md
       ~ 56px   the pill (text + padding + its left margin)
       ~ 16px   the flex gap
       ~118px   "9:00am - 4:00pm", which is nowrap and so cannot give

     That totals ~278px, and because the times may not wrap the surplus
     leaves the box instead of reflowing. Tightening the pill again only
     buys a few px and the row would still be one long word away from
     breaking, so the pill comes out of the layout altogether.

     Nothing is lost. The row already says "today" three times over — the
     green tint, the leaf-coloured bold day name, and now a solid bar down
     its leading edge, which is drawn with an inset shadow so it costs no
     width at all (a border-left would eat 3px of the content box).

     The pill keeps its text and is hidden the .sr-only way rather than
     with display:none, because site.js only ever adds a class here — this
     ::after is the single textual cue that the row is today, so it has to
     stay in the accessibility tree for anyone not seeing the colour.

     Freed of the pill the row needs ~222px against ~273px, which holds
     down to a 360px handset. The tighter flex gap is headroom on top. */
  .visit-hours li { gap: 0.5rem; }
  .visit-hours li.today { box-shadow: inset 3px 0 0 var(--leaf); }
  .visit-hours li.today span:first-child::after {
    position: absolute; width: 1px; height: 1px;
    margin: 0; padding: 0; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  .visit-tag { font-size: var(--fs-sm); }
  .visit-note { font-size: var(--fs-sm); }
  .visit-banner .visit-tel { font-size: var(--fs-md); }
}

/* =========================================================================
   SECTION HEADINGS ON A PHONE — one size, below the hero

   This rule used to collapse d3 and d4 onto --fs-hero-sm, which fixed the
   spread but landed them on 30.4px — the EXACT size of the h1 beside them.
   Measured at 390px, the hero headline and all eight section heads came out
   identical, so the page had no top of hierarchy at all: every heading on
   it shouted equally, which is most of why the type read as oversized.

   1.4rem (22.4px) instead. It sits a clear step under the hero's 30.4px,
   which keeps the h1 the loudest thing on the page (as asked — the hero is
   fine as it is), and it is a normal mobile h2 rather than a desktop size
   dragged down a screen size.

   d2 joins the collapse now. It was the office fold's heading and the one
   size that ALREADY read correctly on a phone, and it was carrying its own
   #office-h override to get there — folding d2 in at the same value means
   the override is redundant and has been deleted rather than left to drift
   out of step with this rule later.

   line-height 1.28, not --lh-display's 1.04: 1.04 is set for one-line
   display type at desktop size. Three of these headings run to two or more
   lines at this width, and at 22.4px a 1.04 ratio sets them almost solid.
   ========================================================================= */
@media (max-width: 640px) {
  .display.d2,
  .display.d3,
  .display.d4 { font-size: 1.4rem; line-height: 1.28; }
}

/* =========================================================================
   THE TYPE SYSTEM — running copy

   Every fold used to set its own paragraph size. Measured across the
   homepage that came to SIX distinct sizes for plain running copy at 1400px
   (17.9 / 16 / 15 / 13.4 / 12.2px, plus the eyebrow micro size) and six
   again at 390px — so each fold announced itself in a different voice and
   the page read as assembled rather than designed. The reviews fold was the
   loudest: .rev-txt sat at --fs-lead (17.9px) on a PHONE, the largest body
   text anywhere on the page.

   The fix is a system rather than another per-fold patch. Two tiers, and
   every paragraph on the page belongs to exactly one of them:

     TIER 1 — running copy. Anything a visitor actually reads a sentence
              of: standfirsts, review quotes, bios, card descriptions,
              stripe copy. ONE size, whatever the fold.
     TIER 2 — supporting copy. Captions, addresses, sub-labels, step
              blurbs — text that annotates something else rather than
              being read on its own. ONE size, a clear step down.

   (Eyebrows/labels are TIER 3 and already consistent at --fs-micro
   everywhere; .btn and the arrow-links carry their own control sizes,
   which were unified in the earlier pass.)

   This block is the single source of truth: the per-fold rules above have
   had their font-size declarations removed rather than left to be
   overridden, so there is exactly one place any of these sizes is set. Each
   fold keeps its own line-height and colour — those are legitimately
   per-context (a quote on dark green wants different leading to a card
   description on white); only the SIZE is centralised.

   It sits at the foot of the file deliberately: folds.css is the
   last-loaded stylesheet, so equal-specificity rules here win on source
   order and a future fold rule can only override it on purpose.
   ========================================================================= */
.lead,
.clin-lead,
.rev-txt,
.doc-bio,
.tx-d,
.lband p,
.apart-check p,
.apart-quote p,
.visit-addr      { font-size: var(--fs-body); }
/* .hero-lede dropped from this list: it is display:none at every width, so
   a size set here would never be seen. It carried --fs-body when it was
   still released on a phone — see the narrow-build block above for where
   that override used to live. */

.hcard-s,
.vstep-txt,
.doc-cred,
.office-card-addr,
.visit-note,
.tx-chip-d       { font-size: var(--fs-sm); }

/* On a phone both tiers come down one notch. 15px keeps running copy above
   the readability floor while taking the visible weight out of the page —
   the ask was explicitly for smaller and consistent, not merely consistent
   — and 12.8px holds supporting copy a clear step under it without
   dropping to the fine-print sizes used for legal text. */
@media (max-width: 640px) {
  .lead,
  .clin-lead,
  .rev-txt,
  .doc-bio,
  .tx-d,
  .lband p,
  .apart-check p,
  .apart-quote p,
  .visit-addr    { font-size: 0.94rem; }

  .hcard-s,
  .vstep-txt,
  .doc-cred,
  .office-card-addr,
  .visit-note,
  .tx-chip-d     { font-size: 0.8rem; }
}

/* =========================================================================
   FOLD 9's TWO CTAs ON A PHONE — one line

   They were set to flex: 1 1 auto so each takes its natural width and grows;
   with wrap allowed that meant each one claimed a full row on anything under
   ~640. flex-basis 0 makes them share the row equally instead, and the label
   and pill padding come in far enough that "Book appointment" and "Get
   directions" both fit beside each other at 320.
   ========================================================================= */
@media (max-width: 640px) {
  .visit-act { flex-wrap: nowrap; gap: 0.5rem; }
  /* 1 1 auto, not 1 1 0: a zero basis splits the row into two equal halves,
     and "Book appointment" is the longer label — at 320 it was handed the
     same 112px as "Get directions" and clipped. Sizing from content and
     sharing only the leftover keeps both labels whole. */
  .visit-act .btn {
    flex: 1 1 auto; min-width: 0;
    white-space: nowrap;
    font-size: var(--fs-xs);
    /* measured, not guessed: at 0.85rem/0.6rem "Get directions" came to 137px
       against 134px of share at exactly 390, and clipped by three pixels */
    padding: 0.66rem 0.5rem 0.66rem 0.75rem;
    gap: 0.4rem;
  }
  .visit-act .btn i { width: 24px; height: 24px; }
  .visit-act .btn i svg { width: 12px; height: 12px; }
}

/* Under ~380 the pair plus their two discs comes to more than the card's
   inner width. The discs go — the same trade the hero's two CTAs make at
   this width — and the labels centre once there is nothing to sit opposite. */
@media (max-width: 380px) {
  .visit-act .btn i { display: none; }
  .visit-act .btn {
    justify-content: center;
    padding: 0.62rem 0.7rem;
    font-size: var(--fs-nano);
  }
}

/* =========================================================================
   FOLD 4's VERTICAL RHYTHM

   .docs is the only white fold with a dark panel on BOTH sides — .apart-panel
   above, .stages-panel below — and no background of its own. Every other fold
   either carries a tint (.fold--quiet) or butts onto a full-bleed band, so its
   padding is visually closed off by an edge. Here there is no edge: the
   section's 144px runs straight into the neighbour's, and the reader sees
   240px of white above the heading and 288px below the cards around a section
   whose content is only ~490px tall.

   Matched to .apart's own padding-block, which is the section directly above
   it, so the two now sit on the same rhythm rather than stacking two
   different ones. Desktop only — the phone build already trims --fold.
   ========================================================================= */
@media (min-width: 641px) {
  /* 48px, and .stages' top trimmed to 96 to match. Those two numbers are
     what make BOTH gaps 144 — the same white run the page shows everywhere
     else a panel edge meets the next section (e.g. .stages-panel's bottom to
     the treatments heading). Halving .docs alone was not enough: the gap
     below it is .docs' padding PLUS .stages', so the second half of it was
     never mine to fix from this rule. */
  .docs.fold { padding-block: clamp(2rem, 3vw, 3rem); }
  .stages.fold { padding-top: clamp(3rem, 6vw, 6rem); }
}

/* =========================================================================
   THE HERO GRAPHICS LAYER  (.hero-deco — homepage only)

   The brief, verbatim: "more graphics on the opening page, without it being
   cluttered in the mobile format." The homepage hero used to have an object
   in it — the tooth specimen — and when that moved to the Treatments page
   (see the .stage--flat block above) fold 1 was left as type, two badges,
   two cards and two buttons on flat white.

   A first pass filled that with drawn icon discs and read as cartoons. The
   practice has real photographs, so this pass uses those instead: a collage
   of the clinic, the team and two of the reviews quoted further down the
   page, sitting behind the hero and covered by white, with a soft torch
   under the cursor revealing whatever it passes over.

   WHY A TORCH RATHER THAN A VISIBLE COLLAGE
   -----------------------------------------
   A collage that is simply there is a busy hero at every moment on every
   screen — which is the exact thing the second half of the brief rules out.
   A collage that is only visible where the reader is pointing is empty by
   default and dense only where their attention already is, so the page's
   resting state is still the white it was designed around.

   It also settles the mobile half outright rather than by compromise: there
   is no cursor on a phone, so there is no torch and nothing to reveal. The
   layer is gated on `(hover: hover) and (pointer: fine)` as well as on
   width, and the tiles carry data-src until a mouse actually moves (see
   torch() in site.js), so a phone downloads none of it. What a phone gets is
   one wash of brand light behind the headline.

   NOTHING HERE CAN AFFECT LAYOUT
   ------------------------------
   .hero-deco is absolute over .stage-pin and every piece is absolute inside
   it. An absolutely positioned child of a grid or flex container is not an
   item of that container, so this subtree is invisible to BOTH hero layouts
   — the .stage--flat grid at 1025up and the ordered flex column the narrow
   build runs. It cannot reflow the headline, reorder anything, or add a
   pixel of height to a phone screen.

   z-index: -1 rather than 0. .stage-pin carries `isolation: isolate`, so a
   negative index is trapped inside it — it paints above .stage-pin's own
   (transparent) background and below every one of its descendants, which is
   what a background layer wants, and what keeps the headline and both cards
   over the photographs rather than under them. z-index:0 would have been
   wrong: in the painting order a positioned z-index:0 element sits ABOVE the
   text of non-positioned siblings, and .hero-actions is non-positioned, so
   the collage would have crossed the two buttons.

   overflow:hidden on the layer itself, not on .stage-pin. Both hero builds
   deliberately set the pin to overflow:visible (it grows past 100svh on a
   short laptop and on every phone), so the collage is cropped here instead.
   ========================================================================= */
.hero-deco {
  position: absolute; inset: 0; z-index: -1;
  overflow: hidden;
  pointer-events: none;   /* scenery: it must never eat a tap meant for a button */
}
.hero-deco > * { position: absolute; }

/* --- a treatment in progress, under a sheet of white ---------------------
   Behind the whole hero at about a tenth of its own strength. The point is a
   tint, not a picture — this sits under the headline, the badges, the cards
   and both calls to action, and every one of them has to stay the first
   thing a reader sees.

   NOTE ON THE ALPHAS BELOW, AFTER THE IMAGE CHANGED
   -------------------------------------------------
   These stops were tuned against a dusk exterior, which was a dark frame: at
   0.80 white its sky and its lit windows still read clearly. This one is a
   pale mint room under clinical lighting, so the SAME alpha shows visibly
   less of it — most of the frame composites to near-white and what survives
   is its dark shapes (the chair, the monitor, hair). Nothing was changed
   here on the swap, so if the desktop background now reads as almost absent,
   the fix is these three numbers rather than anything structural.

   THE VEIL IS A GRADIENT, NOT A FLAT ALPHA
   ----------------------------------------
   Stacked as two background layers on one element, veil first, so there is
   no overlay element and nothing to keep in sync. And it is radial, heaviest
   in the middle: 0.93 white behind the headline where --ink has to stay
   crisp, easing to 0.80 at the corners where nothing is written and the
   photograph can actually be seen. A flat alpha strong enough for the type
   left nothing at the edges, and one weak enough for the edges put a sunset
   behind an h1.

   The one dial: those three alpha stops. Lower them together for more
   photograph, raise them for more white.

   No `filter: saturate()` on it either, though the dusk oranges do sit oddly
   with the sage washes above. White over a colour desaturates it on its own,
   and a filter on an element this size is a full-viewport buffer to repaint
   — on the one page that already repaints a masked collage every frame the
   cursor moves. */
.hero-deco-bg {
  inset: 0;
  background-image:
    /* Taken right down, as asked. 0.93/0.89/0.80 -> 0.66/0.58/0.46.
       It can go this far because on a WIDE screen almost nothing sits on
       this background: .hero-lede is display:none above 1024px, both
       captions are white cards, both .hcards are white and the two buttons
       carry their own fills. The only thing left on it is the h1 — --ink at
       --fs-hero, which is large text and needs 3:1, not 4.5:1. At the 0.46
       edge, over the darkest region of this frame, it still measures about
       4.5:1; at the 0.66 centre, about 5.9:1. The floor is roughly 0.32. */
    radial-gradient(125% 105% at 50% 45%,
      rgba(var(--white-rgb), 0.66) 0%,
      rgba(var(--white-rgb), 0.58) 45%,
      rgba(var(--white-rgb), 0.46) 100%),
    url('../images/hero-care.webp');
  background-size: cover;
  /* 58%, low of centre: the ceiling light and the equipment arm fill the top
     of this frame and the two clinicians the middle. Anchoring low keeps the
     people behind the cards rather than behind the headline, and puts the
     emptier top of the room where the h1 sits. */
  background-position: 50% 58%;
  background-repeat: no-repeat;
}

/* --- brand light ---------------------------------------------------------
   The only piece that survives to a phone, and the only piece a desktop
   shows before the reader's cursor has arrived — so the hero is not bare
   while it waits for a mouse.

   Radial gradients, not blurred boxes: a filter:blur() on something this
   large is a full-size offscreen buffer repainted on every scroll, and the
   gradient renders identically for free. */
.hero-deco-wash { border-radius: 50%; }
.hero-deco-wash--a {
  top: -18%; left: -12%;
  width: min(62vw, 900px); height: min(62vw, 900px);
  background: radial-gradient(circle at 50% 50%,
              rgba(var(--sage-rgb), 0.32) 0%,
              rgba(var(--sage-rgb), 0.12) 42%,
              rgba(var(--sage-rgb), 0) 70%);
}
/* Cooler and wider than A, so the two do not read as one symmetrical pair.
   Built on --shade-rgb (the wash dark, a step under --deep — see tokens.css)
   at an alpha low enough to be depth rather than a shadow. */
.hero-deco-wash--b {
  right: -16%; bottom: -24%;
  width: min(72vw, 1040px); height: min(72vw, 1040px);
  background: radial-gradient(circle at 50% 50%,
              rgba(var(--sage-deep-rgb), 0.18) 0%,
              rgba(var(--shade-rgb), 0.05) 46%,
              rgba(var(--shade-rgb), 0) 72%);
}

/* =========================================================================
   THE TORCH

   HOW THE REVEAL IS BUILT
   -----------------------
   There is no white sheet element over the collage. The layer is masked to
   a single soft-edged circle, and everywhere outside that circle the layer
   is simply transparent — so the "white" the reader sees is the page's own
   white, showing through. One element, one mask, nothing to keep in sync.

   The mask is a `closest-side` radial gradient in a box sized to the torch
   DIAMETER with mask-repeat:no-repeat, which means the whole gradient is
   the circle and everything beyond the box is unmasked-out by definition.
   Moving the torch is then a mask-position change and nothing else.

   That last point is the reason it is written this way rather than as
   `radial-gradient(circle var(--tr) at var(--tx) var(--ty), …)`, which
   reads more naturally: the `at` form has to regenerate the gradient image
   on every frame of a pointer move, while a mask-position offset only moves
   an image the browser has already rasterised. Same picture, and the
   difference is visible on a 1400x900 layer at 60fps.

   --tx/--ty are pixel offsets from the top-left of .stage-pin. The values
   below are the resting position the hint sits at before the pointer
   arrives; site.js overwrites them in px from there on. They must stay
   LENGTHS: a percentage in mask-position means "align the X% point of the
   image with the X% point of the box", not "X% across", and the calc()
   below would quietly mean something else.
   ========================================================================= */
.hero-torch {
  inset: 0;
  display: none;          /* released only where a torch can exist — see below */
  opacity: 0;
  transition: opacity 0.55s var(--ease);
  contain: paint;         /* keeps the per-frame repaint inside this box */

  --tx: 330px;            /* resting spot, out in the left margin  */
  --ty: 560px;
  --tr: 340px;            /* the torch radius — the one size dial   */

  /* Five stops, not three. The falloff is what decides whether this reads
     as a light or as a hole with a blurred edge: a long, slow tail from
     ~40% out means the photograph arrives gradually instead of snapping
     into a disc, and the near-solid core keeps the middle of the beam
     genuinely bright rather than uniformly hazy. */
  -webkit-mask-image: radial-gradient(circle closest-side,
      #000 0%, rgba(0, 0, 0, 0.97) 38%, rgba(0, 0, 0, 0.78) 58%,
      rgba(0, 0, 0, 0.4) 76%, rgba(0, 0, 0, 0.12) 90%, transparent 100%);
          mask-image: radial-gradient(circle closest-side,
      #000 0%, rgba(0, 0, 0, 0.97) 38%, rgba(0, 0, 0, 0.78) 58%,
      rgba(0, 0, 0, 0.4) 76%, rgba(0, 0, 0, 0.12) 90%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: calc(var(--tr) * 2) calc(var(--tr) * 2);
          mask-size: calc(var(--tr) * 2) calc(var(--tr) * 2);
  -webkit-mask-position: calc(var(--tx) - var(--tr)) calc(var(--ty) - var(--tr));
          mask-position: calc(var(--tx) - var(--tr)) calc(var(--ty) - var(--tr));
}

/* Both conditions, not just the width. A 1280px touch laptop and an iPad in
   landscape both clear `min-width: 1025px` and neither has a hovering
   cursor to carry the effect; without the pointer test they would get a
   permanently frozen patch of photograph in the corner of the hero. */
@media (min-width: 1025px) and (hover: hover) and (pointer: fine) {
  .hero-torch { display: block; }
}

/* Two states, set by site.js. The hint is the torch parked at its resting
   spot so the effect is discoverable — a reader who never moves the mouse
   into the hero still sees that there is something under the white. .is-lit
   is the cursor-following state and wins by source order. */
.hero-torch.is-hint { opacity: 0.5; }
.hero-torch.is-lit  { opacity: 1; }

/* THE BEAM ITSELF.
   -------------------------------------------------------------------------
   Without this the effect is a hole cut in white — correct, and flat. A warm
   disc riding the same --tx/--ty as the mask turns it into a light: the
   middle of the reveal is brighter than its edge, which is what an actual
   torch does and what the eye reads as one.

   ::after rather than ::before, and this matters: a pseudo-element inserted
   with ::before is the FIRST child of .hero-torch and would paint beneath
   .hero-torch-collage. ::after is the last, so the glow sits over the
   photographs. It is inside the masked element, so it is clipped by the same
   circle and never shows anywhere the collage does not.

   Warm, not white. The lead photograph is a sunset over a lit storefront and
   the practice's own greens are cool; a neutral bloom over that read as
   haze, where 255,238,208 reads as the light being cast. */
.hero-torch::after {
  content: '';
  position: absolute;
  left: calc(var(--tx) - var(--tr));
  top: calc(var(--ty) - var(--tr));
  width: calc(var(--tr) * 2); height: calc(var(--tr) * 2);
  border-radius: 50%;
  background: radial-gradient(circle closest-side,
              rgba(255, 238, 208, 0.42) 0%,
              rgba(255, 238, 208, 0.16) 42%,
              rgba(255, 238, 208, 0) 76%);
}

/* --- the collage: three strips, none of them behind a word ----------------
   This began as a full-bleed grid and was rebuilt, because a picture behind
   the copy is a picture behind the copy however faint the veil over it is.
   The collage now occupies only the three regions of this hero that carry
   no text at any width:

     RAILS   the margins either side of .hero-cards, running from the trust
             badges down to the bottom of the buttons. Everything in that
             vertical range — badges, cards, buttons — is centred and at
             most .hero-cards' own 940px wide, so whatever is left over at
             the sides is empty by construction.
     BAND    full width, below the buttons. Nothing is under the buttons but
             the stage's bottom padding.

   The headline gets nothing. It is `white-space: nowrap` at --fs-hero and
   comes to roughly 1190px, so at every width this effect runs at there is
   no margin beside it worth the name — and it is the one piece of type on
   the page that must never compete with anything.

   NOT A PERCENTAGE ANYWHERE
   -------------------------
   --safe-top, --safe-bottom and --safe-w are written onto [data-collage] by
   torch() in site.js from the measured badges, cards and buttons. The three
   strips size themselves from those, so they follow the real layout rather
   than an assumption about it — which matters here more than usual: the pin
   is `min-height: 100svh` with its content centred, so the same percentage
   lands above the cards on a 1080px-tall screen and below the buttons on an
   800px one. The values below are only the fallback for the moment before
   the first measurement lands. */
.hero-torch-collage {
  position: absolute; inset: 0;
  --safe-top: 46%;      /* top of the trust badges    */
  --safe-bottom: 80%;   /* bottom of the button row   */
  --safe-w: 940px;      /* .hero-cards' own max width */
  --ht-gap: clamp(10px, 0.9vw, 16px);
  --ht-edge: clamp(10px, 1vw, 22px);
}
/* A veil of the page's own white over the collage. The photographs are the
   practice's real interiors — warm wood, clinical white, a lot of contrast
   — and at full strength they read as a lightbox rather than as something
   surfacing through the page. This is no longer doing any legibility work
   (nothing sits over them any more), so it is purely a matter of how much
   the reveal should assert itself: drop it to 0 for full colour. */
.hero-torch-collage::after {
  content: ''; position: absolute; inset: 0;
  background: rgba(var(--white-rgb), 0.12);
}

/* Width is the interesting part: half of whatever the cards do NOT use,
   less the page edge and one gap. At 1440 that is ~215px, at 1280 ~140px,
   and by 1200 it is thin enough to read as a stripe rather than a
   photograph — which is where the rails switch off below. */
.ht-rail, .ht-band { position: absolute; }
.ht-rail {
  top: var(--safe-top);
  height: calc(var(--safe-bottom) - var(--safe-top));
  width: calc((100% - var(--safe-w)) / 2 - var(--ht-edge) - var(--ht-gap));
  display: none;   /* released at 1280up, see below */
  grid-template-rows: 1fr 1fr;
  gap: var(--ht-gap);
}
.ht-rail--l { left: var(--ht-edge); }
.ht-rail--r { right: var(--ht-edge); }

@media (min-width: 1280px) {
  .ht-rail { display: grid; }
}

/* The band runs the full width and needs no such gate — there is always a
   strip of stage under the buttons, at every width, and it is the piece
   that carries the collage on its own on a narrower laptop. */
.ht-band {
  left: var(--ht-edge); right: var(--ht-edge);
  top: calc(var(--safe-bottom) + var(--ht-gap));
  bottom: var(--ht-edge);
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--ht-gap);
}
/* Five cells, alternating photo / review / photo / review / photo, so a
   quote always has a picture either side of it. Below ~1180 the cells fall
   under ~215px and a review wraps to five lines inside a strip only ~140px
   tall, so the reviews go and the three photographs take the width. */
@media (max-width: 1180px) {
  .ht-band { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ht-band .ht-quote { display: none; }
}

.ht-tile {
  position: relative; overflow: hidden;
  border-radius: var(--r-sm);   /* 3px — the same corner .hcard carries */
  background: var(--n-10);
  /* The hairline is what .hcard uses to sit on white without floating, and
     the tiles need it for the same reason once the veil over them is this
     light. Shadow deepened to match: these are meant to read as prints
     lying under the page, not as holes in it. */
  box-shadow: inset 0 0 0 var(--hair) rgba(var(--white-rgb), 0.5),
              0 30px 54px -32px rgba(var(--deep-rgb), 0.62);
  /* They settle rather than appear. See the .is-lit rule below — the
     resting state is the small one, so a browser that never runs the JS
     shows nothing anyway and this costs it nothing. */
  transform: scale(0.965);
  transition: transform 0.85s var(--ease);
}
.hero-torch.is-hint .ht-tile,
.hero-torch.is-lit .ht-tile { transform: none; }

/* The photographs hold still.
   ---------------------------------------------------------------------
   These carried a cursor-tracked parallax: each image oversized by 6% and
   drifted ±6px against the pointer inside its own overflow:hidden tile.
   The practice asked for them steady, so both halves are gone — the drift
   AND the 1.06 that existed only to give the drift room to move in. Kept
   on its own, that scale would have cropped 6% off every photograph to buy
   an overscan nothing uses any more.

   So no transform at all here now, and the torch is the only thing on this
   layer that moves.

   saturate/contrast rather than the desaturation this carried before:
   these photographs are the good ones now, and dulling them was
   compensating for a veil that has since come down. */
.ht-tile img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: saturate(1.05) contrast(1.02);
}

/* The two review tiles. Same white card, hairline and shadow as .hcard, so
   what the torch uncovers reads as part of this site rather than as a
   screenshot of somewhere else. */
.ht-quote {
  display: flex; flex-direction: column; justify-content: center;
  gap: 0.42rem;
  padding: clamp(0.85rem, 1.3vw, 1.35rem);
  background: var(--white);
  box-shadow: inset 0 0 0 var(--hair) var(--rule),
              0 26px 46px -34px rgba(var(--deep-rgb), 0.45);
}
.ht-quote-stars { display: inline-flex; gap: 2px; color: var(--star); }
.ht-quote-stars svg { width: 13px; height: 13px; fill: currentColor; }
.ht-quote-t {
  font-size: var(--fs-sm); line-height: 1.5; font-weight: 500;
  letter-spacing: var(--track-tight); color: var(--ink);
}
.ht-quote-n { font-size: var(--fs-xs); font-weight: 700; color: var(--ink-mid); }

/* Reduced motion keeps the collage but not the chase: site.js loads the
   tiles, parks the torch at its resting spot and attaches no pointer
   handler, so what is left is a still patch of photograph. The transition
   goes with it — there is nothing left to transition. */
@media (prefers-reduced-motion: reduce) {
  .hero-torch { transition: none; }
  /* The tiles are placed by .is-hint, which torch() adds immediately on this
     path — so the settle would run once on load with nobody having asked for
     it. Nothing else on a tile moves at any time now that the images have
     been made steady; this is the last of it. */
  .ht-tile { transition: none; }
}

/* =========================================================================
   THE SAME LAYER ON A PHONE

   The torch is already gone here — it never leaves `display: none` without
   a fine pointer — and with it the whole collage, including the download.
   What is left to decide is the brand light, and the answer is one wash,
   not two.

   Both survivors are dialled DOWN, not merely repositioned: a wash tuned to
   read across 1400px of white is a green cloud across 390px of it. Neither
   occupies anything — they are still absolute in a layer with no layout —
   so the stacked hero measures exactly what it measured before, to the
   pixel, and the phone build's own carefully-tuned rhythm (see the
   narrow-build block above, where every gap in this fold is a deliberate
   number) is untouched.
   ========================================================================= */
@media (max-width: 1024px) {
  /* Its own file, 800px and 38KB against 1600px and 95KB. A phone has no
     business downloading a 1600px image to put behind a 390px screen at a
     tenth of its strength, and only the matching declaration is ever
     fetched — a background-image the media query has overridden is never
     requested.

     THE PHOTOGRAPH MOVES TO THE TOP
     -------------------------------
     Reported: the clinicians end up behind the two white cards. They did,
     and it was structural rather than a matter of degree — the previous
     build put the image in the BOTTOM 74% of the hero, and the bottom of a
     stacked hero is exactly where .hero-cards sits. Two opaque white boxes
     over the only part of the frame worth seeing.

     So the band and the white swap ends. The photograph now occupies the top
     54% and fades to solid white before the cards begin; below that line
     there is no background element at all, so both cards sit on plain page
     white and cover nothing.

     WHAT THIS MEANS FOR THE COPY
     -----------------------------
     The top of a stacked hero is where the words are, so this only works
     because the h1 is the only text now sitting on it. .hero-lede — the
     standfirst that used to run under the headline — is display:none on
     every width as of this pass (see the un-conditional rule near the top
     of this file, and the removed override that used to release it here);
     it no longer needs a contrast case made for it because it is never
     painted. --ink at --fs-hero is large text and clears 3:1 at every stop
     below.

     THE STOPS, READ AGAINST THE HERO (the element is 0-54% of it)
     -------------------------------------------------------------
       0%   0.80  the fixed header crosses here, and .hdr-burger-bar is bare
                  --ink on whatever is behind it — this frame has a dark
                  equipment arm across its top, so the band is held pale
       20%  0.50  the h1 begins below this
       55%  0.50
       84%  0.86  fading out ahead of the cards
       100% 1.00  solid, so the element's own bottom edge is invisible */
  .hero-deco-bg {
    /* top back to 0 — the base rule's inset:0 gives it, but the build this
       replaces overrode it to 26% and an override left behind is worse than
       one restated. */
    top: 0; bottom: 46%;
    background-image:
      linear-gradient(to bottom,
        rgba(var(--white-rgb), 0.80) 0%,
        rgba(var(--white-rgb), 0.50) 20%,
        rgba(var(--white-rgb), 0.50) 55%,
        rgba(var(--white-rgb), 0.86) 84%,
        rgba(var(--white-rgb), 1) 100%),
      url('../images/hero-care-sm.webp');
    /* Only the X matters. `cover` on a box this shape scales the frame to
       the box's HEIGHT and crops the sides, so there is no vertical slack
       to position — the same reason the band had to be resized rather than
       repositioned to move the picture at all. At 54% of the hero the crop
       keeps about two thirds of the frame's width, which is enough for both
       clinicians and the chair. */
    background-position: 50% 50%;
  }

  /* Up and centred, behind the headline — the one region of a stacked hero
     with real empty space in it, since the h1 is two or three short lines
     inside a full-width column. */
  .hero-deco-wash--a {
    top: -14%; left: 50%; right: auto; transform: translateX(-50%);
    width: min(128vw, 720px); height: min(128vw, 720px);
    background: radial-gradient(circle at 50% 50%,
                rgba(var(--sage-rgb), 0.26) 0%,
                rgba(var(--sage-rgb), 0.10) 44%,
                rgba(var(--sage-rgb), 0) 70%);
  }
  /* The second wash goes. Two of them on a screen this narrow overlap into
     one flat tint across the whole fold, which is a colour change, not a
     graphic. */
  .hero-deco-wash--b { display: none; }
}

/* =========================================================================
   THE HERO CURSOR

   The torch uncovers photographs of the practice and two of its reviews, and
   until now a reader had no way of knowing what they were looking at. So the
   pointer becomes the caption: the brand arrow, and a capsule that names
   whatever is under it — the room, or the reviewer and their five stars.

   WHAT IS AND IS NOT SWITCHED OFF
   -------------------------------
   `cursor: none` is applied by .is-cursor, which torch() adds only after it
   has this element to put in the native arrow's place. The stylesheet never
   removes a cursor on its own: no script, no fine pointer, no torch — the
   reader keeps their own.

   And it comes straight back for anything clickable. The hero has two calls
   to action and two card links sitting in the middle of it, and a custom
   pointer that swallows them would be a real cost for a decorative gain.
   Links keep `pointer` (see below) and torch() hides the follower entirely
   while the cursor is over one, so the two are never both on screen.

   GEOMETRY
   --------
   .hero-cursor is a ZERO-SIZED box at the pointer's position; the arrow and
   the capsule are absolute inside it. That is what lets the capsule flip to
   the other side near the right-hand edge by swapping `left` for `right`
   without any arithmetic — and flipping matters, because .stage-pin is
   overflow:visible on this page and a capsule hanging off the right of the
   band would put a horizontal scrollbar on the document.
   ========================================================================= */
.hero-cursor {
  position: absolute; left: 0; top: 0;
  width: 0; height: 0;
  z-index: 10;              /* over the captions (5) and the cards (4) */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.22s var(--ease);
  will-change: transform;
}
.hero-cursor.is-on { opacity: 1; }

/* The arrow's own tip is at roughly 3% of its 100-unit viewBox, so at 26px
   the point lands within a pixel of this box's origin — the hot spot needs
   no offset.

   max-width: none is load-bearing, and its absence is invisible in the
   worst way. base.css line 21 gives every img/svg/canvas/video
   `max-width: 100%`; this one is absolutely positioned inside .hero-cursor,
   which is deliberately a ZERO-sized box, so its containing block is 0px
   wide and that 100% resolves to 0 — beating the 26px width outright. The
   arrow then renders 0 wide and 26 tall, which looks exactly like a missing
   file, and every minute spent checking the src is a minute wasted.

   Second time this rule has caught something on this page: see .swap-ring
   above, where the same 100% clamped a ring that is meant to be WIDER than
   its parent. Any absolutely-positioned image in this theme needs this
   line. */
.hero-cursor-tip {
  position: absolute; left: 0; top: 0;
  width: 26px; height: 26px; display: block;
  max-width: none;
}

/* --- the capsule ---------------------------------------------------------
   --leaf and white, the same pair the gift strip on the offer card uses, at
   6.13:1. Not the logo sage: that is 2.04:1 and tokens.css bans it from
   sitting under type at all.

   display:none until there is something to say. Over the white between the
   tiles the reader gets the arrow alone rather than an empty green pill. */
.hero-cursor-tag {
  position: absolute; left: 21px; top: 23px;
  display: none;
  align-items: center; gap: 0.34rem;
  padding: 0.24rem 0.62rem;
  border-radius: var(--r-pill);
  background: var(--leaf);
  color: var(--ink-invert);
  font-size: var(--fs-micro);
  font-weight: 700;
  letter-spacing: var(--track-tight);
  white-space: nowrap;
  box-shadow: 0 12px 24px -14px rgba(var(--deep-rgb), 0.85);
}
.hero-cursor.has-tag .hero-cursor-tag { display: inline-flex; }

/* Flipped to the left of the arrow when the capsule would otherwise run off
   the right-hand side. `right` is measured from the same zero-sized origin,
   so this is the whole of it. */
.hero-cursor.is-flip .hero-cursor-tag { left: auto; right: 4px; }

/* White stars, not the --star gold the review cards use. Gold is the right
   colour for a rating on white, and the wrong one on --leaf: #E0A80E over
   #4A6B2A comes to about 2.3:1, where white is 6.13:1. The five of them are
   already saying "rating" by being five of them. */
.hero-cursor-stars { display: none; align-items: center; gap: 1px; }
.hero-cursor.is-review .hero-cursor-stars { display: inline-flex; }
.hero-cursor-stars svg { width: 10px; height: 10px; fill: currentColor; }

/* --- the native cursor ---------------------------------------------------
   Set by torch(), never by the stylesheet alone. */
.stage.is-cursor { cursor: none; }
/* Back again for everything a reader can actually operate. Stated rather
   than left to inheritance: `cursor` inherits, and while a UA stylesheet's
   own `a { cursor: pointer }` does outrank an inherited `none`, <button> is
   not covered by that in every browser — and the two hero CTAs are the last
   place to find out which. */
.stage.is-cursor a,
.stage.is-cursor button { cursor: pointer; }

/* Not on a phone, not on a trackpad-less tablet, and not for a reader who
   has asked for less motion — the same three gates the torch itself is
   behind, restated here because a cursor that has been replaced by an
   element which is not being positioned is the worst outcome available. */
@media (max-width: 1024px), (hover: none), (pointer: coarse) {
  .hero-cursor { display: none; }
  .stage.is-cursor { cursor: auto; }
}
