/* Dr Shantel Naidoo — practice website
 *
 * Editorial bone-and-ink scheme: a warm paper ground, near-black for the dark bands, and
 * the logo's crimson as the single accent (CLAUDE.md S8 tokens, restated for this design).
 * Display face is Fraunces, the S8 display face; Karla carries body; IBM Plex Mono carries
 * labels, indices and the "to confirm" states so unverified facts look unverified.
 *
 * There is no Fraunces Italic in the project, so <em> is an accent-colour shift rather than
 * a synthetic slant — see build_fonts.py.
 */

/* ------------------------------------------------------------------ fonts */
@font-face {
  font-family: 'Fraunces';
  src: url('/assets/fonts/fraunces.woff2') format('woff2-variations');
  font-weight: 300 600;
  font-display: swap;
}
@font-face {
  font-family: 'Karla';
  src: url('/assets/fonts/karla.woff2') format('woff2-variations');
  font-weight: 300 600;
  font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/assets/fonts/plex-mono.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}
/* A real SemiBold cut. Asking the browser for 600 against the regular alone would get a
   synthetic bold, and at the 7.5px the tagline sets at that closes the counters up. */
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('/assets/fonts/plex-mono-600.woff2') format('woff2');
  font-weight: 600;
  font-display: swap;
}

/* ------------------------------------------------------------------ tokens */
:root {
  --bone: #EDE8DF;
  --paper: #F6F3ED;
  --shell: #E3DCD0;
  --ink: #14120E;
  --ink-2: #1B1915;
  --muted: #655E52;
  --muted-d: #A79E90;
  --rule: #D6CEBF;               /* decorative hairlines between rows */
  --rule-d: #2F2C26;
  /* Borders that are the only thing identifying a control — inputs, ghost buttons.
     WCAG 1.4.11 wants 3:1 for those, and the decorative hairline is 1.28:1. */
  --line-ui: #897F72;            /* 3.22:1 on bone, 3.55:1 on paper */
  --line-ui-d: #726B5D;          /* 3.54:1 on ink */
  --crimson: #A3202F;
  /* The accent on the dark bands: the brand crimson's own hue and saturation, lifted in
     lightness only. #A3202F itself measures 2.50:1 against this ground, under the 3.0 a
     large word needs, so it cannot be used here as it is on the light bands. #C42739 is
     the same colour raised to 46% lightness — 3.29:1, and a dE of 11.9 from the crimson
     against the 34.4 of the brighter red this replaces.
     Only display type and non-text marks use it. Nothing small does: the one piece of body
     text that did, the footer link hover, went to paper rather than drag this value
     brighter and further from the brand. */
  --crimson-d: #C42739;

  --display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --body: 'Karla', system-ui, -apple-system, Segoe UI, sans-serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --wrap: 1300px;
  --gutter: clamp(20px, 5vw, 64px);
  --band: clamp(64px, 9vw, 132px);
  --r: 4px;
}

*, *::before, *::after { box-sizing: border-box; }

/* overflow-x lives here, not on body: on body it computes overflow-y to auto, which
   makes body a scroll container that never scrolls — and any view() timeline then
   resolves its scrollport to body instead of the viewport. */
html { -webkit-text-size-adjust: 100%; overflow-x: hidden; }

body {
  margin: 0;
  background: var(--bone);
  color: var(--ink);
  font: 400 17px/1.62 var(--body);
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; }
img { height: auto; }

/* ------------------------------------------------------------------ type */
h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: -0.021em;
  line-height: 1.02;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(42px, 7.2vw, 94px); }
h2 { font-size: clamp(30px, 4.4vw, 58px); line-height: 1.04; }
h3 { font-size: clamp(21px, 2vw, 27px); line-height: 1.16; }
h4 { font-size: clamp(18px, 1.5vw, 21px); line-height: 1.22; }

/* No italic face is loaded, so emphasis reads as an accent shift, not a fake slant. */
em {
  font-style: normal;
  color: var(--crimson);
}
.on-ink em { color: var(--crimson-d); }

p { margin: 0 0 1.05em; }
p:last-child { margin-bottom: 0; }

.lede {
  font-size: clamp(16.5px, 1.25vw, 19.5px);
  line-height: 1.6;
  color: var(--muted);
  max-width: 62ch;
}
.on-ink .lede { color: var(--muted-d); }

.label {
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 18px;
  display: block;
}
.on-ink .label { color: var(--muted-d); }

.idx {
  font: 400 11.5px/1 var(--mono);
  letter-spacing: 0.08em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.on-ink .idx { color: var(--muted-d); }

a { color: inherit; }

/* ------------------------------------------------------------------ layout */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

section { padding: var(--band) 0; }

.on-ink {
  background: var(--ink);
  color: var(--paper);
}
.on-ink h1, .on-ink h2, .on-ink h3, .on-ink h4 { color: var(--paper); }

.rule { height: 1px; background: var(--rule); border: 0; margin: 0; }
.on-ink .rule { background: var(--rule-d); }

/* ------------------------------------------------------------------ skip + focus */
/* The practice this address used to be, under the lockup it replaced. A patient who typed
   the old domain and landed here through the redirect sees it in the same glance as the
   new name, which is the whole point of carrying it. Quieter than the tagline above it. */
/* Set as their own lockup set it: the practitioner over the practice, divided by a rule,
   with "Formerly" alongside and centred between the two lines. Stacking is what makes it
   fit — the same words on one line run to 323px, these two columns to about 175px. */
.brand-formerly {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font: 400 8.5px/1.35 var(--mono);
  letter-spacing: 0.03em;
  color: var(--muted);
  /* The breaks are in the markup, so nothing has to be guessed at from a width. */
  white-space: nowrap;
}
.brand-formerly .fw { flex: 0 0 auto; }
.brand-formerly .fn > span {
  display: block;
  margin-top: 2px;
  padding-top: 2px;
  border-top: 1px solid var(--rule);
}


.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--ink); color: var(--paper);
  padding: 12px 18px; z-index: 100; border-radius: var(--r);
}
.skip:focus { left: 12px; top: 12px; }

:focus-visible {
  outline: 2px solid var(--crimson);
  outline-offset: 3px;
  border-radius: 2px;
}
.on-ink :focus-visible { outline-color: var(--crimson-d); }

/* ------------------------------------------------------------------ header */
.site-header {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bone) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--rule);
}
.header-inner {
  display: flex; align-items: center; gap: 22px;
  min-height: 80px;
}
.brand { display: inline-flex; align-items: center; text-decoration: none; flex: 0 0 auto; }
/* 46px, not the 38px a wordmark would take. This lockup is the supplied artwork used
   whole, so the tooth and the trailing sweep are inside the same box as the name — the
   name is only a third of the height. Below about 46px the script stops being readable. */
.brand-logo { height: 46px; width: auto; display: block; }

/* The lockup and its tagline, stacked. The tagline is a sibling of the link, not a child
   of it: it carries a button now, and a button inside an anchor is invalid markup. */
.brand-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; }
/* The lockup, its chip and the former name all on one line. Stacked, the former name was
   pushing this header to 103px against 80px everywhere else; beside the chip it fits in
   the height the logo already takes. It wraps rather than shrinking the gap, so on a
   narrow screen it drops to its own line instead of crowding the Menu button. */
.brand-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 16px;
}

/* The tagline, typed on in a loop.
   --chars is the length of site.tagline and drives the width, the step count and the
   erase; the face is monospace and its 400 and 600 cuts share metrics, so 1ch is exactly
   one glyph at either weight and the reveal lands on character boundaries without
   measuring anything. The line is sized to its final width up front, so the text types
   into reserved space instead of pushing the header around as it grows. */
.brand-tagline {
  position: relative;
  --chars: 22;
  --track: 0.15em;
  --w: calc(var(--chars) * (1ch + var(--track)));
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  /* The box holds its full width while the line inside it types. Without this the box is
     sized by the line, whose width animates from zero, so the whole row reflowed on every
     frame — on the Westville page that made the former-practice block wrap and unwrap and
     the header jump up and down in time with the typing. */
  width: var(--w);
  flex: 0 0 auto;
  font: 600 7.5px/1 var(--mono);
  letter-spacing: var(--track);
  text-transform: uppercase;
  color: var(--muted);
}
.brand-tagline-line {
  display: block;
  overflow: hidden;
  white-space: nowrap;
  width: var(--w);
  border-right: 1px solid var(--crimson);
}

/* One cycle is 6s: type 1.68s, hold 2.64s, erase 1.08s, rest 0.6s. steps() has to be a
   literal, so 21 is repeated here and audit.mjs asserts it against site.tagline.length.
   The per-keyframe timing functions matter — a single steps() on the whole animation would
   spread its 21 stops across the holds too, and the reveal would drift off the glyphs. */
@media (prefers-reduced-motion: no-preference) {
  .brand-tagline-line {
    animation: brand-type 6s steps(22, end) 400ms infinite,
               brand-caret 620ms steps(1, end) infinite;
  }
}
@keyframes brand-type {
  0%   { width: 0;        animation-timing-function: steps(22, end); }
  28%  { width: var(--w); animation-timing-function: steps(1, end); }
  72%  { width: var(--w); animation-timing-function: steps(22, end); }
  90%  { width: 0;        animation-timing-function: steps(1, end); }
  100% { width: 0; }
}
@keyframes brand-caret { 50% { border-right-color: transparent; } }

/* WCAG 2.2.2: the loop starts on its own, runs past five seconds and sits beside other
   content, so a way to stop it has to exist. The client asked for the button out of the
   header, so it is off the page until focused — the same trick as the skip link above.
   Gone for anyone using a mouse; still there for a keyboard or a screen reader, which is
   what the criterion actually asks for. Sized to the 24px minimum target (2.5.8). */
.tagline-toggle {
  position: absolute;
  left: -9999px;
  top: 0;
  flex: 0 0 auto;
  width: 24px; height: 24px;
  margin: -6px 0;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--muted);
  cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color 160ms ease, background-color 160ms ease;
}
/* An explicit display rule outranks the UA's [hidden] { display: none }, so the attribute
   has to be honoured here or the button shows before the script has wired it up. */
.tagline-toggle[hidden] { display: none; }
.tagline-toggle:focus { position: relative; left: auto; top: auto; }
.tagline-toggle:hover { color: var(--ink); background: rgba(20, 18, 14, .06); }
.tagline-toggle::before {
  content: '';
  width: 7px; height: 7px;
  /* Two bars while it is running; one triangle once it is paused. */
  border-left: 2px solid currentColor;
  border-right: 2px solid currentColor;
}
.tagline-toggle[data-state='paused']::before {
  width: 0; height: 0;
  border: 4px solid transparent;
  border-left: 7px solid currentColor;
  border-right: 0;
  margin-left: 2px;
}
.brand-tagline.is-paused .brand-tagline-line { animation-play-state: paused; }

/* Reduced motion gets the tagline outright — nothing animates, so nothing needs pausing. */
@media (prefers-reduced-motion: reduce) {
  .brand-tagline-line { border-right-color: transparent; }
  .tagline-toggle { display: none; }
}

/* Branch pages name their practice beside the logo rather than inside it. The artwork is
   used exactly as supplied, so the location cannot be set into the lockup — and a caption
   is the better answer anyway: one file serves every practice, including the third. */
.brand-sub {
  /* A chip rather than a caption behind a divider. Crimson: a light tint on a bone header
     measures about 1.1:1 against it and is not a highlight at all, so the fill has to be
     dark or saturated. Paper on crimson is 6.76:1 and the chip is 6.18:1 against the
     header — both clear. It does share its colour with the call to action at the other end
     of the header; that was raised and chosen. */
  margin-left: 13px;
  padding: 6px 12px 5px;
  border-radius: 100px;
  background: var(--crimson);
  color: var(--paper);
  font: 400 9.5px/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}
/* On a phone the header is logo + Menu and there is no room for a caption beside it. The
   practice is named in the <h1> immediately below, so nothing is lost by dropping it. */
@media (max-width: 620px) { .brand-sub { display: none; } }

.site-nav { margin-left: auto; }
.site-nav ul {
  display: flex; gap: 2px; align-items: center;
  list-style: none; margin: 0; padding: 0;
}
.site-nav a {
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--muted);
  padding: 12px 13px;
  border-radius: 2px;
  min-height: 44px;
  display: inline-flex; align-items: center;
}
.site-nav a:hover, .site-nav a[aria-current='page'] { color: var(--ink); }

.header-cta { margin-left: 16px; }

.nav-toggle {
  display: none;
  margin-left: auto;
  align-items: center; gap: 9px;
  background: none; border: 1px solid var(--line-ui);
  border-radius: 100px;
  padding: 10px 16px;
  min-height: 44px;
  font: 400 12px/1 var(--mono);
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink); cursor: pointer;
}
.nav-toggle-bars { display: block; width: 15px; height: 9px; position: relative; }
.nav-toggle-bars::before, .nav-toggle-bars::after {
  content: ''; position: absolute; left: 0; right: 0; height: 1.5px; background: currentColor;
}
.nav-toggle-bars::before { top: 0; }
.nav-toggle-bars::after { bottom: 0; }

/* ------------------------------------------------------------------ buttons */
.btn {
  display: inline-flex; align-items: center; gap: 12px;
  min-height: 48px;
  padding: 13px 14px 13px 24px;
  border-radius: 100px;
  text-decoration: none;
  font: 500 15px/1 var(--body);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
.btn-dark { background: var(--ink); color: var(--paper); }
.btn-dark:hover { background: #2A2720; }
.btn-light { background: var(--paper); color: var(--ink); border-color: var(--rule); }
.btn-light:hover { background: #fff; }
.btn-ghost { border-color: var(--line-ui); color: var(--ink); padding-right: 24px; }

/* A smaller pill, for a control that sits below something else rather than leading a page
   — the form submit. Still 40px tall, well clear of the 24px minimum target (2.5.8), and
   the arrow comes down with it so the proportions hold. */
.btn-sm {
  min-height: 40px;
  padding: 10px 12px 10px 20px;
  gap: 10px;
  font-size: 14px;
}
.btn-sm .btn-arrow { width: 26px; height: 26px; }
.btn-sm .btn-arrow svg { width: 11px; height: 11px; }
.btn-ghost:hover { border-color: var(--ink); }
.on-ink .btn-ghost { border-color: var(--line-ui-d); color: var(--paper); }
.on-ink .btn-ghost:hover { border-color: var(--muted-d); }

.btn-arrow {
  width: 30px; height: 30px; border-radius: 50%;
  display: inline-grid; place-items: center;
  background: color-mix(in srgb, currentColor 14%, transparent);
  flex: 0 0 auto;
}
.btn-arrow svg { width: 12px; height: 12px; display: block; }

.textlink {
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition: border-color .18s ease;
}
.textlink:hover { border-color: var(--crimson); }
.on-ink .textlink { border-color: var(--rule-d); }
.on-ink .textlink:hover { border-color: var(--crimson-d); }

/* ------------------------------------------------------------------ hero */
.hero { padding: clamp(46px, 7vw, 92px) 0 0; }
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr);
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
}
.hero h1 { margin-bottom: 26px; }
.hero .lede { margin-bottom: 32px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* The hero graphic is the logo's tooth, large. There is no photography of the practice
   yet, and S8 is explicit that stock imagery is worse than none on a healthcare site. */
.hero-mark {
  position: relative;
  display: grid; place-items: center;
  padding: clamp(0px, 1vw, 10px);
  min-height: clamp(340px, 44vw, 600px);
}
/* A soft lift behind the render. The supplied asset has a real alpha channel, so this is
   only for depth — it is not hiding a backdrop, and it has no edge to line up with. */
.hero-mark::before {
  content: '';
  position: absolute; inset: -10%;
  background: radial-gradient(circle at 50% 48%,
    rgba(255,255,255,.62) 0%, rgba(255,255,255,.34) 48%, rgba(255,255,255,0) 76%);
  border-radius: 50%;
}
.hero-mark img {
  position: relative;
  /* Client-supplied cutout, resampled to 800px (premultiplied, so the dark stored in the
     transparent pixels is not dragged into the edges). Shown at up to 560px, which keeps
     it below native and therefore crisp. */
  width: min(100%, 620px);
  height: auto;
  animation: hero-float 7s ease-in-out .55s infinite;
  /* drop-shadow follows the alpha channel, so it grounds the objects' actual silhouette
     rather than a box. The supplied render carries no shadow of its own. */
  filter: drop-shadow(0 20px 26px rgba(20, 18, 14, .16));
}

/* A slow drift, roughly one breath per cycle. Enough to feel alive, not enough to pull
   attention off the headline or the phone buttons. */
@keyframes hero-float {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
  50%      { transform: translate3d(0, -15px, 0) rotate(-0.7deg); }
}


/* ------------------------------------------------------------------ hero video */
/* Contained in the hero's second column rather than full-bleed behind the headline.
   Full-bleed would put serif display type over moving footage, which is the scrim-versus-
   contrast fight the previous design lost; here the copy stays on bone. */
/* Full-bleed: the section is the banner. The copy sits over the footage, so the scrim
   below is sized from the footage's measured luminance rather than guessed at. */
.hero-full {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 0;
  min-height: clamp(500px, 74vh, 780px);
  display: grid;
  align-items: center;
  background: var(--ink);
}
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: -1;
}
.hero-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.hero-scrim {
  position: absolute;
  inset: 0;
  display: block;
}
.hero-inner { width: 100%; display: grid; justify-items: start; }
.hero-copy-full {
  max-width: 54ch;
  padding: clamp(56px, 9vw, 104px) 0;
}
.hero-copy-full h1 { margin-bottom: 26px; }
.hero-copy-full .lede { margin-bottom: 32px; max-width: 42ch; }

/* Sized for the 1080p clip. The camera moves through this one, so the dark opening frames
   are not representative: sampled properly it runs 150-255 across the frame and the bright
   subject sweeps through the copy zone, needing 0.60 for the label and 0.48 for the
   headline. Measure only once the clip is fully buffered and the sampled frames are
   verifiably distinct — seeking past the buffered range silently returns the same early
   frame, which reads as "no scrim needed" and is wrong. */
.hero-copy-full .lede { color: var(--paper); }
/* The street address, under the buttons. Paper rather than the muted grey the rest of the
   site uses for supporting lines — this sits over footage, where the scrim was measured
   against paper and nothing dimmer was cleared. */
.hero-note {
  display: flex; align-items: center; gap: 9px;
  margin: 22px 0 0;
  font-size: 14.5px;
  color: var(--paper);
}
.hero-note .ic { width: 15px; height: 15px; flex: 0 0 auto; opacity: .85; }

/* The call to action, in the brand crimson — the same red as the display type on the
   light bands, so the two do not read as different colours.
   White on it is 6.76:1, and against the bone header the pill is 6.18:1. What crimson
   cannot do on its own is sit on a dark ground: 2.50:1 on the ink band and 2.22:1 over the
   hero footage, both under the 3:1 that 1.4.11 wants before a control counts as
   identifiable. So on dark it takes a hairline, below, rather than a brighter fill. */
.btn-red {
  background: var(--crimson);
  color: var(--paper);
  border-color: transparent;
}
.btn-red:hover { background: #B8283A; }           /* paper on it, 5.57:1 */
/* The reference puts a coloured disc on a white pill. Inverted here, because the pill is
   now the coloured one: paper disc, crimson arrow, 6.76:1 for a glyph that needs 3:1. */
.btn-red .btn-arrow { background: var(--paper); color: var(--crimson); }

/* The hairline. Only on the dark bands, where the fill alone is not enough to show where
   the control ends — on the light header the pill already separates itself. Paper at 40%
   over crimson puts the edge at 5.5:1 against the ink band and 4.9:1 over the footage. */
.on-ink .btn-red { border-color: rgba(246, 243, 237, .40); }

/* The hero one only differs in size. */
.btn-hero { font-size: 15.5px; }
.hero-copy-full .label { color: var(--paper); }   /* 4.53:1 as #E4DFD6; 5.35:1 as paper */
/* Paper, not an accent colour, and not by preference — by measurement.
   The accent word sits at 49-81% of the hero width, which is the thin end of the scrim and
   the bright end of the footage. Sampled across all 217 frames at the 95th percentile of
   that exact box: the old rose #F0C3C4 came out at 2.39:1, under the 3.0 floor for large
   text. (An earlier note here claimed 3.05:1; that was measured over the left-weighted
   band where the copy column starts, not where this word actually lands.)
   Red is further out of reach still — #FF3340 measures 1.04:1 there, and no amount of
   scrim rescues it: at twice the current scrim it is still 2.25:1. That is not a styling
   limit but a physical one. A colour bright enough to carry text over this footage has to
   sit near L=0.6, and the only colours in the red family at that luminance are pinks.
   Paper measures 3.40:1 as the scrim stands and 4.12:1 with the modest lift below. */
.hero-copy-full h1 em { color: var(--paper); }

/* Weighted to the left, where the copy is, and thin on the right so the patient reads. */
.hero-scrim {
  /* Cut to 70% of what it was, to let the footage through. That number is measured, not
     picked: the clip was decoded and every copy box composited against it the way the
     browser stacks these two layers, at the painted extent of the text rather than at its
     container. The headline is the binding element and it lands at 5.2:1 on the taller
     location hero against a 3:1 floor for 94px type — still above even the 4.5:1 that
     small text would need. Everything else clears 8.8:1. The floor is not reached until
     about 45%, so this keeps roughly a third of the range in hand.
     An earlier note here read 4.12:1 for the headline. That measured the accent word at
     49-81% of the hero width, which is the thin end of the scrim — but no headline has sat
     there since the copy column was fixed to the left half, so it was reading a box the
     type had moved out of. Re-measured at where the words actually land. */
  background:
    linear-gradient(to right,
      rgba(20, 18, 14, .59) 0%,
      rgba(20, 18, 14, .55) 36%,
      rgba(20, 18, 14, .42) 58%,
      rgba(20, 18, 14, .20) 80%,
      rgba(20, 18, 14, .08) 100%),
    linear-gradient(to bottom,
      rgba(20, 18, 14, .24) 0%,
      rgba(20, 18, 14, .04) 26%,
      rgba(20, 18, 14, .03) 74%,
      rgba(20, 18, 14, .13) 100%);
}

/* The stats band keeps the bone ground; only the hero goes dark. No padding under it:
   the section that follows brings a full --band of its own, and the two were stacking to
   230px of empty ground below the panel. */
.stats-band { padding-top: clamp(44px, 6vw, 84px); padding-bottom: 0; }
.stats-band .stats { margin-top: 0; }

/* WCAG 2.2.2: anything that plays automatically for more than five seconds needs a way to
   stop it, and this loops. Hidden until the script actually starts playback, so a visitor
   who never gets the video never gets a dead control. */
.hero-video-toggle {
  position: absolute;
  right: clamp(16px, 4vw, 40px);
  bottom: clamp(16px, 3vw, 32px);
  z-index: 2;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(246, 243, 237, .34);
  border-radius: 50%;
  background: rgba(20, 18, 14, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--paper);
  cursor: pointer;
  transition: background-color 160ms ease, border-color 160ms ease;
}
.hero-video-toggle[hidden] { display: none; }
.hero-video-toggle:hover { background: rgba(20, 18, 14, .74); border-color: rgba(246, 243, 237, .6); }
.hero-video-toggle:active { transform: scale(0.94); }

/* Two bars, or a triangle — drawn rather than shipped as icons. */
.hero-video-toggle .is-pause {
  width: 10px;
  height: 12px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}
.hero-video-toggle .is-play {
  width: 0;
  height: 0;
  margin-left: 3px;
  border-left: 11px solid currentColor;
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
}
.hero-video-toggle[data-state='playing'] .is-play,
.hero-video-toggle[data-state='paused'] .is-pause { display: none; }

/* ------------------------------------------------------------------ stat grid */
/* The figures under the hero. A hairline grid rather than three bare columns: the cells
   are separated by 1px gaps over a rule-coloured background, which keeps the dividers
   correct when the grid wraps. Only verifiable figures go here (S5) — no efficacy,
   wait-time or satisfaction numbers. */
.stats {
  list-style: none;
  margin: clamp(44px, 6vw, 84px) 0 0;
  padding: 0;
}

/* .on-ink carries the dark-context colours, so the to-confirm marker, <em> and textlinks
   inside a card all pick up their dark variants instead of being restyled here. */
.stat {
  padding: clamp(26px, 3vw, 40px) clamp(20px, 2.2vw, 32px) clamp(28px, 3.2vw, 44px);
  /* The floor exists to keep the crimson rules on one line across cells whose captions
     wrap to different depths. A figure with no caption has nothing to align against, so it
     sizes to its own content instead of leaving a third of the cell hollow. */
  min-height: clamp(148px, 15vw, 186px);
  display: flex;
  flex-direction: column;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* The glow, shared by all three cell types. A fixed circle moved by transform rather than
   a moving gradient stop: only transform and opacity animate, so it stays off the paint
   path. JS writes the transform straight onto this element — setting a custom property on
   the card would recalculate styles for every child of it.
   The circle sits at z-index -1 under the cell's `isolation: isolate`, which is why the
   content above it needs no z-index of its own. Do not "help" it by positioning the cell's
   children: this element is one of them, and giving it `position: relative` drops a 420px
   circle into normal flow and adds 210px of dead height to every cell.
   These cards are not links, so the hover deliberately has no lift, scale or cursor
   change; a glow reads as life, a lift reads as "click me". */
.stat:has(.stat-d) { min-height: clamp(190px, 19vw, 258px); }

.cell-glow {
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  width: 420px;
  height: 420px;
  margin: -210px 0 0 -210px;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle closest-side,
    rgba(196, 39, 57, .16) 0%,
    rgba(196, 39, 57, .07) 45%,
    rgba(196, 39, 57, 0) 100%);
  /* Exit quicker than entry — the system responding should outpace the user deciding. */
  transition: opacity 160ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .stat[data-glow]:hover .cell-glow,
  .step[data-glow]:hover .cell-glow,
  .datagrid > div[data-glow]:hover .cell-glow {
    opacity: 1;
    transition-duration: 200ms;
  }
  /* No JS, or reduced motion: a still glow, centred. The feedback survives; the tracking
     is what goes. */
  .stat:not([data-glow]):hover .cell-glow,
  .step:not([data-glow]):hover .cell-glow,
  .datagrid > div:not([data-glow]):hover .cell-glow {
    opacity: 1;
    top: 50%;
    left: 50%;
    transition-duration: 200ms;
  }
}

/* ---------------------------------------------------- the panel, shared by three grids
   The figures block, the registration strip and the how-it-works grid all wear it, so the
   declarations sit here once instead of being copied and drifting apart. The cells carry
   .on-ink in the markup, which is what gives their headings, captions and to-confirm
   markers their dark-context colours without any of it being restated here. */
.stats,
.datagrid,
.steps {
  display: grid;
  /* Explicit tracks from the item count, set per panel in the markup. auto-fit picked a
     column count that did not divide the cells, and the leftover track showed the seam
     colour through as a phantom empty cell. */
  grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr));
  gap: 1px;
  /* The seams read as divisions within one black block, not as a light frame around it. */
  background: var(--rule-d);
  border: 1px solid var(--rule-d);
  /* A panel rather than a slab. overflow keeps the corner cells inside the radius — they
     have their own backgrounds and would otherwise square off the corners again. */
  border-radius: 14px;
  overflow: hidden;
}

/* Lit from the top-left corner. Flat black next to flat black gives rectangles that read
   as one shape; a few percent of lift per cell separates them without turning the seams
   into a grid. */
.stat,
.datagrid > div,
.step {
  position: relative;
  /* overflow + isolation so the glow circle is clipped to the cell and stacks behind its
     content. .stat carried these already; the other two need them to host the same glow. */
  overflow: hidden;
  isolation: isolate;
  --idx-o: .8;
  background:
    radial-gradient(120% 90% at 0% 0%, rgba(196, 39, 57, .05) 0%, transparent 60%),
    linear-gradient(168deg, #1d1b16 0%, var(--ink) 58%);
}

/* A hairline index in the corner of each cell. The first two grids count theirs in CSS so
   it cannot fall out of step with the markup; the third already authors its own number,
   which moves to the same corner rather than being duplicated. */
.stats, .datagrid { counter-reset: cell; }
.stat, .datagrid > div { counter-increment: cell; }
.stat::after,
.datagrid > div::after {
  content: counter(cell, decimal-leading-zero);
}
.stat::after,
.datagrid > div::after,
.step .idx {
  position: absolute;
  top: clamp(18px, 2.4vw, 30px);
  right: clamp(18px, 2.2vw, 32px);
  font: 400 10px/1 var(--mono);
  letter-spacing: 0.16em;
  color: var(--muted-d);
  /* Quiet, but still text: measured against the light end of the cell gradient, .45 came
     out at 2.33:1 and .8 at 4.6:1. Held in one place so the reveal below cannot drift
     from it. */
  opacity: var(--idx-o);
}
/* The glow lifts the ground under the corner index: at .8 it measured 3.94:1, under the
   floor for 10px text. .9 puts it back to 4.62:1 at the glow's centre, the worst case. */
@media (hover: hover) and (pointer: fine) {
  .stat:hover, .datagrid > div:hover, .step:hover { --idx-o: .9; }
}

.stat-n {
  font-family: var(--display);
  font-size: clamp(52px, 6.4vw, 86px);
  line-height: .86;
  letter-spacing: -0.035em;
  font-variant-numeric: lining-nums tabular-nums;
  /* A fixed block, bottom-aligned, and always rendered even when a cell has no figure.
     That is what keeps the crimson rules on one line across cells whose descriptions
     wrap to different depths — anchoring the rule to the cell foot did not. */
  display: flex;
  align-items: flex-end;
  min-height: clamp(46px, 5.6vw, 74px);
}
/* A telephone number is still a numeral, but twelve characters of it is not a display
   figure — it gets its own size rather than overflowing the cell. */
.stat-n.is-long {
  font-size: clamp(27px, 2.9vw, 38px);
  line-height: 1.06;
  letter-spacing: -0.018em;
}
.stat-n a { text-decoration: none; }
.stat-n a:hover { color: var(--crimson-d); }

/* Draws itself in as the cell reveals. */
.stat-rule {
  display: block;
  height: 1px;
  width: 44px;
  background: var(--crimson-d);
  margin: clamp(18px, 2vw, 26px) 0 clamp(13px, 1.5vw, 17px);
}
.reveal-armed .stat .stat-rule {
  width: 0;
  transition: width .85s cubic-bezier(.22, .7, .2, 1) var(--d, 0ms);
}

/* The figure wipes up from its own baseline, ahead of the rule drawing and the caption
   settling. Everything used to arrive on the same frame, which read as a block appearing
   rather than a figure being presented. clip-path and transform only — no layout. */
.reveal-armed .stat .stat-n {
  clip-path: inset(0 0 100% 0);
  transform: translateY(14px);
}
.reveal-armed .stat.in .stat-n {
  clip-path: inset(0 0 -20% 0);
  transform: none;
  transition: clip-path 900ms cubic-bezier(.23, 1, .32, 1) calc(var(--d, 0ms) + 90ms),
              transform 900ms cubic-bezier(.23, 1, .32, 1) calc(var(--d, 0ms) + 90ms);
}
.reveal-armed .stat .stat-k,
.reveal-armed .stat .stat-d,
.reveal-armed .stat::after {
  opacity: 0;
  transform: translateY(6px);
}
.reveal-armed .stat.in .stat-k,
.reveal-armed .stat.in .stat-d,
.reveal-armed .stat.in::after {
  opacity: 1;
  transform: none;
  transition: opacity 620ms cubic-bezier(.23, 1, .32, 1) var(--sd),
              transform 620ms cubic-bezier(.23, 1, .32, 1) var(--sd);
}
.reveal-armed .stat.in::after  { --sd: calc(var(--d, 0ms) + 120ms); }
.reveal-armed .stat.in .stat-k { --sd: calc(var(--d, 0ms) + 560ms); }
.reveal-armed .stat.in .stat-d { --sd: calc(var(--d, 0ms) + 640ms); }
.reveal-armed .stat.in::after  { opacity: var(--idx-o); }
.reveal-armed .stat.in .stat-rule { width: 44px; }
.reveal-armed .stat:hover .stat-rule {
  width: 78px;
  transition-duration: .4s;
  transition-delay: 0s;
}

.stat-k {
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted-d);
  display: block;
}
.stat-d {
  font-size: 14.8px;
  line-height: 1.55;
  color: var(--muted-d);
  margin: 11px 0 0;
  max-width: 30ch;
}

@media (max-width: 900px) {
  .stats, .datagrid, .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* An odd number of cells in two columns leaves the last track empty, and the seam
     colour behind it reads as one more, blank cell. The last one takes the full row. */
  .stats > :last-child:nth-child(odd),
  .datagrid > :last-child:nth-child(odd),
  .steps > :last-child:nth-child(odd) { grid-column: 1 / -1; }
}
@media (max-width: 620px) {
  .stats, .datagrid, .steps { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .stats { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the min-height is just dead space — three cells ran to 604px. */
  .stat { min-height: 0; padding-top: 24px; padding-bottom: 26px; }
  .stat-n { min-height: 0; }
}

/* ------------------------------------------------------------------ statement */
.statement { text-align: center; }
.statement p {
  font-family: var(--display);
  font-size: clamp(23px, 2.9vw, 40px);
  line-height: 1.26;
  letter-spacing: -0.017em;
  max-width: 22ch;
  margin: 0 auto;
  text-wrap: balance;
}
.statement.wide p { max-width: 27ch; }

/* ------------------------------------------------------------------ profile */
.profile-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(28px, 5vw, 76px);
  align-items: start;
}
.profile-copy h2 { margin-bottom: 22px; }
.profile-copy .lede { margin-bottom: 26px; }

.datagrid {
  margin-top: 34px;
}
/* One detail per row. Seven of these side by side gave columns too narrow for their own
   labels — "TELEPHONE" and "CONSULTING HOURS" were being cut off. Two classes, so it
   outranks the column counts the breakpoints set and stays stacked at every width. */
.datagrid.stacked { grid-template-columns: minmax(0, 1fr); }

/* Room on the right for the corner index.
   Each cell hands its two rows — label, then value — back to the panel, so every label in
   a row shares one track and every value shares the next. Without it a label that wraps to
   two lines ("PRACTICE NUMBER" at four columns) pushed its own value down while its
   neighbours' stayed put. */
.datagrid > div {
  padding: 20px 46px 22px 18px;
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 2;
  align-content: start;
}
/* Where subgrid is not available, reserve two lines for every label instead. Less exact —
   a three-line label would break it again — but it holds for the labels in use. */
@supports not (grid-template-rows: subgrid) {
  .datagrid > div { display: block; }
  .datagrid .k { min-height: 2em; }
}
/* The icon sits on the label, not the value: the label is the shorter, steadier line, and
   keeping them together means the value still starts against the cell's left edge. */
.datagrid .k { display: flex; align-items: center; gap: 7px; }
.datagrid .ic { width: 13px; height: 13px; flex: 0 0 auto; opacity: .75; }

.datagrid .k {
  font: 400 10.5px/1 var(--mono);
  letter-spacing: 0.17em; text-transform: uppercase;
  color: var(--muted-d); margin: 0 0 9px;
}
.datagrid .v { margin: 0; font-size: 15.5px; line-height: 1.4; }

/* ------------------------------------------------------------------ media slot */
/* Stands in for a photograph we do not have. Deliberately looks like a gap, not a design. */
.slot {
  background: var(--paper);
  border: 1px dashed var(--rule);
  display: grid; place-content: center; justify-items: center;
  text-align: center;
  padding: 34px 26px;
  gap: 10px;
  min-height: 260px;
}
.slot.tall { min-height: 420px; }
.slot .slot-t {
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
}
.slot .slot-n { font-size: 14px; color: var(--muted); max-width: 32ch; margin: 0; }
.on-ink .slot { background: var(--ink-2); border-color: var(--rule-d); }

/* A real photograph, once one exists, fills the same box the placeholder held. */
/* ---- pinned facts
   Badges sit outside .shot-frame, because the frame clips its own overflow for the
   reveal — anything inside it would be cut off at the edge. They carry only figures
   verified in S5. Not interactive, so no hover state: these are labels, not controls. */
.portrait { position: relative; }

/* Pointer tilt. The rotation is written straight onto this element by the script, so the
   wrapper's own transform stays out of the way. */
[data-tilt] { will-change: transform; }

/* A sheen that tracks the cursor across the photograph. Fades in only while tilting, so
   the portrait is clean at rest. */
.glare {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: radial-gradient(circle at var(--gx, 50%) var(--gy, 20%),
    rgba(255, 255, 255, .28), transparent 50%);
  opacity: 0;
  transition: opacity 300ms ease;
}
.portrait.is-tilting .glare { opacity: 1; }

.pin {
  position: absolute;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 100px;
  padding: 10px 17px;
  font: 400 13.5px/1.2 var(--body);
  color: var(--ink);
  box-shadow: 0 12px 28px rgba(20, 18, 14, .12);
  white-space: nowrap;
}
.pin strong {
  font-family: var(--display);
  font-weight: 400;
  font-size: 20px;
  letter-spacing: -0.02em;
}
.pin-a { top: 8%; left: -7%; }
.pin-b {
  bottom: 14%; left: -9%;
  width: 56px; height: 56px;
  padding: 0;
  border-radius: 50%;
  justify-content: center;
}
.pin-b img { width: 34px; height: auto; display: block; }
.pin-c { bottom: 5%; right: -7%; }

/* Staggered in behind the frame: the clip runs 760ms, so the badges start as it finishes
   landing rather than racing it. 70ms apart — far enough to read as a sequence, close
   enough not to feel slow. */
.reveal-armed .portrait .pin {
  opacity: 0;
  transform: translateY(10px) scale(0.92);
}
.reveal-armed .portrait.in .pin {
  opacity: 1;
  transform: none;
  transition: opacity 450ms cubic-bezier(0.23, 1, 0.32, 1) var(--pd, 0ms),
              transform 450ms cubic-bezier(0.23, 1, 0.32, 1) var(--pd, 0ms);
}
/* 120ms apart, landing across the tail of the 1000ms clip rather than after it. */
.pin-a { --pd: 800ms; }
.pin-b { --pd: 920ms; }
.pin-c { --pd: 1040ms; }

/* A slow halo on the practice mark — the one piece of continuous motion in the block,
   so the portrait keeps a pulse once everything has landed. Crimson, not the reference's
   cyan: that colour belongs to the other practice. */
.reveal-armed .portrait.in .pin-b {
  animation: pin-pulse 3.2s cubic-bezier(0.23, 1, 0.32, 1) 1.9s infinite;
}
@keyframes pin-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(163, 32, 47, .38), 0 12px 28px rgba(20, 18, 14, .12); }
  70%  { box-shadow: 0 0 0 16px rgba(163, 32, 47, 0), 0 12px 28px rgba(20, 18, 14, .12); }
  100% { box-shadow: 0 0 0 0 rgba(163, 32, 47, 0), 0 12px 28px rgba(20, 18, 14, .12); }
}

/* The frame now owns the ratio and the border, so neither moves with the parallax. */
.shot-frame {
  position: relative;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
}
.shot-frame.tall { aspect-ratio: 4 / 5; }

/* 6% of overhang top and bottom gives the parallax somewhere to travel without ever
   exposing an edge: the layer shifts +/-4.5% of its own height, which is 5.04% of the
   frame's — inside the 6% it has to play with. */
.shot-parallax { position: absolute; inset: -6% 0; display: block; }

.shot {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center top;
  border: 0;
}

/* Scroll-linked, so it runs off the main thread with no scroll listener and no JS. Linear
   because the user's scroll is the clock — easing here would fight their input. Chromium
   has this today; elsewhere it simply does not apply, which is the right failure for
   decoration. */
@keyframes shot-parallax {
  from { transform: translate3d(0, -4.5%, 0); }
  to   { transform: translate3d(0, 4.5%, 0); }
}
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    /* The timeline is named on the wrapper, not taken from view() on the layer itself:
       .shot-frame has overflow:hidden for the clip reveal, which makes it a scroll
       container, and view() would resolve its scrollport to the frame — pinning progress
       at 50% forever. Naming it outside the frame measures the portrait against the page. */
    .reveal:has(> .shot-frame) { view-timeline-name: --shot-view; }
    .shot-parallax {
      animation: shot-parallax linear both;
      animation-timeline: --shot-view;
      animation-range: entry 0% exit 100%;
    }
  }
}
.on-ink .slot .slot-t, .on-ink .slot .slot-n { color: var(--muted-d); }

/* ------------------------------------------------------------------ list rows */
.rows { border-top: 1px solid var(--rule); margin-top: clamp(30px, 4vw, 52px); }
.on-ink .rows { border-top-color: var(--rule-d); }
.row {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr) minmax(0, 0.9fr) 30px;
  gap: 18px; align-items: center;
  padding: clamp(17px, 2vw, 25px) 0;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
}
.on-ink .row { border-bottom-color: var(--rule-d); }
.row h3 { font-size: clamp(19px, 2.1vw, 28px); transition: color .2s ease; }
.row .row-d { font-size: 14.5px; color: var(--muted); margin: 0; transition: color .2s ease; }
.on-ink .row .row-d { color: var(--muted-d); }
.row .row-go {
  width: 30px; height: 30px; border-radius: 50%;
  border: 1px solid var(--rule);
  display: grid; place-items: center;
  transition: background-color .2s ease, border-color .2s ease, color .2s ease;
}
.on-ink .row .row-go { border-color: var(--rule-d); }
.row .row-go svg { width: 11px; height: 11px; }
a.row:hover .row-go { background: var(--crimson); border-color: var(--crimson); color: var(--paper); }
.on-ink a.row:hover .row-go { background: var(--crimson-d); border-color: var(--crimson-d); color: var(--ink); }

/* The crimson hover.
   The tint is painted by a pseudo-element inset past the row rather than by putting a
   background on the row itself: the row's own box is what the divider lines are drawn on,
   and widening it with negative margins would have dragged those lines out into the page
   gutter. isolation keeps the z-index: -1 inside this row, the way the figure cards do it.
   Paper on crimson is 6.76:1, so every piece of text in the row clears 4.5:1 on it. */
a.row { position: relative; isolation: isolate; }
a.row::before {
  content: '';
  position: absolute;
  inset: 2px -18px;
  z-index: -1;
  border-radius: 10px;
  background: transparent;
  transition: background-color .2s ease, box-shadow .25s ease;
}
@media (hover: hover) and (pointer: fine) {
  a.row:hover::before {
    background: var(--crimson);
    box-shadow: 0 14px 34px rgba(163, 32, 47, .42);
  }
  a.row:hover h3,
  a.row:hover .row-d,
  a.row:hover .idx { color: var(--paper); }
  /* The disc inverts, so it stays the brightest thing in the row. */
  a.row:hover .row-go,
  .on-ink a.row:hover .row-go {
    background: var(--paper);
    border-color: var(--paper);
    color: var(--crimson);
  }
}

/* ------------------------------------------------------------------ steps */
.steps {
  margin-top: clamp(30px, 4vw, 52px);
}
/* Room on the right for the index, which now sits in the corner rather than above the
   heading. */
.step {
  padding: clamp(22px, 2.6vw, 34px) clamp(40px, 3.4vw, 52px) clamp(26px, 3vw, 40px)
           clamp(18px, 2vw, 28px);
}
.step h3 { margin-bottom: 11px; font-size: clamp(19px, 1.7vw, 23px); }
.step p { font-size: 14.8px; color: var(--muted-d); margin: 0; }

/* ------------------------------------------------------------------ practice cards */
.practices {
  display: grid;
  /* Explicit tracks from the practice count, as the panels do. auto-fit was leaving a
     collapsed 0px third track here, and with a gap that shows as an extra gap's worth of
     space on the right of the row. */
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: clamp(16px, 2vw, 26px);
  margin-top: clamp(30px, 4vw, 52px);
}
@media (max-width: 760px) { .practices { grid-template-columns: minmax(0, 1fr); } }
.practice {
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: clamp(24px, 2.6vw, 38px);
  display: flex; flex-direction: column; gap: 17px;
}
.practice h3 { font-size: clamp(24px, 2.4vw, 33px); }
.practice address { font-style: normal; color: var(--muted); font-size: 15.5px; line-height: 1.55; }
.practice .practice-tel {
  font-family: var(--display);
  font-size: clamp(22px, 2.1vw, 28px);
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  display: inline-block;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 3px;
}
.practice .practice-tel:hover { border-color: var(--crimson); }
/* These are the numbers the whole site exists to surface — give them a real target. */
.fact .v .practice-tel { min-height: 44px; display: inline-flex; align-items: center; }
.practice-foot { margin-top: auto; padding-top: 6px; display: flex; flex-wrap: wrap; gap: 10px; }

/* ------------------------------------------------------------------ facts band */
.facts {
  display: grid;
  /* One track per practice. auto-fit was leaving collapsed 0px tracks on the end — two
     of them at 1000px — and each one still takes a gap, which pushed the practices out of
     line with the form beside them. */
  grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr));
  gap: clamp(22px, 3vw, 44px);
  margin-top: clamp(30px, 4vw, 52px);
  padding-top: clamp(26px, 3vw, 40px);
  border-top: 1px solid var(--rule);
}
/* --cols is the number of practices, so on a phone this would otherwise hold two 157px
   columns. One per row below 760px, matching .practices above. It has to come after the
   rule it overrides — placed before it, the block above simply won on source order. */
@media (max-width: 760px) { .facts { grid-template-columns: minmax(0, 1fr); } }
.on-ink .facts { border-top-color: var(--rule-d); }
.fact .k {
  font: 400 10.5px/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted); display: block; margin-bottom: 12px;
}
.on-ink .fact .k { color: var(--muted-d); }
/* Map on the left of the details — but only where the block is wide enough to carry both.
   A container query rather than a page-width one, because the same block is full width on a
   practice's own page and half of that where two practices sit side by side; what matters
   is how much room this block has, not how wide the window is. */
.fact { container-type: inline-size; }
/* The single column is stated, not left to `auto`. An auto track sizes to the iframe's
   intrinsic 300px, which is wider than this cell gets on a phone — the map then overflowed
   the cell and sat on top of the practice beside it. minmax(0, 1fr) caps it at the cell.
   It doubles as the no-container-query fallback, which is why the old @supports went. */
.fact-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; margin-top: 4px; }
@container (min-width: 430px) {
  /* :has, so a block without a map does not reserve a column for one. */
  .fact-body:has(.map-shell) { grid-template-columns: minmax(0, 210px) minmax(0, 1fr); align-items: start; }
}
.fact-body .map-shell { aspect-ratio: 1 / 1; border-radius: 10px; }

/* The telephone leads, at its own size, with an icon beside it. */
.fact .v {
  font-size: 16px; line-height: 1.5; margin: 0;
  display: flex; align-items: center; gap: 9px;
}
.fact .v .ic { width: 15px; height: 15px; flex: 0 0 auto; opacity: .8; }

/* The rest of the practice's details, one per line under it. The icon is pinned to the
   first line rather than centred, so an address that wraps hangs correctly beneath it. */
.fact-line {
  display: flex; align-items: flex-start; gap: 9px;
  margin: 9px 0 0;
  font-size: 14.5px; line-height: 1.45;
  color: var(--muted);
}
.on-ink .fact-line { color: var(--muted-d); }
.fact-line .ic { width: 14px; height: 14px; flex: 0 0 auto; margin-top: 2px; opacity: .8; }
.fact-line a { text-decoration: underline; text-underline-offset: 3px; }
.fact-line a:hover { color: var(--paper); }

/* ------------------------------------------------------------------ faq */
.faq-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  gap: clamp(28px, 5vw, 76px);
  align-items: start;
}
/* The list sits against the middle of the column rather than the top of it. The practice
   pages ask fewer questions than the shared ones, so a top-aligned list left all of its
   slack pooled underneath; centred, the space falls either side. Where the list is the
   taller of the two columns this changes nothing. */
.faq-list { align-self: center; }
/* The tooth from the old hero, filling the space under the FAQ heading. It reuses the
   hero-float keyframes rather than introducing a second piece of motion, so the two read
   as the same hand. Decorative, so alt="" — and hidden once the grid stacks, where it
   would only push the questions further down the page. */
.faq-mark {
  display: block;
  width: min(74%, 310px);
  height: auto;
  margin-top: clamp(30px, 4vw, 60px);
  filter: drop-shadow(0 18px 24px rgba(20, 18, 14, .14));
  animation: hero-float 7s ease-in-out .55s infinite;
}

.faq-list { border-top: 1px solid var(--rule); }
.faq-list details { border-bottom: 1px solid var(--rule); }
/* Both pseudo-elements on this summary are already spoken for by the plus marker, so the
   hover tint is carried by the element itself. The padding and the negative margin cancel:
   the question and the marker stay exactly where they were, and only the tinted area is
   wider. The divider sits on the <details>, so it is untouched. */
.faq-list summary {
  list-style: none;
  cursor: pointer;
  padding: 22px 56px 22px 16px;
  margin: 0 -16px;
  border-radius: 10px;
  transition: background-color .2s ease, color .2s ease, box-shadow .25s ease;
  position: relative;
  font-family: var(--display);
  font-size: clamp(17.5px, 1.5vw, 21px);
  line-height: 1.3;
  min-height: 44px;
  display: flex; align-items: center;
}
.faq-list summary::-webkit-details-marker { display: none; }
.faq-list summary::after {
  content: ''; position: absolute; right: 20px; top: 50%;
  width: 11px; height: 1.5px; background: var(--ink);
}
.faq-list summary::before {
  content: ''; position: absolute; right: 24.75px; top: 50%;
  width: 1.5px; height: 11px; background: var(--ink);
  transform: translateY(-50%);
  transition: opacity .2s ease;
}
.faq-list details[open] summary::before { opacity: 0; }
@media (hover: hover) and (pointer: fine) {
  .faq-list summary:hover {
    background: var(--crimson);
    color: var(--paper);
    box-shadow: 0 12px 30px rgba(163, 32, 47, .28);
  }
  /* The plus is drawn in ink; it has to come with the text. */
  .faq-list summary:hover::before,
  .faq-list summary:hover::after { background: var(--paper); }
}
.faq-list .a { padding: 0 44px 24px 0; color: var(--muted); font-size: 16px; }
.faq-list .a p { margin-bottom: .7em; }

/* ------------------------------------------------------------------ contact / form */
.contact-grid {
  display: grid;
  /* The left side holds one column per practice and the right holds the form, so a 50/50
     split made each practice half the width of the form. Weighting the left by the number
     of practices puts all three on the same width. */
  grid-template-columns: minmax(0, var(--practices, 1fr)) minmax(0, 1fr);
  gap: clamp(30px, 5vw, 80px);
  /* stretch, not start: the form column matches the height of the practices beside it. */
  align-items: stretch;
}
/* The callback column fills the row, and the form fills the column — its rows keep their
   own size and the space left over falls between them, so the submit button sits on the
   floor of the section rather than leaving a gap under it. */
/* Landing room. The header does not stick, so nothing covers the target — this is only so
   the label does not sit hard against the top edge of the viewport on arrival. */
.callback-col { scroll-margin-top: 28px; }
.contact-grid > :last-child { display: flex; flex-direction: column; }
.contact-grid .form { flex: 1; align-content: space-between; }
.form { display: grid; gap: 15px; margin-top: 8px; }
/* The grid stretches its children, which had the submit running the full width of the
   column and reading as heavily as the hero CTA. It sizes to its own label instead. */
.form .btn { justify-self: start; }
.field { display: grid; gap: 8px; }
/* A field whose value is settled — the practice, on that practice's own page. It carries
   the label's styling so the form still reads as a form, but there is nothing to operate
   and so nothing that should look operable. */
.field-label {
  font: 400 10.5px/1 var(--mono);
  letter-spacing: 0.17em; text-transform: uppercase; color: var(--muted);
}
.on-ink .field-label { color: var(--muted-d); }
.field-fixed {
  font: 400 16px/1.4 var(--body);
  margin: 0;
  padding: 13px 0;
  min-height: 48px;
  display: flex; align-items: center;
}

.field label {
  font: 400 10.5px/1 var(--mono);
  letter-spacing: 0.17em; text-transform: uppercase; color: var(--muted);
}
.on-ink .field label { color: var(--muted-d); }
.field input, .field select {
  font: 400 16px/1.4 var(--body);
  background: transparent;
  color: inherit;
  border: 1px solid var(--line-ui);
  border-radius: var(--r);
  padding: 13px 14px;
  min-height: 48px;
  width: 100%;
}
.on-ink .field input, .on-ink .field select { border-color: var(--line-ui-d); }
/* appearance: none takes the native arrow with it, which left a control that looked like
   a text input. The chevron is drawn back on, and the padding keeps the longest practice
   name off it. */
.field select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' fill='none' stroke='%23A79E90' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 12px 8px;
  padding-right: 38px;
}

/* The open list is painted by the operating system on its own light background, but the
   options inherit their colour from the select — which on this dark band is near-white,
   so the list came out white on white and only the highlighted row was readable. Both
   halves are stated here rather than inherited. Ink on paper is 15.9:1. */
.field select option {
  background: var(--paper);
  color: var(--ink);
}
.field input::placeholder { color: var(--muted); }
.on-ink .field input::placeholder { color: var(--muted-d); }
.form-note { font: 400 12px/1.5 var(--mono); color: var(--muted); margin: 4px 0 0; }
.on-ink .form-note { color: var(--muted-d); }

/* ------------------------------------------------------------------ scheme rail */
/* A light lane inside the dark contact band. The logos are colour marks drawn for white
   grounds, so they sit on paper rather than being inverted — recolouring a scheme's mark
   to suit our palette would be altering someone else's trademark. The white plates baked
   into the source PNGs are keyed out, so there are no boxes behind them. */
/* A section that ends on the lane needs no trailing padding — the lane is the edge. */
.on-ink:has(> .scheme-lane) { padding-bottom: 0; }

/* A band of its own between the last section and the footer. No top margin: it butts
   against whatever precedes it, so a page ending dark goes straight from that band into
   this one. With a margin it left a sliver of page ground between the two, and its own
   top border then read as an arbitrary hairline across it. */
.scheme-lane {
  background: var(--paper);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: clamp(18px, 2.2vw, 28px) 0 clamp(20px, 2.4vw, 30px);
  overflow: hidden;
}
.scheme-head {
  position: relative;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px;
  margin-bottom: clamp(14px, 1.6vw, 20px);
}
.scheme-head .label { margin: 0; color: var(--muted); }

/* Off the page until it is focused, the same as the tagline control. The marquee starts
   on its own and runs indefinitely beside other content, so 2.2.2 wants a way to stop it;
   this keeps that for a keyboard or a screen reader while taking it out of the band. The
   rail also pauses on hover, which covers a mouse. */
.scheme-toggle {
  position: absolute;
  left: -9999px;
  top: 0;
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted);
  background: none;
  border: 1px solid var(--line-ui);
  border-radius: 100px;
  padding: 10px 18px;
  min-height: 44px;
  display: inline-flex; align-items: center;
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease, transform 160ms ease-out;
}
.scheme-toggle:focus { position: static; }
.scheme-toggle:hover { color: var(--ink); border-color: var(--ink); }
.scheme-toggle:active { transform: scale(0.97); }

.scheme-rail { overflow: hidden; }
.scheme-marquee {
  display: flex;
  width: max-content;
  /* Constant motion, so linear — an eased marquee visibly slows at the seam. */
  animation: scheme-slide 46s linear infinite;
}
.scheme-lane.is-paused .scheme-marquee { animation-play-state: paused; }
.scheme-track { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; }
.scheme-track li { flex: 0 0 auto; padding: 0 clamp(24px, 3.2vw, 52px); }
.scheme-track img {
  display: block;
  height: clamp(32px, 3.2vw, 44px);
  width: auto;
}
@keyframes scheme-slide { to { transform: translateX(-50%); } }

@media (hover: hover) and (pointer: fine) {
  .scheme-rail:hover .scheme-marquee { animation-play-state: paused; }
}

/* ------------------------------------------------------------------ footer */
/* Trimmed from --band: the scheme lane already closes the section above with its own
   padding and a rule, so a full band here read as a hole rather than breathing room. */
.site-footer { background: var(--ink); color: var(--paper); padding: clamp(44px, 5vw, 72px) 0 0; }
.foot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) repeat(auto-fit, minmax(128px, 1fr));
  gap: clamp(26px, 4vw, 56px);
  padding-bottom: clamp(40px, 5vw, 70px);
}
/* The reversed artwork, white on the near-black footer. */
.foot-logo { height: 52px; width: auto; margin-bottom: 20px; display: block; }
/* h2, not h3: these are sections of the footer, not subsections of anything above them.
   As h3 they skipped a level on any page whose main content had no h2 — the 404 page, once
   the contact band stopped carrying one. */
.foot-col h2 {
  font: 400 10.5px/1 var(--mono);
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted-d); margin-bottom: 15px;
}
.foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.foot-col a {
  text-decoration: none; font-size: 15px;
  display: inline-flex; align-items: center; min-height: 32px;
}
/* Paper, not the accent. This is 15px body text, which needs 4.5:1 — and holding the
   accent to that would have forced it brighter, away from the brand crimson it is meant to
   match. Brightening on hover reads the same and measures 15.9:1. */
.foot-col a:hover { color: var(--paper); }
.foot-col p { font-size: 14.5px; color: var(--muted-d); margin: 0 0 9px; }

/* The one social account, under the blurb it belongs to. 32px of height, so it clears the
   24px minimum for a pointer target (2.5.8) on a phone, and paper rather than the muted
   grey beside it because it is a link and has to read as one. */
.foot-social { margin: 14px 0 0; }
.foot-social a {
  display: inline-flex; align-items: center; gap: 9px;
  min-height: 32px; font-size: 15px;
  color: var(--paper); text-decoration: none;
}
.foot-social .ic { width: 17px; height: 17px; flex: 0 0 auto; opacity: .85; }
.foot-social a:hover .ic { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  .foot-social a:hover span { text-decoration: underline; text-underline-offset: 3px; }
}
/* The registration details sit on the same rhythm as the link columns beside them: the
   same 15px, the same 32px row and the same 2px between, so the three columns line up row
   for row rather than drifting apart down the footer. Full paper, not the muted grey the
   footer uses for supporting text — Rule 5 encourages these being shown, and they carry
   the specialist claim, so they are not something to tuck away. 16.9:1 on this ground. */
.foot-reg { display: grid; gap: 2px; }
.foot-reg p {
  font-size: 15px;
  color: var(--paper);
  margin: 0;
  min-height: 32px;
  display: flex; align-items: center;
}

/* The agency name is a proper noun: it moves to the next line whole rather than breaking
   across two, which is what it did at phone width. The sentence in front of it still
   wraps normally. */
.foot-legal a { white-space: nowrap; }

.foot-legal {
  border-top: 1px solid var(--rule-d);
  padding: 22px 0 30px;
  display: flex; flex-wrap: wrap; gap: 8px 24px;
  font-size: 13px; color: var(--muted-d);
}
/* Spread across the footer once the three parts genuinely fit on one line. They need
   1033px between them and the container reaches that at about a 1160px viewport; spreading
   any earlier pushes a wrapped line's items to opposite ends with a hole in the middle,
   which reads worse than the left-aligned row it replaces. The figure is tied to the
   length of this copy — if the credit line is reworded, re-measure it. */
@media (min-width: 1160px) {
  .foot-legal { justify-content: space-between; gap: 8px 32px; }
}

/* ------------------------------------------------------------------ to confirm */
/* Unverified facts are shown as unverified rather than filled with plausible values (S5).
   A wrong trading hour on a healthcare site sends someone to a locked door. */
.to-confirm {
  font-family: var(--mono);
  font-size: 0.82em;
  letter-spacing: 0.04em;
  color: #8A5A16;
  border-bottom: 1px dotted #B98A3E;
  overflow-wrap: anywhere;
}
.on-ink .to-confirm { color: #E7B778; border-bottom-color: #A87A44; }

/* ------------------------------------------------------------------ map */
.map-shell {
  border: 1px solid var(--rule);
  background: var(--paper);
  aspect-ratio: 16 / 10;
  display: grid; place-items: center;
  position: relative;
  overflow: hidden;
}
.map-shell iframe { width: 100%; height: 100%; border: 0; display: block; }

/* ------------------------------------------------------------------ motion */
/* Elements are hidden only once the script has confirmed it can reveal them again. The
   hidden state used to be unconditional, which meant a script that never ran left the page
   blank rather than merely unanimated. */
.reveal-armed .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .6s ease var(--d, 0ms), transform .6s ease var(--d, 0ms);
}
.reveal-armed .reveal.in { opacity: 1; transform: none; }

/* ---- portrait reveal
   The frame clips open from the bottom while the photograph settles back from a slight
   push-in. Two rates rather than one is what makes this read as a camera settling instead
   of a wipe. It is the one image on the site that earns the extra attention — S8: a
   dentist's website sells one person, and this is that person.
   No hover state: the portrait is not a control, and a push-in on hover would imply it
   is. The entrance carries it. */
.shot-frame { display: block; overflow: hidden; }

/* its wrapper's generic fade-up stands down, or the two would fight */
.reveal-armed .reveal:has(> .shot-frame) {
  opacity: 1;
  transform: none;
  transition: none;
}

.reveal-armed .reveal > .shot-frame {
  /* Overshooting to -60px means the clip finishes outside the box, so nothing pinned to
     the frame gets shaved by it. ease-in-out here, not ease-out: the frame is travelling
     across the picture rather than entering, so it wants acceleration at both ends. */
  clip-path: inset(calc(100% + 60px) -60px -60px -60px);
  transition: clip-path 1000ms cubic-bezier(0.77, 0, 0.175, 1) var(--d, 0ms);
}
.reveal-armed .reveal > .shot-frame .shot {
  /* Scaling from 50% 30% pulls back from her face rather than the middle of the frame,
     so the settle lands on the subject instead of drifting across it. */
  transform: scale(1.12);
  transform-origin: 50% 30%;
  transition: transform 1400ms cubic-bezier(0.23, 1, 0.32, 1) var(--d, 0ms);
}
.reveal-armed .reveal.in > .shot-frame { clip-path: inset(-60px); }
.reveal-armed .reveal.in > .shot-frame .shot { transform: scale(1); }

@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal.in { opacity: 1; transform: none; transition: none; }
  .scheme-marquee { animation: none; }
  .scheme-toggle { display: none; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }
}

/* --------------------------------------------------------------- bio dialog */
/* The panel that opens from her name. Her logo, her colours, the same Fraunces display
   and crimson accent as the page behind it, so it reads as part of the practice rather
   than as browser furniture. */
.bio-open {
  display: inline-flex; align-items: center; gap: 10px;
  background: none; border: 0; padding: 0;
  margin-top: 4px; min-height: 32px;
  font: 500 15.5px/1 var(--body);
  color: var(--crimson);
  cursor: pointer;
  /* One line. It is a name followed by a mark, and breaking it mid-name read as three
     stacked fragments beside an arrow. */
  white-space: nowrap;
}
/* The bare glyph, not the pill the .btn variants wrap it in — there is no .btn-arrow
   element here to inherit a size from, and an unsized inline SVG takes its own default,
   which is why this rendered at several times the size of the words beside it. */
.bio-open > svg {
  width: 15px; height: 15px; flex: 0 0 auto;
  transition: transform .2s cubic-bezier(0.23, 1, 0.32, 1);
}
@media (hover: hover) and (pointer: fine) {
  .bio-open:hover > svg { transform: translate(2px, -2px); }
}
@media (hover: hover) and (pointer: fine) {
  .bio-open:hover { text-decoration: underline; text-underline-offset: 4px; }
}

.bio-dialog {
  width: min(680px, calc(100vw - 32px));
  max-height: min(84vh, 760px);
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 16px;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
}
/* Scoped to [open], and it has to be. The UA stylesheet hides a closed dialog with
   `dialog:not([open]) { display: none }`, but that is a UA-origin declaration and every
   author one beats it whatever the specificity — so an unconditional `display: flex` here
   overrode it and rendered the closed panel in normal flow at the end of the document,
   which is to say on top of the footer.
   The panel scrolls, not the page behind it: a long description has to stay inside its own
   box or the close button walks off the top of the screen. */
.bio-dialog[open] { display: flex; flex-direction: column; }
.bio-dialog::backdrop { background: rgba(20, 18, 14, .62); }

/* Above the fold of the panel, and above the scrolling body, so it is reachable at any
   scroll position — which is what makes the dialog dismissable by pointer as well as by
   Escape. 40px square clears the 24px minimum target with room to spare. */
.bio-close-form { position: absolute; top: 10px; right: 10px; z-index: 2; }
.bio-close {
  width: 40px; height: 40px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--rule); border-radius: 999px;
  background: var(--paper); color: var(--ink);
  cursor: pointer;
}
.bio-close svg { width: 18px; height: 18px; }
.bio-close:hover { background: var(--bone); }

.bio-head {
  padding: clamp(26px, 4vw, 38px) clamp(22px, 4vw, 40px) 0;
  border-bottom: 1px solid var(--rule);
  padding-bottom: clamp(18px, 2.5vw, 26px);
}
.bio-head img { height: 54px; width: auto; display: block; margin-bottom: 12px; }
/* The same typed line as the header, a little larger because it has the width of the panel
   rather than a corner of the chrome. Everything that sets its geometry is in ch and em, so
   raising the font size scales the reserved width, the tracking and the caret together and
   the reveal still lands on glyph boundaries. */
.bio-tagline {
  font-size: 9.5px;
  color: var(--crimson);
  margin-bottom: 18px;
}
.bio-head h2 { font-size: clamp(28px, 4.4vw, 38px); margin: 0 0 6px; }
.bio-sub { font: 400 14px/1.5 var(--mono); color: var(--muted); margin: 0; }

.bio-body {
  padding: clamp(22px, 3.4vw, 32px) clamp(22px, 4vw, 40px) clamp(26px, 4vw, 38px);
  overflow-y: auto;
  display: grid; gap: clamp(22px, 3vw, 30px);
}
.bio-prose p { font-size: 16px; line-height: 1.65; margin: 0 0 14px; }
.bio-prose p:last-child { margin-bottom: 0; }
/* The standfirst carries the panel from the name into the prose, so it takes the display
   face at a size between the two rather than reading as another body paragraph. */
/* Scoped under .bio-prose, not standalone. `.bio-prose p` is 0-1-1 and a bare `.bio-lead`
   is 0-1-0, so the paragraph rule was winning and holding both this and the sign-off at
   16px — the !important on the margins hid it, because the spacing obeyed while the type
   size did not. */
.bio-prose .bio-lead {
  font: 400 clamp(18px, 2.2vw, 21px)/1.45 var(--display);
  margin-bottom: 20px;
}
.bio-prose h3 {
  font: 400 clamp(19px, 2.3vw, 22px)/1.3 var(--display);
  margin: 26px 0 12px;
}
.bio-prose h3:first-child { margin-top: 0; }
.bio-list {
  margin: 0 0 16px; padding: 0; list-style: none;
  display: grid; gap: 7px;
}
.bio-list li {
  position: relative; padding-left: 20px;
  font-size: 15.5px; line-height: 1.5;
}
/* The marker is the brand crimson, which is decorative here — the text beside it carries
   the meaning, so it is not held to a text contrast floor. */
.bio-list li::before {
  content: ''; position: absolute; left: 3px; top: .62em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--crimson);
}
/* The sign-off. Centred and set larger than the prose, because it is a strapline and not
   another sentence of it — read left-aligned at body size it looked like a paragraph that
   had lost its ending. The three beats are separate spans so a narrow panel breaks between
   phrases rather than through one. */
.bio-prose .bio-closing {
  display: flex; flex-wrap: wrap;
  justify-content: center; align-items: center;
  gap: 2px 14px;
  text-align: center;
  font: 400 clamp(19px, 2.6vw, 25px)/1.4 var(--display);
  color: var(--crimson);
  margin: 34px 0 4px;
  padding-top: 30px;
  position: relative;
}
.bio-closing span { white-space: nowrap; }
/* One line where there is room, three where there is not — never two. The line needs 304px
   of content width and a 375px phone offers 290, so it missed by 15px and broke 2 + 1,
   which reads as an accident rather than a choice. Shrinking the type to close a 15px gap
   would only move the break to a narrower phone; stacking it is stable at every width and
   looks deliberate. 460px matches the breakpoint the footer already stacks at. */
@media (max-width: 460px) {
  .bio-prose .bio-closing {
    flex-direction: column;
    gap: 4px;
    padding-top: 26px;
    margin-top: 30px;
  }
}
/* A hairline that fades out rather than a rule that stops dead against the panel's edges. */
.bio-closing::before {
  content: ''; position: absolute; top: 0; left: 50%; transform: translateX(-50%);
  width: min(100%, 250px); height: 1px;
  background: linear-gradient(to right, transparent, var(--rule) 22%, var(--rule) 78%, transparent);
}
/* The sparkle is the one from the logo mark. Its own row above the words, decorative only,
   so it carries no contrast obligation and no alternative text. */
.bio-spark {
  flex: 0 0 100%;
  width: 17px; height: 17px;
  margin: 0 auto 10px;
  display: block;
  opacity: .85;
}
.bio-pending { font-size: 15px; }
/* The credentials read as a list of facts, not as prose — the same cells the page uses. */
.bio-facts { margin: 0; }

@media (prefers-reduced-motion: no-preference) {
  .bio-dialog[open] { animation: bio-in .26s cubic-bezier(0.23, 1, 0.32, 1); }
  .bio-dialog[open]::backdrop { animation: bio-fade .26s ease-out; }
}
@keyframes bio-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
@keyframes bio-fade { from { opacity: 0; } to { opacity: 1; } }

/* ------------------------------------------------------------------ responsive */
@media (max-width: 1000px) {
  .hero-grid { grid-template-columns: 1fr; }
  .hero-mark { order: -1; min-height: 290px; }
  .hero-full { min-height: clamp(440px, 76vh, 620px); }
  .hero-inner { justify-items: stretch; }
  .hero-copy-full { max-width: none; }
  /* Stacked, the copy spans the full width, so a left-weighted scrim leaves the end of
     each line over the thin side — measured 3.36:1 on the lede against a 4.5 floor.
     An even wash instead: at .62 the lede clears even against pure white.
     This one is NOT cut the way the wide scrim above was. Re-measured against the mobile
     encode at 375x617: the lede sits at 5.32:1 and the button's hairline at 5.26:1, so the
     whole wash has about 8% in hand before either drops under its floor — a change too
     small to see. The room the wide scrim had came from the copy occupying only the left
     half of the frame; here it crosses the bright side as well, and there is nowhere to
     thin it. */
  .hero-scrim {
    background: linear-gradient(to bottom,
      rgba(20, 18, 14, .70) 0%,
      rgba(20, 18, 14, .62) 38%,
      rgba(20, 18, 14, .62) 72%,
      rgba(20, 18, 14, .72) 100%);
  }
  .hero-mark img { width: min(80%, 330px); }
  .profile-grid, .faq-grid, .contact-grid { grid-template-columns: 1fr; }
  /* Nothing to hang into once the grid stacks — bring the badges back over the frame. */
  .pin-a { left: 3%; }
  .pin-b { left: 4%; }
  .pin-c { right: 3%; }
  .faq-mark { display: none; }
  .row { grid-template-columns: 40px minmax(0, 1fr) 30px; }
  .row .row-d { display: none; }
}

@media (max-width: 760px) {
  .site-header { position: relative; }
  .site-nav {
    display: none;
    position: absolute; left: 0; right: 0; top: 100%;
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
    padding: 8px var(--gutter) 16px;
    z-index: 50;
  }
  .site-nav.is-open { display: block; }
  .site-nav ul { flex-direction: column; align-items: stretch; gap: 0; }
  .site-nav li { border-bottom: 1px solid var(--rule); }
  .site-nav li:last-child { border-bottom: 0; }
  .site-nav a { padding: 14px 0; }
  .nav-toggle { display: inline-flex; }
  .header-cta { display: none; }
  .foot-top { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 460px) {
  .foot-top { grid-template-columns: 1fr; }
  /* The footer is one full-width column from here down, so the mark centres over it and
     takes the room to be seen. 52px was sized for a narrow column sharing a desktop row,
     not for this. Width rather than height, so it scales with the column on a small phone
     instead of running to the edges. */
  .foot-logo { width: min(56%, 188px); height: auto; margin-inline: auto; }
  .btn { width: 100%; justify-content: space-between; }
  .practice-foot .btn { width: auto; }
}
