/* ===========================================================================
   Align Dental Care — pitch redesign
   Scholar rail (sarah). Plain static CSS. No framework, no build step.

   Colour contract (measured, from tokens.css + brand.json — never re-derived):
     --colour-brand-teal #0D8586  DISPLAY ONLY, >24px. 4.02:1 on --colour-bg.
                                  NEVER white normal-size text on it (4.45:1).
     --colour-primary    #038283  filled interactive; white on it 4.64:1.
     --gold-num          #c06600  section numerals + hairlines on LIGHT grounds.
                                  3.69:1 on bg, 3.54:1 on surface, 3.43:1 on wash,
                                  3.32:1 on surface-alt — clears the 3:1 display
                                  floor on every light surface this site uses.
                                  (#d37419 is only 2.89:1 on --colour-surface, so
                                  the darker step of the same gold carries it.)
     --gold-num-dark     #d37419  the same role on the warm-ink ground (5.11:1).
     --ink-quiet         #635e58  captions / secondary ink. >=5.2:1 on all four
                                  light grounds (text-muted is floored on bg only).
     --link              #007273  body links. >=4.67:1 on all four light grounds.
     --focus-ink         #1f1c19  focus ring, OUTER tone. >=13.8:1 on every
                                  light ground, 3.65:1 on the FAB teal.
     --focus-halo        #ffffff  focus ring, INNER tone. >=4.64:1 on every
                                  dark ground and on all five teal fills.
   Gold is never body text. Small-cap labels use full ink, quieted by size and
   tracking rather than by contrast. The focus ring is deliberately TWO tones:
   no single colour clears 3:1 on both the warm paper and the warm ink without
   disappearing into the teal that sits between them.
   ======================================================================== */

/* ------------------------------- tokens -------------------------------- */
:root {
  --font-display: "Newsreader", "Iowan Old Style", Georgia, serif;
  --font-body: "Source Sans 3", "Segoe UI", system-ui, sans-serif;

  --ink-display: #201e1a;
  --ink-quiet: #635e58;
  --gold-num: #c06600;
  --gold-num-dark: #d37419;
  --gold-rule: #c06600;
  --link: #007273;
  --link-hover: #006263;

  /* The focus indicator is TWO tones, not one, because no single colour can
     do the job here. A ring clearing 3:1 on the warm paper needs luminance
     <=0.266; clearing 3:1 on the warm-ink band needs >=0.136 — a real window,
     but the teal fills the dock and the buttons use (#038283, L 0.176) sit
     inside it, so any colour that survives the paper and the ink disappears on
     the button. A light tone and a dark tone together have no such gap.
     Measured, every ground this site paints:
       --focus-ink  #1f1c19  15.32 paper / 14.67 surface / 13.80 surface-alt /
                             14.23 wash / 16.96 white field / 3.65 on FAB teal
       --focus-halo #ffffff  16.96 warm ink / 13.48 and 10.92 dock-chip states /
                             7.17 WhatsApp chip / 4.64 on FAB teal
     One of the two clears 3:1 on all thirteen; the boundary between them is
     16.96:1, so the indicator is never ambiguous. The halo fills what was a
     bare 3px gap, so the ring's geometry and footprint are unchanged. */
  --focus-ink: #1f1c19;
  --focus-halo: #ffffff;

  /* type scale, pinned per the brief */
  --fs-hero: clamp(2.375rem, 1.35rem + 3.6vw, 3.5rem);   /* 38 -> 56 */
  --fs-h1: clamp(2.125rem, 1.4rem + 2.4vw, 2.75rem);      /* 34 -> 44 */
  --fs-h2: clamp(1.625rem, 1.25rem + 1.3vw, 2rem);        /* 26 -> 32 */
  --fs-h3: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);        /* 20 -> 24 */
  --fs-body: 1.125rem;                                     /* 18 */
  --fs-small: 0.9375rem;                                   /* 15 */
  --fs-caption: 0.8125rem;                                 /* 13 */

  --maxw: 1160px;
  --maxw-text: 66ch;
  --pad-x: clamp(1.25rem, 4vw, 2.75rem);
  --sect-y: clamp(3.25rem, 6vw, 5.5rem);
  --hairline: 1px solid var(--colour-border);
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--colour-bg);
  color: var(--colour-text);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.55;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

img, svg { max-width: 100%; }
img { height: auto; display: block; }

/* Heading-styled divs must never inherit the body line-height (see /redesign-site pitfalls) */
[class*="__title"], [class*="__name"], [class*="__label"], [class*="__num"], [class*="__tag"] { line-height: 1.25; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;          /* the Scholar rail bans bold serif display */
  margin: 0 0 0.6em;
  color: var(--ink-display);
  /* 13 of the 16 measured widows were a two-line heading dropping its last word
     alone at 360px ("General and specialist / dentistry"). `balance` evens the
     lines instead of filling the first; on a 2-6 line heading that is exactly
     the right instrument, and where it is unsupported the current wrap stands. */
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); line-height: 1.1; letter-spacing: -0.015em; }
h2 { font-size: var(--fs-h2); line-height: 1.15; letter-spacing: -0.01em; color: var(--colour-brand-teal); }
h3 { font-size: var(--fs-h3); line-height: 1.25; letter-spacing: 0; color: var(--colour-primary-dark); }
h4 { font-size: 1.0625rem; line-height: 1.3; letter-spacing: 0; }

p { margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
/* `pretty`, not `balance`, for prose: it leaves the body of the paragraph alone
   and only works the last lines, which is where a ten-line lede was stranding a
   single word ("...under one / roof."). */
p, .lede, .se-faq__a, .review__quote, .pullquote { text-wrap: pretty; }

a { color: var(--link); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--link-hover); }
/* The PRESSED step. Every control already had hover and a focus ring; active was
   the state nothing had, so a tap on a phone gave no acknowledgement at all.
   One step darker than hover throughout: --colour-primary-active #006263 is
   6.48:1 on the page ground, 6.21:1 on surface, 5.84:1 on surface-alt and
   6.02:1 on the teal wash, so it clears AA as text and 3:1 as a boundary on
   every light ground this site uses. On the warm-ink ground it would fall to
   2.36:1, so there the pressed step goes to white (16.96:1) instead — see the
   .section--dark / .site-footer rules. `:not(.btn)` keeps the filled buttons out,
   which carry their own pressed fill and must keep a white label. */
main a:not(.btn):active { color: var(--colour-primary-active); }

/* The ring used to be --colour-primary-active #006263, which measures 2.36:1
   on the warm-ink ground — the SAME 2.36:1 an earlier round fixed on the link
   TEXT three pixels away and left in the ring. Every footer link, phone number,
   email and the InkySites credit failed WCAG 2.2 SC 1.4.11's 3:1 floor on all
   ten pages. Two tones now: the halo hugs the element, the ink sits outside it.
   The outline paints 3-5px out, the box-shadow's 3px spread fills 0-3px, so the
   pair reads as one 5px indicator and nothing moved. */
:focus-visible {
  outline: 2px solid var(--focus-ink);
  outline-offset: 3px;
  box-shadow: 0 0 0 3px var(--focus-halo);
}

strong, b { font-weight: 600; }

::selection { background: var(--colour-brand-teal-wash); color: var(--ink-display); }

/* ------------------------------- layout -------------------------------- */
.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad-x); }
.section { padding-top: var(--sect-y); padding-bottom: var(--sect-y); }
.section--surface { background: var(--colour-surface); }
.section--alt { background: var(--colour-surface-alt); }
.section--wash { background: var(--colour-brand-teal-wash); }
.section--dark { background: var(--colour-navy); color: var(--colour-on-dark); }
.section--dark h1, .section--dark h3, .section--dark h4 { color: #fff; }
.section--dark h2 { color: var(--colour-brand-teal); }
/* `.section--dark a` is (0,1,1), the same specificity as the global `a:hover` and
   declared after it, so it was silently cancelling hover on every link in a dark
   band. Both interactive states are therefore restated here, at (0,2,1), in white
   (16.96:1 on the warm ink) — the teal steps do not survive this ground. */
.section--dark a { color: #dbeceb; }
/* `:not(.btn)` is here for SPECIFICITY as well as scope: the light-ground pressed
   rule is `main a:not(.btn):active`, which counts as (0,2,2), so a bare
   `.section--dark a:active` at (0,2,1) lost to it and the pressed link came out
   dark teal on the warm ink at 2.36:1. Matching the `:not(.btn)` takes this to
   (0,3,2) and the guard holds. Buttons keep their own .btn--onDark states. */
.section--dark a:not(.btn):hover, .section--dark a:not(.btn):active,
.section--dark a:not(.btn):focus-visible { color: #fff; }
.section--dark .opener__num { color: var(--gold-num-dark); }
.section--dark .opener__rule { background: var(--gold-num-dark); }
.section--tight { padding-top: clamp(2rem, 3.5vw, 3rem); padding-bottom: clamp(2rem, 3.5vw, 3rem); }

.measure { max-width: var(--maxw-text); }
/* the measure is held at 62ch on the lede too, so a stacked hero at 768px does
   not run a 95-character line */
.lede { font-size: clamp(1.1875rem, 1.05rem + 0.5vw, 1.375rem); line-height: 1.5; color: var(--colour-text); max-width: 62ch; }

/* Measure discipline is a DEFAULT, not a per-block decision. The brief pins body
   copy at 62-72 characters. `.lede` carries its own 62ch and `.measure` was
   applied by hand, but any leaf text block with neither ran the full container
   the moment a two-column band collapsed at 880px — measured 90ch of 18px body
   in a closing action band, 85ch inside a full-width panel, 80ch on a definition
   value, 118ch on a 13px caption, and 97ch of 20px display inside an uncapped
   review quote at 1366. Capping the LEAF blocks rather than their containers is
   what makes it hold everywhere: the cap is in `ch`, so each block measures
   against its own font size, while headings, figures, panels and grids stay free
   to run as wide as the layout wants. `.lede`'s tighter 62ch wins on
   specificity, as it should. */
main p, main dd, main li { max-width: var(--maxw-text); }

.eyebrow {
  font-size: var(--fs-caption);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--colour-text);
  margin: 0 0 1rem;
}
.section--dark .eyebrow { color: var(--colour-on-dark); }

.skip-link {
  position: absolute; left: 0.5rem; top: -3rem; z-index: 200;
  background: var(--colour-navy); color: #fff; padding: 0.6rem 1rem;
  border-radius: 2px; text-decoration: none; transition: top 0.15s ease;
}
/* No :hover pair here on purpose — the link parks at top:-3rem, above the
   document origin, so a pointer can never reach it. :focus is its only entry and
   :active only ever fires from there, where the label must stay white on navy. */
.skip-link:focus, .skip-link:active { top: 0.5rem; color: #fff; }

/* -------------------------- section openers ---------------------------- */
/* Signature move 2: a gold numeral, a hairline running out to the margin,
   the title beneath. The hierarchy is carried by numerals and rules, so no
   card, box or drop shadow has to carry it. */
.opener {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  column-gap: clamp(0.9rem, 2vw, 1.5rem);
  row-gap: clamp(0.7rem, 1.5vw, 1.1rem);
  margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
  /* one source for the numeral's size: the rule's optical nudge below is
     expressed as a fraction of it, so the two can never drift apart */
  --opener-num-size: clamp(1.75rem, 1.1rem + 2vw, 2.5rem);
}
.opener__num {
  font-family: var(--font-display);
  font-size: var(--opener-num-size);
  font-weight: 400;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--gold-num);
  line-height: 1;
  margin: 0;
}
/* Optical, not geometric. Newsreader's lining figures sit HIGH in their box —
   the font's ascent exceeds the cap height — so at line-height 1 the box has
   more clearance below the digits than above, and `align-items: center` put the
   rule 4.5px BELOW the numerals' ink centre at a 40px numeral (3.5px at 33px).
   Invisible on a short mark, obvious when the rule then runs 900px across the
   page, on the site's signature element. Measured ink-centre offset is a
   constant 0.1125 of the numeral's own size at every clamp step, so the nudge
   scales with it. A transform, not a margin: a margin on a centre-aligned grid
   item only shifts the box by half its value, and would move layout too. */
.opener__rule {
  height: 1px; background: var(--gold-rule); opacity: 0.85;
  transform: translateY(calc(var(--opener-num-size) * -0.1125));
}
.opener__title { grid-column: 1 / -1; margin: 0; }

/* -------------------------------- header ------------------------------- */
/* Built AROUND the full logo lockup, not the lockup squeezed into a bar.
   Lockup 477x215 (ratio 2.2186). Desktop 150px wide = 67.6px tall; mobile
   128px wide = 57.7px tall — both well under the 215px source, so nothing is
   upscaled. On scroll the lockup is NOT shrunk: it is swapped for the roundel
   (logo-mark.png), because at a docked height the lower registered-name
   wordmark would render at ~4.9px cap height. */
.site-header {
  position: sticky; top: 0; z-index: 60;
  background: var(--colour-bg);
  border-bottom: var(--hairline);
  padding-top: 1.15rem; padding-bottom: 1.15rem;
}
.site-header > .container {
  display: flex; align-items: center; justify-content: space-between;
  gap: clamp(0.75rem, 2vw, 2rem);
  flex-wrap: nowrap;
}

.brand {
  position: relative; display: block; flex: 0 0 auto;
  width: 128px; height: 57.7px;
  text-decoration: none;
}
.brand__lockup { position: absolute; left: 0; top: 0; width: 100%; height: auto; }
.brand__mark { display: none; }

@media (min-width: 1080px) {
  .brand { width: 150px; height: 67.6px; }
}

/* nav */
.nav { display: flex; align-items: center; gap: clamp(0.5rem, 1.4vw, 1.1rem); min-width: 0; }
.nav__list {
  display: none;
  list-style: none; margin: 0; padding: 0;
  align-items: center; gap: clamp(0.9rem, 1.8vw, 1.6rem);
  flex-wrap: nowrap;
}
.nav__item { position: relative; }
.nav__link {
  display: inline-block;
  font-size: var(--fs-small); font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--colour-text); text-decoration: none;
  white-space: nowrap;
  padding: 0.4rem 0;
  border-bottom: 2px solid transparent;
}
.nav__link:hover, .nav__link:focus-visible { color: var(--link-hover); border-bottom-color: var(--gold-rule); }
.nav__link:active { color: var(--colour-primary-active); border-bottom-color: var(--colour-primary-active); }
/* Two states, one look. `aria-current="page"` is reserved for a link that
   really points at the current document; a dropdown TRIGGER whose child is the
   current page carries the class instead, so a screen reader is never told
   that "Treatment" is the current page when it navigates elsewhere. */
.nav__link[aria-current="page"],
.nav__link--in-section { border-bottom-color: var(--colour-brand-teal); color: var(--ink-display); }
.nav__link--group::after { content: ""; display: inline-block; width: 0.42em; height: 0.42em; margin-left: 0.45em; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: translateY(-0.15em) rotate(45deg); }

/* zero-JS dropdown: hover + focus-within, no script anywhere */
.nav__panel {
  position: absolute; top: calc(100% + 0.65rem); left: -1rem;
  min-width: 16rem;
  background: var(--colour-bg);
  border: var(--hairline);
  border-top: 2px solid var(--colour-brand-teal);
  padding: 0.5rem;
  list-style: none; margin: 0;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
  box-shadow: 0 1px 1px rgba(32,30,26,.04), 0 4px 8px rgba(32,30,26,.05), 0 10px 22px rgba(32,30,26,.05);
}
.nav__item:hover > .nav__panel,
.nav__item:focus-within > .nav__panel { opacity: 1; visibility: visible; transform: translateY(0); }
.nav__panel a {
  display: block; padding: 0.55rem 0.75rem;
  font-size: var(--fs-small); font-weight: 600; text-decoration: none;
  color: var(--colour-text); white-space: nowrap;
}
.nav__panel a:hover, .nav__panel a:focus-visible { background: var(--colour-brand-teal-wash); color: var(--link-hover); }
.nav__panel a:active { background: var(--colour-brand-teal-wash); color: var(--colour-primary-active); }
.nav__panel .nav__desc { display: block; font-weight: 400; font-size: var(--fs-caption); color: var(--ink-quiet); white-space: normal; margin-top: 0.1rem; }

/* compact click-to-call: the visible number is what overloads a clinic nav,
   so the bar carries the icon and the number lives in the drawer + footer */
.nav__call {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; flex: 0 0 auto;
  border: var(--hairline); border-radius: 999px;
  color: var(--link);
}
.nav__call:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.nav__call:active { border-color: var(--colour-primary-active); color: var(--colour-primary-active); background: var(--colour-brand-teal-wash); }
.nav__call svg { width: 19px; height: 19px; fill: currentColor; }
.nav__call-num { display: none; }

/* the one primary CTA */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-body); font-size: var(--fs-small); font-weight: 600;
  letter-spacing: 0.01em; gap: 0.35em;   /* flex collapses the space in "Call <span>9840971316</span>" */
  padding: 0.72rem 1.25rem;
  border-radius: 2px; border: 1px solid transparent;
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.btn--primary { background: var(--colour-primary); color: var(--colour-on-primary); }
.btn--primary:hover { background: var(--colour-primary-hover); color: #fff; }
.btn--primary:active { background: var(--colour-primary-active); }
/* grey-500, not grey-300: a control boundary is non-text UI and has to clear
   3:1 (WCAG 1.4.11). Measured from the token file, never eyeballed —
   grey-300 #aca9a3 is 2.12:1 on the page ground, grey-500 #7b7670 is 4.07:1. */
.btn--ghost { background: transparent; color: var(--colour-text); border-color: var(--colour-grey-500); }
.btn--ghost:hover { border-color: var(--colour-primary); color: var(--link-hover); }
/* the pressed fill is stated explicitly rather than left to the ground, so the
   label's contrast is the same 6.02:1 wherever a ghost button sits */
.btn--ghost:active { border-color: var(--colour-primary-active); color: var(--colour-primary-active); background: var(--colour-brand-teal-wash); }
.btn--onDark { background: transparent; color: #fff; border-color: rgba(255,255,255,.45); }
.btn--onDark:hover { background: rgba(255,255,255,.1); color: #fff; border-color: #fff; }
.btn--onDark:active { background: rgba(255,255,255,.2); color: #fff; border-color: #fff; }
.btn--lg { font-size: 1rem; padding: 0.85rem 1.6rem; }
.nav__cta { flex: 0 0 auto; display: none; }   /* mobile carries it in the drawer */

/* mobile drawer — pure CSS. The toggle IS the checkbox (natively focusable,
   Space-operable, no label indirection), drawn with three background stripes so
   it needs no pseudo-elements on a void element. Works with JS disabled. */
.nav__toggle {
  appearance: none; -webkit-appearance: none; margin: 0;
  width: 46px; height: 44px; flex: 0 0 auto;
  border: var(--hairline); border-radius: 2px;
  background-color: transparent;
  background-image:
    linear-gradient(var(--colour-text), var(--colour-text)),
    linear-gradient(var(--colour-text), var(--colour-text)),
    linear-gradient(var(--colour-text), var(--colour-text));
  background-size: 22px 1.5px;
  background-repeat: no-repeat;
  background-position: center calc(50% - 6px), center 50%, center calc(50% + 6px);
  cursor: pointer;
}
.nav__toggle:hover { border-color: var(--colour-grey-500); }   /* 4.06:1 on the ground */
.nav__toggle:active { border-color: var(--colour-primary-active); background-color: var(--colour-brand-teal-wash); }
.nav__toggle:checked { border-color: var(--colour-primary); background-color: var(--colour-brand-teal-wash); }

.nav__drawer {
  position: absolute; left: 0; right: 0; top: 100%;
  background: var(--colour-bg);
  border-bottom: var(--hairline);
  box-shadow: 0 12px 26px rgba(32,30,26,.08);
  display: none;
  /* dvh, not vh: on mobile Safari `100vh` is the LARGE viewport height, so a
     vh-sized drawer is taller than the space it actually has and its last rows
     sit under the browser chrome. vh stays as the fallback for anything that
     does not know dvh. */
  max-height: calc(100vh - 100%);
  max-height: calc(100dvh - 100%);
  /* the LINK LIST scrolls, the drawer itself does not: see .nav__cta-block */
  overflow: hidden;
}
.nav__toggle:checked ~ .nav__drawer { display: flex; flex-direction: column; }
.nav__drawer-inner {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 1rem var(--pad-x) 0.5rem;
}
.nav__group-label {
  font-size: var(--fs-caption); letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--ink-quiet); margin: 1.05rem 0 0.4rem;
}
.nav__group-label:first-child { margin-top: 0; }
.nav__drawer ul { list-style: none; margin: 0; padding: 0; }
.nav__drawer li + li { border-top: var(--hairline); }
.nav__drawer a:not(.btn) {
  display: block; padding: 0.55rem 0;
  font-size: 1.0625rem; font-weight: 600; text-decoration: none; color: var(--colour-text);
}
.nav__drawer a:not(.btn):hover { color: var(--link-hover); }
.nav__drawer a:not(.btn):active { color: var(--colour-primary-active); }
/* Ten rows of links plus two buttons cannot fit the 573px drawer a 375x667
   phone allows, at any sane rhythm — so rather than let both conversion buttons
   fall below a nested scroll with no affordance, the drawer is a column: the
   link list takes the flexible, scrolling half and the two buttons dock to the
   bottom edge, always visible and in the thumb zone. A sticky block would have
   done the same job but floated over the last link at rest; a real docked
   footer never covers anything. */
.nav__drawer .nav__cta-block {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: stretch; gap: 0.6rem;
  padding: 0.9rem var(--pad-x) 1.1rem;
  background: var(--colour-bg);
  border-top: var(--hairline);
}
.nav__drawer .nav__cta-block .btn { width: 100%; }
.nav__drawer .nav__phone-num { font-variant-numeric: tabular-nums; white-space: nowrap; }

@media (min-width: 1080px) {
  .nav__list { display: flex; }
  .nav .nav__cta { display: inline-flex; }
  .nav .nav__toggle, .nav .nav__drawer { display: none; }
}

/* the docking swap — pure CSS scroll-driven, so the header needs no JS at all.
   Where scroll-driven animations are unsupported the header simply stays tall,
   which is a correct, legible state rather than a broken one. */
@supports (animation-timeline: scroll()) {
  .brand__mark {
    display: block; position: absolute; left: 0; top: 0;
    height: 100%; width: auto; opacity: 0;
  }
  .brand {
    animation: brandDock linear both;
    animation-timeline: scroll(root block);
    animation-range: 44px 128px;
  }
  .brand__lockup {
    animation: fadeOut linear both;
    animation-timeline: scroll(root block);
    animation-range: 44px 112px;
  }
  .brand__mark {
    animation: fadeIn linear both;
    animation-timeline: scroll(root block);
    animation-range: 60px 128px;
  }
  .site-header {
    animation: headerDock linear both;
    animation-timeline: scroll(root block);
    animation-range: 44px 128px;
  }
}
@keyframes brandDock { to { height: 40px; } }
@keyframes headerDock { to { padding-top: 0.6rem; padding-bottom: 0.6rem; } }
@keyframes fadeOut { to { opacity: 0; } }
@keyframes fadeIn { to { opacity: 1; } }
@media (min-width: 1080px) {
  @keyframes brandDock { to { height: 48px; } }
}
@media (prefers-reduced-motion: reduce) {
  .brand, .brand__lockup, .brand__mark, .site-header { animation: none !important; }
  .brand__mark { display: none !important; }
}

/* --------------------------------- hero -------------------------------- */
/* ONE top-aligned two-column row. The portrait column takes the small
   desktop top-offset, so the copy's first line always sits at or above the
   image top and the void-beside-the-headline shape cannot form. */
.hero { padding-top: clamp(2.5rem, 5vw, 4.25rem); padding-bottom: clamp(2.75rem, 5vw, 4.5rem); }
.hero__row { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 4vw, 3.25rem); align-items: start; }
.hero__copy { align-self: start; }
.hero__figure { align-self: start; margin: 0; }
/* No cap while the row is STACKED. Unscoped, the 460px cap left the photograph
   129-349px short of its own full-width figure across the whole 640-879px band,
   and at 820px it ran narrower than the lede's own measure, which reads as an
   accident rather than a choice. The cap only ever mattered once the row is two
   columns, so it lives in the 880px block below. */
.hero__figure img { width: 100%; border-radius: 2px; }
.hero__figure figcaption { font-size: var(--fs-caption); color: var(--ink-quiet); margin-top: 0.75rem; max-width: 34ch; }
.hero__title {
  font-size: var(--fs-hero); line-height: 1.05; letter-spacing: -0.02em;
  margin: 0 0 1.1rem;
}
.hero__title .accentline { color: var(--colour-brand-teal); display: block; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }

@media (min-width: 880px) {
  .hero__row { grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr); }
  /* margin, not padding: the offset must not collide with the block's own
     internal padding when the aside is a ruled block rather than a figure */
  .hero__figure { margin-top: clamp(1.5rem, 3vw, 2.75rem); }
  .hero__figure img { max-width: 460px; }
}

/* page hero (inner pages) */
/* The one accidental gap on the site was this seam. A pagehead's own
   padding-bottom (41px at 1366) stacked with the following section's
   padding-top (82px) put 123px of a 768px fold onto a single joint, so section
   01's first real content — a photograph, the two-location map, the first
   question — sat below the fold on seven interior pages while the pageheads
   themselves were well composed. Tightened to an ASYMMETRIC 24 above / 46
   below: less air trailing a title block than leading the numbered opener it
   introduces, which is the rhythm a title block should have. Only this one
   joint moves. Every section-to-section seam keeps its full 82 + 82, and the
   pagehead's own padding-TOP is untouched, so the generous breathing room the
   register depends on is not what got spent. */
.pagehead { padding-top: clamp(2.25rem, 4vw, 3.5rem); padding-bottom: clamp(1.1rem, 2vw, 1.5rem); }
.pagehead + .section:not(.section--tight) { padding-top: clamp(2.25rem, 3.5vw, 2.875rem); }
.pagehead__row { display: grid; grid-template-columns: 1fr; gap: clamp(1.75rem, 3.5vw, 3rem); align-items: start; }
.pagehead h1 { margin-bottom: 0.75rem; }
@media (min-width: 880px) {
  .pagehead__row--split { grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr); }
  .pagehead__row--split .pagehead__aside { margin-top: clamp(1.25rem, 2.5vw, 2.25rem); }
}

/* ------------------------------ the record ----------------------------- */
.record { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; border-top: 1px solid var(--gold-rule); }
.record__cell { padding: 1.1rem 1rem 1.2rem 0; border-bottom: var(--hairline); }
.record__cell:nth-child(odd) { padding-right: 1.25rem; }
.record__val { font-family: var(--font-display); font-size: clamp(1.25rem, 1rem + 1vw, 1.625rem); line-height: 1.15; color: var(--ink-display); margin: 0 0 0.25rem; }
.record__key { font-size: var(--fs-small); color: var(--ink-quiet); margin: 0; }
@media (min-width: 900px) {
  .record { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .record__cell { border-bottom: none; padding-right: 1.5rem; }
}

/* ------------------------------- prose --------------------------------- */
.prose > * + * { margin-top: 1.1em; }
.prose h3 { margin-top: 2em; }
.prose ul, .prose ol { padding-left: 1.35em; margin: 0 0 1.1em; }
.prose li + li { margin-top: 0.4em; }

.cols2 { display: grid; grid-template-columns: 1fr; gap: clamp(1.75rem, 3.5vw, 3rem); align-items: start; }
@media (min-width: 900px) { .cols2 { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); } }
.cols2--even { grid-template-columns: 1fr; }
@media (min-width: 900px) { .cols2--even { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* credential list */
.creds { margin: 0; display: grid; gap: 0; border-top: var(--hairline); }
.creds div { display: grid; grid-template-columns: 1fr; gap: 0.15rem; padding: 0.85rem 0; border-bottom: var(--hairline); }
/* line-height set for the LABEL, not inherited from body prose. A 13px small-cap
   label at body 1.55 renders on a 20.15px slug, which only shows itself on the
   one label long enough to wrap ("05 Sterilisation protocol" on the-clinic, at
   both 820 and 1280) — where the two lines then read as two separate labels.
   1.25 is the same value the global heading-styled-div guard already applies to
   `__label` / `__title` / `__num` classes; these two `<dt>`s are the same kind of
   element with no class to catch them. */
.creds dt { font-size: var(--fs-caption); line-height: 1.25; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600; color: var(--ink-quiet); }
.creds dd { margin: 0; font-size: var(--fs-small); color: var(--colour-text); }
@media (min-width: 620px) {
  .creds div { grid-template-columns: 12rem 1fr; gap: 1.25rem; align-items: baseline; }
}

/* ---------------------------- a ruled block ----------------------------- */
/* A RULE ABOVE, NOT A BOX AROUND. The brief locks "no cards, no boxes, no drop
   shadows anywhere" — hierarchy is carried by the gold numerals and the hairline
   rules. This slot used to hold `.panel`: a hairline on all four sides, a 2px
   top rule, internal padding and a background fill, shipping in 11 places across
   7 pages. It drew exactly the box the brief forbids, its top rule was teal on
   one column and gold on the next for no semantic reason, and the two on the
   homepage sat side by side as equal boxes — the "equal feature cards in a row"
   family the anti-slop rubric names.
   `.ruled` is the vocabulary the site already speaks everywhere else: ONE gold
   hairline ABOVE the block, the same rule `.record`, `.reviews`, `.se-faq` and
   `.loc` all use, and the same gold the numbered openers set their numerals in.
   Parallel blocks still read as parallel — the rules start at the same y, one
   per column — with nothing drawn around them, no fill and no shadow. The
   padding-top is the old panel's padding, kept so the vertical rhythm under the
   rule is unchanged. `.loc` is this same primitive plus address spacing, so it
   shares the declaration rather than restating it.
   Shadows survive ONLY on the floating overlays that need to sit above the page:
   the mega-menu panel, the mobile drawer, the contact dock and the focus halo. */
.ruled, .loc { border-top: 1px solid var(--gold-rule); padding-top: clamp(1.25rem, 2.5vw, 1.75rem); }
.ruled > :first-child, .loc h3 { margin-top: 0; }

.pullquote {
  border-left: 2px solid var(--gold-rule);
  padding: 0.25rem 0 0.25rem 1.25rem;
  margin: 1.75rem 0;
  font-family: var(--font-display); font-size: clamp(1.25rem, 1.05rem + 0.8vw, 1.5rem);
  line-height: 1.35; color: var(--ink-display);
}
.pullquote cite { display: block; margin-top: 0.6rem; font-family: var(--font-body); font-size: var(--fs-small); font-style: normal; color: var(--ink-quiet); letter-spacing: 0.04em; }

/* --------------------------- before / after ---------------------------- */
/* Every plate arrived with its baked BEFORE / AFTER caption cropped away, so
   the build supplies the labels itself, in the site's own faces. Without them
   a visitor cannot tell which panel is which. */
.ba-grid { display: grid; grid-template-columns: 1fr; gap: clamp(1.75rem, 3vw, 2.5rem); }
@media (min-width: 700px) { .ba-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1040px) { .ba-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.ba { margin: 0; }
.ba__pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--colour-border); border: var(--hairline); }
/* flex column + margin-top:auto keeps the two labels on one baseline even when
   the two halves of a plate differ by a few pixels in height (the full clinical
   record is 572x514 beside 577x514) */
.ba__panel { background: var(--colour-bg); display: flex; flex-direction: column; align-items: stretch; }
.ba__panel img { width: 100%; }
.ba__tag {
  display: block; margin-top: auto;
  font-size: 0.75rem; letter-spacing: 0.16em; text-transform: uppercase; font-weight: 600;
  padding: 0.5rem 0.6rem;
  color: var(--ink-quiet);
}
.ba__tag--after { color: var(--colour-primary-active); }
.ba__caption { font-size: var(--fs-small); color: var(--colour-text); margin-top: 0.7rem; }
.ba__caption b { color: var(--ink-display); font-weight: 600; }

.ba--feature .ba__pair { grid-template-columns: 1fr; }
@media (min-width: 700px) { .ba--feature .ba__pair { grid-template-columns: 1fr 1fr; } }

/* ------------------------------- figures ------------------------------- */
.fig { margin: 0; }
.fig img { width: 100%; border-radius: 2px; }
.fig figcaption { font-size: var(--fs-small); color: var(--ink-quiet); margin-top: 0.7rem; }
.fig--card img { max-width: 340px; }
.fig--band img { width: 100%; }
/* `.fig--theatre` used to live here, a 3.3:1 documentary band built to hold the
   recovered theatre plate below the surgeons' cut foreheads. The plate has been
   withdrawn: a 2:1 source forced through that band rendered as headless torsos
   with a hand entering from the right edge, and the ledger never established
   that the theatre is even this practice's. The oral-surgery section now runs on
   prose and its radiograph, so the rule has no subject and is gone with it. */

.grid-photos { display: grid; grid-template-columns: 1fr; gap: clamp(1.25rem, 2.5vw, 2rem); }
@media (min-width: 640px) { .grid-photos { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1040px) { .grid-photos--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* ---------------------------- the schematics --------------------------- */
.sch { margin: 0; }
/* NOTE the specificity: `.sch svg {…}` is (0,1,1) and would beat a bare
   `.sch-tall {display:none}` at (0,1,0) regardless of source order, which is
   exactly how both variants once rendered at the same time. Every display rule
   below is therefore (0,2,0) and resolved by source order. */
.sch svg { width: 100%; height: auto; }
.sch figcaption { font-size: var(--fs-small); color: var(--ink-quiet); margin-top: 0.9rem; }
/* Capped at the viewBox width so a hand-set line drawing is never rendered
   above 1:1 (its stroke weights and label sizes are designed at that scale). */
.sch .sch-wide { display: none; max-width: 1080px; margin-inline: auto; }
.sch .sch-tall { display: block; max-width: 360px; margin-inline: auto; }
@media (min-width: 780px) {
  .sch .sch-wide { display: block; }
  .sch .sch-tall { display: none; }
}
.sch text { font-family: var(--font-body); fill: var(--colour-text); }
.sch .sch-num { font-family: var(--font-display); fill: var(--gold-num); font-size: 26px; }
.sch .sch-lab { font-size: 16px; font-weight: 600; fill: var(--ink-display); }
.sch .sch-sub { font-size: 14px; fill: var(--ink-quiet); }
.sch .sch-cap { font-size: 13px; letter-spacing: 0.12em; font-weight: 600; fill: var(--colour-text); }
.sch .sch-stroke { fill: none; stroke: var(--colour-primary-dark); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.sch .sch-thin { fill: none; stroke: var(--colour-border); stroke-width: 1.5; }
.sch .sch-link { fill: none; stroke: var(--colour-grey-300); stroke-width: 1.5; }
.sch .sch-fill-gold { fill: var(--gold-num); }
.sch .sch-fill-teal { fill: var(--colour-primary-dark); }

/* ------------------------------- reviews ------------------------------- */
.reviews { display: grid; grid-template-columns: 1fr; gap: 0; border-top: 1px solid var(--gold-rule); }
.review { padding: clamp(1.5rem, 3vw, 2.25rem) 0; border-bottom: var(--hairline); }
.review__quote { font-family: var(--font-display); font-size: clamp(1.0625rem, 1rem + 0.35vw, 1.25rem); line-height: 1.5; color: var(--ink-display); margin: 0; }
.review__by { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.6rem; margin-top: 1rem; }
.review__name { font-size: var(--fs-small); font-weight: 600; letter-spacing: 0.04em; color: var(--colour-text); }
.review__flag { font-size: var(--fs-caption); letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-quiet); }
/* Twelve reviews of wildly different lengths: a two-column GRID would leave
   ragged half-empty rows, so the long set flows as balanced multi-column text. */
@media (min-width: 900px) {
  .reviews--two { display: block; columns: 2; column-gap: clamp(2rem, 4vw, 3.5rem); }
  .reviews--two .review { break-inside: avoid; }
}
.section--dark .reviews { border-top-color: var(--gold-num-dark); }
.section--dark .review { border-bottom-color: rgba(194,189,181,.25); }
.section--dark .review__quote { color: #fff; }
.section--dark .review__name, .section--dark .review__flag { color: var(--colour-on-dark); }

/* --------------------------------- FAQ --------------------------------- */
.se-faq { border-top: 1px solid var(--gold-rule); }
.se-faq__title {
  font-family: var(--font-display); font-weight: 400; font-size: var(--fs-h2);
  color: var(--colour-brand-teal); margin: clamp(1.5rem, 3vw, 2.25rem) 0 0.35rem;
}
.se-faq__item { border-bottom: var(--hairline); }
.se-faq__q {
  cursor: pointer; list-style: none;
  font-family: var(--font-display); font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.3125rem);
  line-height: 1.3; color: var(--ink-display);
  padding: 1.15rem 2.5rem 1.15rem 0; position: relative;
  text-wrap: balance;   /* a <summary>, so the h1-h4 balance rule does not reach it */
}
.se-faq__q::-webkit-details-marker { display: none; }
.se-faq__q::after {
  content: ""; position: absolute; right: 0.35rem; top: 1.45rem;
  width: 0.6rem; height: 0.6rem;
  border-right: 1.5px solid var(--gold-num); border-bottom: 1.5px solid var(--gold-num);
  transform: rotate(45deg); transition: transform 0.15s ease;
}
.se-faq__item[open] .se-faq__q::after { transform: rotate(-135deg); top: 1.6rem; }
.se-faq__q:hover { color: var(--link-hover); }
.se-faq__q:active { color: var(--colour-primary-active); }
.se-faq__item[open] > .se-faq__q { color: var(--colour-primary-dark); }
.se-faq__a { padding: 0 2.5rem 1.35rem 0; max-width: var(--maxw-text); }
.se-faq__a p { margin: 0; }

/* ------------------------------ visit page ----------------------------- */
/* `.loc` is `.ruled` with address spacing — the shared rule and the shared
   first-child reset both live with `.ruled` above, so this block only carries
   what is genuinely location-specific. */
.loc address { font-style: normal; margin: 0 0 1rem; }
.contact-rows { display: grid; gap: 0; margin: 0; border-top: var(--hairline); }
.contact-rows div { display: grid; grid-template-columns: 1fr; gap: 0.1rem; padding: 0.7rem 0; border-bottom: var(--hairline); }
/* same shape, same label line-height as .creds dt */
.contact-rows dt { font-size: var(--fs-caption); line-height: 1.25; letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600; color: var(--ink-quiet); }
.contact-rows dd { margin: 0; font-size: var(--fs-small); }
.contact-rows dd a { font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (min-width: 520px) {
  .contact-rows div { grid-template-columns: 8.5rem 1fr; gap: 1rem; align-items: baseline; }
}

.form { display: grid; gap: 1rem; max-width: 34rem; }
.form label { display: block; font-size: var(--fs-small); font-weight: 600; margin-bottom: 0.35rem; }
/* The booking form sits on the teal wash, where a white fill is only 1.19:1
   against its ground — so the BORDER is the only thing that says "field", and
   it has to carry the 3:1 non-text floor on its own. grey-500 measures 3.78:1
   on the wash (grey-300 was 1.97:1). Same token as .btn--ghost. */
.form input, .form textarea, .form select {
  width: 100%; font-family: var(--font-body); font-size: 1rem;
  padding: 0.65rem 0.75rem;
  background: var(--colour-white); color: var(--colour-text);
  border: 1px solid var(--colour-grey-500); border-radius: 2px;
}
/* a field had no pointer feedback at all before it was focused — grey-600 is
   5.39:1 on the wash, a clear step up from the resting grey-500's 3.78:1 */
.form input:hover, .form textarea:hover, .form select:hover { border-color: var(--colour-grey-600); }
/* Same two tones as everything else, at the field's own tighter 1px offset:
   halo 0-1px, ink 1-3px. The ink is 14.23:1 on the teal wash the form sits on
   and 16.96:1 on the white fill, so the field's ring is the site's ring rather
   than a second, softer one. */
.form input:focus, .form textarea:focus, .form select:focus {
  border-color: var(--colour-primary);
  outline: 2px solid var(--focus-ink);
  outline-offset: 1px;
  box-shadow: 0 0 0 1px var(--focus-halo);
}
.form textarea { min-height: 8rem; resize: vertical; }
.form__note { font-size: var(--fs-caption); color: var(--ink-quiet); }

/* -------------------------------- footer ------------------------------- */
.site-footer { background: var(--colour-navy); color: var(--colour-on-dark); padding-top: clamp(2.75rem, 5vw, 4rem); padding-bottom: clamp(6.5rem, 8vw, 7rem); }
.site-footer h2, .site-footer h3 { color: #fff; }
.site-footer h3 { font-size: 1rem; font-family: var(--font-body); font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; margin: 0 0 0.9rem; color: #fff; }
.site-footer a { color: #dbeceb; }
/* :focus-visible belongs in this list. Without it the ring was the ONLY thing
   a keyboard user got in the footer, and the ring was the sub-threshold one —
   so the footer had no compliant focus affordance at all. White is 16.96:1 on
   the warm ink, the same step hover and press already take. */
.site-footer a:hover, .site-footer a:active, .site-footer a:focus-visible { color: #fff; }
.footer__grid { display: grid; grid-template-columns: 1fr; gap: clamp(2rem, 4vw, 3rem); }
@media (min-width: 760px) { .footer__grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr) minmax(0, 1fr); } }
.footer__brand { display: flex; flex-direction: column; align-items: flex-start; gap: 1rem; }
.footer__brand img { width: 190px; height: auto; }
.footer__blurb { font-size: var(--fs-small); color: var(--colour-on-dark); margin: 0; max-width: 32ch; }
.site-footer address { font-style: normal; font-size: var(--fs-small); }
.footer__list { list-style: none; margin: 0; padding: 0; font-size: var(--fs-small); }
.footer__list li + li { margin-top: 0.5rem; }
.footer__list a { text-decoration: none; }
.footer__list a:hover, .footer__list a:focus-visible { text-decoration: underline; }
.footer__num { font-variant-numeric: tabular-nums; white-space: nowrap; }
.footer__base {
  margin-top: clamp(2.25rem, 4vw, 3rem); padding-top: 1.35rem;
  border-top: 1px solid rgba(194,189,181,.25);
  display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; align-items: baseline; justify-content: space-between;
  font-size: var(--fs-caption); color: var(--colour-on-dark);
}
.footer__credit { margin: 0; }
.footer__credit a { color: var(--colour-on-dark); text-decoration: underline; text-underline-offset: 0.2em; }
.footer__credit a:hover, .footer__credit a:active, .footer__credit a:focus-visible { color: #fff; }

/* ----------------------------- tap targets ----------------------------- */
/* WCAG 2.2 SC 2.5.8 puts a 24px floor under every target, and a 15px link is
   only 21px tall while a 13px one is 18px — so the phone numbers, the email,
   the InkySites credit and every ".more" were all under it, on a site whose
   market is overwhelmingly phones.
   Only the BOX grows. Type size, colour, tracking and the address's own line
   rhythm are untouched, so the footer keeps its quiet register: the numbers
   simply read as the tappable rows they always were. The 2px block margin on
   the footer numbers is the second half of the audit — spacing — buying a 31px
   centre-to-centre pitch so no two targets touch, where the bare line-height
   gave 23.25px. */
.site-footer address a {
  display: inline-block; min-height: 24px; line-height: 1.25;
  padding-block: 4px; margin-block: 2px;
}
.footer__credit a { display: inline-block; min-height: 24px; padding-block: 4px; }
.contact-rows dd a { display: inline-block; min-height: 24px; padding-block: 3px; }

/* The open mobile drawer runs to the bottom of the viewport, where the dock
   would sit over its Call button. Pure CSS: stand the dock down while the
   drawer is open, so it can never cover a control. */
html:has(.nav__toggle:checked) .contact-dock { display: none; }

/* ------------------------------- utilities ----------------------------- */
.stack > * + * { margin-top: clamp(1.5rem, 3vw, 2.25rem); }
.stack-sm > * + * { margin-top: 1rem; }
.text-quiet { color: var(--ink-quiet); }
.small { font-size: var(--fs-small); }
.caption { font-size: var(--fs-caption); color: var(--ink-quiet); }
.visually-hidden {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* The 3px of block padding clears the 24px target floor; the matching 3px off
   the top margin means the text does not move a pixel. */
.more {
  display: inline-block; margin-top: calc(1.25rem - 3px);
  min-height: 24px; padding-block: 3px;
  font-size: var(--fs-small); font-weight: 600;
}

/* --------------------------------- print ------------------------------- */
@media print {
  .contact-dock, .site-header, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
  .section--dark, .site-footer { background: #fff !important; color: #000 !important; }
  .site-footer a, .footer__credit a { color: #000 !important; }
}

/* ===========================================================================
   CHANGE ROUND 1 — client request, 2026-08-18
   ---------------------------------------------------------------------------
   Ten changes asked for by the practice. Seven of them are new components and
   they all live here, appended rather than interleaved, so the original build
   above stays readable as one piece and this round stays revertible as one
   piece.

   Everything below obeys the three rules the build was held to:
     - No cards, no boxes, no drop shadows. Hierarchy is gold hairlines, gold
       numerals and space. The two exceptions are the floating dock (which has
       to lift off the page to read as floating) and the reel plates (which sit
       on the white ground they were composited on).
     - Never white normal-size text on --colour-brand-teal (#0D8586, 4.45:1).
       Filled interactive surfaces use --colour-primary / -hover / -active.
     - Anything that moves has a pause control and honours
       prefers-reduced-motion. WCAG 2.2.2 applies to both the banner and the
       reel: they start on their own and run for longer than five seconds.
   ======================================================================== */

/* ------------------------- 1. the banner carousel ----------------------- */
/* The "auto sliding banners" ask. Four slides: clear aligners, smile
   transformations, braces, the rooms.

   Built as a scroll-snap strip, NOT as absolutely-positioned layers. Three
   reasons: with JavaScript blocked it degrades to a swipeable strip that is
   still complete and still readable, rather than to a stack of four slides
   printed on top of one another; the browser's own scrolling handles touch,
   trackpad and keyboard for free; and no slide is ever display:none, so the
   text in slides 2 to 4 stays in the document for a screen reader and for
   search.

   The copy is NOT overlaid on the photograph. This site carries no scrims and
   no text-on-image anywhere else, and a gradient wash over a clinical
   before-and-after would be the first. Image panel and copy panel sit side by
   side instead, which is also the only arrangement that keeps every photograph
   at or below its native size. */
.banner { position: relative; margin-top: clamp(2.25rem, 4vw, 3.25rem); }
.banner__frame { overflow: hidden; border-top: 1px solid var(--gold-rule); }
.banner__track {
  display: flex; margin: 0; padding: 0; list-style: none;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.banner__track::-webkit-scrollbar { display: none; }
.banner__slide {
  flex: 0 0 100%; min-width: 0;
  scroll-snap-align: start; scroll-snap-stop: always;
  padding-top: clamp(1.5rem, 2.5vw, 2rem);
}
.banner__inner {
  display: grid; grid-template-columns: 1fr; align-items: center;
  gap: clamp(1.25rem, 3vw, 2.5rem);
}
@media (min-width: 820px) {
  .banner__inner { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); }
}
.banner__figure { margin: 0; min-width: 0; }
/* One image or two, same shell. The pair keeps the 1px gutter and hairline
   frame the results grid already uses, so a banner before-and-after and a
   results before-and-after are visibly the same object. */
.banner__pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--colour-border); border: var(--hairline); }
.banner__pair > div { background: var(--colour-bg); display: flex; flex-direction: column; }
.banner__pair img, .banner__figure > img { width: 100%; height: auto; display: block; }
.banner__figure > img { border: var(--hairline); border-radius: 2px; }
.banner__tag {
  font-size: var(--fs-caption); letter-spacing: 0.14em; text-transform: uppercase;
  font-weight: 600; color: var(--ink-quiet); padding: 0.45rem 0.7rem 0.5rem; margin-top: auto;
}
.banner__tag--after { color: var(--colour-primary-active); }
.banner__figcap { font-size: var(--fs-caption); color: var(--ink-quiet); margin-top: 0.6rem; }
.banner__copy h2 {
  font-size: clamp(1.5rem, 1.15rem + 1.5vw, 2.125rem);
  line-height: 1.12; margin: 0.35rem 0 0.75rem;
}
.banner__copy .eyebrow { margin-bottom: 0; }
.banner__copy p { margin: 0 0 1rem; max-width: 40ch; }

/* controls — a real pause button, because the strip advances on its own */
.banner__controls { display: none; align-items: center; gap: 0.75rem; margin-top: 1.25rem; }
/* With JavaScript blocked nothing advances on its own, so a pause control
   would be a button that pauses nothing. It is revealed by the same .js flag
   nav.js has always set on the root element. */
.js .banner__controls { display: flex; }
.banner__dots { display: flex; gap: 0.4rem; margin: 0; padding: 0; list-style: none; }
.banner__dot {
  appearance: none; -webkit-appearance: none;
  width: 30px; height: 24px; padding: 0; border: 0; background: none;
  cursor: pointer; display: grid; place-items: center;
}
.banner__dot::before {
  content: ""; display: block; width: 26px; height: 2px;
  background: var(--colour-grey-300); transition: background-color .18s ease;
}
.banner__dot:hover::before { background: var(--colour-grey-500); }
.banner__dot[aria-current="true"]::before { background: var(--gold-rule); height: 3px; }
.banner__pause {
  appearance: none; -webkit-appearance: none;
  min-height: 24px; padding: 0.2rem 0.55rem;
  font: inherit; font-size: var(--fs-caption); letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
  color: var(--ink-quiet); background: none;
  border: 1px solid var(--colour-grey-300); border-radius: 2px; cursor: pointer;
}
.banner__pause:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.banner__pause:active { border-color: var(--colour-primary-active); color: var(--colour-primary-active); background: var(--colour-brand-teal-wash); }

/* --------------------------- 2. the case reel --------------------------- */
/* The "auto scrolling case gallery" ask. A continuous marquee: the item list
   is printed twice and the track is translated by exactly half its width plus
   half a gap, so the loop point is invisible and there is no jump.

   The duplicate set is aria-hidden, so a keyboard or screen-reader user meets
   each case once.

   The pause control is a checkbox and a label, not a script, so it works with
   JavaScript blocked — which matters, because the animation does too. */
.reel-wrap { position: relative; }
.reel {
  overflow: hidden;
  border-top: 1px solid var(--gold-rule);
  border-bottom: var(--hairline);
  padding-block: clamp(1.1rem, 2vw, 1.6rem);
}
.reel__track {
  display: flex; gap: clamp(0.75rem, 1.5vw, 1.25rem);
  width: max-content;
  animation: reelScroll 160s linear infinite;
}
.reel:hover .reel__track,
.reel:focus-within .reel__track { animation-play-state: paused; }
.reel__item { margin: 0; flex: 0 0 auto; width: clamp(230px, 34vw, 380px); }
.reel__item img {
  display: block; width: 100%; height: auto;
  border: var(--hairline); border-radius: 2px; background: #fff;
}
.reel__item figcaption { font-size: var(--fs-caption); color: var(--ink-quiet); margin-top: 0.45rem; }
@keyframes reelScroll { to { transform: translateX(calc(-50% - clamp(0.375rem, 0.75vw, 0.625rem))); } }

.reel-pause { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none; }
.reel-pause:checked ~ .reel .reel__track { animation-play-state: paused; }
.reel__ctl {
  display: inline-flex; align-items: center; gap: 0.4rem;
  min-height: 24px; margin-top: 0.9rem; padding: 0.2rem 0.55rem;
  font-size: var(--fs-caption); letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600;
  color: var(--ink-quiet); border: 1px solid var(--colour-grey-300); border-radius: 2px; cursor: pointer;
}
.reel__ctl:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.reel-pause:focus-visible ~ .reel__ctl { outline: 3px solid var(--focus-ink); outline-offset: 2px; box-shadow: 0 0 0 6px var(--focus-halo); }
.reel__ctl .reel__ctl-play { display: none; }
.reel-pause:checked ~ .reel__ctl .reel__ctl-play { display: inline; }
.reel-pause:checked ~ .reel__ctl .reel__ctl-pause { display: none; }

/* --- the aligner strip, round 7, 2026-08-25 -----------------------------
   The same marquee as the case reel above, with two differences, both of which
   fall out of there being FOUR photographs here rather than twenty.

   1. Three printed sets, not two. The loop travels exactly one set; for the
      joint to stay off-screen, whatever is left in front of the viewport has
      to be wider than the viewport, and one set of four is not. Two sets
      always are. (Hence the -33.3333% here against the reel's -50%: the same
      derivation, one set of three rather than one of two, so the gap
      correction is a third of a gap rather than half.)

   2. The items are sized by HEIGHT, with width left to follow. Three of these
      photographs are upright and the fourth is landscape. Giving them a common
      WIDTH would have meant cropping the upright ones to a common shape, and
      cropping the practice's photographs to fit a slot is the thing round 6
      was corrected for. A common height lets each keep its own proportions and
      still sit on one line.

   These rules sit ABOVE the reduced-motion block deliberately. That block
   stops .reel__track with the `animation` shorthand, at the same specificity
   as .reel__track--triple, so it only still wins while it comes later in the
   file. Moving this below it would quietly restart the marquee for exactly the
   people who asked for it to stop. */
.reel--tall .reel__item { width: auto; }
.reel--tall .reel__item img {
  height: clamp(230px, 30vw, 420px);
  width: auto;
  max-width: none;
}
.reel__track--triple {
  animation-name: reelScrollTriple;
  animation-duration: 44s;
}
@keyframes reelScrollTriple { to { transform: translateX(calc(-33.3333% - clamp(0.25rem, 0.5vw, 0.41667rem))); } }

/* The cover photograph that opens the section, above the strip. */
.cover { margin: 0 0 clamp(1.4rem, 3vw, 2.2rem); }
.cover img { display: block; width: 100%; height: auto; border: var(--hairline); border-radius: 2px; }
.cover figcaption { font-size: var(--fs-caption); color: var(--ink-quiet); margin-top: 0.75rem; }

/* Reduced motion stops the marquee dead and hands the visitor the same content
   as a strip they scroll themselves. Nothing is lost, and the pause control is
   hidden because there is no longer anything to pause. */
@media (prefers-reduced-motion: reduce) {
  .reel { overflow-x: auto; }
  .reel__track { animation: none; }
  .reel__ctl, .reel-pause { display: none; }
}

/* ------------------------ 3. animated statistics ------------------------ */
/* Four figures that count up when the band comes into view. The final value is
   in the HTML, so with JavaScript blocked the visitor reads the number rather
   than a zero that never moves. */
.stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; border-top: 1px solid var(--gold-rule); }
@media (min-width: 900px) { .stats { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.stat { padding: 1.15rem 1.25rem 1.25rem 0; border-bottom: var(--hairline); }
.stat__val {
  font-family: var(--font-display);
  font-size: clamp(2rem, 1.35rem + 2.4vw, 3rem); line-height: 1.05;
  color: var(--colour-brand-teal); margin: 0 0 0.3rem;
  font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.stat__key { font-size: var(--fs-small); color: var(--ink-quiet); margin: 0; }
.section--dark .stats { border-top-color: var(--gold-num-dark); }
.section--dark .stat { border-bottom-color: rgba(194,189,181,.25); }
.section--dark .stat__val { color: #7fd4d4; }

/* --- round 11, item 7: the figures as a band that carries itself past -------
   "make the statistics to autoscroll somewhere like a sticky navigation bar and
   make it animated."

   A bar, edge to edge, on the brand wash, with a rule top and bottom so it
   reads as a strip laid across the page rather than another section of it. The
   travel is the claims band's mechanism exactly: identical sets printed in a
   row, the track slid left by exactly one set, so the moment the animation
   restarts every figure is standing where its twin just was and the join is
   invisible. No script, so it cannot stutter against the counting.

   .stat is reused rather than reinvented, but its grid-cell dressing is undone
   here: in the old row each figure was a cell with a hairline under it, and in a
   moving strip that hairline becomes a dash sliding past. Each figure is a
   fixed-width block instead, so the four keep their rhythm however long the
   labels are.

   It stops on hover and on focus, like every other moving thing on this site,
   because a visitor who wants to read "Average patient rating" needs it to hold
   still while they do.

   ON THE COLOUR, which took a second pass. Built first on the teal wash, the
   band was invisible as a band: the Technology section directly above it is
   also wash, so the two shared a ground and the strip read as that section
   having grown a rule at the bottom. A bar has to differ from what is above AND
   below it, and below it is white. So it takes the deep teal, which is neither -
   and deliberately NOT --colour-primary, which is what the fixed claims band at
   the foot of the window already wears. On the tall middle of this page both are
   on screen at once, and two strips in the same teal would read as one component
   drawn twice. Figures take the pale teal and labels the warm grey that every
   other dark section on this site uses, so the pairing is not invented here. */
.statsbar {
  overflow: hidden;
  background: var(--colour-primary-dark);
  border-top: 1px solid var(--gold-num-dark);
  border-bottom: 1px solid var(--gold-num-dark);
  padding: 1.15rem 0;
}
.statsbar__track {
  display: flex; width: max-content;
  animation: statsScroll 54s linear infinite;
}
.statsbar:hover .statsbar__track,
.statsbar:focus-within .statsbar__track { animation-play-state: paused; }
.statsbar__group { display: flex; flex: 0 0 auto; }
.statsbar .stat {
  flex: 0 0 auto; width: 17.5rem; max-width: 62vw;
  padding: 0 clamp(1.1rem, 2.4vw, 2rem);
  border-bottom: 0; border-left: 1px solid rgba(255, 255, 255, 0.2);
}
.statsbar .stat__val {
  color: #7fd4d4;
  font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.4rem); margin-bottom: 0.15rem;
}
.statsbar .stat__key { color: var(--colour-on-dark); font-size: var(--fs-caption); }
@keyframes statsScroll { from { transform: translateX(0); } to { transform: translateX(-25%); } }
@media (prefers-reduced-motion: reduce) {
  /* Same reasoning as the claims band: parking a finished animation leaves the
     strip standing at the end of its travel, which is three empty sets. Stop it
     where it starts, show one set, and let the four share the width evenly -
     which lands on the four-column row these figures were before this round, so
     the visitor who asked their system for less movement gets the old layout
     rather than a short row with a third of a dark bar empty beside it. */
  .statsbar__track { animation: none; width: 100%; overflow-x: auto; }
  .statsbar__group:nth-child(n + 2) { display: none; }
  .statsbar__group:first-child { flex: 1 1 auto; width: 100%; }
  .statsbar .stat { flex: 1 1 0; width: auto; min-width: 0; max-width: none; }
  .statsbar .stat:first-child { border-left: 0; padding-left: 0; }
}
.section--dark .stat__key { color: var(--colour-on-dark); }

/* --------------------------- 4. the timeline ---------------------------- */
/* Dr Srinivas's record as a dated spine. The reveal is a scroll-driven CSS
   animation, the same mechanism the header already docks with — no script, and
   where it is unsupported every entry simply starts visible, which is a
   correct state and not a broken one. */
.tl { list-style: none; margin: 0; padding: 0; position: relative; }
.tl::before {
  content: ""; position: absolute; left: 6px; top: 0.9rem; bottom: 0.9rem;
  width: 1px; background: var(--colour-border);
}
.tl__item { position: relative; padding: 0 0 clamp(1.5rem, 3vw, 2.25rem) 2.25rem; }
.tl__item:last-child { padding-bottom: 0; }
.tl__item::before {
  content: ""; position: absolute; left: 0; top: 0.55rem;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--colour-bg); border: 2px solid var(--gold-rule);
}
.tl__item--now::before { background: var(--gold-rule); }
.tl__when {
  display: block; font-family: var(--font-display);
  font-size: clamp(1.125rem, 1rem + 0.5vw, 1.375rem); line-height: 1.2;
  color: var(--gold-num); margin: 0 0 0.15rem;
}
.tl__what { display: block; font-size: var(--fs-h3); font-family: var(--font-display); line-height: 1.25; color: var(--ink-display); margin: 0 0 0.3rem; }
.tl__note { font-size: var(--fs-small); color: var(--ink-quiet); margin: 0; max-width: 52ch; }
.section--surface .tl__item::before { background: var(--colour-surface); }
.section--wash .tl__item::before { background: var(--colour-brand-teal-wash); }

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .tl__item {
      animation: tlIn linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 20%;
    }
  }
}
@keyframes tlIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

/* --------------------------- 5. treatment icons ------------------------- */
/* Line icons in the same hand as the page schematics — 2px round-joined
   strokes in --colour-primary-dark. Each one sits beside a real heading, never
   instead of one, so the meaning never depends on reading a pictogram. */
.tx { display: grid; grid-template-columns: 1fr; gap: 0; border-top: 1px solid var(--gold-rule); list-style: none; margin: 0; padding: 0; }
@media (min-width: 620px) { .tx { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: clamp(1.5rem, 3vw, 2.5rem); } }
@media (min-width: 1000px) { .tx--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.tx__item { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 0.9rem; align-items: start; padding: 1rem 0 1.05rem; border-bottom: var(--hairline); }
.tx__icon { width: 40px; height: 40px; display: block; }
.tx__icon .s { fill: none; stroke: var(--colour-primary-dark); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.tx__icon .g { fill: none; stroke: var(--gold-num); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.tx__name { display: block; font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.2; color: var(--ink-display); margin: 0 0 0.2rem; }
.tx__name a { text-decoration: none; }
.tx__name a:hover { text-decoration: underline; text-underline-offset: 0.15em; }
.tx__desc { font-size: var(--fs-small); color: var(--ink-quiet); margin: 0; }
.section--dark .tx { border-top-color: var(--gold-num-dark); }
.section--dark .tx__item { border-bottom-color: rgba(194,189,181,.25); }
.section--dark .tx__icon .s { stroke: #7fd4d4; }
.section--dark .tx__icon .g { stroke: var(--gold-num-dark); }
.section--dark .tx__name { color: #fff; }
.section--dark .tx__desc { color: var(--colour-on-dark); }

/* --------------------------- 6. bullet lists ---------------------------- */
/* The practice asked for the writing to be shorter and carried on bullets.
   These are the bullets: a gold tick, a hanging indent, a 15px line. */
.blist { list-style: none; margin: 0; padding: 0; }
.blist li { position: relative; padding-left: 1.5rem; margin: 0 0 0.55rem; font-size: var(--fs-small); }
.blist li:last-child { margin-bottom: 0; }
.blist li::before {
  content: ""; position: absolute; left: 0; top: 0.36em;
  width: 9px; height: 5px; border-left: 2px solid var(--gold-rule); border-bottom: 2px solid var(--gold-rule);
  transform: rotate(-45deg);
}
.blist--lg li { font-size: var(--fs-body); margin-bottom: 0.7rem; }
.section--dark .blist li::before { border-color: var(--gold-num-dark); }

/* ---------------------- 7. the archive plate grid ----------------------- */
/* The practice asked for every case photograph from the old site. These are
   their own composite plates, kept whole: each one already carries its own
   BEFORE / AFTER caption baked in, so re-cutting and re-labelling them in our
   own type would be work that changed nothing a visitor can see.
   They sit on a white cell because that is the ground they were composited on;
   a hairline holds them off the warm paper. */
.plates { display: grid; grid-template-columns: 1fr; gap: clamp(1rem, 2vw, 1.5rem); }
@media (min-width: 700px) { .plates { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1040px) { .plates { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.plate { margin: 0; }
.plate img { display: block; width: 100%; height: auto; background: #fff; border: var(--hairline); border-radius: 2px; }

/* ---------------- 8. WhatsApp in the sticky bar and the dock ------------ */
/* The header was already sticky, and already docks the lockup down to the
   roundel on scroll. What the practice asked for on top of that is WhatsApp
   beside the telephone, in the bar, on every page and at every width. */
.nav__wa {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 auto;
  border: 1px solid var(--colour-grey-500); border-radius: 2px;
  color: var(--colour-text); text-decoration: none;
}
.nav__wa:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.nav__wa:active { border-color: var(--colour-primary-active); color: var(--colour-primary-active); background: var(--colour-brand-teal-wash); }
.nav__wa svg { width: 20px; height: 20px; fill: currentColor; }

/* --- the location QR, round 6, 2026-08-24 ------------------------------------
   The practice asked for a QR code of the clinic's location beside the
   WhatsApp and call buttons. It is a <details>, so the panel opens, closes and
   takes the keyboard with the file below blocked; nav.js only adds the two
   courtesies it adds to the drawer (Escape, and a click outside).

   Sized and placed for the job it actually does. A QR is a DESKTOP gesture -
   the visitor is at a computer and wants the address on the phone in their
   hand - so the code is 150px, comfortably above the ~100px where phone
   cameras start to struggle at arm's length, and it is pure black on white
   with a three-module quiet zone because a tinted or tightly-cropped code is
   the usual reason one will not scan. The phone gets the same gesture as a
   plain "Get directions" link in the drawer instead, which is the useful form
   of this when you cannot scan your own screen.

   The panel hangs from the button's right edge and is capped at the viewport
   width, so it cannot push a scrollbar on a narrow window. It needs no
   stacking-context gymnastics: the header is sticky at z-index 60 with no
   transform and no overflow clip, so an absolutely positioned child simply
   sits over the page. */
.nav__qr { position: relative; flex: 0 0 auto; }
.nav__qr > summary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border: 1px solid var(--colour-grey-500); border-radius: 2px;
  color: var(--colour-text); cursor: pointer; list-style: none;
}
.nav__qr > summary::-webkit-details-marker { display: none; }
.nav__qr > summary::marker { content: ""; }
.nav__qr > summary svg { width: 19px; height: 19px; fill: currentColor; }
.nav__qr > summary:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.nav__qr > summary:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--focus-halo), 0 0 0 6px var(--focus-ink);
}
.nav__qr[open] > summary {
  border-color: var(--colour-primary-active); color: var(--colour-primary-active);
  background: var(--colour-brand-teal-wash);
}
.nav__qr-panel {
  position: absolute; top: calc(100% + 0.7rem); right: 0; z-index: 70;
  width: 16.5rem; max-width: calc(100vw - 2rem);
  padding: 1rem 1rem 0.9rem;
  background: var(--colour-bg);
  border: 1px solid var(--colour-grey-500); border-radius: 3px;
  box-shadow: 0 14px 38px rgba(32, 30, 26, 0.20);
  text-align: center;
}
.nav__qr-title {
  margin: 0; font-weight: 600;
  font-size: var(--fs-caption); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--colour-text);
}
.nav__qr-code {
  display: block; width: 150px; height: 150px;
  margin: 0.7rem auto 0.55rem;
  background: #fff; border-radius: 2px;
}
.nav__qr-hint { margin: 0 0 0.55rem; font-size: 0.9rem; color: var(--colour-text); }
.nav__qr-addr {
  margin: 0 0 0.75rem; font-size: 0.82rem; line-height: 1.45;
  color: var(--ink-quiet);
}
.nav__qr-link {
  display: block; padding: 0.5rem 0.6rem;
  border: 1px solid var(--colour-primary); border-radius: 2px;
  color: var(--link-hover); text-decoration: none; font-size: 0.9rem; font-weight: 600;
}
.nav__qr-link:hover { background: var(--colour-brand-teal-wash); }
.nav__qr-alt {
  display: inline-block; margin-top: 0.6rem;
  font-size: 0.82rem; color: var(--colour-text);
}

/* The dock used to hide both actions behind a toggle: two taps to reach
   WhatsApp. It is now two standing buttons — one tap — with the telephone
   beneath. The toggle, its label and its script hook are gone rather than left
   hidden, so nav.js no longer has a dock to synchronise. */
.contact-dock {
  position: fixed; right: max(1rem, env(safe-area-inset-right)); bottom: max(1rem, env(safe-area-inset-bottom));
  z-index: 80; display: flex; flex-direction: column; align-items: flex-end; gap: 0.6rem;
}
.dock-fab-link {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0 1.05rem; height: 52px; border-radius: 26px;
  font-size: var(--fs-small); font-weight: 600; letter-spacing: 0.02em;
  text-decoration: none; color: #fff; white-space: nowrap;
  box-shadow: 0 1px 2px rgba(32,30,26,.12), 0 6px 16px rgba(32,30,26,.18);
}
.dock-fab-link svg { width: 22px; height: 22px; fill: currentColor; flex: 0 0 auto; }
.dock-fab-link--wa { background: var(--colour-primary-active); }          /* white 7.17:1 */
.dock-fab-link--wa:hover { background: var(--colour-primary-dark); color: #fff; }
.dock-fab-link--wa:active { background: #003838; color: #fff; }           /* white 12.94:1 */
.dock-fab-link--call { background: var(--colour-navy); }                  /* white 15.32:1 */
.dock-fab-link--call:hover { background: #322e29; color: #fff; }
.dock-fab-link--call:active { background: #413c35; color: #fff; }
.dock-fab-link:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-halo), 0 0 0 6px var(--focus-ink), 0 6px 16px rgba(32,30,26,.18); }
/* On a phone a labelled pill pair crowds the thumb zone and can cover content,
   so below 520px the two collapse to round icon buttons. The accessible name
   is on aria-label, not on the visible text, so nothing is lost when the text
   is. */
@media (max-width: 519px) {
  .dock-fab-link { width: 52px; padding: 0; justify-content: center; }
  .dock-fab-link span { display: none; }
}
/* The open-drawer stand-down rule is unchanged and still lives in the block
   above: `html:has(.nav__toggle:checked) .contact-dock { display: none; }`. */

/* --------------------- the measure rule, and the new list -------------------
   `main p, main dd, main li { max-width: 66ch }` at the top of this file is a
   deliberate global: it caps every LEAF text block so a collapsed two-column
   band cannot run 90 characters wide. It is right, and it stays.

   But three of the components added in this round are built on <li> and are not
   leaf text blocks at all — they are layout boxes that happen to be list items.
   The 66ch cap was landing on them and clamping the carousel slide to 590px
   inside a 1072px track, which is why two slides showed at once. Anything whose
   width is set by the layout, rather than by how many characters should sit on
   a line, opts out here. The text INSIDE them is still capped, by .tl__note
   (52ch), .banner__copy p (40ch) and .blist's own 66ch, which is where the rule
   belongs. */
main .banner__slide,
main .banner__dots li,
main .tx__item,
main .tl__item,
main .reel__item { max-width: none; }

/* --------------------- round-1 corrections, same session ------------------ */

/* The slides share one height, set by the tallest of the four. A short slide
   was pinning its content to the top and leaving the difference as a hole
   underneath. Centring the content in the slide splits that difference above
   and below, where it reads as air rather than as a gap. */
.banner__slide { display: grid; align-content: center; }

/* The fourth slide is a single 4:3 interior against three slides of near-square
   pairs, which made it a third taller than the rest and set that height for all
   four. Banding it to the pairs' proportion keeps the carousel from jumping. */
.banner__figure > img { aspect-ratio: 16 / 10; object-fit: cover; }

/* WhatsApp is the primary action, so it sits LOWEST — nearest the thumb on a
   phone. It stays first in the DOM, where it is first for a keyboard and a
   screen reader too; only the visual order is reversed. */
.contact-dock { flex-direction: column-reverse; }

/* The timeline reveals itself on scroll, which means every entry below the fold
   is legitimately at opacity 0 until it is scrolled to. On paper there is no
   scrolling and no fold, so a printed page would come out blank where the
   record should be. */
@media print {
  .tl__item { opacity: 1 !important; transform: none !important; animation: none !important; }
  .banner__track { display: block !important; overflow: visible !important; }
  .banner__slide { break-inside: avoid; }
  .reel { display: none; }
  .contact-dock { display: none; }
}

/* ===========================================================================
   BRAND ENERGY LAYER  —  added 2026-08-20 at the practice's request
   ---------------------------------------------------------------------------
   The brief: the site read "plain, white, text-heavy, like a CV". The practice
   asked for the visual language of their PREVIOUS site — teal as a real
   identity rather than an occasional accent, arcs and circular photo masks,
   diagonal shapes, photographs built into the design — brought forward rather
   than copied. Their words: "modernize those visual characteristics within the
   current website structure."

   What this layer may NOT do, and does not:
     - invent colours. Everything here resolves to a token whose contrast was
       measured in tokens.css. The one rule that matters: white normal-size
       text never sits on --colour-brand-teal (4.45:1); filled controls and
       filled bands use --colour-primary, whose white-on-colour is verified.
     - move content. Not one sentence, page, nav item or link changes here.
     - animate without an off switch. Every motion below is disabled outright
       under prefers-reduced-motion, and the marquee degrades to a static,
       scrollable band rather than freezing mid-travel.
--------------------------------------------------------------------------- */

/* --- 1. the arc ---------------------------------------------------------
   Their signature: a photograph in a circle, ringed by a teal arc that is
   deliberately BROKEN — two sides of the ring are drawn, two are not, and the
   whole thing is rotated. Drawn in CSS so it stays crisp at any size and
   costs no bytes. */
.arcframe { position: relative; display: inline-block; line-height: 0; }
.arcframe img { border-radius: 50%; position: relative; z-index: 1; }
.arcframe::before,
.arcframe::after { content: ""; position: absolute; border-radius: 50%; pointer-events: none; }
/* the closed inner ring, tight to the photograph */
.arcframe::before {
  inset: -7px; border: 3px solid var(--colour-brand-teal); opacity: 0.9;
}
/* the broken outer arc, rotated so the gap falls off-axis */
.arcframe::after {
  inset: -17px; border: 3px solid transparent;
  border-top-color: var(--colour-brand-teal);
  border-right-color: var(--colour-brand-teal);
  transform: rotate(-28deg);
}
.arcframe--flip::after { transform: rotate(152deg); }

/* --- 3. the hero -------------------------------------------------------- */
.hero { position: relative; overflow: clip; }
/* A large, very quiet teal disc behind the portrait column, and a smaller
   gold one — the "dynamic shapes" of the old site, at a weight that stays
   behind the words rather than competing with them. */
.hero::before {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  width: clamp(260px, 30vw, 450px); aspect-ratio: 1;
  right: 1%; top: 3%; border-radius: 50%;
  background: var(--colour-brand-teal-wash);
  opacity: 0.9;
}
/* Held fully INSIDE the container on purpose. Bled past the edge it would be
   sliced flat by the clip below and read as a rendering fault rather than a
   shape; and below 880px the row stacks, where a wash behind the headline is
   clutter on a small screen — which is the one thing the brief said not to do. */
@media (max-width: 879px) { .hero::before { display: none; } }
.hero > * { position: relative; z-index: 1; }
.hero__figure { position: relative; }
/* The portrait sits in an arch inside a solid teal frame - "teal frames or
   accents" and "rounded/organic cropping", both named in the brief - with the
   pale disc above showing past its shoulder for depth.
   Two things were tried and rejected before this. An absolutely-positioned
   ring on the FIGURE drew a rectangle around the caption text and a stray line
   across the photograph, because the figure contains the caption too. An
   offset second arch (box-shadow) then read as a printing misregistration: at
   any offset small enough not to unbalance the column, it showed only as a
   thin crescent down one side. A frame that closes is unmistakably deliberate,
   and it is the thing they actually asked for. */
.hero__figure img {
  border-radius: 999px 999px 16px 16px;
  border: 10px solid var(--colour-brand-teal);
}
/* clearance for the offset arch, so the caption never sits on top of it */
.hero__figure figcaption { margin-top: 1.9rem; }
@media (max-width: 700px) {
  .hero__figure img { border-radius: 220px 220px 14px 14px; border-width: 7px; }
  .hero__figure figcaption { margin-top: 1.4rem; }
}
/* the rule above the headline, two-tone the way their old banners drew it */
.hero__eyebrow-rule { display: block; width: 76px; height: 4px; margin: 0 0 1.1rem;
  background: linear-gradient(90deg, var(--colour-navy) 0 46%, var(--colour-brand-teal) 46% 100%); }

/* --- 4. section rhythm --------------------------------------------------
   The alternating ground is now white / light teal rather than white / warm
   paper; the marquee below is the one full-strength teal band. */

/* --- 5. the caption marquee --------------------------------------------
   "a short horizontal scrolling at the bottom of the website at all the time".
   A band above the footer on every page, carrying the practice's own claims.
   Two identical halves travel left; when the first has fully left, the second
   is exactly where the first began, so the loop is seamless with no script. */
.marquee { background: var(--colour-primary); color: #fff; overflow: hidden; padding: 0.85rem 0; }
.marquee__track {
  display: flex; width: max-content; gap: 0;
  animation: marqueeScroll 46s linear infinite;
}
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }
.marquee__group { display: flex; flex: 0 0 auto; }
.marquee__item {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 0.9rem;
  padding-inline: clamp(1.1rem, 2.6vw, 2.1rem);
  font-family: var(--font-body); font-weight: 600; font-size: var(--fs-small);
  letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap; color: #fff;
}
/* the separator is the old site's small cross motif, not a bullet */
.marquee__item::after {
  content: ""; width: 9px; height: 9px; flex: 0 0 auto; opacity: 0.65;
  background:
    linear-gradient(#fff, #fff) center / 100% 3px no-repeat,
    linear-gradient(#fff, #fff) center / 3px 100% no-repeat;
}
@keyframes marqueeScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  /* Not "instant animation" — that parks the band at the END of its travel and
     shows a half-empty strip. Stop it, let the row scroll by hand instead. */
  .marquee__track { animation: none; width: 100%; overflow-x: auto; }
  .marquee__group:nth-child(2) { display: none; }
}

/* --- 6. the banner carousel, restyled ----------------------------------
   Same component, same behaviour, dressed in the brand: a soft teal ground,
   rounded frames, and teal headings instead of a hairline-and-paper box. */
.banner__frame { border-top: 0; border-radius: 22px; background: var(--colour-surface); }
.banner__inner { padding: clamp(1.1rem, 2.4vw, 1.9rem); }
.banner__figure > img,
.banner__pair { border-radius: 16px; overflow: hidden; }
.banner__figure > img { border: 0; }
.banner__pair { border: 0; background: var(--colour-border); }
.banner__copy h2 { color: var(--colour-brand-teal); }
.banner__copy .eyebrow { color: var(--colour-accent-hover); }
.banner__dot[aria-current="true"]::before { background: var(--colour-primary); }
/* A photograph that fills its slide rather than sitting in a letterboxed box,
   cut with the sweep: a large radius on one diagonal and a small one on the
   other, so the landscape frames carry the same curved language as the arched
   portraits without repeating the arch. */
.banner__figure--full > img {
  width: 100%; height: clamp(220px, 34vw, 380px); object-fit: cover;
  border-radius: 120px 14px 120px 14px;
}
@media (max-width: 700px) { .banner__figure--full > img { border-radius: 64px 12px 64px 12px; } }

/* the circular-medallion slides: the old site's signature shape, given room to
   breathe. The arc rings sit 17px outside the photograph, so the figure needs
   that much clearance or the strip clips them. */
.banner__figure--round { display: flex; flex-direction: column; align-items: center; padding: 20px 20px 0; }
.banner__figure--round .arcframe { width: min(300px, 62vw); }
.banner__figure--round .arcframe img { width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; }
.banner__figure--round .banner__figcap { text-align: center; margin-top: 1.4rem; }

/* --- 8. buttons: a little more presence --------------------------------- */
.btn { border-radius: 999px; transition: transform 140ms ease, background-color 140ms ease, border-color 140ms ease; }
.btn--primary:hover { transform: translateY(-1px); }
.btn--ghost { border-color: var(--colour-primary); color: var(--colour-primary-active); }
@media (prefers-reduced-motion: reduce) { .btn:hover { transform: none; } }

/* --- 9. entrance motion -------------------------------------------------
   Opt-in per element via data-rise, and DELIBERATELY not a `.js`-gated
   opacity:0. Both scripts on this site are deferred, so a CSS-only gate would
   paint the section, then blank it the moment the flag landed, then fade it
   back — a flash on every load. Instead motion.js arms only the elements that
   are still BELOW the fold when it runs; anything already on screen is never
   hidden for an instant, and an element the script never reaches stays
   visible. A blocked script cannot leave this page blank. */
[data-rise] { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  [data-rise][data-rise-armed] {
    opacity: 0; transform: translateY(14px);
    transition: opacity 620ms ease, transform 620ms ease;
  }
  [data-rise][data-rise-armed].is-in { opacity: 1; transform: none; }
}
/* Belt to the script's braces: on paper nothing is ever scrolled into view. */
@media print {
  [data-rise][data-rise-armed] { opacity: 1 !important; transform: none !important; }
}

/* ===========================================================================
   ROUND 3 - 2026-08-20. The practice's second set of change requests.

   Four asks, in their words: the banners should open the page at full screen
   and move on their own; the gallery photographs should enlarge when tapped;
   the scrolling claims bar should stay on screen instead of scrolling away;
   and the whole thing should be more colourful.

   The two judgement calls, recorded here because the next person will wonder:

   1. THE CLAIMS BAR IS PINNED TO THE BOTTOM, AND EVERYTHING ELSE MOVED UP FOR
      IT. They asked for the bar that "runs at the bottom" to stay put, so it is
      fixed at the bottom, which is the literal reading. But the WhatsApp and
      call dock was already fixed there and is the control that actually earns
      this practice appointments - a bar dropped on top of it would have covered
      the one thing we would never want covered. So the bar owns the bottom
      edge, and the dock, the slideshow controls and the end of every page are
      all offset by exactly its height via --marquee-h. Change that variable and
      all four move together.

   2. WHITE TYPE SITS ON A WASH, NEVER ON A BARE PHOTOGRAPH. The six hero
      pictures are their own and vary a lot in brightness; the waiting room is
      nearly white in the top third. Every slide therefore carries its own
      gradient wash, weighted to the bottom where the type is, and the headline
      also has a shadow. Checked against the LIGHTEST of the six, not an
      average.
   =========================================================================== */

:root {
  /* The height of the pinned claims bar: 0.85rem padding top and bottom plus
     one line of --fs-small uppercase. Everything that has to clear the bar
     reads this, so the four offsets can never drift apart. */
  --marquee-h: calc(2.85rem + env(safe-area-inset-bottom, 0px));
  /* The sticky header's own height: 1.15rem of padding top and bottom, the
     128px-wide brand lockup at its natural 57.7px, and the hairline. The hero
     subtracts it so that header + hero is exactly one screen and the slideshow
     controls are never pushed below the fold. */
  --hdr-h: 95px;
}
@media (min-width: 1080px) { :root { --hdr-h: 105px; } }

/* --- 1. the hero slideshow ----------------------------------------------
   Driven by the SAME motion.js carousel as the old strip ([data-banner]) - it
   reads its position from scrollLeft rather than holding it in a variable, so
   a swipe, an arrow key, a dot and the timer cannot disagree. With the script
   blocked this degrades to a horizontally swipeable band of six photographs
   with the headline over the first: nothing is display:none, nothing is
   stacked on anything, and the page still says what it is. */
.stage {
  position: relative;
  height: calc(100svh - var(--hdr-h));
  min-height: 30rem;
  background: var(--colour-primary-dark);   /* what shows for the instant before the first photograph paints */
  overflow: hidden;
}
/* The 66ch measure cap at the top of this file lands on every <li> in <main>,
   and these two are layout boxes that merely happen to be list items - not leaf
   text blocks. Round 1 hit this exactly: the cap clamped a carousel slide to
   590px inside a 1072px track and put two slides on screen at once. It did it
   again here, at the same 590px, before this line existed. Anything whose width
   is decided by the layout rather than by how many characters belong on a line
   opts out; the text inside them is still capped where it should be. */
main .stage__slide,
main .why__item { max-width: none; }

.stage__frame { position: absolute; inset: 0; }
.stage__track {
  display: flex; height: 100%; margin: 0; padding: 0; list-style: none;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; -ms-overflow-style: none;
  overscroll-behavior-x: contain;
}
.stage__track::-webkit-scrollbar { display: none; }
.stage__track:focus-visible { outline: 3px solid var(--focus-ink); outline-offset: -3px; }
/* Sized exactly like the banner strip this replaced - flex-basis and nothing
   else. Giving the slide its own width AND height as well looked harmless and
   was not: with a percentage basis, an in-flow image carrying an aspect-ratio,
   and a percentage height all pointing at each other, the basis stopped
   resolving against the track and every slide came out 590px instead of a full
   1425, so four of the six were on screen at once. The image is taken out of
   flow below for the same reason - an absolutely positioned child contributes
   nothing to its parent's intrinsic size, so the cycle cannot re-form. */
.stage__slide {
  position: relative; flex: 0 0 100%; min-width: 0;
  scroll-snap-align: start; scroll-snap-stop: always;
}
.stage__slide img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; display: block;
  object-fit: cover;
  /* Per-slide, because the principal's portrait is a tall frame and a centred
     crop takes the top of his head off. --focal moves the anchor up for that
     one; the five room photographs are landscape and want the default.

     His slide is set to 6%, and the number is load-bearing rather than taste.
     The hero is as wide as the window and only 100svh minus the header tall, so
     its aspect swings from about 0.5 on a phone to nearly 3 on a short desktop
     window, and a portrait source cropped to cover loses more of its top the
     wider and shorter that gets. What bounds the damage is that the offset is a
     percentage of the HIDDEN height, so the worst cut any shape can produce is
     focal x the image's own height: 18% allowed 280px and clipped his scalp at
     1366x768 and 1920x800, both ordinary laptop shapes; 6% caps it at 93px and
     his hairline sits about 165px down. Checked at eleven viewport shapes, not
     two — the two that were checked first were both safe, which is exactly why
     it shipped. */
  object-position: var(--focal, 50% 50%);
}
.stage__veil {
  position: absolute; inset: 0; pointer-events: none;
  /* Round 6, 2026-08-24. The words moved to the top right corner, so the
     darkening had to move with them: this was a single bottom-heavy gradient,
     which is precisely where the caption no longer is, and it would have left
     white type sitting on the brightest ceiling in every photograph.

     Two layers now. A wedge anchored in the top right, deep enough to carry
     the headline over a lit ceiling and gone by the middle of the frame; and a
     light wash down the whole picture that keeps the dots and Pause legible
     along the bottom edge and stops the photograph meeting the caption band
     below on a hard line.

     Both are weaker than the one they replace - 0.86 along the bottom is now
     0.62 - and that is the point rather than an accident. The practice asked
     for the photograph to be the loudest thing in the frame, and every point
     of scrim is taken straight off the photograph. */
  background:
    linear-gradient(to bottom left,
      rgba(16, 24, 24, 0.76) 0%,
      rgba(16, 24, 24, 0.50) 20%,
      rgba(16, 24, 24, 0.18) 42%,
      rgba(16, 24, 24, 0) 62%),
    linear-gradient(to bottom,
      rgba(16, 24, 24, 0.20) 0%,
      rgba(16, 24, 24, 0.10) 38%,
      rgba(16, 24, 24, 0.32) 74%,
      rgba(16, 24, 24, 0.62) 100%);
}
/* There is still no visible per-slide caption, and that is a decision rather
   than an omission. Every photograph is described in its alt text, which is
   where a caption's real work is done, and a line that changed every four
   seconds under a headline that does not would read as noise. The corners are
   spoken for in any case: bottom right is the WhatsApp and call dock - the one
   control on this page that books an appointment - bottom left is the dots and
   Pause, and the top right is now the headline itself. */

/* The headline does NOT change between slides. One that swapped every four
   seconds could not be read, and the page needs one stable H1 to have a
   subject at all. The layer ignores the pointer so a swipe still reaches the
   photographs underneath; only the two buttons take it back. */
.stage__over {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: flex-start;
  padding-top: clamp(1.3rem, 4.5vh, 3.1rem);
  pointer-events: none;
}
.stage__over > .container { width: 100%; }
.stage__over a, .stage__over button { pointer-events: auto; }

/* The caption is pinned to the RIGHT edge of the page container rather than
   the right edge of the window, so its margin is the same one the logo and
   the nav are set to and it reads as part of the page rather than as a label
   dropped on the photograph. */
.stage__caption {
  margin-left: auto;
  max-width: min(34rem, 58%);
  text-align: right;
  display: flex; flex-direction: column; align-items: flex-end;
}
.stage__title {
  color: #fff; margin: 0.5rem 0 0;
  font-family: var(--font-display, inherit);
  /* Down from clamp(2.15rem, 6vw, 4.15rem). The practice's note was that the
     photograph should be the highlight and the words minimal, and a headline
     that spans half the window is not minimal whichever corner it is in. */
  font-size: clamp(1.6rem, 3.3vw, 2.85rem);
  line-height: 1.06; letter-spacing: -0.015em;
  text-shadow: 0 2px 18px rgba(10, 16, 16, 0.55);
  max-width: 16ch;
}
.stage__title .accentline { display: block; color: var(--colour-primary-subtle); }
.eyebrow--over { color: var(--colour-primary-subtle); margin: 0; }
.stage__over .hero__actions { margin-top: 1.25rem; justify-content: flex-end; }

/* The ghost button's grey border and dark label vanish on a photograph, so the
   hero gets its own variant. White on the wash clears AA comfortably; the
   translucent fill is there so it still reads over the lightest slide. */
.btn--over {
  background: rgba(255, 255, 255, 0.14); color: #fff;
  border-color: rgba(255, 255, 255, 0.75);
  backdrop-filter: blur(3px);
}
.btn--over:hover { background: rgba(255, 255, 255, 0.26); border-color: #fff; color: #fff; }
.btn--over:active { background: rgba(255, 255, 255, 0.34); color: #fff; }

/* Hidden until motion.js sets the .js flag, exactly as the old strip's controls
   were: with no script the dots navigate nothing and Pause pauses nothing, and
   dead controls are worse than none. The track itself stays swipeable and
   tabbable either way. */
.stage__controls { display: none; }
.js .stage__controls {
  display: block;
  position: absolute; left: 0; right: 0; z-index: 3;
  bottom: calc(var(--marquee-h) + clamp(0.9rem, 2.4vh, 1.5rem));
}
/* Dots and Pause sit together on the LEFT at every width, rather than pushing
   Pause out to the right edge. Spread apart they collided with the contact dock
   across roughly 620-1400px: the row is bounded by the page container while the
   dock is offset from the VIEWPORT, so the gap between them closes as the window
   narrows and Pause ends up underneath the WhatsApp button. Answering that with
   another breakpoint would only move the seam; keeping the controls away from
   the corner the dock owns removes it. */
.stage__controls .container {
  display: flex; align-items: center; justify-content: flex-start; gap: 1.25rem;
}
/* Round 14, 2026-08-31. The banner went from eight pictures to sixteen, and
   sixteen dashes do not fit across a phone: at the 22px they narrow to below
   619px the row alone wants about 450px, and a 360px screen has roughly 320px
   to give it, so Pause was pushed off the side. They wrap instead of shrinking
   because the dash is already the smaller half of a 44px touch target - taking
   width off it to win a single row would make the control harder to hit, which
   is the opposite of what round 9 widened it for. Nothing moves on a desktop
   window, where all sixteen still sit on one line with room to spare; the row
   only breaks up on a small phone, where it measures three rows of dashes at
   360px, and it grows upward into the scrim rather than down into the marquee,
   because the bar is anchored by its bottom edge. */
.stage__controls .banner__dots { margin: 0; flex-wrap: wrap; }
.stage__controls .banner__dot::before { background: rgba(255, 255, 255, 0.5); }
.stage__controls .banner__dot:hover::before { background: rgba(255, 255, 255, 0.8); }
.stage__controls .banner__dot[aria-current="true"]::before { background: #fff; height: 3px; }
.stage__controls .banner__pause {
  display: inline-flex; color: #fff; border-color: rgba(255, 255, 255, 0.55);
  background: rgba(16, 24, 24, 0.35); backdrop-filter: blur(3px);
}
.stage__controls .banner__pause:hover { border-color: #fff; color: #fff; background: rgba(16, 24, 24, 0.5); }

@media (max-width: 860px) {
  .stage__title { max-width: none; }
}
/* The caption is held to 58% of the container so it never runs across the
   middle of a face on a desktop window. On a phone the hero is taller than it
   is wide, there is no middle to protect, and 58% would wrap the headline one
   word per line - so it takes the full width and stays right-aligned. */
@media (max-width: 700px) {
  .stage__caption { max-width: 100%; }
}
/* Below this width the contact dock collapses to two round buttons in the
   bottom right corner, and Pause - which was over on that side - ended up
   underneath them. Both controls move to the left, where the only thing they
   share the corner with is each other. The dock keeps the corner: it is the
   button that books appointments, and nothing gets to sit on it. */
@media (max-width: 619px) {
  /* Six dashes at their desktop width leave Pause a long way from the dots on a
     phone; the row tightens instead. */
  .stage__controls .container { gap: 1rem; }
  .stage__controls .banner__dot { width: 22px; }
  .stage__controls .banner__dot::before { width: 18px; }
  /* Round 14, 2026-08-31. Moving the controls left in round 6 got Pause out of
     the dock's way at the widths that were breaking then, but it never cleared
     the corner on a small phone: at 360px Pause has been sitting across the
     WhatsApp button since long before this round - measured at x257-319 against
     a dock at x277-329, so the right third of it could not be pressed. Sixteen
     dashes made a standing bug into an obvious one by pushing Pause further
     right still, which is why it is being fixed here rather than left.

     Two lines do it. The row now keeps a hand's width clear on the right so
     nothing can reach under the dock - the dock is offset from the WINDOW while
     this row is bounded by the page container, so the two can only be kept
     apart by reserving the space - and Pause is ordered ahead of the dashes so
     the control that has to be hit sits at the far left, furthest from the
     corner, rather than being carried rightward every time a slide is added.
     The dashes take whatever is left and wrap into it. */
  .stage__controls .container { padding-right: 5rem; }
  .stage__controls .banner__pause { order: -1; }
}

/* These dots are now the control for the first thing on the page, on a phone,
   and 24px of height is a poor thing to ask a thumb to find. The dash it draws
   does not change; only the box around it does. */
.stage__controls .banner__dot { height: 44px; }
@media (prefers-reduced-motion: reduce) {
  .stage__track { scroll-behavior: auto; }
}

/* --- 2. the band that used to sit under the headline --------------------- */
.intro { background: var(--colour-brand-teal-wash); padding: clamp(1.9rem, 4vw, 2.9rem) 0; }
.intro__row {
  display: grid; grid-template-columns: 1fr; gap: clamp(1rem, 2.5vw, 2rem);
  align-items: start;
}
.intro__claims { margin: 0; }
.intro__where { margin: 0; align-self: end; }
@media (min-width: 900px) {
  .intro__row { grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); }
}

/* --- 3. the three claims the slide strip made and nothing else does ------ */
.why {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: 1fr; gap: clamp(1.6rem, 3vw, 2.4rem);
}
.why__item { border-top: 3px solid var(--colour-brand-teal); padding-top: 1.1rem; }
/* Two of these three photographs were the practice's own and, once the slide
   strip became the hero, were left displayed nowhere on the site: the principal
   with a young patient, and their radiography room. Each one is now evidence
   for the claim it sits under, which is where it was always doing the most
   work. (The third slide photograph, the surgical team, is deliberately NOT
   brought back - a note on treatments.html records that it was reconstructed
   from a promotional banner and nobody has established it is their theatre. The
   home page should not assert what that page declines to.) */
.why__img {
  width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
  object-position: var(--focal, 50% 50%);
  border-radius: 2px; margin-bottom: 1.1rem;
}
.why__title {
  margin: 0.5rem 0 0.9rem;
  font-size: clamp(1.15rem, 1.9vw, 1.4rem); line-height: 1.25;
  max-width: 26ch;
}
/* Round 6: a fourth card (the intraoral scanner) arrived, and four cards in a
   three-column grid leave the last one alone on a second row looking like an
   afterthought. Two-up from 780 and four-up from 1120 both divide four
   exactly. 1120 rather than the container's own 1160 so the switch happens
   just before the grid stops growing, and a 1160 window gets four columns of
   about 260px rather than two of 540. */
@media (min-width: 780px)  { .why { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1120px) { .why { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* --- 4. the claims bar, pinned ------------------------------------------
   It was above the footer and scrolled away with it. It is now fixed to the
   bottom edge on every page. Three consequences, all handled from --marquee-h:
   the document gets that much padding so the footer's last line is never
   underneath it, the contact dock rides above it, and the slideshow controls
   clear it too (see .stage__controls). */
body { padding-bottom: var(--marquee-h); }
.marquee {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 70;
  height: var(--marquee-h);
  padding: 0 0 env(safe-area-inset-bottom, 0px);
  display: flex; align-items: center;
  box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.14), 0 -6px 18px rgba(20, 24, 24, 0.18);
}
.marquee__track { align-items: center; }
.contact-dock { bottom: calc(var(--marquee-h) + 0.85rem); }
/* Not part of the change request, and fixed anyway because it is the second
   most important control on the site: the call button is filled with
   --colour-navy, which is very nearly the colour of the footer, so over the
   footer it disappeared and left a white telephone glyph floating on nothing.
   A hairline of light keeps its shape on any ground; the WhatsApp button is
   teal and never had the problem, but it takes the same rule so the pair still
   look like a pair. */
.dock-fab-link { border: 1px solid rgba(255, 255, 255, 0.28); }
/* The drawer already stands the dock down; the bar goes with it, or it sits on
   top of an open menu. */
html:has(.nav__toggle:checked) .marquee { display: none; }
@media print { .marquee { position: static; height: auto; padding: 0.85rem 0; } body { padding-bottom: 0; } }

/* --- 5. tap to enlarge ---------------------------------------------------
   Pure CSS, on :target, because everything else interactive on this site works
   with the scripts blocked and this should too. Each panel sits in the source
   directly after the photograph that opens it, so tabbing off the trigger
   lands on Close rather than travelling the length of the page.

   The trigger is a link and the closer is a link, so it opens and closes from
   the keyboard alone. Escape does not close it - that needs script - so Close
   is always visible rather than a corner glyph. */
.zoom { display: block; cursor: zoom-in; border: 0; }
.zoom:focus-visible { outline: 3px solid var(--focus-ink); outline-offset: 3px; }
.lightbox {
  position: fixed; inset: 0; z-index: 130;
  display: none;
  background: rgba(14, 20, 20, 0.94);
  padding: var(--lb-pad-y) var(--lb-pad-x) var(--lb-pad-b);
  align-items: center; justify-content: center;
  --lb-pad-y: clamp(3.5rem, 8vh, 5rem);
  --lb-pad-b: clamp(1.5rem, 5vh, 3rem);
  --lb-pad-x: clamp(1rem, 4vw, 3rem);
}
.lightbox:target { display: flex; }
/* The cap is written out in viewport units rather than left as max-height:100%.
   A percentage max-height only means anything against a DEFINITE height, and
   the first version of this was a grid whose single row was sized by the very
   image it was meant to be constraining - so the percentage was ignored and a
   portrait plate opened at natural size with the subject's mouth below the fold.
   Spelling out the arithmetic cannot go circular. */
.lightbox__img {
  max-width: calc(100vw - 2 * var(--lb-pad-x));
  max-height: calc(100svh - var(--lb-pad-y) - var(--lb-pad-b));
  width: auto; height: auto;
  object-fit: contain; border-radius: 2px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.lightbox__close {
  position: absolute; top: clamp(0.75rem, 2.5vh, 1.5rem); right: clamp(0.75rem, 3vw, 1.75rem);
  display: inline-flex; align-items: center; gap: 0.45rem;
  padding: 0.6rem 1.15rem; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.6); background: rgba(255, 255, 255, 0.1);
  color: #fff; text-decoration: none;
  font-family: var(--font-body); font-size: var(--fs-small); font-weight: 600;
}
.lightbox__close:hover { background: rgba(255, 255, 255, 0.22); border-color: #fff; color: #fff; }
.lightbox__close:focus-visible { outline: 3px solid #fff; outline-offset: 2px; }
/* A backdrop click closes it too: the same link, stretched behind the picture
   but under the Close button and the picture itself. */
.lightbox__sheet { position: absolute; inset: 0; }

/* --- 6. more colour, as asked -------------------------------------------
   Their words were "more colorful and attractive and appealing" with "the teal
   colour the highlighted one". Teal was already the accent, so this is MORE of
   it rather than a second hue: every other section now sits on the teal wash
   that carries a memory of their old site, and the treatment icons come up from
   near-black teal to the brand teal itself.

   Two things deliberately NOT recoloured. The section numerals and their long
   rules stay #c06600: that warm rule running nine hundred pixels across the
   page is the signature of the design, and teal-ing it would take colour OUT of
   the page while claiming to add it. And the statistics are already brand teal.

   Both pairings below are ones the palette note at the top of tokens.css has
   already measured: body text on #DCEFEE is 7.71:1, and #0D8586 on it is
   3.74:1, which is over the 3:1 floor these icons are held to as graphics. */
.section--surface { background: var(--colour-brand-teal-wash); }
/* This mask hides the timeline's spine behind each marker, so it has to be the
   section's colour, not a fixed one - it moves with the surface above. */
.section--surface .tl__item::before { background: var(--colour-brand-teal-wash); }
.tx__icon .s { stroke: var(--colour-brand-teal); }

/* ===========================================================================
   ROUND 4 - 2026-08-21. The practice's third set of change requests.

   Five asks, in their words: the gallery should carry all the images the old
   site had; the pictures should be better quality; the gallery should enlarge
   on tap; the WhatsApp button should get bigger as you scroll; and the section
   called "What we treat" should have something animated in it, "because it is
   very static".

   Three of those were answered in the HTML and in the image files themselves -
   twenty-seven plates the first build never carried across, every plate put
   back to the original file rather than the compressed copy of it, and
   enlarge-on-tap extended to the treatment pages, which never had it. The two
   that needed CSS are below.

   The judgement call worth recording: WHAT GROWS, AND WHAT DOES NOT. They asked
   for the WhatsApp button to enlarge. Enlarging BOTH controls would have moved
   the telephone up the screen and squeezed the claims bar, and it would have
   said the two actions are equally important, which is not what this practice's
   own dock says - WhatsApp sits lowest, nearest the thumb, precisely because it
   is the one that earns appointments. So only WhatsApp grows, and the call
   button holds its size. The gap between them is now the hierarchy, visibly.
   =========================================================================== */

/* --- 1. the WhatsApp control grows once you are reading ---------------------
   Not on load. A visitor who has just arrived has not asked us anything yet,
   and a large button shouting at them over the top of the hero is the thing
   every small-business site does that this one has avoided. It grows once they
   have scrolled past the first screen - which is to say, once they are looking
   through the work rather than deciding whether to. `is-prominent` is set by
   motion.js; with the script blocked the control simply stays its usual size,
   which is a working dock, not a broken one. */
@media (prefers-reduced-motion: no-preference) {
  .dock-fab-link { transition: height 260ms ease, padding 260ms ease, border-radius 260ms ease; }
  .dock-fab-link svg { transition: width 260ms ease, height 260ms ease; }
}
.contact-dock.is-prominent .dock-fab-link--wa {
  height: 64px; border-radius: 32px; padding: 0 1.35rem;
  font-size: var(--fs-body);
  box-shadow: 0 2px 4px rgba(32, 30, 26, .16), 0 10px 24px rgba(32, 30, 26, .24);
}
.contact-dock.is-prominent .dock-fab-link--wa svg { width: 28px; height: 28px; }
/* On a phone the pair are round glyphs with their labels hidden. When WhatsApp
   grows it takes its word back: a 64px teal circle is just a bigger mystery,
   whereas "WhatsApp" is an instruction. The call button stays a glyph, so the
   two never read as a pair of equal buttons. */
@media (max-width: 519px) {
  .contact-dock.is-prominent .dock-fab-link--wa { width: auto; padding: 0 1.2rem; justify-content: flex-start; }
  .contact-dock.is-prominent .dock-fab-link--wa span { display: inline; }
}

/* --- 2. "What we treat" stops sitting still --------------------------------
   Their note was that the section is very static, and it was: twelve rows of
   an icon and two lines of text, all arriving at once and then doing nothing.

   Two things happen now, both hung off the reveal the page already had, so
   there is no new script and nothing new to fail. The rows come in one after
   another rather than as a block, and each icon DRAWS ITSELF - these are line
   drawings, so the stroke can be paid out from nothing, which is the one piece
   of movement that is actually about dentistry rather than decoration.

   Every path in those icons carries pathLength="1" so that a single dashoffset
   animation draws a straight rule and a curled tooth outline at the same rate;
   without it the short paths snap in and the long ones crawl.

   Both are inside prefers-reduced-motion: no-preference, and both are keyed to
   `.is-in`, which only ever lands on something motion.js armed. With the script
   blocked the rows are simply there and the icons are simply drawn. */
.tx__item:nth-child(1)  { --d: 0ms; }
.tx__item:nth-child(2)  { --d: 55ms; }
.tx__item:nth-child(3)  { --d: 110ms; }
.tx__item:nth-child(4)  { --d: 165ms; }
.tx__item:nth-child(5)  { --d: 220ms; }
.tx__item:nth-child(6)  { --d: 275ms; }
.tx__item:nth-child(7)  { --d: 330ms; }
.tx__item:nth-child(8)  { --d: 385ms; }
.tx__item:nth-child(9)  { --d: 440ms; }
.tx__item:nth-child(10) { --d: 495ms; }
.tx__item:nth-child(11) { --d: 550ms; }
.tx__item:nth-child(12) { --d: 605ms; }

@media (prefers-reduced-motion: no-preference) {
  [data-rise-armed].is-in .tx__item {
    animation: txRise 560ms cubic-bezier(.22, .68, .3, 1) backwards;
    animation-delay: var(--d, 0ms);
  }
  /* Round 11, 2026-08-28, item 8: ".s, .g" rather than "path". Every stroke in
     these icons carries one of those two classes, but three of them are drawn
     with <rect> - the braces icon is a line and three brackets - and a selector
     of "path" left those brackets standing still while the wire drew itself
     across them. Checked: 72 shapes across the site, every one classed. */
  [data-rise-armed].is-in .tx__icon .s,
  [data-rise-armed].is-in .tx__icon .g {
    animation: txDraw 820ms ease-out backwards;
    animation-delay: var(--d, 0ms);
  }
}
@keyframes txRise {
  from { opacity: 0; transform: translateY(12px); }
}
/* dasharray is set in BOTH frames rather than on the path itself: outside the
   animation the icon must be an ordinary undashed drawing, so that a browser
   that never runs the animation never shows a dotted tooth. */
@keyframes txDraw {
  from { stroke-dasharray: 1; stroke-dashoffset: 1; }
  to   { stroke-dasharray: 1; stroke-dashoffset: 0; }
}

/* --- item 8: the icons keep moving after they have been drawn -------------
   "in the section what we treat, make all the small icons animated."

   They already animated, once: each one drew itself in as the row rose into
   view and was then completely still, which on a page the visitor scrolls past
   in two seconds is a thing most people never see happen. So the draw stays -
   it is the good bit - and underneath it every icon now breathes: a slow rise
   and fall of about six per cent, on the same stagger as the draw, so the grid
   ripples down the list instead of pulsing in unison.

   Why a breathe and not a redraw on a loop: a repeating draw leaves the icon
   part-erased for most of its cycle, and these icons are labels - a half-drawn
   tooth beside the words "Root canal treatment" reads as a rendering fault, not
   as motion. Scale never makes the drawing wrong, only bigger.

   The hover state has to WIN, and an infinite animation on transform would
   outrank a transition on the same property, so hover cancels the animation
   outright and lets the 1.12 scale transition in from wherever the breathe had
   got to. */
.tx__icon { transition: transform 220ms ease; transform-origin: 20px 20px; }
@media (prefers-reduced-motion: no-preference) {
  /* A NEGATIVE delay, and four times the stagger the draw uses. Positive would
     hold each icon still for its own delay and then start it, which on an
     infinite loop just means twelve icons sitting motionless at slightly
     different moments. Negative starts each one already part-way through the
     cycle, so the twelve are spread across about seven tenths of it from the
     first frame and the row reads as a ripple rather than a heartbeat. */
  .tx__icon {
    animation: txBreathe 3.4s ease-in-out infinite;
    animation-delay: calc(var(--d, 0ms) * -4);
  }
}
@keyframes txBreathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}
.tx__item:hover .tx__icon,
.tx__item:focus-within .tx__icon { animation: none; transform: scale(1.12); }
@media (prefers-reduced-motion: reduce) {
  .tx__icon { transition: none; animation: none; }
  .tx__item:hover .tx__icon,
  .tx__item:focus-within .tx__icon { transform: none; }
}

/* --- 3. the plates the old gallery kept square ------------------------------
   Every plate in this archive is a two-up before-and-after, twice as wide as it
   is tall, and the grid was built on that. Five of the plates brought across in
   this round are square instead - the practice's own gallery published them
   that way, some as a stacked before-above-after, some as a four-panel record.
   Dropped into the grid as they are, each one stood a cell taller than its
   neighbours and tore a hole in the row beside it.

   They are MOUNTED rather than cropped: the same 2:1 cell as every other plate,
   with the picture contained inside it and the plate's own white showing at the
   sides. Cropping was never an option - these are clinical records, and a
   record trimmed to fit a layout is not a record any more. */
.plate--square img { aspect-ratio: 2 / 1; object-fit: contain; }

/* A group of one would otherwise render at a third of a row with two empty
   columns beside it, which reads as a mistake rather than as a single case. */
@media (min-width: 700px) {
  .plates--one { grid-template-columns: minmax(0, 640px); }
}

/* ===========================================================================
   ROUND 9 - 2026-08-26
   Thirteen numbered items from the practice. The seven that needed CSS are
   below, each against the words that asked for it.
   ======================================================================== */

/* --- item 11: a word under the QR in the header ---------------------------
   "Add a small tag line as location below the location QR in the sticky
   navigation bar."

   The control was a bare grid glyph between a telephone and a WhatsApp mark,
   and a QR glyph at 19px is not self-evident - it reads as "menu" as easily as
   it reads as "map". It now says what it is.

   The button grows a column rather than a second row: it keeps the 40px height
   the header bar is built on, so nothing below it moves, and the glyph comes
   down 2px to make room. The word is decoration for a control that already has
   a full accessible name on aria-label, so it is aria-hidden - a screen reader
   should hear "Where to find us: a QR code for the Anna Nagar clinic on the
   map", not that phrase with "Location" stuck on the end. */
.nav__qr > summary {
  flex-direction: column; justify-content: center; gap: 1px;
  width: auto; min-width: 44px; padding: 0 0.4rem;
}
.nav__qr > summary svg { width: 17px; height: 17px; }
.nav__qr-tag {
  font-size: 0.5rem; line-height: 1; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-quiet);
}
.nav__qr > summary:hover .nav__qr-tag,
.nav__qr[open] > summary .nav__qr-tag { color: inherit; }
/* On a phone the header bar has no spare millimetres: the logo, WhatsApp, the
   telephone, this control and the hamburger already fill 360px, and the word
   pushed the hamburger 8px off the right edge - which made the whole document
   scroll sideways and, with it, reported the fixed claims bar and every
   carousel slide as overflowing too. One cause, six symptoms.

   So below 420px the control goes back to the 40px glyph it was. Losing the
   word there costs least of anywhere: a QR code is for a SECOND device to
   photograph, and nobody scans the screen they are holding - the phone visitor
   wants the "Open in Google Maps" link inside the panel, which is untouched. */
@media (max-width: 419px) {
  .nav__qr > summary { width: 40px; min-width: 0; padding: 0; gap: 0; }
  .nav__qr > summary svg { width: 19px; height: 19px; }
  .nav__qr-tag { display: none; }
}

/* --- item 1: the arch on /the-practice carries all ten rooms --------------
   "i want you to add all the clinic interior photos in a autoscrolling manner
   inside the same semicircle."

   The arch was drawn on the IMG, which is why it could only ever frame one
   photograph. It is drawn on the window now and the pictures pass behind it.
   Same radius, same 10px teal, same silhouette on the page - the shape they
   asked to keep is untouched; only what is inside it moves.

   The slides are sized by flex-basis alone. Giving a slide its own width AND
   height as well is what broke the hero in round 1: with a percentage basis, an
   in-flow image carrying an aspect ratio, and a percentage height all pointing
   at each other, the basis stops resolving against the track and several slides
   land on screen at once. The frame owns the aspect ratio; the slide owns
   nothing but its share of the track. */
main .archshow__slide,
main .roomshow__slide { max-width: none; }

.archshow { position: relative; }
.archshow__frame {
  position: relative; overflow: hidden;
  aspect-ratio: 4 / 3;
  border-radius: 999px 999px 16px 16px;
  border: 10px solid var(--colour-brand-teal);
  background: var(--colour-primary-dark);
}
.archshow__track {
  display: flex; height: 100%; margin: 0; padding: 0; list-style: none;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; -ms-overflow-style: none;
  overscroll-behavior-x: contain;
}
.archshow__track::-webkit-scrollbar { display: none; }
.archshow__track:focus-visible { outline: 3px solid var(--focus-ink); outline-offset: -3px; }
.archshow__slide { flex: 0 0 100%; scroll-snap-align: center; height: 100%; margin: 0; }
.archshow .archshow__slide img {
  display: block; width: 100%; height: 100%; max-width: none;
  object-fit: cover; border: 0; border-radius: 0;
}
/* Hidden until motion.js sets the .js flag, exactly as the hero's Pause is: a
   button offering to stop something that is not moving is worse than no button. */
.archshow__pause { display: none; }
.js .archshow__pause {
  display: inline-block; margin: 0.6rem 0 0; padding: 0.15rem 0.55rem;
  font: inherit; font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-quiet); background: none;
  border: 1px solid var(--colour-grey-300); border-radius: 2px; cursor: pointer;
}
.js .archshow__pause:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.archshow figcaption { display: flex; flex-direction: column; align-items: flex-start; }
@media (max-width: 879px) { .archshow__frame { max-width: 460px; } }
@media (max-width: 700px) { .archshow__frame { border-radius: 220px 220px 14px 14px; border-width: 7px; } }
/* Under reduced motion nothing advances by itself, so Pause would offer to
   start the very movement the visitor asked their system not to show them. */
@media (prefers-reduced-motion: reduce) { .js .archshow__pause { display: none; } }

/* --- item 3: the rooms on /the-clinic, in a rounded rectangle -------------
   "add all the clinic interior images in a autoscrollable manner with just a 5
   second interval between each images and also add them inside a shape like a
   rectangle with curved edges and highlight the teal color in those images."

   A rectangle with curved edges, teal round it, ten rooms, five seconds.

   The teal is on the FRAME, not over the photographs. This is the page that
   promises "this is what you will actually walk into", so tinting the rooms a
   colour the rooms are not would be the one dishonest thing on it.

   Round 11, 2026-08-28, items 3 and 4: "in look round the clinic section, dont
   add some caption to the image" and "remove all the captions for the rooms or
   clinic interiors." Every room caption is gone - the ten in this window, the
   lead photograph's on the same page, and the one under the arch on
   /the-practice (whose Pause button stays, because it is a control and not a
   caption). The rules that drew them are deleted rather than left unused: the
   caption was an absolutely-positioned teal wash over the bottom of each
   photograph, so keeping the rule would have kept the wash. The rooms are shown
   plain now, which is what was asked, and the alt text still describes every one
   of them to a screen reader. */
.roomshow { position: relative; }
.roomshow__frame {
  position: relative; overflow: hidden;
  aspect-ratio: 3 / 2;
  border-radius: 22px;
  border: 8px solid var(--colour-brand-teal);
  background: var(--colour-primary-dark);
}
.roomshow__track {
  display: flex; height: 100%; margin: 0; padding: 0; list-style: none;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none; -ms-overflow-style: none;
  overscroll-behavior-x: contain;
}
.roomshow__track::-webkit-scrollbar { display: none; }
.roomshow__track:focus-visible { outline: 3px solid var(--focus-ink); outline-offset: -3px; }
.roomshow__slide { position: relative; flex: 0 0 100%; scroll-snap-align: center; height: 100%; margin: 0; }
.roomshow__slide img {
  display: block; width: 100%; height: 100%; max-width: none;
  object-fit: cover; border: 0; border-radius: 0;
}
.roomshow__controls {
  display: flex; align-items: center; gap: 0.9rem; flex-wrap: wrap;
  margin-top: 0.9rem;
}
.roomshow__dots { display: flex; gap: 0.45rem; margin: 0; padding: 0; list-style: none; }
.roomshow__dots li { margin: 0; max-width: none; }
.roomshow__dot {
  display: block; width: 11px; height: 11px; padding: 0; cursor: pointer;
  border-radius: 50%; border: 1px solid var(--colour-primary);
  background: transparent;
}
.roomshow__dot[aria-current="true"] { background: var(--colour-brand-teal); }
.roomshow__dot:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--focus-halo), 0 0 0 5px var(--focus-ink); }
.roomshow__pause { display: none; }
.js .roomshow__pause {
  display: inline-block; padding: 0.15rem 0.55rem;
  font: inherit; font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink-quiet); background: none;
  border: 1px solid var(--colour-grey-300); border-radius: 2px; cursor: pointer;
}
.js .roomshow__pause:hover { border-color: var(--colour-primary); color: var(--link-hover); }
.js .roomshow__controls { display: flex; }
@media (prefers-reduced-motion: reduce) { .js .roomshow__pause { display: none; } }
@media (max-width: 640px) {
  .roomshow__frame { aspect-ratio: 4 / 3; border-radius: 16px; border-width: 6px; }
}

/* --- item 8: the claims row is three, Technology is four ------------------
   Four cards divided exactly into two columns and four; three do not. Without
   this, the claims row at 1120 and up would put three cards in a four-column
   grid and leave a hole where the X-ray card used to be. */
@media (min-width: 780px)  { .why--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1040px) { .why--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Round 11, item 2: with the Adjustments card gone the row is two. It divides
   into two columns and stays there - promoting it to three at 1040 would leave
   exactly the hole this rule was written to close.

   The photographs go landscape at the same time, and they have to. These plates
   are square, which was right at a third of the container - about 345px a side.
   Across a half they become 520px squares, and the row stopped being a claims
   row and became two large pictures with a caption each: on a 1440 screen the
   scanner alone stood taller than the fold. Four-by-three at the same width is
   130px shorter, reads as a pair of plates rather than two tiles, and costs
   nothing in fidelity - the rooms photograph is natively 4:3, so it is now
   uncropped for the first time, and the scanner is centre-focal so the trim
   comes off empty desk. */
@media (min-width: 780px) {
  .why--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .why--2 .why__img { aspect-ratio: 4 / 3; }
}
/* The equipment renders are studio shots on a pale ground; against the section
   wash they float without the hairline, and the teal rule ties the four
   together as one set. */
.why--tech .why__img {
  border: 1px solid var(--colour-grey-300);
  border-top: 3px solid var(--colour-brand-teal);
  border-radius: 4px; background: #fff;
}
.why--tech .why__title { font-size: clamp(1.05rem, 1.6vw, 1.2rem); }

/* --- item 6: the QR beside the address ------------------------------------
   "in that add the location QR or you can directly add the google map location
   of align dental care near the address."

   Beside the address on anything wider than a phone, under it below that. The
   card is capped at its own content so a long address line cannot stretch it. */
.findus { display: grid; gap: 1.4rem; align-items: start; }
@media (min-width: 560px) { .findus { grid-template-columns: minmax(0, 1fr) auto; } }
.findus__detail { min-width: 0; }
.findus__qr {
  margin: 0; padding: 0.85rem; text-align: center;
  background: #fff;
  border: 1px solid var(--colour-grey-500);
  border-top: 3px solid var(--colour-brand-teal);
  border-radius: 4px;
}
.findus__qr img { display: block; width: 132px; height: 132px; margin: 0 auto 0.5rem; }
.findus__qr figcaption {
  font-size: var(--fs-caption); color: var(--ink-quiet);
  letter-spacing: 0.08em; text-transform: uppercase; font-weight: 600;
  margin-bottom: 0.55rem;
}
.findus__qr-link {
  display: block; padding-top: 0.55rem;
  border-top: 1px solid var(--colour-grey-300);
  font-size: 0.85rem; font-weight: 600;
  color: var(--link-hover); text-decoration: none;
}
.findus__qr-link:hover { text-decoration: underline; }

/* --- item 7: the aligner strip keeps moving, and stops being plain --------
   "make the images to scroll continuously and add some teal highlights to those
   images. it looks very plain."

   It was already a continuous marquee, so "make it scroll continuously" looked
   like a request for something it already did - until you notice that ANY reel
   on this site stops while the cursor rests on it. On a strip that runs the
   width of the page a mouse parked anywhere near the middle freezes it, and
   from the practice's side the thing simply does not move. That is the trap the
   full-bleed hero hit in round 2 and it is fixed the same way: hovering no
   longer stops this one.

   Keyboard focus still does, and so does the Pause button, and so does
   reduced-motion. Those are the visitor asking; a resting cursor is not.

   The pause CHECKBOX still wins on specificity (0,4,0 against 0,3,0), so
   pressing Pause holds the strip even under the pointer. Worth stating because
   getting that backwards would make the button appear broken. */
.reel--aligners:hover .reel__track { animation-play-state: running; }
.reel--aligners:focus-within .reel__track { animation-play-state: paused; }
.reel--aligners .reel__item img {
  border: 3px solid var(--colour-brand-teal);
  border-radius: 10px;
}
.cover--teal img {
  border: 6px solid var(--colour-brand-teal);
  border-radius: 14px;
}
.cover--teal figcaption { border-left: 3px solid var(--colour-brand-teal); padding-left: 0.7rem; }

/* --- item 12: the WhatsApp control asks for attention ---------------------
   "I want the whatsapp button at the bottom right corner to pop up and be
   animated so that it would be visible and eye catcher for the people."

   Two things, both slow on purpose. A teal ring opens out of the button and
   fades, and once the visitor is past the first screen the button itself gives
   a small hop. The cycle is 3.2s with roughly half of it still, because a
   control that never rests stops reading as an invitation and starts reading as
   a fault.

   All of it is inside prefers-reduced-motion: no-preference. Under reduced
   motion the button is simply its larger self, which is the size change
   motion.js already makes and which is not an animation.

   The ring is a pseudo-element on the button rather than a box-shadow on the
   dock, so nothing that moves can catch the telephone button beside it, and
   nothing here transforms an ancestor of a position:fixed element. */
/* The ring is a BOX-SHADOW, and it has to be. It was a ::after pseudo-element
   first, which is the usual way to draw one, and that quietly broke the CTA fit
   gate on every page: an absolutely-positioned child that sits outside its
   parent counts towards the parent's scrollable overflow, so the 52px circle
   reported scrollWidth 60 against clientWidth 50 and read as a button with its
   label crammed inside it. Pulling the ring back to inset:0 did NOT fix it -
   still 57 against 50 - because the scale() in the keyframes is counted too.
   A box-shadow is a paint effect and never touches layout or overflow, so it
   is the only version of this that cannot trip a fit check. Measured, not
   assumed: both pseudo-element variants were probed in the browser first.

   Two keyframe sets rather than one because the button carries a different
   elevation once it has grown, and an animation on box-shadow overrides the
   static rule for as long as it runs - so the resting shadow has to be
   restated inside the frames or the button loses its lift while pulsing. */
@media (prefers-reduced-motion: no-preference) {
  .contact-dock .dock-fab-link--wa {
    animation: waRing 3.2s ease-out infinite;
  }
  @keyframes waRing {
    0%   { box-shadow: 0 1px 2px rgba(32,30,26,.12), 0 6px 16px rgba(32,30,26,.18), 0 0 0 0    rgba(13,133,134,.55); }
    55%  { box-shadow: 0 1px 2px rgba(32,30,26,.12), 0 6px 16px rgba(32,30,26,.18), 0 0 0 15px rgba(13,133,134,0); }
    100% { box-shadow: 0 1px 2px rgba(32,30,26,.12), 0 6px 16px rgba(32,30,26,.18), 0 0 0 15px rgba(13,133,134,0); }
  }
  .contact-dock.is-prominent .dock-fab-link--wa {
    animation: waRingLg 3.2s ease-out infinite, waHop 3.2s ease-in-out infinite;
  }
  @keyframes waRingLg {
    0%   { box-shadow: 0 2px 4px rgba(32,30,26,.16), 0 10px 24px rgba(32,30,26,.24), 0 0 0 0    rgba(13,133,134,.55); }
    55%  { box-shadow: 0 2px 4px rgba(32,30,26,.16), 0 10px 24px rgba(32,30,26,.24), 0 0 0 17px rgba(13,133,134,0); }
    100% { box-shadow: 0 2px 4px rgba(32,30,26,.16), 0 10px 24px rgba(32,30,26,.24), 0 0 0 17px rgba(13,133,134,0); }
  }
  @keyframes waHop {
    0%, 78%, 100% { transform: translateY(0); }
    85%           { transform: translateY(-5px); }
    92%           { transform: translateY(-1px); }
  }
  /* A control someone is actually pointing at should hold still. */
  .contact-dock .dock-fab-link--wa:hover,
  .contact-dock .dock-fab-link--wa:focus-visible,
  .contact-dock.is-prominent .dock-fab-link--wa:hover,
  .contact-dock.is-prominent .dock-fab-link--wa:focus-visible { animation: none; }
}

/* --- item 13: more teal, here and there ----------------------------------
   "You can highlight the teal color in the website here and there so that it
   would be more visually appealing."

   The eyebrow above every section title was near-black, which is the one
   repeating element on the site that appears on nearly every band and carried
   none of the brand. It is the primary teal rather than the brand teal because
   it is small text: #038283 clears 4.5:1 on white and #0D8586 does not, and the
   token file says so in as many words.

   Two exclusions, both deliberate. The eyebrow over the hero photograph keeps
   its pale tint - dark teal on a photograph is unreadable - and dark sections
   keep theirs, which they already win by specificity. */
.eyebrow:not(.eyebrow--over) { color: var(--colour-primary); }
.creds dt { color: var(--colour-primary); }
.tl__when { color: var(--colour-primary); }

/* ==========================================================================
   Round 10, 2026-08-27
   ========================================================================== */

/* --- items 3 and 9: the equipment scrolls -------------------------------
   "make that section more live by making the opg, intraoral camera, autoclave
   sterilisation, uv steriliser and portable x ray machine to autoscroll."

   The four-card grid becomes the marquee the rest of the site already runs, so
   there is one scrolling mechanism on this site rather than a second one that
   behaves almost the same. Five cards at up to 300px is at least 1500px of
   printed set, wider than the container at every width the reel is visible, so
   the two-set loop never shows its seam.

   The teal top rule and the hairline are carried over from .why--tech verbatim:
   the renders are studio shots on a pale ground and float against the section
   wash without it. */
.reel--tech .reel__track { animation-duration: 52s; }
.reel--tech .reel__item { width: clamp(224px, 25vw, 300px); }
.reel--tech .reel__item img {
  aspect-ratio: 1 / 1; object-fit: cover;
  border: 1px solid var(--colour-grey-300);
  border-top: 3px solid var(--colour-brand-teal);
  border-radius: 4px; background: #fff;
}
.techcard { display: block; margin-top: 0.65rem; }
.techcard__eyebrow {
  display: block; font-size: var(--fs-caption); font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--colour-primary);
}
.techcard__title {
  display: block; margin-top: 0.2rem;
  font-size: clamp(0.98rem, 1.3vw, 1.08rem); line-height: 1.25;
  color: var(--colour-text);
}
.techcard__note {
  display: block; margin-top: 0.3rem;
  font-size: 0.86rem; line-height: 1.45; color: var(--ink-quiet);
}

/* --- item 12: the tray photographs, an inch smaller ----------------------
   "I want the images to be little smaller and dont add any caption for each and
   every image. Just make the images to scroll by itself continuosuly."

   Everything else about this strip is the home page's, deliberately: same
   marquee, same teal frame, same triple print. This is only the "little
   smaller", the .reel--tall rule with each step taken down. */
.reel--collect .reel__item img { height: clamp(190px, 24vw, 330px); }

/* --- item 5: the location QR, smaller and sitting with the address --------
   "the location QR can be smaller and align it properly."

   It was not misaligned so much as marooned. The detail column was a 1fr track
   in a two-track grid, so it stretched to the full width of the column while the
   address text inside it stopped less than halfway across, which pushed the QR
   card hard against the right margin with a hand's width of nothing between the
   two. It read as a thing that had drifted rather than as a pair.

   So the text track shrinks to its own content and the card follows it. They now
   sit together, left to right, and the card is the smaller of the two rather
   than a block the size of the address it annotates. Below 560px this collapses
   to one column exactly as before. */
@media (min-width: 560px) {
  .findus { grid-template-columns: minmax(0, max-content) auto; justify-content: start; gap: 1.6rem; }
}
.findus__qr { padding: 0.7rem 0.75rem; }
.findus__qr img { width: 104px; height: 104px; margin-bottom: 0.4rem; }
/* Round 11, 2026-08-28, item 9: "make the location QR code near the address at
   the bottom of the home page smaller." Down from 104px to 80px - a fifth of the
   card's width given back, and still a comfortable scan, because this QR is an
   SVG drawn at the device's own pixel ratio rather than a bitmap being shrunk.
   The card's padding comes in with it so the code does not sit in a moat. */
.findus__qr { padding: 0.6rem 0.65rem; }
.findus__qr img { width: 80px; height: 80px; margin-bottom: 0.35rem; }

/* --- item 2: the review QR beside the location one -----------------------
   "add the review QR in the sticky navigation bar ... and also add a tag line as
   review below the QR (same as location tag line for location QR)."

   Same control, same 40px bar height, same tag treatment. The tagline is the
   thing that was asked to match, and it does. The GLYPH is a star rather than a
   second grid of squares, because two identical QR marks side by side name
   neither of them: at 17px the only thing telling a visitor which is which would
   be a 0.5rem word underneath. The star says "review" before the word is read.

   Both panels are absolutely positioned to the right of their own button and
   overlap each other, so nav.js closes one when the other opens. */
.nav__qr--review > summary svg { width: 16px; height: 16px; }

/* Below 560px the header bar is full: logo, WhatsApp, telephone, the location
   QR and the hamburger already spend every millimetre, and a fifth control
   pushes the hamburger off the right edge, which is the same overflow the
   location tag caused at 420px with the same six symptoms. The review QR is the
   one to drop there, and it costs nothing: a QR code is for a SECOND device to
   photograph, and nobody scans the screen in their hand. The drawer carries
   "Leave a Google review" as a plain tappable button instead, which is what a
   phone visitor wanted anyway. */
@media (max-width: 559px) { .nav__qr--review { display: none; } }

/* --- item 13: the treatments, scrolling, in teal and gold ----------------
   "add all these images in an autoscrollable manner continuously under the
   section SIX SPECIALISATIONS, ONE ROOF. It should scroll continuously and add
   some teal and gold highlights to the image or u can make the images to pop up
   as it scroll."

   Both halves of that are taken, and both are taken on the CARD rather than on
   the photograph: a teal rule across the top of every picture, a gold hairline
   under every name, and a lift as each card comes past. Tinting the pictures
   themselves is the one thing not done. A crown or a denture is something a
   patient may be trying to recognise, and a colour wash over a clinical render
   reads as a filter rather than as brand.

   The "pop" is a slow staggered rise rather than anything sudden: eight cards
   sharing one 8-second cycle, each a second behind the last, so at any moment
   two or three of them stand slightly proud of the rest and the strip has a
   pulse instead of being a conveyor belt. It stops dead under
   prefers-reduced-motion, along with the marquee. */
.reel--spec .reel__track { animation-duration: 76s; }
.reel--spec .reel__item { width: clamp(230px, 25vw, 300px); }
.reel--spec .reel__item img {
  aspect-ratio: 3 / 2; object-fit: cover;
  border: 1px solid var(--colour-grey-300);
  border-top: 3px solid var(--colour-brand-teal);
  border-radius: 4px; background: #fff;
}
.speccard { display: block; margin-top: 0.6rem; }
.speccard__name {
  display: block; padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--gold-rule);
  font-size: clamp(0.98rem, 1.3vw, 1.08rem); line-height: 1.25;
  color: var(--colour-text);
}
.speccard__note {
  display: block; margin-top: 0.35rem;
  font-size: 0.84rem; line-height: 1.45; color: var(--ink-quiet);
}
.reel--spec .reel__item--spec {
  animation: specPop 8s ease-in-out infinite;
  will-change: transform;
}
.reel--spec .reel__item--spec:nth-child(8n+2) { animation-delay: -1s; }
.reel--spec .reel__item--spec:nth-child(8n+3) { animation-delay: -2s; }
.reel--spec .reel__item--spec:nth-child(8n+4) { animation-delay: -3s; }
.reel--spec .reel__item--spec:nth-child(8n+5) { animation-delay: -4s; }
.reel--spec .reel__item--spec:nth-child(8n+6) { animation-delay: -5s; }
.reel--spec .reel__item--spec:nth-child(8n+7) { animation-delay: -6s; }
.reel--spec .reel__item--spec:nth-child(8n+8) { animation-delay: -7s; }
@keyframes specPop {
  0%, 100%  { transform: translateY(0);      filter: none; }
  50%       { transform: translateY(-7px);   filter: saturate(1.06) brightness(1.02); }
}
/* Pausing the strip should stop everything moving in it, not leave eight cards
   bobbing on a stationary belt. */
.reel-pause:checked ~ .reel--spec .reel__item--spec { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .reel--spec .reel__item--spec { animation: none; }
}
