/* CareDent site stylesheet.
 *
 * Authored from 3-design-system/design.md, with every measurement checked
 * against the live site's own stylesheet. That sheet defined 2,645 classes and
 * the six page shapes used 376 of them, so this is a re-authoring rather than a
 * copy: the numbers are the site's, the structure is not.
 *
 * No Sass, no framework, no build step for CSS. Naming is Client-First
 * flavoured: a block name, then utilities that say what they do.
 *
 * TWO STANDING RULES, both learned the expensive way on other builds:
 *   1. Nothing changes appearance after the first paint. No styling applied by
 *      a deferred script, and scrollbar-gutter reserved so a page that gains a
 *      scrollbar does not shift 7px sideways.
 *   2. A component reads its ground rather than guessing it. Sections set
 *      --ground and --on-ground; components inside them use those.
 */

/* ---------------------------------------------------------------- tokens */
:root {
  /* Brand. Values from the 2021 guidelines, which win over the live CSS.
     The site had drifted the lilac to #DAC1E1; the guidelines say #DAC2E2. */
  --primary:        #033B6D;
  --primary-hover:  #205686;
  --on-primary:     #A9E0D5;
  --accent:         #A9E0D5;
  --accent-hover:   #6FCFBC;
  --on-accent:      #033B6D;

  --surface:        #FFFFFF;
  --surface-alt:    #F7FDFF;
  --surface-sky:    #CBE2EE;
  --surface-lilac:  #DAC2E2;
  --surface-grey:   #DADADA;

  --on-surface:        #033B6D;
  --on-surface-muted:  #4A5568;   /* 7.53:1 on white, safe for body copy */
  --on-surface-subtle: #A0AEC0;   /* 2.26:1. NOT a text colour. Rules and
                                     disabled states only. The live site sets
                                     readable copy in it; that is the defect. */
  --on-dark:        #FFFFFF;
  --border:         #CCD2D4;
  --placeholder:    #757575;

  /* Ground: every section declares these two, every component reads them. */
  --ground:     var(--surface);
  --on-ground:  var(--on-surface);

  --font: 'Prompt', system-ui, -apple-system, 'Segoe UI', sans-serif;

  /* Type. Fluid between the 479 and 991 breakpoints, so nothing steps.
     Floors are deliberate: prose never below 17px, labels never below 14px. */
  /* THE LIVE SCALE, STEPPED AT WEBFLOW'S OWN BREAKPOINTS. These were fluid
     clamps chosen from the brand guidelines, and they landed in the wrong
     place on every page: the hero h1 rendered at 60px against live's 48px and
     wrapped to eight lines in a half-width column, while every h2 came out at
     32px against live's 40px. Same words, different page.

     Live sets them as three fixed steps rather than a clamp, so these do the
     same and can be compared value for value. */
  --t-display-2xl: clamp(2.5rem, 1.2rem + 5.4vw, 6rem);
  --t-display-xl:  clamp(2.25rem, 1.3rem + 4vw, 4.75rem);
  --t-display-lg:  3rem;        /* h1 */
  --t-h2:          2.5rem;
  --t-h3:          2rem;
  --t-h4:          1.5rem;
  --t-h5:          1.25rem;
  --t-h6:          1rem;
  --t-body:        17px;        /* prose floor, in px on purpose */
  /* R6: THE PROSE FLOOR IS 17px AND THIS TIER WAS 15. Every card summary,
     patient testimonial, team bio and why-us paragraph on the site read as
     sentences, so by R6's own test ("if a thing is read as a sentence it is
     prose, whatever class it carries") they are prose, and 15px was under the
     floor on all six pages. The 14px label tier below is the only tier that
     may sit under 17, and it is for things nobody reads as a sentence:
     the legal line, meta rows, badge captions.

     Set in px on purpose. A floor in rem is not a floor — it rides the very
     engine it exists to protect against. */
  --t-body-sm:     17px;
  --t-label:       14px;        /* floor, in px on purpose */
  --t-caption:     14px;        /* floor, in px on purpose */

  --lh-display: 1.1;
  --lh-heading: 1.2;
  --lh-body:    1.6;
  --track-wide: 0.28em;         /* the logo lockup's letterspacing */

  /* Space: quarter-rem scale, the site's own. */
  --s-1:.25rem;  --s-2:.5rem;   --s-3:.75rem;  --s-4:1rem;
  --s-5:1.25rem; --s-6:1.5rem;  --s-8:2rem;    --s-10:2.5rem;
  --s-12:3rem;   --s-16:4rem;   --s-20:5rem;   --s-24:6rem;
  --s-32:8rem;   --s-40:10rem;  --s-48:12rem;

  /* Sections run on their own vertical scale. 6rem is the default and it is
     what produces the site's calm; protect it first when a page gets busy. */
  --band:     var(--s-24);
  --band-sm:  4.5rem;
  --band-xs:  var(--s-12);

  --r-sm:.25rem; --r-md:.5rem; --r-lg:.75rem; --r-xl:1rem; --r-pill:9999px;

  --shadow-sm: 0 2px 5px -1px #32325d40, 0 1px 3px -1px #0000004d;
  --shadow-md: 0 10px 15px -3px #32325d33, 0 4px 6px -2px #00000080;

  /* 90rem = 1440px at the design root, and it GROWS with the engine:
     ~1620px at 1920, ~1860px at 2560. A px cap would freeze the layout while
     the type grew, which reads as zoomed-in text rather than a scaled design.

     Live draws to Webflow's container-1024 and responsive-system §5 would
     normally keep that as 64rem. Overridden deliberately (Leke, 13 Aug 2026):
     1024 is Webflow's default, not an approved band, and it stranded the site
     on any laptop. See notes/reflow-map.md and deviation-log.md. */
  --container: 90rem;
  --measure:   68ch;
}

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

/* ===================================================== THE FLUID ENGINE ==
 * Canonical block from docs/design/responsive-system.md §2, verbatim.
 * It interpolates the ROOT font-size across four viewport segments, so every
 * rem in this stylesheet rides it and the whole design breathes instead of
 * stepping.
 *
 * WHY IT MATTERS HERE: without it the root sat at 16px at every width, so the
 * 64rem container was a hard 1024px forever. At 1920 that was a 1024px island
 * with 448px of dead space each side, which is exactly the failure §9's pass
 * bar forbids. Measured before the change:
 *
 *     393 -> band 378px |  768 -> 753px | 1024 -> 1009px
 *    1440 -> band 1024px, 208px dead each side
 *    1920 -> band 1024px, 448px dead each side
 *
 * The 480 -> 479 jump from 14 back up to 16 is DELIBERATE (doc §2): on phones
 * text must run proportionally larger to stay readable. Do not "fix" it.
 *
 * Tuning happens on the --font-from/--font-to pairs only. Never by adding
 * per-element width overrides. */
:root {
  --font-from: 18;
  --font-to: 20;
  --vw-from: calc(1920 / 100);
  --vw-to: calc(2400 / 100);
  --coefficient: calc((var(--font-to) - var(--font-from)) / (var(--vw-to) - var(--vw-from)));
  --base: calc((var(--font-from) - var(--vw-from) * var(--coefficient)) / 16);
}
html {
  font-size: calc(var(--base) * 1rem + var(--coefficient) * 1vw);
  /* Reserved always, so gaining a scrollbar never shifts the layout. */
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%;
}

/* ------------------------------------------------- the scrollbar ---
 * Leke, 29 Sep 2026: a custom bar in the brand, and "when no scrolling is
 * happening, the scroll bar disappears."
 *
 * ON THE ROOT ONLY, on purpose. `::-webkit-scrollbar` with no selector in
 * front of it hits every scrollable box on the page, including the 100px
 * probe the booking script appends to measure the OS scrollbar. That probe
 * exists to find out how wide the bar is inside DoctoSmart's frame, which is
 * another origin and draws the OS bar whatever we write here, so restyling
 * the probe would have made it measure OUR bar and clip the embed by the
 * wrong amount. The probe now asks for `scrollbar-width: auto` as well.
 *
 * VISIBLE IS THE DEFAULT AND HIDING IS THE ENHANCEMENT. The auto-hide hangs
 * off `.sb-auto`, which only a script can add, so a reader with no JavaScript
 * gets a bar that is always there rather than a page with no scroll
 * affordance at all. That is the right way round for a failure nobody sees.
 *
 * The thumb is drawn inside a transparent border so the bar reads as a slim
 * 4px line with air around it rather than a 10px slab against the edge. */
html { scrollbar-width: thin; scrollbar-color: var(--primary) transparent; }
html::-webkit-scrollbar { width: 10px; height: 10px; }
html::-webkit-scrollbar-track { background: transparent; }
html::-webkit-scrollbar-thumb {
  background-color: var(--primary); border-radius: 999px;
  border: 3px solid transparent; background-clip: content-box;
  transition: background-color 200ms ease-in-out;
}
html::-webkit-scrollbar-thumb:hover { background-color: var(--primary-hover); }

/* Gone while nothing is moving. `data-scrolling` is set by the script for as
   long as the page is being scrolled and for a beat afterwards; hover brings
   it back too, so somebody reaching for the bar finds it under the pointer
   instead of having to scroll first to make it appear. */
html.sb-auto { scrollbar-color: transparent transparent; }
html.sb-auto::-webkit-scrollbar-thumb { background-color: transparent; }
html.sb-auto[data-scrolling],
html.sb-auto:hover { scrollbar-color: var(--primary) transparent; }
html.sb-auto[data-scrolling]::-webkit-scrollbar-thumb { background-color: var(--primary); }
html.sb-auto::-webkit-scrollbar-thumb:hover { background-color: var(--primary-hover); }
@media screen and (max-width: 1920px) {
  :root { --font-from: 16; --font-to: 18; --vw-from: calc(1440 / 100); --vw-to: calc(1920 / 100); }
}
@media screen and (max-width: 1440px) {
  :root { --font-from: 14; --font-to: 16; --vw-from: calc(479 / 100); --vw-to: calc(1440 / 100); }
}
@media screen and (max-width: 479px) {
  :root { --font-from: 12; --font-to: 16; --vw-from: calc(1 / 100); --vw-to: calc(479 / 100); }
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--on-surface);
  background: var(--surface);
  -webkit-font-smoothing: antialiased;
}

h1,h2,h3,h4 { margin: 0 0 var(--s-4); font-weight: 600; line-height: 1.1; color: var(--on-ground); }
h1 { font-size: var(--t-display-lg); line-height: 1.15; }
h2 { font-size: var(--t-h2); }
h3 { font-size: var(--t-h3); }
h4 { font-size: var(--t-h4); }
h5 { font-size: var(--t-h5); margin: 0 0 var(--s-3); font-weight: 600; line-height: 1.1; color: var(--on-ground); }
h6 { font-size: var(--t-h6); margin: 0 0 var(--s-3); font-weight: 600; line-height: 1.1; color: var(--on-ground); }

/* Webflow's two steps down, reproduced so the sizes match at every width. */
@media (max-width: 991px) {
  :root { --t-display-lg: 2.5rem; --t-h2: 2rem; --t-h3: 1.75rem; --t-h4: 1.5rem; }
}
@media (max-width: 767px) {
  /* R3 sizes display type BY MEASURE, not by scaling the desktop value.
     Live's own 1.75rem landed the phone h1 at 26.7px / ~20 CPL — inside the
     14-28 CPL band but under R3's 28-40px landing zone. 2rem gives 30.6px at
     ~17.5 CPL: still inside the band, and it reads like a headline. */
  :root { --t-display-lg: 2rem; --t-h2: 1.625rem; --t-h3: 1.375rem;
          --t-h4: 1.25rem; --t-h5: 1.125rem; }
}
p  { margin: 0 0 var(--s-4); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

/* No heading or paragraph ends on a lone last-line word. */
h1,h2,h3,h4 { text-wrap: balance; }
p, li { text-wrap: pretty; }

/* --------------------------------------------------- section title measure
   Section titles hold a short line. "It's all about you" is the longest line
   the comps ever set, and it renders 382px at the 40px h2 = 9.55em, so 10em
   is the cap.

   The unit is em ON PURPOSE. --t-h2 is in rem, so the h2 size rides the fluid
   engine and steps down at 991 and 767; a cap in em rides with it, and the
   characters-per-line stays put at every width and every breakpoint from one
   declaration. (The prose cap is in ch for the opposite reason: that type is
   pinned in px, so a rem cap would have kept widening past it.)

   Exempted: anything that is an h2 for document outline reasons rather than a
   section title. Footer column heads, the info-bar labels, the card title in
   the Patient Education hero, and the headings inside an article body all set
   their own size and their own width. */
h2 { max-width: 10em; }
.footer h2, .eyebrow, .quickinfo__title, .pe-hero__title,
.container--prose h2 { max-width: none; }
/* A capped block still needs the auto margins to stay centred. */
h2.text-center { margin-inline: auto; }

a { color: inherit; }
img, svg, video { max-width: 100%; height: auto; display: block; }
ul { margin: 0; padding: 0; list-style: none; }

:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

.u-visually-hidden {
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
}
.u-skip {
  position:absolute; left:var(--s-4); top:-100vh; z-index:100;
  background:var(--primary); color:var(--on-primary);
  padding:var(--s-3) var(--s-4); border-radius:var(--r-sm);
}
.u-skip:focus { top: var(--s-4); }

/* ---------------------------------------------------------------- layout */
/* Doc §5: content is capped, backgrounds bleed. Section wrappers run full
   width; the .container inside caps the content. Gutters are 2.5rem, stepping
   to 1.25rem at mobile — they were a flat 1.25rem at every width. */
.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--s-10); }
@media (max-width: 767px) { .container { padding-inline: var(--s-5); } }

.band { padding-block: var(--band); background: var(--ground); color: var(--on-ground); }
/* THE MINT IS AN INSET PANEL, NOT A FULL-BLEED BAND (Leke, 14 Aug 2026).
   It matches the hero's lilac panel: same container inset, same radius, so the
   two mint sections read as one rounded block floating on white rather than a
   stripe running to the window edge.

   The vertical space moves INTO the panel. A band that paints nothing has no
   reason to hold padding, and leaving it there would have put 96px of white
   between the two panels and split the block in half.

   The services card still pulls up out of the panel's top edge — that is the
   jut, and it needs the panel to keep `overflow: visible`.

   SCOPED WITH .has-panel, and that matters more than it looks: `ground-mint`
   is on 35 files — about's "Want more?" band, the testimonial page's hero, and
   a band on every one of the 30 article pages. A bare `.band.ground-mint`
   rule stripping the background would have taken the mint off all of them,
   and no copy or image diff can see a missing background colour. Only the two
   sections that actually carry a panel opt in. */
.band.ground-mint.has-panel { background: transparent; padding-block: 0; }
.mint-panel {
  background: var(--accent); color: var(--on-accent);
  padding: var(--s-16) var(--s-10);
}
.mint-panel--top    { border-radius: var(--r-lg) var(--r-lg) 0 0; }
.mint-panel--bottom { border-radius: 0 0 var(--r-lg) var(--r-lg); padding-top: 0; }
/* The block still needs air above and below it, which the bands used to give.
   Both scoped to the panelled sections for the same reason as above. */
.band.ground-mint.has-panel:first-of-type { margin-top: var(--band); }
.band.ground-mint.has-panel:last-of-type  { margin-bottom: var(--band); }
@media (max-width: 767px) { .mint-panel { padding-inline: var(--s-5); } }

/* Two bands on the SAME ground are one visual block, so their facing paddings
   stack into a void with no edge to justify it: services and testimonials are
   both mint, and 96px + 96px put ~190px of empty green between the card and
   "What our patients say". One band's worth of air is the intent; two is an
   accident of them being separate <section>s. */
.band.ground-mint  + .band.ground-mint,
.band.ground-white + .band.ground-white,
.band.ground-alt   + .band.ground-alt,
.band.ground-navy  + .band.ground-navy { padding-top: 0; }
.band.is-sm { padding-block: var(--band-sm); }
.band.is-xs { padding-block: var(--band-xs); }

/* A band names its ground; everything inside reads it. */
.ground-white  { --ground: var(--surface);       --on-ground: var(--on-surface); }
.ground-alt    { --ground: var(--surface-alt);   --on-ground: var(--on-surface); }
.ground-sky    { --ground: var(--surface-sky);   --on-ground: var(--on-surface); }
.ground-mint   { --ground: var(--accent);        --on-ground: var(--on-accent); }
.ground-lilac  { --ground: var(--surface-lilac); --on-ground: var(--on-surface); }
.ground-navy   { --ground: var(--primary);       --on-ground: var(--on-dark); }

.stack > * + * { margin-top: var(--s-4); }
.grid { display: grid; gap: var(--s-6); }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
.row { display:flex; gap: var(--s-4); align-items:center; flex-wrap:wrap; }

/* ------------------------------------------------------------ type parts */
.eyebrow {
  font-size: var(--t-label); font-weight: 500;
  letter-spacing: var(--track-wide); text-transform: uppercase;
  color: var(--on-surface-muted); margin: 0 0 var(--s-3);
}
.ground-navy .eyebrow, .ground-mint .eyebrow { color: inherit; opacity: .85; }
.lede { font-size: clamp(1.125rem, 1rem + .5vw, 1.375rem); color: var(--on-surface-muted); }
.section-head { margin-bottom: var(--s-10); }

/* --------------------------------------------------------------- buttons */
.btn {
  display:inline-block; font: inherit; font-size: var(--t-body);
  padding: .8rem 1.3rem; border-radius: var(--r-sm); border: 0;
  text-decoration: none; cursor: pointer; text-align: center;
  min-height: 44px;                       /* touch target floor */
  transition: color .2s ease-in-out, background-color .2s ease-in-out;
}
.btn-primary   { background: var(--primary); color: var(--on-primary); }
.btn-primary:hover  { background: var(--primary-hover); color: var(--on-dark); }
.btn-secondary { background: var(--accent); color: var(--on-accent); }
.btn-secondary:hover { background: var(--accent-hover); color: var(--on-dark); }
.btn-ghost { background: transparent; color: var(--on-ground); box-shadow: inset 0 0 0 1px currentColor; }
.btn-ghost:hover { background: var(--on-ground); color: var(--ground); }
.btn-sm { font-size: var(--t-label); padding: .5rem .75rem; }
.btn-lg { font-size: var(--t-h4); padding: 1rem 1.75rem; }

/* ------------------------------------------------------------------- nav */
/* The top utility bar is WHITE on live, with a light-blue WhatsApp chip and
   the three social links pushed to the right. It was lilac and centred here,
   which is the wrong colour in the wrong place. */
.chatbar { background: var(--surface); color: var(--on-surface); font-size: var(--t-label); }
.chatbar__inner { display:flex; justify-content:flex-end; align-items:center; gap:var(--s-4); padding-block: var(--s-3); }
.chatbar__chip {
  display:inline-flex; align-items:center; gap: var(--s-2);
  background: var(--surface-sky); border:1px solid var(--border);
  padding: var(--s-1) var(--s-3); border-radius: var(--r-md); text-decoration:none;
}
.chatbar__social { display:inline-flex; align-items:center; gap: var(--s-3); }
.chatbar a { font-weight: 500; }

/* No rule under the nav (Leke, 22 Sep 2026). The first panel on every page
   now runs flush into it, so the line sat between two touching surfaces and
   read as a seam. The open mobile menu keeps its own bottom edge because it
   floats over the page. */
.nav { background: var(--surface); }
.nav__inner { display:flex; align-items:center; justify-content:space-between; gap: var(--s-4); padding-block: var(--s-4); }
/* 320px: up from live's 190, back down from the 380 first tried (Leke,
   14 Aug 2026). The unit stays px, matching the nav links, which inherit the
   px-pinned body size — a rem logo beside px links would grow away from them
   on a big screen. It steps down rather than scaling, because below 992 it
   shares the row with the Menu button and the row must stay one line.

   ON CRISPNESS: solved, but not the way the file name suggests. The nav now
   draws caredent-logo.svg, supplied by the client 17 Aug 2026. Opened, it is
   NOT a vector — it is a <rect> filled by a <pattern> holding one base64
   <image>, with zero paths in the file. A bitmap in an SVG envelope.

   It is still a real fix, because that bitmap is 2200x491 against the old
   500x151. Crisp at 2x up to 1100px; at the 320px box the density is 6.9x,
   so it is sharp on any display anyone owns. But it does NOT scale
   arbitrarily, so the ask for a true vector stays open — see deviation-log.md. */
.nav__logo img { width: 320px; }
/* The steps are set by MEASUREMENT, not taste. The menu needs 622px flat (its
   text is px-pinned, so it barely moves), the gap 16px, and the container's
   own padding comes off the top. At 992 that leaves 901px of content and a
   logo over 271px squeezes the row until "Patient Education" wraps onto two
   lines — which is what 300px did. */
@media (max-width: 1199px) { .nav__logo img { width: 300px; } }
@media (max-width: 1099px) { .nav__logo img { width: 240px; } }
@media (max-width: 767px)  { .nav__logo img { width: 240px; } }
@media (max-width: 479px)  { .nav__logo img { width: 200px; } }
.nav__menu { display:flex; align-items:center; gap: var(--s-6); }
/* nowrap, because a nav item is a label and not prose. Adding Gallery as a
   sixth item (Leke, 29 Sep 2026) squeezed the row enough that "Patient
   Education" broke onto two lines at 992 and made the bar 24px taller than it
   is at every other width. The gap tightens in the same band rather than the
   label breaking. */
.nav__link { color: color-mix(in srgb, var(--primary) 80%, transparent); text-decoration:none; padding: var(--s-2) 0; white-space: nowrap; }
@media (min-width: 992px) and (max-width: 1199px) { .nav__menu { gap: var(--s-4); } }
.nav__link:hover, .nav__link[aria-current="page"] { color: var(--primary); }
.nav__toggle { display:none; }

/* --------------------------------------------------------------- surface */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-6); }
.card-tint { background: var(--surface-alt); border: 1px solid var(--border); border-radius: var(--r-md); padding: var(--s-6); }

/* Service list: an icon and a name, icons live in the design not the CMS. */
.services { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--s-5) var(--s-8); }
.service { display:flex; align-items:center; gap: var(--s-3); font-weight: 500; }
/* (.service__dot removed 14 Aug 2026 — dead. The list draws its bullets with
   `<img class="service__icon" src="..._Oval.svg">`, not a styled span, and no
   page has ever carried a .service__dot. Halving THIS is what looked like the
   change and would have done nothing.) */

/* Article cards */
.post { display:block; text-decoration:none; color: inherit; }
/* Every part of a card is display:block ON PURPOSE. They are spans, because a
   block element cannot legally sit inside an <a> in older parsers and the whole
   card is one link. An inline element IGNORES vertical margin, so without this
   the title and summary pile over whatever follows the grid. That is exactly
   what the first render did. */
.post__media { display:block; border-radius: var(--r-md); overflow:hidden; background: var(--surface-alt); aspect-ratio: 3 / 2; }
.post__media img { width:100%; height:100%; object-fit: cover; }
/* A small gap between tags (Leke, 29 Sep 2026): two pills set edge to edge
   read as one. The feature and services cards used to reset this row to
   display:block, which is what took the gap away; they keep the flex row now
   and only change the margin. */
.post__tags { display:flex; gap:var(--s-2); flex-wrap:wrap; margin: var(--s-3) 0 var(--s-2); }
.tag { font-size: var(--t-caption); letter-spacing:.06em; text-transform:uppercase;
       background: var(--surface-sky); color: var(--on-surface);
       padding: .15rem .5rem; border-radius: var(--r-sm); }
.post__title { display:block; font-size: var(--t-h5); font-weight:600; margin:0 0 var(--s-2); }
.post__summary { display:block; color: var(--on-surface-muted); font-size: var(--t-body-sm); margin:0; }

/* Testimonials */
/* The card is a column so the attribution can be pushed to the bottom. The
   cards are already equal height — the grid stretches them — but the name sat
   directly under its quote, so a short review left its attribution stranded in
   the middle of the card while its neighbours' sat at the foot. Same height,
   three different baselines. `margin-top:auto` puts them on one line. */
.quote { background: var(--surface); border-radius: var(--r-md); padding: var(--s-6); box-shadow: var(--shadow-sm);
         display: flex; flex-direction: column; height: 100%; }
.quote__body { font-size: var(--t-body-sm); }
.quote__who { display:flex; align-items:center; gap: var(--s-3); margin-top: auto; padding-top: var(--s-4); }
.quote__who img { width:44px; height:44px; border-radius: var(--r-pill); object-fit:cover; }
.quote__name { font-weight:600; }
.quote__source { font-size: var(--t-caption); color: var(--on-surface-muted); }
/* SIX, IN TWO ROWS OF THREE (clinic review, 29 Sep 2026), and a long review
   stops at five lines instead of stretching its whole row. The clamp is only
   on when the script has switched it on (`.is-clamped`), because the script
   is also what adds the "Read more" that finishes the sentence. */
.quotes.is-clamped .quote__body {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5;
  line-clamp: 5; overflow: hidden;
}
.quote__more {
  align-self: flex-start; min-height: 44px; padding: 0; margin-top: var(--s-1);
  border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--t-body-sm); font-weight: 600; color: var(--primary);
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .2em;
  transition: text-decoration-color 200ms ease-in-out, color 200ms ease-in-out;
}
.quote__more:hover { text-decoration-color: var(--accent); }
.quote__more[hidden] { display: none; }
/* The tablet widths get two columns: three reviews side by side at 768 run
   about 13 words a line and every card becomes a clamp. */
@media (min-width: 768px) and (max-width: 991px) {
  .quotes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* A phone gets ONE ROW THAT SWIPES rather than six cards stacked into a
   column two screens tall. The next card peeks in at the right edge, which
   is what says there is more (touch rule: a scroller shows its next item).
   Nothing moves on its own. */
@media (max-width: 767px) {
  .quotes {
    display: flex; gap: var(--s-4); overflow-x: auto; scroll-snap-type: x mandatory;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    padding-bottom: var(--s-2);
  }
  .quotes::-webkit-scrollbar { display: none; }
  .quotes > .quote { flex: 0 0 86%; scroll-snap-align: start; height: auto; }
}

/* The whole review, in the same dialog language as the team and booking
   dialogs: white panel, navy close, the scrim and choreography shared with
   them further down. */
.quote-modal {
  width: 100%; max-width: none; height: 100%; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden;
}
.quote-modal__panel {
  position: absolute; inset: 0; margin: auto;
  width: min(92vw, 40rem); height: fit-content; max-height: 86svh; overflow-y: auto;
  padding: var(--s-12) var(--s-10) var(--s-10);
  background: var(--surface); color: var(--on-surface);
  border-radius: var(--r-lg); box-shadow: var(--shadow-md);
}
.quote-modal__body { margin: 0; font-size: var(--t-body); line-height: var(--lh-body); }
.quote-modal__who { margin-top: var(--s-8); padding-top: 0; }
.quote-modal__close {
  position: absolute; top: var(--s-4); right: var(--s-4);
  width: 44px; height: 44px; padding: 0; border: 0; cursor: pointer;
  background: radial-gradient(circle, var(--primary) 0 15px, transparent 15px);
  display: grid; place-items: center;
}
.quote-modal__close::before, .quote-modal__close::after {
  content: ''; grid-area: 1 / 1; width: 13px; height: 2px; border-radius: 1px;
  background: var(--on-primary);
}
.quote-modal__close::before { rotate: 45deg; }
.quote-modal__close::after  { rotate: -45deg; }
@media (max-width: 479px) {
  .quote-modal__panel { padding: var(--s-12) var(--s-6) var(--s-8); }
}

/* Logo strips */
/* A LOGO RAIL, not a flex row (Leke, 14 Aug 2026). Flex with `max-height:44px`
   sized every logo by its own height, so five files with aspect ratios from
   1.47 to 4.49 came out at five different widths and five different optical
   weights — Health Partners read as a stamp beside AXA's banner.

   The grid gives every logo an identical cell and `object-fit: contain` fits
   the artwork inside it, so a wide logo is limited by the cell's width and a
   tall one by its height. That is as close to equal weight as artwork this
   mismatched allows: nothing here is trimmed or rescaled, because the files
   are the client's and the whitespace baked into them is theirs to fix.

   Five columns is the rail both rows share. Payments has three logos and
   leaves the last two cells empty ON PURPOSE, so its icons sit directly under
   the first three HMO logos. */
.logos {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--s-8);
  align-items: center;
}
/* 7:3 is the shared canvas the files are normalised onto (560x240), so the cell
   and the picture are the same shape and `contain` has nothing left to fit —
   which is the point. Contain fits to the BOX, so it would have undone the
   equal-area work if the canvases differed; identical canvases are what let
   the normalisation survive into the page. See tools/normalise-logos.py. */
.logos img { width: 100%; aspect-ratio: 7 / 3; height: auto; object-fit: contain; }
@media (max-width: 991px) { .logos { grid-template-columns: repeat(3, minmax(0,1fr)); } }
@media (max-width: 479px) { .logos { grid-template-columns: repeat(2, minmax(0,1fr)); gap: var(--s-6); } }

/* The two rows of the HMO band. */
.payrow + .payrow { margin-top: var(--s-16); }

/* (.paylist removed 14 Aug 2026 — the payments row moved to the footer strip,
   so the only user of this class is gone. See .footer__pay.) */

/* FAQ, native disclosure so it works with no JavaScript */
.faq { border-top: 1px solid var(--border); }
.faq__item { border-bottom: 1px solid var(--border); }
.faq__q { cursor:pointer; list-style:none; padding: var(--s-5) 0; font-weight:600; display:flex; justify-content:space-between; gap:var(--s-4); }
.faq__q::-webkit-details-marker { display:none; }
/* THE MARK IS DRAWN, NOT TYPED (Leke, 29 Sep 2026). A plus swapped for a
   minus is a content change, and content cannot be animated: the glyph is
   simply one thing and then another. Two bars painted as backgrounds can be,
   so opening shrinks the upright one to nothing and the plus becomes a minus
   in front of you. No rotation, which is the usual way this is done and the
   one this project has already said no to. */
.faq__q::after {
  content: ''; flex: 0 0 auto;
  background-image: linear-gradient(var(--primary), var(--primary)),
                    linear-gradient(var(--primary), var(--primary));
  background-size: 12px 2px, 2px 12px;
  background-position: center, center;
  background-repeat: no-repeat;
}
.faq__item[open] .faq__q::after { background-size: 12px 2px, 2px 0; }

/* AND THE PANEL OPENS RATHER THAN APPEARING. `::details-content` is the only
   way to do this without script, and it needs `interpolate-size` because a
   height of `auto` is not a number the browser can interpolate towards
   without being told it may.
   The content does not stagger in: it was already there, it is only becoming
   visible. A browser that does not know these rules ignores them and the
   panel opens the way it always did, which is the failure mode we want. */
:root { interpolate-size: allow-keywords; }
.faq__item::details-content {
  block-size: 0; overflow: hidden;
  transition: block-size 200ms ease-in-out, content-visibility 200ms allow-discrete;
  transition-behavior: allow-discrete;
}
.faq__item[open]::details-content { block-size: auto; }
@media (prefers-reduced-motion: reduce) {
  .faq__item::details-content { transition: none; }
}
.faq__a { padding: 0 0 var(--s-5); color: var(--on-surface-muted); }

/* FAQ search — the control live has and the rebuild was missing.
   HIDDEN UNTIL THE SCRIPT SAYS OTHERWISE. `.is-live` is added by the filter
   in index.html, so a visitor with no JavaScript never sees a search box that
   cannot search. Showing a dead input is worse than showing none: the reader
   types, nothing happens, and they conclude the site is broken rather than
   that a feature is unavailable. */
.faq-search { display: none; }
.faq-search.is-live { display: block; flex: 0 1 20rem; }
.faq-search__label {
  display: block; font-size: var(--t-label); font-weight: 500;
  margin-bottom: var(--s-2); color: var(--on-surface-muted);
}
.faq-search__input {
  width: 100%; font: inherit; font-size: var(--t-body); color: var(--on-surface);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--s-3); min-height: 44px;
}
.faq-search__input::placeholder { color: var(--placeholder); }
.faq-search__count { font-size: var(--t-body-sm); color: var(--on-surface-muted); margin: 0 0 var(--s-5); }
.faq-search__count:empty { margin: 0; }
/* The heading and the field share a row and sit on the same baseline edge. */
.faq-head { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--s-8); flex-wrap: wrap; }
.faq-head > div { margin-bottom: 0; }
@media (max-width: 767px) { .faq-search.is-live { flex-basis: 100%; } }

/* Forms */
.field { display:block; margin-bottom: var(--s-4); }
.field__label { display:block; font-size: var(--t-label); font-weight:500; margin-bottom: var(--s-2); }
.field input, .field textarea, .field select {
  width:100%; font: inherit; font-size: var(--t-body); color: var(--on-surface);
  background: var(--surface); border:1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--s-3); min-height:44px;
}
.field input::placeholder, .field textarea::placeholder { color: var(--placeholder); }
.u-hp { position:absolute; left:-9999px; }   /* honeypot: off screen, not display:none */

/* Footer */
/* MINT, NOT NAVY. `.background-color-4` on the live footer resolves to
   var(--green) #a9e0d5 with the navy body text on top. The rebuild had it
   inverted, which also meant a light panel had to be invented behind the navy
   lockup — on mint the lockup sits directly on the ground, exactly as the
   brand guidelines' own cover does. */
.footer { background: var(--accent); color: var(--on-accent); padding-block: var(--s-16); }
.footer a { text-decoration: none; }
.footer a:hover { text-decoration: underline; }
/* The phone number and the email ARE links and were the only ones on the page
   drawn as plain text — the same two details in the quick-info card up top are
   underlined. Colour cannot carry it here: the footer's navy-on-mint is the
   same navy as its body copy, so there was nothing to tell a reader the number
   was tappable. Underline them in prose only; the social row and the map are
   whole objects, not words in a sentence, and would only get noisier. */
/* THE PHONE NUMBER AND THE EMAIL ARE ALREADY UNDERLINED, so the footer's
   usual answer, adding an underline, changed nothing at all on the two
   links most likely to be tapped. The audit found that; no eye would have.
   The underline thickens instead: the same signal, spoken by a link that is
   already speaking it. */
.footer p a {
  text-decoration: underline; text-underline-offset: .15em;
  transition: text-decoration-thickness 200ms ease-in-out, text-underline-offset 200ms ease-in-out;
}
.footer p a:hover, .footer p a:focus-visible,
a[href^="tel:"]:hover, a[href^="tel:"]:focus-visible,
a[href^="mailto:"]:hover, a[href^="mailto:"]:focus-visible {
  text-decoration-thickness: 2px; text-underline-offset: .25em;
}
/* The same two links sit in the contact card on the home page, outside the
   footer, underlined by a different rule. Named by what they are rather than
   by where they happen to be, so the next one is covered before it exists. */
a[href^="tel:"], a[href^="mailto:"], .quickinfo__alert a {
  transition: text-decoration-thickness 200ms ease-in-out, text-underline-offset 200ms ease-in-out;
}
.quickinfo__alert a:hover, .quickinfo__alert a:focus-visible {
  text-decoration-thickness: 2px; text-underline-offset: .25em;
}
/* `.footer .eyebrow` is in this list ON PURPOSE. It used to read `.footer h2,
   .footer h3` only, so a footer eyebrow got the brand navy if it happened to
   be a heading and kept the default grey if it did not. The payments label is
   a span — deliberately, so it does not add a heading to 34 pages' outlines —
   and it came out rgb(74,85,104) beside three siblings at rgb(3,59,109).
   Colour in the footer should depend on being in the footer, not on the tag
   that carries it. */
.footer h2, .footer h3, .footer .eyebrow { color: var(--on-accent); }
.footer__logo { display:inline-block; }
/* The footer lockup is the SAME artwork as the nav, recoloured all-navy
   (Leke, 17 Aug 2026) — caredent-logo-navy.svg, built by tools/recolour-logo.py
   from the file he supplied. The mint "DENT" would have sat mint-on-mint here.
   Two files rather than one plus a CSS filter, because a filter cannot repaint
   one ink and leave the other, and the artwork is a bitmap either way. */
.footer__logo img { width: 14.375rem; }   /* 230px at the design root */
.footer__cols { display:grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--s-8); }
/* THE FOOTER MENU (clinic review, 29 Sep 2026) sits under the logo, in the
   column that held the logo alone, so the footer keeps its four columns.
   Plain navy words on the mint, like the rest of the footer: the hover is
   the footer's own underline. Book appointment is the one that matters, so
   it carries weight and the arrow the site already uses for "go". */
.footer__menu { margin-top: var(--s-6); }
.footer__menu ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.footer__menu a {
  display: inline-flex; align-items: center; min-height: 2rem;
  color: var(--on-accent); text-underline-offset: .2em;
}
.footer__book { font-weight: 600; gap: var(--s-2); }
.footer__book::after {
  content: ''; width: .5em; height: .5em;
  border-top: 2px solid currentColor; border-right: 2px solid currentColor;
  rotate: 45deg; translate: 0 .05em;
  transition: translate 200ms ease-in-out;
}
.footer__book:hover::after, .footer__book:focus-visible::after { translate: .2em .05em; }
@media (prefers-reduced-motion: reduce) { .footer__book::after { transition: none; } }
/* Two columns of short words on a phone rather than six lines, so the menu
   takes the room of three. */
@media (max-width: 479px) {
  .footer__menu ul { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--s-6); }
  .footer__menu a { min-height: 44px; }
  .footer__menu li:last-child { grid-column: 1 / -1; }
}
/* max-width:none because the global `p { max-width: var(--measure) }` was
   capping this at 68ch, so its border-top ran out halfway across the footer
   and read as a broken rule rather than a divider. A measure cap is right for
   prose and wrong for a full-width rule. */
/* THE FOOTER BASE ROW. Legal notice and payment methods on one line, so the
   footer closes with a single rule rather than two stacked strips. It wraps at
   767, where one row would crush both halves.

   The icons are hand-drawn 24px line SVGs (assets/img/icon-*.svg) on the same
   1.6 stroke in brand navy. They replaced a photograph, a 3D render and a flat
   glyph-with-baked-in-text — three things pretending to be a set. Normalising
   canvas and area got them the same SIZE; nothing gets a photo and a line icon
   to agree on STYLE, which is why they are drawn rather than processed. */
.footer__base {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: var(--s-5) var(--s-8);
  margin-top: var(--s-10); padding-top: var(--s-6);
  border-top: 1px solid #033b6d33;
}
.footer__pay {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
}
/* No opacity. It was the second half of why this label looked faded — grey
   from the tag mismatch above, then knocked back another quarter on top. The
   three column heads beside it sit at full strength, so this does too. Margin
   stays 0 because it is a flex item on a centred row, not a block head. */
.footer__pay-label { margin: 0; }
.footer__pay-list {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  list-style: none; margin: 0; padding: 0;
}
.footer__pay-list li { display: flex; align-items: center; gap: var(--s-2); font-size: var(--t-label); }
.footer__pay-list img { width: 1.5rem; height: 1.5rem; flex: 0 0 auto; }
/* The base row draws the rule, so the legal line inside it must not draw a
   second one 24px below. */
.footer__base .footer__legal { margin-top: 0; padding-top: 0; border-top: 0; }
/* KEEP CLEAR OF THE FLOATING WHATSAPP BUTTON. Once payments sat at the right
   end of a space-between row it became the first footer content to reach the
   right edge, and the button covered "Bank transfer" at 1440. The footer
   already reserved the button's footprint BELOW 767; this reserves it
   horizontally above it.

   Reserved unconditionally rather than behind a media query, because the
   collision depends on the container's right edge versus the viewport's, and
   BOTH move: the container is 90rem of a root that itself grows with the
   viewport, so the overlap band is not a fixed range of widths. At 2560 the
   inset costs nothing visible — space-between simply stops 5rem earlier. */
@media (min-width: 768px) { .footer__base { padding-right: 5rem; } }
@media (max-width: 479px) { .footer__base { gap: var(--s-4); } }

.footer__legal { margin-top: var(--s-12); padding-top: var(--s-6); border-top:1px solid #033b6d33; font-size: var(--t-label); opacity:.7; max-width: none; }

/* ----------------------------------------------------------- responsive */
@media (max-width: 991px) {
  :root { --band: var(--s-20); --band-sm: var(--s-16); }
  .footer__cols { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); }

  /* THE NAV CONDENSES AT 991, NOT 767. It was written at 767 while the touch-
     target block below revealed .nav__toggle from 991 down, so between 768 and
     991 the page showed the toggle AND the full menu, and the row was 820px of
     content in 680px of container. Everything shrank to fit and the logo — an
     <img>, whose flex min-width is 0 — took the whole loss and collapsed to
     45px. Nothing overflowed, so the overflow sweep called it clean.
     notes/reflow-map.md has said "condenses to the toggle at 991" since the
     responsive pass; this is the CSS catching up with it. */
  .nav__menu { display:none; }
  .nav__toggle { display:inline-block; }
  .nav__menu.is-open { display:flex; flex-direction:column; align-items:flex-start;
    position:absolute; inset-inline:0; top:100%; background:var(--surface);
    border-bottom:1px solid var(--border); padding: var(--s-4) var(--s-5); z-index:20; }
  .nav { position:relative; }
}
@media (max-width: 767px) {
  .grid-3, .grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 479px) {
  :root { --band: var(--s-16); }
  /* Text-bearing cards go one up on a phone. */
  .services { grid-template-columns: 1fr; }
  .grid-4, .footer__cols { grid-template-columns: 1fr; }
  .logos { gap: var(--s-5); }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration:.01ms !important; transition-duration:.01ms !important; }
}

/* ------------------------------------------------- restored from the live page */
/* .post--featured, .post__body and .post__more lived here. The home page's
   patient-education block was rebuilt to the original two-column design and
   nothing uses them now, so they are deleted rather than left behind. */

/* An earlier .wa block sat here and was overridden in full by the panel rules
   further down, so it was dead code that still read as the live definition.
   Removed rather than left to mislead the next reader. */
/* 7px, halved from 14 (Leke, 14 Aug 2026): the dots were reading as bullets
   competing with their own labels, and at half size they are punctuation.
   Sized here as well as on the element so the decision lives with the rest of
   the design; the attributes are kept in step so the two never contradict.
   Stays in px like the type beside it — the service names inherit the
   px-pinned body size, so a rem dot would drift away from them at 2560. */
.service__icon { flex: 0 0 auto; width: 7px; height: 7px; }
.dentist { margin: var(--s-8) 0 var(--s-4); position: relative; }
.dentist__photo { border-radius: var(--r-md); }
/* The shape is placed by .dentist__badge now, not by itself. This rule used to
   anchor it `left`, which beat the new `right` (both set, fixed width, left
   wins) and parked it in the middle of the photo. */
.dentist__nametag { display: block; width: 100%; height: auto; }
.note { display: flex; align-items: flex-start; gap: var(--s-2);
  color: var(--on-surface-muted); }
.note img { margin-top: .35em; flex: 0 0 auto; }
.eyebrow img { display: inline-block; vertical-align: -2px; margin-right: var(--s-1); }
/* WhatsApp panel */
.wa { position: fixed; right: var(--s-5); bottom: var(--s-5); z-index: 40;
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s-3); }
/* A 4rem NAVY CIRCLE holding the glyph alone, with a small green presence dot,
   pinned 30px from the bottom right. Live: `.whatsapp-container` is
   `background-color: var(--body-text)` at `border-radius: 100%`, and
   `.whatsapp-block` is `inset: auto 30px 30px auto`. The rebuild had a lilac
   pill with a text label, which is a different object in a different place. */
.wa__launch { display:grid; place-items:center; cursor:pointer; position:relative;
  background: var(--primary); color: var(--on-primary); border: 1px solid #ffffff4d;
  font: inherit; width: 4rem; height: 4rem; padding: 0; border-radius: 50%;
  box-shadow: var(--shadow-md); transition: background-color 200ms ease-in-out, translate 200ms ease-in-out; }
/* A LIFT, NOT A SCALE. Scaling an element is the one hover device this
   project has already rejected, and it was the only thing on the site still
   doing it. 2px up is the same answer every other icon now gives. */
.wa__launch:hover, .wa__launch:focus-visible { background: var(--accent-hover); translate: 0 -2px; }
.wa__launch img { width: 1.75rem; height: 1.75rem; }
/* The live presence dot: #00b94a, 15px, top-left of the circle. */
.wa__launch::before { content:''; position:absolute; top:0; left:0;
  width:15px; height:15px; border-radius:50%; background:#00b94a; }
.wa__label { position:absolute; width:1px; height:1px; overflow:hidden;
  clip-path: inset(50%); white-space:nowrap; }
/* TIDIED 29 SEP 2026 (Leke: "let's also make sure this is cleaned up").
   Four things were wrong and all four were inherited from the widget this
   replaced: the close was somebody else's grey PNG, it sat in the flex row
   and so competed with the name for width, which wrapped "Customer Success
   Team" onto two lines and left the row ragged; the text ran at the page's
   reading line-height, which is too loose for a 19rem card; and the button's
   glyph sat hard left of a centred label. */
.wa__panel { position: relative;
  width: min(19rem, calc(100vw - var(--s-10))); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-md);
  box-shadow: var(--shadow-md); padding: var(--s-5); }
/* The close is lifted out of the row, so the name has the full width. */
.wa__top { display:flex; align-items:center; gap: var(--s-3);
  margin-bottom: var(--s-4); padding-right: var(--s-6); }
.wa__avatar { border-radius: var(--r-pill); object-fit: cover; flex: 0 0 auto; }
.wa__who { font-weight: 600; line-height: 1.2; }
.wa__meta { font-size: var(--t-caption); color: var(--on-surface-muted); line-height: 1.3; }
/* DRAWN, like every other close on the site. The bars are the primary rather
   than on-primary because this one sits on white with no disc behind it. */
.wa__close {
  position: absolute; top: var(--s-2); right: var(--s-2);
  width: 44px; height: 44px; padding: 0; border: 0; cursor: pointer;
  background: none; display: grid; place-items: center; border-radius: 50%;
}
.wa__close::before, .wa__close::after {
  content: ''; grid-area: 1 / 1; width: 13px; height: 2px; border-radius: 1px;
  background: var(--primary);
}
.wa__close::before { rotate: 45deg; }
.wa__close::after  { rotate: -45deg; }
.wa__close:hover { background: var(--surface-alt); }
.wa__msg { font-size: var(--t-body-sm); line-height: 1.45; margin-bottom: var(--s-4); }
/* inline-flex, so the glyph travels with the words instead of anchoring to
   the left edge of a block button. */
.wa__cta { display:flex; align-items:center; justify-content:center;
  gap: var(--s-2); text-decoration:none; }
@media (max-width: 479px) { .wa__label { position:absolute; width:1px; height:1px;
  overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; } }

/* The article card inside the services section is a card, not a banner. Without
   a width it took the full container and dominated a section that is about the
   services list. */


/* ------------------------------------------------------------------ about */
.promise { max-width: var(--measure); }
.promise li { padding-left: var(--s-6); position: relative; margin-bottom: var(--s-3); }
.promise li::before { content: ''; position: absolute; left: 0; top: .65em;
  width: 10px; height: 10px; border-radius: var(--r-pill); background: var(--accent); }

.team { align-items: start; }
.member__photo { width: 100%; aspect-ratio: 1 / 1; object-fit: cover;
  border-radius: var(--r-md); margin-bottom: var(--s-4); }
.member__name { font-size: var(--t-h4); margin-bottom: var(--s-1); }
.member__role { color: var(--on-surface-muted); font-size: var(--t-body-sm); margin-bottom: var(--s-3); }




.member__bio { font-size: var(--t-body-sm); color: var(--on-surface-muted); }

/* The gallery is a masonry-ish column layout on the live site. CSS columns give
   the same effect with no script; the Refokus masonry library is gone. */
.gallery { columns: 3; column-gap: var(--s-5); }
.gallery__item { break-inside: avoid; margin: 0 0 var(--s-5); }
.gallery__item img { width: 100%; border-radius: var(--r-md); }
.gallery figcaption { font-size: var(--t-caption); color: var(--on-surface-muted);
  margin-top: var(--s-2); }

.booking { width: 100%; border: 0; border-radius: var(--r-md); min-height: 800px; }

@media (max-width: 991px) { .gallery { columns: 2; } }
@media (max-width: 479px) { .gallery { columns: 1; } }
.why__icon { margin-bottom: var(--s-4); }
.why__title { font-weight: 600; font-size: var(--t-h6); margin-bottom: var(--s-2); }
.about-hero__portrait { margin-top: var(--s-10); }

@media (min-width: 992px) {
  .about-hero__portrait { position: absolute; right: 0; bottom: 0; margin: 0; max-width: 26rem; }
}

/* ------------------------------------------------ decorative brand shapes */
/* The live site scatters careDent_Asset_2/4 as background shapes. They carry
   no meaning, so they are aria-hidden with empty alt, but they ARE on the page
   and leaving them out is an omission, not a simplification. */
.hero-marks { display:flex; gap: var(--s-4); align-items:center; margin: var(--s-6) 0 0; opacity:.45; }
.hero-marks img { height: 3.5rem; width:auto; }
.hero-marks img:nth-child(2) { transform: rotate(-100deg); }
.hero-marks img:nth-child(3) { transform: rotate(40deg); }

.about-portrait { position: relative; display:flex; justify-content:flex-end; }
/* AND THE BLOB FILLS THE COLUMN IT IS IN. At 25rem it was 415px inside a
   613px cell, right-aligned, so 204px of that cell was empty and the hole
   Leke was looking at was mostly this rather than the 40px grid gap.
   Measured, not guessed: the ink used to start at x=937 in a column that
   starts at 733. */
/* NO CAP. A max-width made the picture stop growing while its COLUMN kept
   going, so the lane between the two columns was 21px at 1280 and 72px at
   1440: the same page looked balanced on one screen and gappy on the next.
   The column ratio sizes the picture now and the lane is just the grid gap,
   21 to 25px at every width from 992 to 1920. */
.about-portrait__blob { width: 100%; max-width: none; transform: rotate(-100deg); }
.about-portrait__over { position:absolute; inset: 0; top: -12%; display:block; }
/* AND THE WINDOW IS THE WIDTH OF THE BLOB, NOT OF THE COLUMN. The blob is
   75% here and 58% on a phone; a window at inset:0 was the full column, so it
   was always the wider of the two and the blob read as a smear behind a
   cut-out. The insets centre a box of the blob's own width. */
/* 768 to 991 is still TWO columns, so the picture already has a column of its
   own and does not need to be a share of it as well. At 75% it drew 206px
   wide, which was narrower than the same picture on a 393 phone: the tablet
   was the worst place on the site to look at it. It fills its column here,
   like it does above 992, and only the single-column phone layout takes a
   share. */
@media (max-width: 991px) {
  .about-portrait { justify-content:center; }
  .about-portrait__over { top: -10%; }
}

/* ---------------------------------------------------------- footer map --
 * The map used to be a full band on the home page with a second "Contact us"
 * card beside it. Moved into the footer (Leke, 13 Aug 2026): a location map is
 * footer furniture, and as its own section it bought a screen of scroll for
 * something nobody visits the home page to see. It now sits on every page
 * rather than one, which is the point of putting it in shared chrome.
 *
 * The old .findus rules were deleted with the section rather than left behind.
 * Dead CSS reads as the live definition to the next person in the file. */
/* ZOOMED IN, not fitted. The source is 1000x500 of mostly lagoon; `cover` in a
   312px column drew the whole map at 31% scale, which put the street names at
   about 3px and turned the thing into a pale wash that reads as a failed image
   rather than a place. At 220% the block the clinic actually sits on fills the
   strip and Admiralty Way, Layi Yusuf Crescent and Lekki-Epe Expressway are
   legible — which is the only reason to show a map at all. */
.footer__map {
  display: block; position: relative; height: 8rem;
  border-radius: var(--r-md); margin-bottom: var(--s-5); overflow: hidden;
  background: url("/assets/img/6151b4cbbbb7a2a5f829387e_caredent_lekki-location.png") 52% 46% / 220% no-repeat;
}
.footer__pin {
  position: absolute; left: 50%; top: 50%; width: .75rem; height: .75rem;
  border-radius: 50%; background: var(--primary); transform: translate(-50%,-50%);
  box-shadow: 0 0 0 .5rem color-mix(in srgb, var(--primary) 25%, transparent);
}

/* ------------------------------------------------------- script eyebrows */
/* THE BRAND'S HANDWRITING, and leaving it out changed the whole feel of the
   site. Live sets every section eyebrow in Brittany Signature at 1.5rem,
   sentence case, via `.subtitle`. The rebuild had them as small uppercase
   letter-spaced grey sans, which is a perfectly good eyebrow and is not
   CareDent's. Nothing in a copy-and-image diff can see a font.

   The file is one of the twelve assets the export missed, because it is
   referenced from the stylesheet rather than from markup.

   LICENCE: the filename is "FontsFree-Net-BrittanySignature.ttf". Brittany
   Signature is a commercial typeface and that is a free-download copy, so the
   clinic is running unlicensed on their current site and this carries it over
   unchanged. Buying a licence is on the client ask list; it is their asset and
   their decision, and phase one is not the place to silently substitute a
   different typeface. */
@font-face {
  font-family: 'Brittany Signature';
  src: url('/assets/fonts/614b4b176fd11e6f3b2e136b_FontsFree-Net-BrittanySignature.ttf') format('truetype');
  font-weight: 400; font-style: normal; font-display: swap;
}

.subtitle {
  font-family: 'Brittany Signature', 'Segoe Script', cursive;
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: 0;
  text-transform: none;
  color: var(--on-ground);
  margin: 0 0 var(--s-2);
}
.ground-navy .subtitle, .ground-mint .subtitle { color: inherit; }

/* ============================================================ home hero == */
/* Live: `background-color-2` = --light-pink, which is #dac2e2 at 25% alpha, on
   a rounded panel inside the 1024 container. Two equal columns. */
.hero-panel {
  background: color-mix(in srgb, var(--surface-lilac) 25%, transparent);
  border-radius: var(--r-lg);
  /* The extra bottom padding is the LANDING ROOM for the info row that rides up
     over this panel. Without it the cards would sit on the "From Naija to the
     world" line rather than on empty lilac. */
  padding: var(--s-12) var(--s-10) calc(var(--s-16) + 3rem);
  display: grid; gap: var(--s-10); align-items: center;
}
/* MORE MARGIN, SMALLER PICTURE (Leke, 29 Sep 2026): "increase the horizontal
   margin of this hero section... let's reduce the image on the right and the
   elements on the right, reduce their size a bit."
   The side padding and the column gap move together. Widening the margin on
   its own pushes both columns inward and OPENS the lane between them, which
   is the hole he had me close earlier the same day; taking the gap down by
   the same step keeps them the distance apart they were. */
@media (min-width: 992px) {
  .hero-panel { padding-inline: var(--s-16); gap: var(--s-8); }
}
.hero-panel__text h1 { margin-top: var(--s-2); }
.naija { margin-top: var(--s-8); display:flex; align-items:center; gap: var(--s-2); }
.naija svg { flex: 0 0 auto; }
@media (max-width: 479px) { .hero-panel__services { display: none; } }

/* THE ANNOUNCEMENT SLIDER (22 Sep 2026). Slides are stacked in one square
   box and CROSSFADE (Leke): the current one is opaque, the rest sit under
   it at opacity 0 and, once faded, visibility hidden so their links leave
   the tab order. 420ms, the "arrived" band of the motion contract, on the
   site's one curve. With no script the first slide shows and the rest stay
   hidden, so the page is never blank and never shows four posters at once.
   Nothing moves unless the reader moves it: a swipe, a dot, or the arrow
   keys on the focused strip. No autoplay. */
.announce { position: relative; }
.announce__track {
  position: relative; display: grid; border-radius: var(--r-md); overflow: hidden;
  touch-action: pan-y; user-select: none; -webkit-user-select: none;
}
.announce__track:focus-visible { outline-offset: -3px; }
.announce__slide {
  grid-area: 1 / 1; position: relative;
  opacity: 0; visibility: hidden;
  transition: opacity 420ms ease-in-out, visibility 0s linear 420ms;
}
.announce__slide.is-current, .announce__track:not(.is-live) .announce__slide:first-child {
  opacity: 1; visibility: visible; transition-delay: 0s;
}
.announce__media { display:block; width:100%; height:auto; aspect-ratio: 1 / 1; object-fit: cover; -webkit-user-drag: none; }
@media (prefers-reduced-motion: reduce) { .announce__slide { transition: none; } }
.announce__cta { position:absolute; left: var(--s-4); bottom: var(--s-4); }
/* A white outline on the button, so it holds against any poster behind it:
   the mint fill alone vanished on the mint arcs of the August artwork. The
   edge is taken out of the padding so the box matches the other small
   buttons (css-architecture: every control in a row shares one box). */
.announce__cta .btn { border: 2px solid #fff; padding: calc(.5rem - 2px) calc(.75rem - 2px); }
/* Dots UNDER the picture, not on it (Leke, 22 Sep 2026): a short row, left
   aligned on the same vertical line as the Learn more button (both start
   s-4 in from the picture's left edge). The dot is 7px and the pitch 24px:
   under the 44px touch size, but each target is its own 24px square with
   nothing else inside a 24px circle around it, which is the WCAG 2.2 spacing
   exception for exactly this kind of control. */
.announce__dots {
  display: flex; align-items: center;
  padding-left: calc(var(--s-4) - 8.5px);   /* first DOT lands on the button's left edge */
  margin-top: var(--s-1);
}
.announce__dot {
  width: 24px; height: 24px; padding: 0; border: 0; background: transparent;
  display: grid; place-items: center; cursor: pointer; touch-action: manipulation;
}
.announce__dot::before {
  content: ''; width: 7px; height: 7px; border-radius: 50%;
  background: var(--primary); opacity: .3;
  transition: opacity 200ms ease-in-out, transform 200ms ease-in-out;
}
.announce__dot:hover::before, .announce__dot:focus-visible::before { opacity: .6; }
.announce__dot[aria-current]::before { opacity: 1; transform: scale(1.3); }
@media (prefers-reduced-motion: reduce) { .announce__track { scroll-behavior: auto; } }

/* ===================================================== clinic quick info == */
/* Rides up 3rem over the hero panel, 2.75fr / 1fr, as live does. */
.quickinfo { padding-bottom: var(--band-xs); }
/* LAYERED ON THE HERO, which is the aesthetic the site was designed around.
   It was not overlapping at all before, and -3rem looked like it should have
   been: the hero SECTION carries 4.5rem of its own bottom padding, so -3rem
   only ate two thirds of the gap and left the cards 24px BELOW the panel. The
   pull has to clear the section padding first and then bite into the panel.

   `position: relative` is load-bearing, not decoration: .hero-panel is a
   positioned element, so it paints above later static siblings. Overlapping
   without this puts the cards UNDERNEATH the lilac.

   Narrower than the panel on purpose, so it reads as a separate layer sitting
   on top rather than a block that happens to be flush with it. */
.quickinfo__grid {
  display: grid; gap: var(--s-4);
  position: relative; z-index: 1;
  width: 94%; margin-inline: auto;
  margin-top: calc(-1 * (var(--band-sm) + 3rem));
}
@media (min-width: 768px) { .quickinfo__grid { grid-template-columns: 2.75fr 1fr; } }
.quickinfo__card {
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: var(--shadow-sm); padding: var(--s-6);
  display: grid; gap: var(--s-6);
}
@media (min-width: 560px) { .quickinfo__card { grid-template-columns: 1fr 1fr; } }
.quickinfo__title {
  display:flex; align-items:flex-start; gap: var(--s-2);
  font-size: var(--t-h5); font-weight: 600; line-height: 1.15;
  margin: 0 0 var(--s-4); text-transform: none; letter-spacing: 0;
}
.quickinfo__title img { flex: 0 0 auto; margin-top: .2em; }
.hours { display:grid; grid-template-columns: auto 1fr; gap: var(--s-1) var(--s-5); }

/* #f43319, white text. The loudest thing on the page. */
.quickinfo__alert {
  background: #f43319; color: #fff; padding: var(--s-6);
  box-shadow: var(--shadow-sm);
}
.quickinfo__alert a { color: #fff; }
.quickinfo__alert p:last-child { margin-bottom: 0; }
/* The alert borrowed .quickinfo__title, which is sized for the two-word heads
   next to it ("Contact us", "Opening Hours"). Two words at h5 is a label; two
   SENTENCES at h5, bold, in white on #f43319 is a shout — it was beating the
   h1 for attention and the card is not the page's message. Down to body size,
   still bold, still red, still the loudest CARD. */
.quickinfo__alert .quickinfo__title { font-size: var(--t-body); line-height: var(--lh-heading); }

/* ==================================================== the brand blob clip == */
/* Live clips the About portrait and every team photo with an SVG clipPath in
   the page head. Reproduced here against the same path, which is inlined at
   the top of about.html. A rectangle where the brand draws a blob is not a
   detail: it is most of what the page looks like. */
/* ------------------------------------------- the hero crossfade ---
 * Leke, 29 Sep 2026: "have it cycle through the images that have Thank you
 * for trusting us... make sure the transition between images is a crossfade
 * and has like some nice easing."
 *
 * The same vocabulary as the home page's announcement slider, deliberately:
 * stacked in one grid cell, opacity 420ms on the site's one curve, and
 * `visibility` held until the fade finishes so the one underneath is not
 * exposed to a screen reader or the tab order while it is invisible.
 *
 * WITHOUT SCRIPT THE FIRST PICTURE SHOWS AND THE REST STAY HIDDEN. The
 * attribute that says which one is current is written into the markup on the
 * first image, so a page whose script never runs is a page with one
 * photograph on it rather than four stacked on top of each other.
 *
 * AND IT DOES NOT CYCLE FOR SOMEBODY WHO ASKED FOR LESS MOTION. This is the
 * only thing on the site that moves on its own; reduced-motion gets the first
 * picture, still. The script reads the same query.
 *
 * Each picture carries its own `--pos`, because four photographs of four
 * different rooms do not share a subject point. */
.hero-cycle { display: grid; }
.hero-cycle .hero-clipped {
  grid-area: 1 / 1;
  opacity: 0; visibility: hidden;
  transition: opacity 420ms ease-in-out, visibility 0s linear 420ms;
}
.hero-cycle .hero-clipped[data-shown] {
  opacity: 1; visibility: visible; transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  .hero-cycle .hero-clipped { transition: none; }
}

/* A RATIO, NOT A HEIGHT. The lilac blob behind this photo is width-driven
   (a share of the column), so a portrait pinned at a fixed rem height only
   nested inside it at one viewport width: at 992 a 38rem photo hung 113px
   below a 470px blob. The blob's own layout box measures 1.139:1 at every
   width from 992 to 1920, so the window gets a ratio of its own and the two
   now scale together. */
.hero-clipped {
  -webkit-clip-path: url(#cd-element-minus-43-degrees-clip);
  clip-path: url(#cd-element-minus-43-degrees-clip);
  object-fit: cover; width: 100%; height: auto; aspect-ratio: 640 / 608;
  position: relative; z-index: 1;
}
.team-clipped {
  -webkit-clip-path: url(#cd-element-minus-43-degrees-clip);
  clip-path: url(#cd-element-minus-43-degrees-clip);
  object-fit: cover; width: 100%; height: 18rem;
}
@media (max-width: 991px) {
  .team-clipped { height: 15rem; }
}
@media (max-width: 767px) {
  .hero-clipped { margin: auto; }
  .team-clipped { width: 80%; height: 18rem; margin: auto; }
}
/* The hero panel needs a positioning context for the decorative mark. */
.hero-panel { position: relative; overflow: hidden; }

/* The first panel runs flush into the nav (Leke, 14 Aug 2026, home; every
   page since 21 Sep 2026): no band padding above it, so the panel starts where
   the nav ends. The nav's bottom rule went on 22 Sep 2026 and the panel's
   top corners got their curve back the same day: the square shoulders only
   ever existed to avoid a 12px curve meeting a straight 1px line. */
/* .band.hero-band, not .hero-band: `.band.is-sm` sets padding-block with two
   classes, so a single-class rule loses to it however far down the file it
   sits. Specificity beats source order every time. */
.band.hero-band { padding-top: 0; }
/* All four corners curved again (Leke, 22 Sep 2026): with the nav's rule
   gone there is no straight line for a top curve to fight, and a panel with
   square shoulders under a plain white nav read as cut off. Same for every
   first panel below. */
.hero-band .hero-panel { border-radius: var(--r-lg); }

/* ------------------------------------------------------- about: why us --- */
/* One pale panel, three columns, no boxes round the individual items. */
.why-panel {
  background: var(--surface-alt); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--s-12) var(--s-10);
}
.why-grid { row-gap: var(--s-10); }
.why-item p { font-size: var(--t-body-sm); margin-bottom: 0; }
.why__icon { margin-bottom: var(--s-3); width: 28px; height: 28px; }

/* --------------------------------------------------------- about: team --- */
/* Live centres the name and the role under each photo. (The "More" control
   this file used to style went with the turn-over on 23 Sep 2026; the name
   is the control now. Its rules are further down, with the modal.) */
.member { text-align: center; }
.member__name { margin: var(--s-4) 0 var(--s-1); font-size: var(--t-h5); }
.member__role { font-size: var(--t-h6); margin-bottom: var(--s-2); }

/* Live sets the promise as plain lines, not a bulleted list: the mint dots
   were an addition, and an addition to a client's page is still an addition
   however small. Overrides the earlier rule rather than editing it, so the
   change is visible in the diff. */
.promise { list-style: none; padding: 0; margin: 0 0 var(--s-6); max-width: var(--measure); }
.promise li { padding-left: 0; margin-bottom: var(--s-1); }
.promise li::before { content: none; display: none; }

/* ================================================== TOUCH TARGETS (§7) ==
 * NAME THE ELEMENTS, NOT THE COMPONENTS. The sweep at 393 found 10 targets
 * under 24px on the home page — a failed WCAG 2.2 SC 2.5.8 at AA, not a rough
 * edge — and 17 under 44. They were the ones no component rule mentions: the
 * three top-bar social glyphs at 20x20, the footer glyphs at exactly 24x24,
 * and the tel/email links at 20px tall.
 *
 * Two tiers on purpose:
 *   - Standalone controls and icon-only links take 44 (SC 2.5.5, Apple's
 *     figure). An icon link is not "inline in a sentence" and gets a box.
 *   - Links inside a run of text (the phone number and email inside the
 *     address block, "click here" inside a sentence) take 24, which is SC
 *     2.5.8's floor. 44 there would put visible gaps between the lines of an
 *     address, which is why the SC exempts inline links in the first place.
 */
@media (max-width: 991px) {
  .btn, .nav__toggle, .chatbar__chip, .post__more, .link-arrow,
  .member__open, .wa__cta, .pe-card__link {
    min-height: 2.75rem;
  }
  .btn, .nav__toggle, .chatbar__chip, .post__more, .link-arrow, .wa__cta {
    display: inline-flex; align-items: center; justify-content: center;
  }
  .pe-card__link { display: block; }        /* a card link is a block, not a row */

  /* Icon-only links: a real 44px box round a 20-24px glyph. */
  .chatbar__social a, .footer .row a {
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.75rem; height: 2.75rem;
  }

  /* Inline links inside prose: 24px floor via padding, no layout shift.
     The map card was missed the first time round, which is the whole point of
     sweeping every visible element and reading the COUNT rather than trusting
     the selector list. */
  .quickinfo__card a, .quickinfo__alert a, .footer__cols p a,
  .findus__card a, .band > .container > p > a {
    display: inline-block; padding-block: .125rem; min-height: 24px;
  }

  /* Form controls are controls, not sentences: full 44. 16px minimum on the
     field itself or iOS zooms the page when it takes focus (§7). */
  input, select, textarea { min-height: 2.75rem; font-size: max(1rem, 16px); }
}
/* Kills the double-tap zoom hesitation on every tappable control (§7). */
a, button, [role="button"], input, select, summary, label { touch-action: manipulation; }

/* --------------------------------------------------------------- welcome */
/* Live's `grid-2-columns smaller-first-grid`: prose left, portrait right. */
.welcome { display: grid; gap: var(--s-12); align-items: center; }
@media (min-width: 992px) {
  .welcome { grid-template-columns: 1.15fr 1fr; }
  /* ALIGN THE LEFT EDGE WITH THE INFO CARD ABOVE (Leke, 14 Aug 2026). The
     quick-info grid rides on a 94% rail, centred, so its border sits 3% in
     from the container while everything else — hero panel, services card,
     this section — runs flush to it. The text started 40px left of the card
     edge at 1440 and the step was visible.

     `padding-left: 3%` and not a pixel nudge, because that is literally the
     other half of `.quickinfo__grid`'s `width: 94%`: one number, derived from
     the same rail, so the two cannot drift apart when the container changes.
     Measured 40px at 1440, 46 at 1920, 27 at 992 — which is exactly the card's
     own inset at each.

     On the grid rather than on .welcome__text: a percentage inside a grid item
     resolves against the ITEM, so the same 3% there would have been 3% of a
     694px column, not of the 1345px container — 21px, half the distance, and
     wrong by a different amount at every width. The right edge is untouched,
     so the portrait still lines up with the services card below it. */
  .welcome { padding-left: 3%; }
}
/* The photo carries the section, so it takes the whole column rather than a
   24rem cap. The name label sits in the brand shape at its lower right. */
.welcome .dentist { margin: 0; position: relative; justify-self: stretch; }
/* WAIST UP. The asset is a full-length cut-out (854x1280, subject from y=208
   to the bottom edge); her hands sit on her hips at about 62% of the subject
   and the skirt starts around 68%. Cropping to a 854x760 window positioned at
   28% shows y=146 to y=906 — a little headroom above the hair, cut just below
   the hands.

   Done with object-fit rather than by re-cutting the file: the original stays
   the original, the crop is a design decision that lives in the stylesheet
   where it can be changed, and the image diff still sees the same asset. */
.dentist__photo {
  width: 100%; height: auto;
  aspect-ratio: 854 / 760;
  object-fit: cover;
  /* 39.5%, up from 28% (Leke, 14 Aug 2026): "top of her head just a bit over
     the subtitle in column one".

     Done by moving the CROP, not the element. Lifting her further with the
     transform would have cost the contact with the services card all over
     again — she has only 16px of overlap left. Cropping more off the top of
     the source moves the subject up inside a box that does not move, so the
     head rises and the card, the badge and the row are all untouched.

     40.2% is the hard ceiling and it is width-independent: the head reaches
     the frame edge when P = (209/854) / ((1280-760)/854), and every term
     scales with the box, so the same number holds at 992 and at 2560. 39.5%
     leaves about 2px of hair above the crop. Past 40.2% it cuts into her. */
  object-position: center 39.5%;
  border-radius: 0;
}
/* The badge is the positioned box: shape and name move together, and the name
   is placed against the SHAPE rather than against the whole figure, which is
   what sent it off the right edge the first time. 20.5625rem = the SVG's own
   329px, so it never scales past its drawn size. */
.dentist__badge {
  position: absolute; right: 0; bottom: 0;
  width: min(20.5625rem, 62%); pointer-events: none;
}
/* 33%, up from 24% (Leke, 14 Aug 2026): the name sat low in the shape, so the
   empty navy piled up above it. MEASURED rather than nudged — the blob's area
   centroid is at (175.7, 127.9) inside the badge box and the text's ink centre
   was at (175.5, 148.0). Horizontally it was already perfect; vertically it
   was 20px low, and 20px of a 221px box is the 9 points added here.

   The centroid, not the bounding box, because the shape is an organic blob
   with a narrow tail at the top: its bbox centre is 6px higher than its centre
   of area, and centring on the bbox would have left it looking low again. */
.dentist__name {
  position: absolute; right: 11%; bottom: 33%;
  text-align: right; color: #fff; line-height: 1.3; max-width: 74%;
}
.dentist__name b { display: block; font-weight: 600; font-size: var(--t-h6); }
.dentist__name span { display: block; font-size: var(--t-label); opacity: .9; }
@media (max-width: 991px) {
  .welcome .dentist { max-width: 26rem; justify-self: center; }
}
/* .dentist__frame and the `position: static` override that went with it were
   for the previous markup, where the nametag sat beside a caption. The name is
   inside the shape now, so the figure IS the positioning context again. Leaving
   them in place kept the shape positioning against the wrong ancestor and it
   disappeared off the section entirely. */

/* The floating WhatsApp button is fixed bottom-right and sat over the legal
   line on a phone, where the footer is the last thing on screen. Reserve the
   button's footprint at the bottom of the footer rather than moving the button
   — live pins it in the same place, and it should stay reachable. */
@media (max-width: 767px) {
  .footer { padding-bottom: calc(var(--s-16) + 4rem); }
}

/* ======================================= home: patient education (2 col) ==
 * The original design splits this section: heading + one featured article down
 * the left, four smaller ones stacked down the right with a thumbnail beside
 * each. Stacks to one column at 991, lead first, so the featured article keeps
 * its position at the top of the reading order on a phone.
 */
.pe-home { display: grid; gap: var(--s-12); }
@media (min-width: 992px) {
  /* TWO ROWS, not one. The head sits in row 1 of column 1; the featured
     article and the list of four share row 2, so both start on the same line.
     Before this the list was a column of its own running the full height, so
     the small posts began level with the SUBTITLE and the big picture began
     three lines lower. */
  .pe-home { grid-template-columns: 1fr 1fr; column-gap: var(--s-16); }
  /* The head spans BOTH columns, not just the first. Confined to column one it
     was capped by the 414px column at 992 rather than by its own 12em, so it
     went back to three lines at exactly the width where the two-column layout
     starts. Spanning, the 12em cap is what limits it at every width, which is
     the point of having a cap. Visually identical above 1200, where the cap
     was already the narrower of the two. */
  .pe-home__head { grid-column: 1 / -1; grid-row: 1; }
  .pe-feature     { grid-column: 1; grid-row: 2; }
  .pe-home__list  { grid-column: 2; grid-row: 2; }
}

/* THE HEADING RUNS TO 12em HERE, NOT THE SITE-WIDE 10em. Flagged rather than
   buried: 10em is the rule set on 14 Aug from "It's all about you", and this
   is the one heading that breaks it. At 10em "Learn more about caring for Your
   teeth" needs three lines (the string is ~20em, and 20/10 leaves no room for
   a word boundary); Leke asked for two, which needs ~11.1em minimum. 12em is
   that plus enough slack that the break lands between "about" and "caring"
   rather than mid-phrase. If the site-wide cap is what matters more, delete
   this rule and the heading goes back to three lines. */
.pe-home__head h2 { max-width: 12em; }

/* No margin-top: the row gap now sets the distance from the head, and a margin
   here would push the feature down while the list stayed put — which is the
   misalignment this change exists to remove. */
.pe-feature { display: block; text-decoration: none; color: inherit; }
.pe-feature__media { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; border-radius: var(--r-sm); }
.pe-feature .post__tags { margin-top: var(--s-4); }
.pe-feature__title { font-size: var(--t-h5); margin: var(--s-2) 0; }
.pe-feature__summary { display: block; color: var(--on-surface); margin-bottom: var(--s-4); }

/* The right-hand list: a fixed thumbnail rail beside the text. The rail is a
   fixed side element, so responsive-system §6 applies — below 480 it stacks
   rather than shrinking, because shrinking buys pixels and stacking buys the
   whole measure. Checked: the text column would land near 230px otherwise. */
.pe-home__list { display: grid; gap: var(--s-8); align-content: start; }
/* (.pe-home__lead removed 14 Aug 2026 — the wrapper it named is gone; the
   head is now its own grid item, .pe-home__head.) */
.pe-mini { display: grid; grid-template-columns: 8.125rem 1fr; gap: var(--s-5);
  text-decoration: none; color: inherit; align-items: start; }
.pe-mini__media { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--r-sm); }
.pe-mini__text { display: block; }
.pe-mini__title { font-size: var(--t-h5); margin: 0 0 var(--s-1); }
.pe-mini__summary { display: block; color: var(--on-surface); }
@media (max-width: 479px) {
  .pe-mini { grid-template-columns: 1fr; gap: var(--s-3); }
  .pe-mini__media { aspect-ratio: 16 / 9; }
  /* ONE ROW THAT SWIPES on a phone (mobile pass, 29 Sep 2026). Stacked, the
     four posts under the feature ran to two screens of picture-title-summary,
     the same shape four times. In a row the next one peeks in at the right
     edge, the same pattern as the testimonials above it. */
  .pe-home__list {
    display: flex; gap: var(--s-4); overflow-x: auto; scroll-snap-type: x mandatory;
    scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: var(--s-2);
  }
  .pe-home__list::-webkit-scrollbar { display: none; }
  .pe-home__list > .pe-mini { flex: 0 0 80%; scroll-snap-align: start; align-content: start; }
}
/* The article's "Continue reading" three get the same row below 992, for the
   same reason: three stacked cards were a screen and a half after the read on
   a phone, and two columns left the third on a row of its own on a tablet. */
@media (max-width: 991px) {
  .grid-3.related {
    display: flex; gap: var(--s-4); overflow-x: auto; scroll-snap-type: x mandatory;
    scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: var(--s-2);
  }
  .grid-3.related::-webkit-scrollbar { display: none; }
  .grid-3.related > .pe-card { flex: 0 0 80%; scroll-snap-align: start; }
}
/* At tablet widths the same row shows two and a half, rather than two in a
   row and the third on a line of its own. */
@media (min-width: 480px) and (max-width: 991px) {
  .grid-3.related > .pe-card { flex-basis: 42%; }
}

/* ===================== RESTORED 14 Aug 2026 ==============================
 * These blocks were deleted by accident. Removing the dead .findus rules was
 * done by slicing the stylesheet between two comment markers, and the slice
 * asserted only that it CONTAINED .findus__map — not that it contained
 * nothing else. It ran from the patient-education header all the way to the
 * next marker and took the whole patient-education index and article-page
 * styling with it: 20 selectors across 31 pages, none of which the copy or
 * image diff can see, because a missing stylesheet rule is not missing text.
 *
 * Assert what a slice EXCLUDES, not just what it includes.
 * ====================================================================== */

/* ---------------------------------------------------- patient education */
.pe-hero { position: relative; display:block; border-radius: var(--r-lg); overflow:hidden; text-decoration:none; color:inherit; }
.pe-hero__media { display:block; width:100%; height: clamp(16rem, 34vw, 25rem); object-fit: cover; }
.pe-hero__panel {
  display:block; background: color-mix(in srgb, var(--surface) 92%, transparent);
  border-radius: var(--r-md); padding: var(--s-6); margin: var(--s-4);
  max-width: 32rem;
}
@media (min-width: 768px) { .pe-hero__panel { position:absolute; left: var(--s-8); top:50%; transform: translateY(-50%); margin:0; } }
.pe-hero__title { display:block; font-size: var(--t-h3); font-weight:600; line-height: var(--lh-heading); margin: var(--s-1) 0 var(--s-2); }
.pe-hero__summary { display:block; color: var(--on-surface-muted); margin-bottom: var(--s-3); }

.pe-search { max-width: 26rem; margin: 0 auto var(--s-10); }
.pe-search__count { margin: var(--s-2) 0 0; color: var(--on-surface-muted); font-size: var(--t-label); min-height: 1.25rem; }

.pe-card { display:flex; flex-direction:column; justify-content:space-between; }
.pe-card.hidden { display:none; }
.pe-card__link { display:block; text-decoration:none; color:inherit; }
.pe-card__media { display:block; width:100%; height: 10rem; object-fit: cover; border-radius: var(--r-md); }
.pe-card__cat { display:block; font-size: var(--t-label); color: var(--on-surface-muted); padding: var(--s-1) 0; }
.pe-card__title { font-size: var(--t-h5); font-weight:600; margin: var(--s-1) 0 var(--s-2); }
.pe-card__summary { display:block; color: var(--on-surface-muted); }
.link-arrow { display:inline-block; margin-top: var(--s-3); font-weight:500; text-decoration:none; }
.link-arrow img { margin-left: var(--s-1); vertical-align: -1px; }
.pe-more__wrap { text-align:center; margin-top: var(--s-10); max-width: none; }

/* ------------------------------------------------------------ article page */
.post-head { display:grid; gap: var(--s-8); align-items:center; }
@media (min-width: 768px) { .post-head { grid-template-columns: 1fr 1fr; } }
.post-head__media { display:block; width:100%; height: clamp(14rem, 30vw, 22rem); object-fit: cover; border-radius: var(--r-lg); }
.post-head__date { color: var(--on-surface-muted); font-size: var(--t-label); margin: 0; }
.container--prose { max-width: 46rem; }
.container--prose img { max-width:100%; height:auto; border-radius: var(--r-md); }
.container--prose h2 { font-size: var(--t-h3); margin-top: var(--s-10); }
.container--prose h3 { font-size: var(--t-h4); margin-top: var(--s-8); }
.text-center { text-align:center; }
/* Live renders 87 of the 90 "Continue reading" thumbnails blank, so the card
   keeps its shape without one. Phase two restores the pictures. */
.pe-card__media--blank { display:block; width:100%; height:10rem; border-radius: var(--r-md); background: var(--surface-2, #eef2f6); }

/* ------------------------------------------------------- script eyebrows */
/* THE BRAND'S HANDWRITING, and leaving it out changed the whole feel of the
   site. Live sets every section eyebrow in Brittany Signature at 1.5rem,
   sentence case, via `.subtitle`. The rebuild had them as small uppercase
   letter-spaced grey sans, which is a perfectly good eyebrow and is not
   CareDent's. Nothing in a copy-and-image diff can see a font.

   The file is one of the twelve assets the export missed, because it is
   referenced from the stylesheet rather than from markup.

   LICENCE: the filename is "FontsFree-Net-BrittanySignature.ttf". Brittany
   Signature is a commercial typeface and that is a free-download copy, so the
   clinic is running unlicensed on their current site and this carries it over
   unchanged. Buying a licence is on the client ask list; it is their asset and
   their decision, and phase one is not the place to silently substitute a
   different typeface. */
/* (duplicate @font-face for Brittany Signature removed 14 Aug 2026 — byte-identical to the declaration above) */




/* ============================================================ home hero == */
/* Live: `background-color-2` = --light-pink, which is #dac2e2 at 25% alpha, on
   a rounded panel inside the 1024 container. Two equal columns. */

/* THE PICTURE TAKES MORE OF THE ROW (Leke, 29 Sep 2026): the gap between
   the columns was reading as a hole. Two equal columns left the copy ending
   on a ragged right edge with a wide empty lane beside it. Giving the media
   column a tenth more width closes the lane from the right, and the copy
   loses nothing it was using: the longest line already fitted.
   Declared once. It was written twice, identically, four hundred lines
   apart, which is the same two-names-for-one-thing that has now cost this
   project three separate bugs. */
@media (min-width: 768px) { .hero-panel { grid-template-columns: 1fr .85fr; } }



@media (max-width: 479px) { .hero-panel__services { display: none; } }





/* ===================================================== clinic quick info == */
/* Rides up 3rem over the hero panel, 2.75fr / 1fr, as live does. */


@media (min-width: 768px) { .quickinfo__grid { grid-template-columns: 2.75fr 1fr; } }

@media (min-width: 560px) { .quickinfo__card { grid-template-columns: 1fr 1fr; } }




/* #f43319, white text. The loudest thing on the page. */




/* Live renders 87 of the 90 "Continue reading" thumbnails blank, so the card
   keeps its shape without one. Phase two restores the pictures. */
/* (duplicate .pe-card__media--blank removed 14 Aug 2026 — identical to the one above) */

/* The chevron was breaking to its own line: the global `img { display:block }`
   makes it a block inside an inline-block link. Also the mint underline the
   design and the live site both give this control (live: `link-w-arrow_underline`,
   background-color-4, height-1). */
.link-arrow {
  display: inline-flex; align-items: center; gap: var(--s-1);
  border-bottom: 2px solid var(--accent); padding-bottom: 2px;
}
.link-arrow img { display: inline-block; margin-left: 0; }

/* The comp letterboxes the featured image; 16/10 made it the loudest thing in
   the section and pushed the article's own title below the fold of the block. */

/* Inside the flex/grid cards the underline stretched the full column, because
   a flex child stretches by default. The rule is a text link, not a divider. */
.link-arrow { align-self: flex-start; width: fit-content; }

/* ================================================ home: services card ==
 * The Figma layers this card between the white section above and the mint band
 * below: it rides UP over the boundary and leaves mint showing at its left and
 * underneath. Live does the same thing (margin-top -8rem, 95% width, offset,
 * off-white fill, small shadow); the rebuild had it flat inside one band.
 */
.services-card {
  /* Positioned and above .dentist on purpose: the card crosses IN FRONT of
     Dr Folayemi. A static card loses to her `position: relative` regardless of
     DOM order, so this is the rule that decides the layering. */
  position: relative; z-index: 2;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-md);
  padding: var(--s-12);
  /* THE JUT. -8rem left the card only 32px clear of the mint, so it read as a
     card that happened to start early rather than one shooting out of the
     band. -14rem puts ~130px of white above the mint edge. */
  margin-top: -14rem;
  margin-left: auto;          /* offset right, so mint shows on the left */
  width: 96%;
}
.services-card__grid { display: grid; gap: var(--s-12); }
@media (min-width: 992px) {
  .services-card__grid { grid-template-columns: 1.25fr 1fr; column-gap: var(--s-16); }
}
.services-card__cta { margin: var(--s-10) 0 0; }

/* Below 992 the jut would pull the card over copy rather than over a boundary,
   because the sections are already stacked and there is no white/mint edge to
   straddle. It sits normally instead. */
@media (max-width: 991px) {
  .services-card { margin-top: 0; margin-left: 0; width: 100%; padding: var(--s-8); }
}

.svc-post { display: block; text-decoration: none; color: inherit; }
.svc-post__media { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; border-radius: var(--r-sm); }
.svc-post__title { font-size: var(--t-h5); margin: var(--s-2) 0; }
.svc-post__summary { display: block; color: var(--on-surface); }
.svc-post .post__tags { margin-top: var(--s-4); }
.svc-post__more { margin-top: var(--s-4); }

/* The badge shrinks with the column but the text does not, so at 393 the name
   climbed out of the top of the shape and sat on the photo. Below 992 the shape
   takes more of the column and the label steps down with it — the two have to
   scale together or the label is only correct at the width it was drawn. */
@media (max-width: 991px) {
  /* 78% was sized against the FULL-LENGTH photo. Cropping to waist-up made the
     figure much shorter without changing the badge, so at 393 it covered her
     arm and most of the torso. Re-measured against the cropped box. */
  .dentist__badge { width: min(20.5625rem, 66%); }
  /* The name wraps to two lines at this size and that is fine — it is a name,
     and the shape has the vertical room. What is not fine is the role line
     running past the shape's right edge, where the blob curves inward, so the
     label sits further in. Scaling the type down proportionally instead was
     the wrong fix: at 393 it would land near 11px, under R6's 14px label
     floor. */
  .dentist__name { right: 13%; bottom: 20%; max-width: 78%; }
  .dentist__name b { font-size: 0.875rem; }
  .dentist__name span { font-size: 0.8125rem; }
}

/* Below 768 the hero panel is stacked and its bottom element is the
   announcement image, so a deep overlap would drop the cards onto artwork
   rather than onto empty lilac. Small overlap, full width. */
@media (max-width: 767px) {
  .hero-panel { padding-bottom: calc(var(--s-16) + 1.5rem); }
  .quickinfo__grid { width: 100%; margin-top: calc(-1 * (var(--band-sm) + 1rem)); }
}
/* An ARTICLE'S head has nothing riding up over it, so it needs no landing
   room. On a phone that empty lilac, the band's own padding and the body's
   top padding stacked to 180px between the picture and the first sentence
   (mobile pass, 29 Sep 2026). The panel's bottom now matches its top. */
/* THE HOME HERO SPLITS AT 992, NOT 768 (mobile pass, 29 Sep 2026). At
   768 to 991 the headline got a 220px column and ran to seven lines of
   display type beside a postage-stamp announcement. Stacked, it reads in
   three lines and the announcement is big enough to read. The About and
   article heads keep their split at 768: their text is body copy, and it
   fits. The contact cards under it stack for the same reason: the red
   notice had a 140px column and broke a word a line. */
/* 992 to 1199: split, but the words get the larger share, and the red notice
   gets a column wide enough to hold a phrase a line. At 1024 the headline ran
   to six lines and the notice broke nearly every word. */
@media (min-width: 992px) and (max-width: 1199px) {
  .hero-panel.home-hero { grid-template-columns: 1.3fr .85fr; }
  .quickinfo__grid { grid-template-columns: 2fr 1fr; }
}
@media (min-width: 768px) and (max-width: 991px) {
  .hero-panel.home-hero { grid-template-columns: 1fr; padding-bottom: calc(var(--s-16) + 1.5rem); }
  .home-hero .announce, .home-hero .home-portrait { max-width: 30rem; }
  .quickinfo__grid { grid-template-columns: 1fr; width: 100%; margin-top: calc(-1 * (var(--band-sm) + 1rem)); }
}
@media (max-width: 991px) {
  .hero-panel.post-hero { padding-bottom: var(--s-8); }
  .post-band .post-body { padding-top: 0; }
}

/* Dr Folayemi SITS ON the services card rather than floating above a gap. She
   hangs below her own section and paints over the card's top edge.
   `position: relative` on .dentist already beats the static .services-card in
   paint order; the z-index makes that explicit rather than incidental, because
   the day someone positions the card it would silently swap. */
@media (min-width: 992px) {
  /* She sits BEHIND the services card: the card's top edge crosses in front of
     her, which is what makes the two read as stacked layers rather than as one
     picture resting on another. She keeps `position: relative` for the badge,
     so the card has to be positioned too and later in the DOM — see
     .services-card's z-index. Without that she wins on paint order simply by
     being positioned, which is how this was wrong the first time.

     Her crop line no longer needs to clear the article photograph, because the
     card now covers it. */
  .welcome .dentist { align-self: end; }
  /* Slightly left of the column edge, per the comp, and lifted 3rem so more of
     her clears the services card (Leke, 14 Aug 2026).

     3rem, not the fifth of her height that was asked for. A fifth is 6.5rem,
     and measured, that lifts her 40px CLEAR of the card — the floating crop
     line rejected on 13 August, when the instruction was that she must sit on
     it. Her overlap is 64px, so 4rem is the arithmetic limit and 3rem leaves
     16px of contact. Growing the crop window instead was tried and is worse:
     it keeps the overlap but pushes the nametag badge, which is anchored to
     her bottom edge, down behind the card.

     If the lift matters more than the contact, this goes to 6.5rem and she
     floats; the two cannot both be had while the card juts 14rem. */
  .welcome .dentist { transform: translate(-2.5rem, -3rem); }

  /* EQUAL AIR ABOVE AND BELOW THE TEXT. The block was centred in its grid row,
     which is not the same as centred in the space a reader sees: the row's top
     is set by the band's padding under the info cards, while its bottom is
     eaten by the services card jutting up 14rem. Measured 209px above and 1px
     below at 1440.

     The correction is exactly (padding above + card overlap) / 2, and that
     comes to 6.5rem at EVERY width — 104px at 1440, 117 at 1920, 98 at 992 —
     because every quantity feeding it is rem-based. One number, not a nudge
     per breakpoint.

     `position: relative` rather than a margin: this is a visual correction, so
     it must not resize the row or move the portrait that shares it. */
  .welcome__text { position: relative; top: -6.5rem; }
}

/* THE TEXT COLUMN IS CAPPED NARROWER THAN ITS GRID TRACK. At 1440 the track is
   about 750px, which runs the body copy near 88 characters per line — well past
   R3's 50-75 desktop ideal, and it reads as a wall.

   IN ch, NOT rem, and that is the whole point. The prose floor is pinned at
   17px (R6: a floor in rem is not a floor), so the TYPE does not grow with the
   engine — but a rem cap does. 34rem held ~64 characters at 1440 and drifted to
   83 at 2560, past the ideal, on a measure that was supposed to be fixed. A ch
   cap tracks the font instead of the viewport, so the line length is the same
   at every width.

   52ch, VERIFIED BY COUNTING, not by the usual width/(0.5em) shortcut. That
   approximation assumes an average glyph of half an em and Prompt's is nearer
   0.64, so it read 62ch as "62 characters" when the line actually held 76 —
   still over R3's ideal on a cap chosen to fix exactly that. Counting the
   characters and dividing by the rendered line count gives 64 at 52ch, which is
   inside 50-75. */
.welcome__text { max-width: 52ch; }

/* ===================================== PHASE TWO: THE REMAINING PAGES ==
 * 21 Sep 2026. The home page set the language; About, Patient Education, the
 * article template, Appointment and Testimonial take it here. Decisions per
 * page are in notes/reflow-map.md. Everything below is additive and scoped to
 * the page's own classes, because `ground-mint`, `hero-panel` and `band` are
 * on 35 files and a bare rule on any of them is a site-wide change. */

/* ------------------------------------------------------ about: why us ---
 * Rides up over the hero panel as .quickinfo__grid does on the home page:
 * the section drops its own top padding, the panel pulls up through the
 * hero band's bottom padding and 3rem into the lilac, 94% wide so it reads as
 * a layer rather than a flush block. `position: relative` is what puts it
 * ABOVE the positioned .hero-panel; without it the panel paints underneath. */
.band.why-band { padding-top: 0; }
.why-band .why-panel {
  position: relative; z-index: 1;
  width: 94%; margin-inline: auto;
  margin-top: calc(-1 * (var(--band-sm) + 3rem));
}
@media (max-width: 767px) {
  .why-band .why-panel { width: 100%; margin-top: calc(-1 * (var(--band-sm) + 1rem)); padding: var(--s-8) var(--s-5); }
}

/* ---------------------------------------------------- about: gallery ---
 * Captions OVERLAY the picture, top right, on a navy pill. Live shows them
 * only on hover, which is a caption that does not exist on a phone; under the
 * image in grey they read as stray lines between tiles. Navy at full opacity
 * is its own scrim (R5), white on it is 11:1 at any patch of any photo. */
.gallery__item { position: relative; }
.gallery figcaption {
  position: absolute; top: var(--s-3); right: var(--s-3);
  max-width: calc(100% - 2 * var(--s-3));
  margin: 0; padding: .3rem .6rem;
  background: var(--primary); color: var(--on-dark);
  font-size: var(--t-label); line-height: 1.35; font-weight: 500;
  border-radius: var(--r-sm); text-align: right;
}

/* ---------------------------------------------------- mint CTA panel ---
 * A single inset panel with all four corners rounded; --top and --bottom are
 * the two halves of the home page's joined block. */
.mint-panel--solo { border-radius: var(--r-lg); }

/* ---------------------------------------------------- about: portrait ---
 * The cut-out PNG is cropped flat across the top of her head (ink starts at
 * row 232 of 2560 and the first row of ink is a straight line), and a
 * centred cover crop in a 27rem window showed her mouth and nothing above
 * it. Shifting the picture up 5rem puts that flat edge above the clip path,
 * so what cuts the top of her head is the blob's own curve, and her eyes are
 * in the panel. Measured at 1440; the smaller windows below get their own
 * number because the crop scale changes with the box. */
/* THE PORTRAIT SCALES WITH THE BLOB. It was pinned at a fixed 27rem, so
   widening the column grew the shape behind her and left her floating in the
   top half of it with a lake of empty lilac underneath. Tied to the box now,
   which is what it should have been: one number, not two that have to be
   kept in step by hand. */
/* A PERCENTAGE, NOT A NEGATIVE OFFSET. The rem values here pushed a
   background-removed portrait up so her head broke past the top of the blob.
   The picture is a photograph now (Leke, 29 Sep 2026), the blob is a window
   rather than a backdrop, and what has to stay in frame is a face at 40% / 25%
   of the file. A percentage holds that at every size; a rem does not. */
.hero-clipped { object-position: var(--pos, 40% 25%); }

/* A MODULAR GRID, NOT MASONRY. CSS columns balance by height and cannot split
 * a picture, so with 25 photos of every orientation the last column ran a
 * whole tall image past the other two and left a 600px hole at 1440. Equal
 * 4:3 tiles, cover-cropped from the top (faces and signs sit high), read as
 * one composed block at every width. 3-up, 2-up at 767, never 1-up: a
 * gallery tile carries no prose, so the R11 one-up rule does not apply and
 * the gallery floor does. */
.gallery { columns: auto; column-count: auto; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-5); }
.gallery__item { margin: 0; }
.gallery__item img { aspect-ratio: 4 / 3; object-fit: cover; object-position: center 25%; }
@media (max-width: 991px) { .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 479px) { .gallery { gap: var(--s-3); } }

/* ------------------------------------------- the missing 2-up step ---
 * `.grid-3` went straight from three columns to one at 767, so at 768 the
 * why-us copy ran in 150px columns and at 767 a 700px-wide page held one
 * card per row. Three-up grids of cards step 3 → 2 at 991 → 1 at 479, per
 * the reflow map, which had said so since the responsive pass. */
@media (max-width: 991px) {
  .why-grid, .team, .pe-list, .grid-3.related { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 767px) {
  .why-grid, .team, .pe-list, .grid-3.related { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 479px) {
  .why-grid, .team, .pe-list, .grid-3.related { grid-template-columns: 1fr; }
}

/* ----------------------------------------- patient education index ---
 * The featured article runs flush into the nav, square top, radius below,
 * like every other first panel on the site. */
.hero-band .pe-hero { border-radius: var(--r-lg); }
/* The page name sits at section-title size: it is an h1 for the outline and
   a section title to the eye, on the same row as the search field. */
.pe-title { font-size: var(--t-h2); margin: 0; max-width: 10em; }
.pe-search { margin: 0; max-width: none; }
.faq-head .pe-search.is-live { flex: 0 1 20rem; }
@media (max-width: 767px) { .faq-head .pe-search.is-live { flex-basis: 100%; } }
/* The category reads as the pill it is on the home page. */
.pe-card__cat.tag { display: inline-block; width: fit-content; margin: var(--s-3) 0 0; padding: .15rem .5rem; color: var(--on-surface); }

/* ---------------------------------------------------- article pages ---
 * The head is the site's lilac hero panel. The picture is a content image,
 * not a composition, so it takes a fixed 16:10 window and is cover-cropped;
 * the title column sits first in the source so it leads when the panel
 * stacks at 767. The panel's landing-room padding is for a layer that rides
 * up over it, and here that layer is the mint body panel. */
.post-hero { align-items: center; }
.post-hero__media { height: auto; aspect-ratio: 16 / 10; border-radius: var(--r-md); }
/* A DIAGRAM IS NOT A PHOTOGRAPH (Leke, 27 Sep 2026). Eleven of the thirty
   key visuals are labelled diagrams, x-rays or clinical illustrations, and
   cover-cropping them to a 16:10 window cut the labels off: the cracked
   tooth page shipped with "CTURED CUSP" and "SPLIT TOO" down its sides.
   Those are shown whole, on a white ground, because on their own article
   they are content rather than decoration. */
.post-hero__media--contain {
  object-fit: contain; background: var(--surface);
  padding: var(--s-4); border-radius: var(--r-lg);
}
/* And where the file is not publishable at all, a brand panel takes its
   place: the same vocabulary the index cards use. */
.post-hero__panel {
  order: 2; position: relative; display: block; width: 100%;
  aspect-ratio: 16 / 10; border-radius: var(--r-lg); overflow: hidden;
  background-color: var(--surface-sky);
  background-image: radial-gradient(#033b6d1a 1.5px, transparent 1.6px);
  background-size: 14px 14px;
}
.post-hero__panel::before {
  content: ''; position: absolute; right: -6%; top: -14%;
  height: 128%; aspect-ratio: 1 / 1; background: #033b6d1f;
  clip-path: url(#cd-element-minus-43-degrees-clip);
}
/* The group is a way back, not a label. It returns to the index with that
   group already picked, which is the other half of the finder. */
.post-hero__group { color: inherit; text-decoration: none;
  border-bottom: 2px solid var(--accent); }
.post-hero__group:hover { border-bottom-color: var(--primary); }
.post-hero__text h1 { font-size: var(--t-h2); max-width: 14em; }
/* The summary the clinic already writes for every article, in the one place
   it never appeared: the article. */
.post-hero__summary {
  display: block; margin-top: var(--s-4);
  font-size: var(--t-body); line-height: var(--lh-body);
  color: var(--on-surface-muted); max-width: 46ch;
}
.post-hero__text .post-head__date { margin-top: var(--s-4); }
.post-hero__text .subtitle { margin-bottom: var(--s-1); }
/* The picture is second in the grid at EVERY width: it is first in the source
   so a reader without CSS meets the key visual, but on the page the title
   leads, stacked or side by side. */
.post-hero__media { order: 2; }
@media (min-width: 768px) { .post-hero { grid-template-columns: 1.15fr 1fr; } }
/* The body rides up 3rem over the hero panel, 94% wide, exactly as the info
   cards do on the home page; the section drops its own top padding so the
   pull is measured from the hero band's bottom edge. */
.band.post-band { padding-top: 0; }
/* The body wrapper is a <div> and the first div in <article>, so the home
   page's `.has-panel:first-of-type { margin-top }` rule, written for the
   services section, landed a 6rem gap between the hero and the body here.
   Four classes to outweigh its four. */
.band.ground-mint.has-panel.post-band { margin-top: 0; }
/* THE ARTICLE IS READ ON WHITE (Leke, 27 Sep 2026). It used to sit in a
   mint slab riding up over the hero, and three things were wrong with it:
   long navy-on-mint reading is tiring where these articles run to thirty
   paragraphs, the slab was far wider than the measure it held so most of it
   was colour doing nothing, and the Webflow bodies end in empty paragraphs
   and empty lists, which left a screen of dead mint under the last line.
   The junk is stripped at the source now, and the page reads
   lilac head, white article, mint ask-us, white related. */
.post-body { padding-block: var(--s-12) var(--s-10); }
/* THE MEASURE STARTS WHERE THE TITLE STARTS. Centred in a 90rem container
   the column floats with white on both sides and nothing holding it; run
   down the container's own left edge it lines up under the h1 in the head
   above, and the page has a spine. */
.post-body .container--prose { margin-inline: auto; }
@media (min-width: 992px) { .post-body .container--prose { margin-inline: 0; } }
.post-body .container--prose > :first-child { margin-top: 0; }
/* The global reset strips list markers, which is right for navigation and
   wrong for an article: a list of things a reader should do needs to look
   like a list. */
.container--prose ul { list-style: disc; padding-left: 1.4em; margin: var(--s-4) 0; }
.container--prose ol { list-style: decimal; padding-left: 1.6em; margin: var(--s-4) 0; }
.container--prose li { margin-bottom: var(--s-2); }
.container--prose li:last-child { margin-bottom: 0; }
/* THE FIRST PARAGRAPH IS THE DEFINITION, on nearly every one of these
   articles, because that is how the clinic writes them. Not one of the
   thirty carries a single subheading, so this is the only structure the
   source gives us to work with, and inventing headings would be writing
   their copy for them. */
.post-lead { font-size: clamp(1.125rem, .35vw + 1rem, 1.25rem); line-height: 1.6; }
@media (max-width: 767px) { .post-body { padding-block: var(--s-10) var(--s-12); } }

/* ============================ THE ARTICLE IS TWO COLUMNS, AND THE SECOND
 * ONE IS A WAY TO BOOK (Leke, 29 Sep 2026).
 *
 * It used to be a contents rail. That asked the reader to navigate a page
 * they had not read yet, and asked the clinic to keep their headings tidy
 * enough to be navigation, which is work nobody wanted. An article about a
 * condition should offer the one thing the reader is there to decide.
 *
 * The text column got wider and the second one narrower, and the two sit at
 * the container's edges with the air between them: a measure at the left,
 * an action at the right, nothing floating in the middle. Every article has
 * it, so there is no rule to remember and nothing conditional in the
 * layout.
 */
/* A PICTURE ON TOP, A LINE, AND THE BUTTON (clinic review, 29 Sep 2026).
   The same order the sidebar cards in the survey use (Mobbin, 29 Sep 2026:
   Uxcel's tutorial page, GoFundMe's campaign page and Skillshare's class page
   all put the picture first, the words under it and a full width action at
   the foot). The picture runs to the card's edges so it reads as part of the
   card rather than a thumbnail dropped into it; the button fills the width
   so it is the obvious thing to press. */
.post-aside {
  margin-top: var(--s-12);
  background: var(--surface-sky); color: var(--on-surface);
  border-radius: var(--r-lg); overflow: hidden;
}
.post-aside__img {
  display: block; width: 100%; height: auto; aspect-ratio: 3 / 2;
  object-fit: cover; object-position: 50% 35%;
}
.post-aside__text { padding: var(--s-6) var(--s-6) var(--s-6); }
.post-aside__line {
  font-size: var(--t-h5); font-weight: 600; line-height: var(--lh-heading);
  margin: 0 0 var(--s-2);
}
.post-aside__note { font-size: var(--t-body-sm); margin: 0 0 var(--s-5); }
.post-aside .btn { width: 100%; justify-content: center; text-align: center; }
@media (min-width: 992px) {
  .post-body {
    display: grid;
    /* 20rem, up from 17, now the panel carries a picture (29 Sep 2026).
       The text column is 57.5rem, up from 50 (Leke, 29 Sep 2026: "too much
       space between the CTA and the text beside it... cut the current space
       by half"). The lane was 14.4rem at 1440 and is 6.9rem now, and the
       rem keeps that proportion at every width the split applies. */
    grid-template-columns: minmax(0, 57.5rem) 20rem;
    justify-content: space-between;
    gap: var(--s-10);
    align-items: start;
  }
  /* The column sets the measure now, so the inner cap comes off or the
     text would stay at its old width inside a wider column and the change
     would be invisible. */
  .post-body .container--prose { max-width: none; margin-inline: 0; }
  .post-aside { position: sticky; top: var(--s-8); margin-top: 0; }
}
/* Below that it is one column, and the card sits under the article rather
   than being hidden: an action at the end of a read is worth more on a
   phone than on a desktop, not less. */
@media (max-width: 991px) {
  .post-aside { max-width: 24rem; margin-inline: auto; }
}

/* A jump should not land the heading under the top of the window. */
.container--prose h2, .container--prose h3 { scroll-margin-top: var(--s-8); }

/* ----------------------------------------------------- appointment ---
 * The booking frame sits in a pale card that runs flush into the nav. */
.booking-panel {
  background: var(--surface-alt); border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: var(--s-8) var(--s-8) var(--s-8);
}
.booking-panel__title { font-size: var(--t-h2); margin-bottom: var(--s-6); }
.booking-panel .booking { background: var(--surface); border: 1px solid var(--border); }
@media (max-width: 767px) { .booking-panel { padding: var(--s-6) var(--s-4); } }

/* ----------------------------------------------------- testimonial ---
 * Mint hero panel flush into the nav; the form card rides up over its
 * bottom edge. The panel keeps landing room at the bottom for the card. */
/* POSITIONED, and that is load-bearing: the form's negative margin collapses
   through its section, so the whole white section moves up over this panel,
   and a static panel's background is painted before a later section's. With
   position: relative the mint paints above the white and the card above the
   mint, which is the layering the home page's hero already relies on. */
.mint-panel--hero { position: relative; border-radius: var(--r-lg); padding: var(--s-12) var(--s-10) calc(var(--s-16) + 3rem); }
.testi-hero h1 { max-width: 12em; }
.band.testi-band { padding-top: 0; }
.testi-form {
  position: relative; z-index: 1;
  width: min(44rem, 94%); margin-inline: auto;
  margin-top: calc(-1 * (var(--band-sm) + 3rem));
  padding: var(--s-10);
  box-shadow: var(--shadow-sm);
}
.testi-back { text-align: center; max-width: none; margin-top: var(--s-10); }
@media (max-width: 767px) {
  .mint-panel--hero { padding-bottom: calc(var(--s-16) + 1.5rem); }
  .testi-form { width: 100%; margin-top: calc(-1 * (var(--band-sm) + 1rem)); padding: var(--s-6) var(--s-5); }
}
@media (max-width: 479px) { .hero-marks { display: none; } }
/* The three brand marks sit in the panel's empty right half rather than as a
   row of grey blobs under the heading, which read as icons for nothing. */
@media (min-width: 992px) {
  .testi-hero .hero-marks { position: absolute; right: var(--s-16); top: 45%; transform: translateY(-50%); margin: 0; }
  .testi-hero .hero-marks img { height: 5.5rem; }
}

/* ------------------------------------------- about: hero on a phone ---
 * At 393 the hero ran 1047px, 123% of the viewport (L2 cap 88%), with the
 * portrait alone taking a screen. SHRINK, not DROP: the blob goes to 58% and
 * the window to 15rem, so the picture reads as a portrait beside the promise
 * rather than a second hero under it. */
/* 80%, not 58%. The 58% was set when this was a cut-out portrait and the
   hero ran 123% of a 393 screen. The picture is a photograph of a room now,
   and at 58% it drew 156px wide, which is a thumbnail of a person rather than
   a person. 80% draws 215px and costs 52px of panel height. The fold is still
   over, but it is over because the promise is six long sentences, not because
   of the picture: at 58% the panel was already 105% of the screen. */
/* 480 to 767 is ONE column too (the hero only splits at 768), so the same
   share applies there. Without it the picture filled a 700px column at 767 and
   the About hero ran 1300px. */
@media (max-width: 767px) {
  .about-portrait__blob { width: 80%; }
  .about-portrait__over { left: 10%; right: 10%; top: -6%; }
}

/* ------------------------------------------------ top bar on a phone ---
 * notes/reflow-map.md has said since the responsive pass that the chip's
 * label hides at 479 and the glyph stays; no rule ever did it, and at 393
 * "Chat with us on WhatsApp" wrapped to two lines inside the chip. The
 * label leaves the screen, not the link: it stays the accessible name. */
@media (max-width: 479px) {
  .chatbar__chip span {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
  .chatbar__chip { padding-inline: var(--s-3); }
}

/* Two chevrons under every team card: the summary carries the design's
   chevron as an <img> AND the ::after from the first cut of this file drew
   a second one. The image is the chevron; it turns when the bio is open. */



/* On a phone the tiles are ~170px wide and a two-line caption overlay
   covered the picture, so captions sit under the tile there, in full.
   Nothing is clamped: a caption cut with an ellipsis hides copy. */
@media (max-width: 479px) {
  .gallery figcaption {
    position: static; max-width: none; margin-top: var(--s-2); padding: 0;
    background: transparent; color: var(--on-surface-muted);
    font-weight: 400; text-align: left; border-radius: 0;
  }
}

/* ============================ ABOUT, ROUND TWO (22 Sep 2026) ============
 * Three changes, all Leke's: the nav needed a real current-page state, the
 * About header needed air, and the team photos should be curved rectangles
 * with the brand shape behind them rather than cut into that shape. */

/* ------------------------------------------- nav: you are here ---------
 * The bug was that hover and current were the SAME declaration, so hovering
 * any link made it look exactly like the page you were on: the state carried
 * no information at all.
 *
 * Hover stays what the guidelines say it is, 80% navy to full navy, no
 * underline, no movement. The CURRENT page gets a mark the guidelines never
 * specified: a 3px mint rule under the label. Mint under navy is the site's
 * own vocabulary for "this is the thing" already, on .link-arrow and on the
 * team More control, so this is the existing language applied to the nav.
 *
 * The rule is an absolutely positioned pseudo-element and the label's weight
 * never changes, so no page's nav row is a different width from another's.
 * It grows from the left on the site's 200ms link timing rather than
 * appearing, which reads as the mark arriving with the page. */
.nav__link { position: relative; }
.nav__link::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -2px;
  height: 3px; border-radius: 2px; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 200ms ease-in-out;
}
.nav__link[aria-current="page"]::after { transform: scaleX(1); }
/* The Appointment control is a button, not a link, so it cannot take the
   rule. On its own page it wears a mint ring instead: the same inversion,
   read as an edge rather than an underline. box-shadow, not a border, so
   the button's box stays identical to the one on every other page. */
.nav__menu .btn[aria-current="page"] { box-shadow: 0 0 0 2px var(--accent); }
@media (prefers-reduced-motion: reduce) { .nav__link::after { transition: none; } }

/* --------------------------------------- about: air round the header ---
 * The promise is the page's opening statement and it was sitting 3rem below
 * the nav with its buttons almost touching the why-us panel that rides up
 * over the bottom edge. Top goes to 5rem. The bottom has to clear the
 * overlap before it is worth anything: the panel above pulls up 7.5rem, so
 * 7.5 of the old 7rem was hidden under it. 11.5rem leaves 4rem of real air
 * under the buttons. */
/* 7rem, not 5: the content comes down 2rem so the panel carries more air
   above the eyebrow than below the buttons (Leke, 22 Sep 2026). */
.about-hero { padding-top: 7rem; padding-bottom: 11.5rem; }
/* The phone keeps the extra air at the BOTTOM, where the overlap eats it,
   and gives back at the top, where the fold is already under pressure: the
   promise is six lines plus two buttons plus a portrait and the section
   runs past one screen whatever happens (logged in deviation-log.md). */
@media (max-width: 767px) {
  .about-hero { padding-top: var(--s-10); padding-bottom: 7rem; }
}
/* The home hero when no announcement is live carries the same butterfly as
   the About header (render.mjs), so it gets the About header's air above and
   below, or the shape is cut at the top and crowds the info cards
   (Leke, 5 Nov 2026). Only then: a poster is square and sits fine as it is. */
.home-portrait[hidden] { display: none; }
.hero-panel.home-hero:has(.home-portrait:not([hidden])) { padding-top: 7rem; padding-bottom: 11.5rem; }
@media (max-width: 767px) {
  .hero-panel.home-hero:has(.home-portrait:not([hidden])) { padding-top: var(--s-10); padding-bottom: 7rem; }
  /* ON A PHONE THE PICTURE COMES FIRST, then the words (Leke, 5 Nov 2026).
     The rotated blob overhangs its box at the top, so it keeps its 4%
     top margin; the grid gap already spaces it from the words below. */
  .hero-panel.home-hero .home-portrait { order: -1; margin-top: 4%; margin-bottom: 0; }
}

/* ------------------------------------- about: the team photographs -----
 * BACK IN THE CUTOUT (clinic review, 29 Sep 2026). The clinic prefers the
 * photograph cut into the brand blob, which is what live does, to the curved
 * rectangle with the shape behind it that replaced it on 22 Sep: the cutout
 * is more distinctive. This reverses that change on their word.
 *
 * What makes it hold for ANY photograph the clinic uploads later:
 *  - THE FRAME IS SQUARE. The clip is drawn in the frame's own box
 *    (objectBoundingBox), so a square frame gives the same silhouette on
 *    every card whatever the file's ratio. The photograph covers it.
 *  - THE SHAPE IS LIVE'S OWN, the clipPath the page already carries, drawn
 *    upright exactly as live draws it. (Live also carries a turned blank
 *    placeholder behind each photograph; that went in round one.)
 *  - THE FACE SITS IN THE MIDDLE, a little above centre, which is where a
 *    head-and-shoulders portrait puts it. The photo field in the CMS says
 *    so: "Square works best, with the face in the middle."
 *
 * The photograph is 84% of its cell, live's proportion (ten twelfths). */
.member { --photo-w: 84%; }
.member__frame {
  position: relative; display: block; overflow: hidden;
  width: var(--photo-w); margin-inline: auto; aspect-ratio: 1 / 1;
  -webkit-clip-path: url(#cd-element-minus-43-degrees-clip);
  clip-path: url(#cd-element-minus-43-degrees-clip);
}
.member__photo {
  display: block; width: 100%; height: 100%; aspect-ratio: auto;
  object-fit: cover; object-position: 50% 30%;
  border-radius: 0; margin: 0;
}
/* The rows need room between them that the name alone does not give. */
.team { row-gap: var(--s-16); }
.member__name { margin-top: var(--s-6); }
@media (max-width: 767px) { .member { --photo-w: 80%; } }
/* TWO UP ON A PHONE (mobile pass, 29 Sep 2026). One face per row ran the team
   to two and a half screens of the same shape. A portrait with a name and a
   role is a short label, which is what two-up is for (R11), and a face reads
   at 150px. The name comes down a step so the longest one takes two lines,
   not three. */
@media (max-width: 479px) {
  .team { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--s-4); row-gap: var(--s-10); }
  .member { --photo-w: 96%; }
  .member__name { font-size: var(--t-body); line-height: 1.25; margin-top: var(--s-4); }
  .member__role { font-size: 15px; line-height: 1.35; }
}

/* ------------------------------------------ about: the team card -------
 * The card is a photograph, a name and a role. The name is the control: it
 * opens the person in a modal (Leke, 23 Sep 2026). Nothing happens on
 * hover but the link's own underline, because a card that opened itself
 * under a passing pointer was the thing that got rejected.
 *
 * The bio sits in the card, hidden, and the modal is filled from it. With
 * no scripting the name is still a link to the card's own id, and the bio
 * appears under the name when the card is the page's :target: the content
 * is reachable without a line of JavaScript, and there is one copy of it.
 */
/* THE WHOLE CARD IS THE CONTROL (Leke, 23 Sep 2026), and it is still ONE
   control: the name's link stretches its hit area over the card with an
   empty ::after, so a click anywhere opens that person. No second button,
   no second tab stop, no JavaScript to keep in step, and the accessible
   name stays the person's name rather than "read more".

   Hover answers on the card, not on the words: the name underlines in mint
   and the photograph takes a mint ring. Both are colour, nothing moves. */
.member { position: relative; }
.member__open { color: inherit; text-decoration: none; }
.member__open::after { content: ''; position: absolute; inset: 0; z-index: 4; }
/* The picture RISES 1rem (Leke, 23 Sep 2026), on the curve that accelerates
   and then slows: ease-in-out, which is the site's own curve on every other
   control. The shape behind stays where it is, so the photograph lifts off
   it rather than the whole card sliding. The ring this replaces read as a
   selected state rather than as an invitation. */
/* The whole cutout lifts now, frame and photograph together: moving the
   photograph inside the clip would only slide it about in its window. */
/* The way down: easeOutSine, so it drifts to rest instead of dropping. */
.member__frame { transition: translate 700ms cubic-bezier(.61, 1, .88, 1); }
.member:hover .member__frame,
.member:focus-within .member__frame { translate: 0 -.5rem; }
/* THE WINGS FLUTTER (Leke, 3 Nov 2026: "the hover effect for these cards
   could be the butterfly wings moving a bit, coming in a bit and out, almost
   like it's fluttering, and just raising a bit"). The cutout's two lobes are
   the wings. On the way in they close toward the body and open again twice,
   the second beat smaller, while the whole cutout rises: 900ms, once per
   hover, then it rests raised. Nothing moves until the pointer or the
   keyboard arrives on the card.

   ALONG THE SHAPE'S OWN AXIS, NOT THE SCREEN'S (second cut, same day: "it's
   flapping out along the wrong axis... the butterfly shape is diagonal").
   The body runs between the two notches in the outline, from about (32%,
   36%) to (78%, 78%) of the frame, a line 42 degrees below the horizontal.
   The wings fold perpendicular to that, so the squeeze is turned to 132
   degrees: rotate, squeeze, rotate back, about the middle of the body at
   (55%, 57%), and both lobes close on it by the same amount.

   THE SHAPE NARROWS, THE PERSON DOES NOT. The photograph gets the exact
   inverse about the same point (1 / 0.88, 1 / 0.93), so only the silhouette
   moves and the face never squeezes. */
/* FLOATING, NOT BOUNCING (third cut, 3 Nov 2026: "the up and down movements
   don't feel graceful... more like floating"). The lift used to be a 260ms
   step that finished before the first beat. Now the rise is part of the
   flight: each time the wings close the body lifts a little further, each
   time they open it eases back a touch, and it settles at the resting height
   (half a rem up). Every segment runs on a sine curve, which has no corners,
   and the whole flight takes 1.2s. Leaving the card, it floats back down over
   0.7s on the same family of curve. */
@keyframes member-wings {
  0%        { translate: 0 0;       transform: rotate(132deg) scaleX(1)   rotate(-132deg); }
  20%       { translate: 0 -.65rem; transform: rotate(132deg) scaleX(.88) rotate(-132deg); }
  42%       { translate: 0 -.42rem; transform: rotate(132deg) scaleX(1)   rotate(-132deg); }
  62%       { translate: 0 -.7rem;  transform: rotate(132deg) scaleX(.93) rotate(-132deg); }
  82%, 100% { translate: 0 -.5rem;  transform: rotate(132deg) scaleX(1)   rotate(-132deg); }
}
@keyframes member-wings-photo {
  0%        { transform: rotate(132deg) scaleX(1)      rotate(-132deg); }
  20%       { transform: rotate(132deg) scaleX(1.1364) rotate(-132deg); }
  42%       { transform: rotate(132deg) scaleX(1)      rotate(-132deg); }
  62%       { transform: rotate(132deg) scaleX(1.0753) rotate(-132deg); }
  82%, 100% { transform: rotate(132deg) scaleX(1)      rotate(-132deg); }
}
.member__frame, .member__photo { transform-origin: 55% 57%; }
/* easeInOutSine, the gentlest curve that still starts and stops at rest. */
.member:hover .member__frame,
.member:focus-within .member__frame { animation: member-wings 1200ms cubic-bezier(.37, 0, .63, 1) 1; }
.member:hover .member__photo,
.member:focus-within .member__photo { animation: member-wings-photo 1200ms cubic-bezier(.37, 0, .63, 1) 1; }
@media (prefers-reduced-motion: reduce) {
  .member__frame { transition: none; }
  .member:hover .member__frame, .member:focus-within .member__frame { translate: none; animation: none; }
  .member:hover .member__photo, .member:focus-within .member__photo { animation: none; }
}
.member__bio { display: none; }
.member:target .member__bio {
  display: block; margin-top: var(--s-4);
  text-align: left; font-size: var(--t-body-sm); line-height: var(--lh-body);
  background: var(--surface-sky); border-radius: var(--r-md); padding: var(--s-5);
}

/* ============================================ the team member modal =====
 * One third picture, two thirds words, which is the proportion the home
 * page's services card already uses. White panel, navy scrim, the brand's
 * own radius: the system is flat, so the panel is held by the scrim rather
 * than by a stack of shadows.
 */
.bio-modal {
  width: 100%; max-width: none; height: 100%; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden;
}
.bio-modal__panel {
  position: absolute; inset: 0; margin: auto;
  width: min(92vw, 60rem); height: fit-content; max-height: 86svh;
  display: grid; grid-template-columns: 1fr 2fr;
  background: var(--surface); color: var(--on-surface);
  border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--shadow-md);
}
.bio-modal__photo {
  width: 100%; height: 100%; min-height: 100%;
  object-fit: cover; object-position: center 20%;
}
.bio-modal__text {
  padding: var(--s-12) var(--s-10);
  overflow-y: auto; overscroll-behavior: contain;
}
.bio-modal__role { margin-bottom: var(--s-1); }
.bio-modal__name { font-size: var(--t-h3); max-width: 14em; margin-bottom: var(--s-5); }
.bio-modal__body { font-size: var(--t-body); line-height: var(--lh-body); max-width: 60ch; margin: 0; }
/* Navy disc, mint cross: the same pair the card's own control used, and the
   same 44px target as every other close on the site. */
.bio-modal__close {
  position: absolute; top: var(--s-4); right: var(--s-4);
  width: 44px; height: 44px; padding: 0; border: 0; cursor: pointer;
  background: radial-gradient(circle, var(--primary) 0 15px, transparent 15px);
  display: grid; place-items: center;
}
.bio-modal__close::before, .bio-modal__close::after {
  content: ''; grid-area: 1 / 1; width: 13px; height: 2px; border-radius: 1px;
  background: var(--on-primary);
}
.bio-modal__close::before { rotate: 45deg; }
.bio-modal__close::after  { rotate: -45deg; }
@media (max-width: 767px) {
  /* One column on a phone, picture on top and capped, so the words are what
     the screen is mostly holding (R15: an overlay re-composes). */
  .bio-modal__panel { grid-template-columns: 1fr; max-height: 90svh; }
  .bio-modal__photo { height: 15rem; min-height: 0; object-position: center 22%; }
  .bio-modal__text { padding: var(--s-8) var(--s-6) var(--s-10); }
}

/* A centred section head, for the sections whose title belongs in the middle
   of the row rather than at its left edge. The h2 carries the 10em cap, so
   it needs the auto margins to sit in the centre of the section. */
.section-head--center { text-align: center; }
.section-head--center h2 { margin-inline: auto; }
.section-head--center .subtitle { max-width: none; }
/* And room under it, so the row of faces starts clear of the title. */
.section-head--center { margin-bottom: var(--s-16); }

/* --------------------------------------------- about: the lightbox ------
 * A native <dialog>: Escape, the top layer and focus containment are the
 * browser's job, not a library's. The dialog fills the screen and the
 * picture sits in the middle of it, so the area around the picture IS the
 * backdrop and a click there closes it.
 *
 * 85svh, not vh: on a phone the browser's own chrome eats the difference and
 * a picture sized in vh ends up under it (R15). */
.lightbox {
  width: 100%; max-width: none; height: 100%; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent;
  overflow: hidden;
}
.lightbox__frame {
  position: absolute; inset: 0;
  display: grid; align-content: center; justify-items: center; gap: var(--s-4);
  padding: var(--s-16) var(--s-6);
  pointer-events: none;   /* the click passes through to the dialog, which closes */
}
.lightbox__img {
  max-width: min(92vw, 1100px); max-height: 78svh; width: auto; height: auto;
  border-radius: var(--r-md); pointer-events: auto;
  background: var(--surface);
}
.lightbox__caption {
  color: var(--on-dark); font-size: var(--t-body-sm); text-align: center;
  max-width: 44ch; margin: 0; pointer-events: auto;
}
.lightbox__caption[hidden] { display: none; }

/* Three controls, all drawn rather than typed: a cross and two chevrons, so
   they carry no copy and no icon file. 48px targets, clear of the picture. */
.lightbox__btn {
  position: absolute; width: 48px; height: 48px; padding: 0;
  border: 0; border-radius: 50%; cursor: pointer;
  background: #ffffff1f; color: var(--on-dark);
  display: grid; place-items: center;
  transition: background-color 200ms ease-in-out;
}
.lightbox__btn:hover { background: #ffffff33; }
.lightbox__close { top: var(--s-4); right: var(--s-4); }
.lightbox__close::before, .lightbox__close::after {
  content: ''; grid-area: 1 / 1; width: 18px; height: 2px; border-radius: 1px; background: currentColor;
}
.lightbox__close::before { transform: rotate(45deg); }
.lightbox__close::after  { transform: rotate(-45deg); }
.lightbox__prev { left: var(--s-4); top: 50%; margin-top: -24px; }
.lightbox__next { right: var(--s-4); top: 50%; margin-top: -24px; }
.lightbox__prev::before, .lightbox__next::before {
  content: ''; width: 14px; height: 14px;
  border-top: 2px solid currentColor; border-right: 2px solid currentColor;
}
.lightbox__prev::before { transform: rotate(-135deg); margin-left: 5px; }
.lightbox__next::before { transform: rotate(45deg); margin-right: 5px; }
@media (max-width: 479px) {
  /* On a phone the arrows would sit on the picture, so they go to the bottom
     where a thumb is, and the swipe is the main way through anyway. */
  .lightbox__prev, .lightbox__next { top: auto; bottom: var(--s-6); margin-top: 0; }
  .lightbox__frame { padding: var(--s-16) var(--s-4) 6rem; }
  .lightbox__img { max-height: 62svh; }
}
/* The picture itself is a link, so it takes the pointer and a focus ring. */
.gallery__open { display: block; border-radius: var(--r-lg); }
.gallery__open:focus-visible { outline: 3px solid var(--primary); outline-offset: 3px; }

/* The gallery's own page opens on the pictures: the head sits in a shallow
   band that runs flush into the nav, so the first row is in view rather than
   a screen of white above it. */
.band.gallery-page { padding-top: var(--s-12); }

/* A SECONDARY BUTTON READS ITS GROUND. The mint fill is invisible on a mint
   panel, which is exactly the failure css-architecture describes: one hover
   rule that never asks what the control is sitting on. On mint it becomes an
   outline in navy. The edge is an inset shadow rather than a border, so the
   box is the same one the primary button beside it has and the row lines up
   (same doc, "every control in a row shares one box"). */
.mint-panel .btn-secondary,
.ground-mint .btn-secondary {
  background: transparent; color: var(--primary);
  box-shadow: inset 0 0 0 2px var(--primary);
}
.mint-panel .btn-secondary:hover,
.ground-mint .btn-secondary:hover {
  background: var(--primary); color: var(--on-primary);
}

/* ANCHOR LANDINGS. #services is on the mint band, but the services card
   juts 14rem UP out of it, so a jump to the band's own top leaves the card
   above the window and the reader lands on the middle of it. The scroll
   margin gives the jump the card's overhang back. #contact is a footer on
   every page but the home one, where it is the contact card itself. */
#services { scroll-margin-top: 15rem; }
#contact { scroll-margin-top: var(--s-8); }

/* ================================= PATIENT EDUCATION, REBUILT ===========
 * WHAT THE PAGE IS FOR (Leke, 27 Sep 2026). Somebody arrives with a
 * question or a fear: my gums bleed, does my child need braces, is
 * whitening at a spa safe. The old page answered that with a date-ordered
 * list, a small search box and Load more, so a patient with a cracked
 * tooth had to read thirty titles to find "Cracked Tooth".
 *
 * So the page is organised by WHAT THE READER IS DEALING WITH. Seven
 * groups, a count against each, and picking one cuts the list to it. The
 * chips are the map: they also tell the reader how much is here, which a
 * list of six cards and a Load more button never did.
 */

/* The page names itself before it shows anything, and the search sits with
   the name rather than floating over the grid. */
.pe-intro { display: grid; gap: var(--s-6); padding-top: var(--s-10); }
@media (max-width: 767px) { .pe-intro { padding-top: var(--s-8); } }
.pe-intro__lede { max-width: 42ch; margin: 0; color: var(--on-surface-muted); font-size: var(--t-body); }
@media (min-width: 992px) {
  .pe-intro {
    grid-template-columns: 1fr auto; align-items: end;
    column-gap: var(--s-12);
  }
  .pe-intro__search { justify-self: end; width: min(24rem, 100%); }
}

/* THE FINDER. One row, always, that scrolls and clips on a narrow screen
   (R14): a chip row folded on to two lines stops reading as a control and
   starts reading as a paragraph of links. The fade on the right edge is
   what says there is more to the side. */
.pe-filter { display: none; margin-top: var(--s-8); }
.pe-filter.is-live { display: block; }
.pe-filter__row {
  display: flex; gap: var(--s-2); overflow-x: auto; scrollbar-width: none;
  padding-bottom: var(--s-2); scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
}
/* Eight chips do not fit one line on a desktop, and a row that runs off the
   right edge of a wide screen reads as a mistake rather than as something
   to scroll. Above the phone widths it wraps instead; R14's one-line rule is
   about a thumb, not about a 1440 monitor. */
@media (min-width: 768px) {
  .pe-filter__row { flex-wrap: wrap; overflow: visible; }
}
/* And where it does scroll, the last chip fades out rather than stopping at
   a hard edge, so the row says there is more to the side. */
@media (max-width: 767px) {
  .pe-filter__row {
    mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 2.5rem), transparent 100%);
  }
}
.pe-filter__row::-webkit-scrollbar { display: none; }
.pe-chip {
  flex: 0 0 auto; scroll-snap-align: start;
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 44px; padding: var(--s-2) var(--s-3);
  border: 2px solid var(--border); border-radius: var(--r-pill);
  background: var(--surface); color: var(--on-surface);
  font: inherit; font-size: var(--t-body-sm); font-weight: 500;
  cursor: pointer; white-space: nowrap;
  transition: background-color 200ms ease-in-out, border-color 200ms ease-in-out, color 200ms ease-in-out;
}
.pe-chip:hover { border-color: var(--primary); }
.pe-chip__n { font-size: var(--t-label); color: var(--on-surface-muted); }
.pe-chip[aria-pressed="true"] {
  background: var(--primary); border-color: var(--primary); color: var(--on-primary);
}
.pe-chip[aria-pressed="true"] .pe-chip__n { color: var(--accent); }

/* THE CARD. One control, not two: the whole card is the link, so the More
   line under every card went (thirty redundant tab stops on one page). */
.pe-list { margin-top: var(--s-10); }
.pe-card { display: flex; flex-direction: column; }
.pe-card__link {
  display: flex; flex-direction: column; gap: var(--s-2);
  height: 100%; text-decoration: none; color: inherit;
}
.pe-card__title {
  font-size: var(--t-h5); font-weight: 600; margin: var(--s-2) 0 0;
  align-self: flex-start;
}
.pe-card__summary { display: block; color: var(--on-surface-muted); font-size: var(--t-body-sm); }

/* WHERE THE PICTURE DOES NOT EARN ITS PLACE, a brand panel takes it.
   Seventeen of the thirty card images fail one of three tests: words baked
   into the file, somebody else's mark on it, or a clinical photograph that
   works against a clinic. The file is not thrown away; it still runs on the
   article page where it is informative. Here the card gets a colour field
   with the brand element in it, which is the same vocabulary the team cards
   use, so a grid that is half photographs and half panels still reads as
   one set. */
.pe-card__panel {
  position: relative; display: block; width: 100%; height: 10rem;
  border-radius: var(--r-md); overflow: hidden;
  background-color: var(--panel-fill, var(--surface-sky));
  background-image: var(--panel-texture);
  background-size: var(--panel-texture-size, auto);
}
/* BIG AND CROPPED, not small and centred. At card size a shape sitting
   politely inside its box reads as a blob; the same shape run past two
   edges reads as a brand element the card is a window on to, which is
   exactly how the team cards use it. */
.pe-card__panel::before {
  content: ''; position: absolute; right: 4%; top: -28%;
  height: 156%; aspect-ratio: 1 / 1;
  background: #033b6d1f;
  clip-path: url(#cd-element-minus-43-degrees-clip);
}
/* The fills cycle down the grid so no two neighbours match, and the mirror
   alternates so the element does not march. */
/* FIVE STEPS, NOT THREE. A three step cycle in a three column grid paints
   the columns one colour each, which reads as a mistake. Five is coprime
   with both three and two, so the fills travel diagonally at every column
   count. The team cards cycle on five for the same reason. */
.pe-card:nth-child(5n+1) .pe-card__panel { --panel-fill: var(--accent); }
.pe-card:nth-child(5n+2) .pe-card__panel { --panel-fill: var(--surface-lilac); }
.pe-card:nth-child(5n+3) .pe-card__panel { --panel-fill: var(--surface-sky); }
.pe-card:nth-child(5n+4) .pe-card__panel { --panel-fill: var(--surface-lilac); }
.pe-card:nth-child(5n)   .pe-card__panel { --panel-fill: var(--surface-sky); }
.pe-card:nth-child(even) .pe-card__panel::before { right: auto; left: 4%; scale: -1 1; }
.pe-card:nth-child(odd) .pe-card__panel {
  --panel-texture: radial-gradient(#033b6d1a 1.5px, transparent 1.6px);
  --panel-texture-size: 12px 12px;
}
.pe-card:nth-child(even) .pe-card__panel {
  --panel-texture: repeating-linear-gradient(45deg, #033b6d14 0 1.5px, transparent 1.5px 9px);
}

/* The closing band. This is the highest intent page on the site after the
   home page and it used to end on a Load more button. */
/* THE CLOSING BAND, ONE COMPONENT FOR THE WHOLE SITE (Leke, 28 and 29 Sep
   2026). On mint it ran straight into the mint footer and the two read as
   one block, so the band lost its edges and the footer gained a heading. On
   white it is its own thing, and a short turquoise rule above it does the
   separating that a whole panel of colour was doing badly.

   Renamed from the Patient Education one, because it is not that any more:
   About and Gallery ended in their own mint panel, built from a different
   set of classes, saying the same thing in a different voice. Two names for
   one component is how the last two rounds of bugs started.

   The rule is drawn, not marked up: it is a divider, not content, so it has
   no business in the HTML and nothing for a CMS to fill. */
/* Leke, 29 Sep 2026: "the top and bottom padding should be doubled. It just
   seems to not have enough breathing room." Doubled from the band token rather
   than typed as a figure, so it still steps down with the band at 991 and 767.

   The second selector is not a duplicate. `.band.ground-white +
   .band.ground-white` collapses the top padding between two bands of the same
   ground, which is right for two bands of content and wrong for a closing
   band, whose job is to sit apart from what it follows. Three of the four page
   kinds that carry one follow a white band, so without this line they would
   have kept `padding-top: 0` and only the About page would have changed. */
.band.closing,
.band.ground-white + .band.ground-white.closing { padding-block: calc(var(--band) * 2); }
.closing { text-align: center; }
.closing .container::before {
  content: ''; display: block;
  width: 3rem; height: 4px; border-radius: 2px;
  background: var(--accent);
  margin: 0 auto var(--s-10);
}
.closing h2 { margin-inline: auto; max-width: 20ch; }
.closing .subtitle { text-align: center; max-width: none; margin-inline: auto; }
.closing__body { max-width: 46ch; margin: var(--s-4) auto var(--s-8); }
/* The Smile Club signup (29 Sep 2026): the field and the button share one
   row, the field doing the growing, and stack on a phone where a 44px button
   beside a field would squeeze the address to a few characters. */
.smile-club__form {
  display: flex; gap: var(--s-3); max-width: 30rem; margin: 0 auto;
}
.smile-club__input {
  flex: 1 1 auto; min-width: 0; font: inherit; font-size: var(--t-body); color: var(--on-surface);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-sm); padding: var(--s-3); min-height: 44px;
}
.smile-club__input::placeholder { color: var(--placeholder); }
.smile-club__input:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.smile-club__status { max-width: none; margin: var(--s-4) auto 0; font-size: var(--t-body-sm); }
.smile-club__status:empty { display: none; }
@media (max-width: 479px) {
  .smile-club__form { flex-direction: column; }
  .smile-club__form .btn { justify-content: center; }
}
.closing__row {
  max-width: none; margin-top: var(--s-8);
  display: flex; gap: var(--s-4); justify-content: center; flex-wrap: wrap;
}

/* ======================================== BOOKING, IN A POP-UP ==========
 * THE EMBED COMES TO THE READER (Leke, 27 Sep 2026). Booking is one
 * third-party frame and nothing else, so sending somebody to a page whose
 * whole content is that frame is a detour with a page load in it. Every
 * Request Appointment on the site opens it here, on the page the reader is
 * already on, which is also why it has to exist on every page.
 *
 * /appointment stays a real page behind the same link. That is not a
 * leftover: it is what answers a reader with no JavaScript, and anybody who
 * arrives on the URL from a search result, an old email or a bookmark.
 *
 * THE PANEL IS A SHEET, NOT A CARD. What is inside it is a booking app that
 * needs height, so the panel takes the screen it can get and the embed
 * scrolls inside its own frame.
 */
.booking-modal {
  width: 100%; max-width: none; height: 100%; max-height: none;
  margin: 0; padding: 0; border: 0; background: transparent; overflow: hidden;
}
.booking-modal__panel {
  position: absolute; inset: 0; margin: auto;
  width: min(94vw, 46rem); height: min(88svh, 56rem);
  display: grid; grid-template-rows: 1fr;
  background: var(--surface); border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--shadow-md);
}
/* The close sits IN the panel, over the top right of the frame (Leke, 27
   Sep 2026), not in a strip of its own above it: a bar with one button in
   it is a row of chrome buying nothing, and it pushed the booking form
   down the screen. The embed carries its own padding up there, so the disc
   sits in space the app is not using. */
.booking-modal__close {
  position: absolute; top: var(--s-3); right: var(--s-3); z-index: 2;
  width: 44px; height: 44px; padding: 0; border: 0; cursor: pointer;
  background: radial-gradient(circle, var(--primary) 0 15px, transparent 15px);
  display: grid; place-items: center;
}
.booking-modal__close::before, .booking-modal__close::after {
  content: ''; grid-area: 1 / 1; width: 13px; height: 2px; border-radius: 1px;
  background: var(--on-primary);
}
.booking-modal__close::before { rotate: 45deg; }
.booking-modal__close::after  { rotate: -45deg; }
/* The stage is pale while it waits and white once the frame is there: an
   embed that paints no background of its own would otherwise sit on the
   waiting colour for the whole booking. */
/* NO SCROLLBAR DOWN THE SIDE OF THE PANEL (Leke, 28 Sep 2026). The bar
   belongs to DoctoSmart's own document, so it cannot be styled from here:
   the frame is another origin and nothing we write reaches inside it. What
   we can do is make the frame wider than the box it sits in by exactly the
   width of a scrollbar and clip the difference, so the bar is drawn outside
   the panel and the embed still scrolls by wheel, trackpad and touch.
   The width is MEASURED, not assumed. On a machine with overlay scrollbars,
   a Mac by default, it measures zero, nothing is added and nothing shifts;
   guessing 17px there would have pushed the form eight pixels off centre
   for no reason. The script below does the measuring. */
.booking-modal__stage {
  position: relative; display: grid; overflow: hidden; background: var(--surface);
}
.booking-modal__stage[data-loading] { background: var(--surface-sky); }
.booking-modal__frame {
  grid-area: 1 / 1; width: calc(100% + var(--sb, 0px)); height: 100%; border: 0;
  transition: opacity 200ms ease-in-out;
}
/* WHILE IT IS COMING. The frame is held back and a ring sits in its place,
   so the reader is not looking at a white panel wondering whether the click
   worked. It clears on the frame's own load event.
   The ring is the one piece of motion on the site that repeats, and it is
   the exception the rule allows: it is not decoration, it is the answer to
   "did that work". It stops the moment the frame is there. */
.booking-modal__stage[data-loading] .booking-modal__frame { opacity: 0; }
.booking-modal__loader {
  grid-area: 1 / 1; place-self: center;
  width: 48px; height: 48px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--primary) 18%, transparent);
  border-top-color: var(--primary);
  animation: booking-wait 900ms linear infinite;
}
.booking-modal__stage:not([data-loading]) .booking-modal__loader { display: none; }
@keyframes booking-wait { to { rotate: 360deg; } }
@media (max-width: 767px) {
  /* A sheet on a phone (R15): full width, pinned to the bottom edge, with a
     strip of scrim left above it so the page is still visibly behind. */
  .booking-modal__panel {
    width: 100%; height: 96svh; max-height: none; margin-block: auto 0;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  /* The ring stays, as a mark that something is happening, and stops
     turning. */
  .booking-modal__loader { animation: none; }
  .booking-modal__frame { transition: none; }
}

/* ================== WHAT A CONTROL SAYS WHEN YOU TOUCH IT ==============
 * Leke, 29 Sep 2026: go through the site and make sure everything you can
 * click says so.
 *
 * COUNTED FIRST. 42 controls answered the pointer and 24 said nothing,
 * measured by putting the pointer on every control and diffing the computed
 * styles of it and everything inside it (tools/dev/hover-audit.mjs). The
 * silent ones were the cards that look like content: the featured post in
 * Our Services, the Patient Education features, the small post links, every
 * gallery tile, the FAQ rows, and the small chrome.
 *
 * DERIVED, NOT INVENTED. The site already had a hover language in two
 * places: a mint underline grows under a title, and a picture lifts. This
 * extends that rather than adding a third idea. Nothing scales, nothing
 * rotates, nothing overshoots, and nothing moves unless the reader touched
 * it.
 *
 * ONE PRIMARY AND TWO QUIET SECONDARIES per card: the title underlines, the
 * picture lifts 4px, the arrow slides 4px. That is the difference between a
 * state change and a designed response.
 *
 * WHY 4px HERE AND 16px ON THE TEAM CARDS. The team photo lifts out of the
 * shape behind it and has room to travel. These sit in a grid against their
 * neighbours, where the same distance would read as a shove.
 */

/* --- the whole-card link ------------------------------------------- */
.svc-post, .pe-feature, .pe-mini, .pe-hero, .gallery__open,
.pe-card__link { --lift: -4px; }
.svc-post__media, .pe-feature__media, .pe-mini__media, .gallery__open img,
.pe-card__media, .pe-card__panel {
  transition: translate 200ms ease-in-out;
}
.svc-post:hover .svc-post__media,     .svc-post:focus-visible .svc-post__media,
.pe-feature:hover .pe-feature__media, .pe-feature:focus-visible .pe-feature__media,
.pe-mini:hover .pe-mini__media,       .pe-mini:focus-visible .pe-mini__media,
.gallery__open:hover img,             .gallery__open:focus-visible img,
.pe-card__link:hover .pe-card__media, .pe-card__link:focus-visible .pe-card__media,
.pe-card__link:hover .pe-card__panel, .pe-card__link:focus-visible .pe-card__panel {
  translate: 0 var(--lift);
}
/* THE TITLE CHANGES COLOUR (Leke, 29 Sep 2026). It underlined first, which
   is what the article cards and the team cards were already doing, and he
   did not want it: an underline appearing under a heading reads as a
   correction more than as an invitation.
   `--primary-hover` is the brand's own lighter navy, already in the palette
   and already what the buttons use, so this introduces no new colour. 7.7:1
   on white, so the title is still properly readable while it is lit.
   Applied to all five card families at once, because the point of a hover
   language is that a reader meets one idea and not two. */
.svc-post__title, .pe-feature__title, .pe-mini__title, .pe-hero__title,
.pe-card__title, .member__open {
  transition: color 200ms ease-in-out;
}
.svc-post:hover .svc-post__title,     .svc-post:focus-visible .svc-post__title,
.pe-feature:hover .pe-feature__title, .pe-feature:focus-visible .pe-feature__title,
.pe-mini:hover .pe-mini__title,       .pe-mini:focus-visible .pe-mini__title,
.pe-hero:hover .pe-hero__title,       .pe-hero:focus-visible .pe-hero__title,
.pe-card__link:hover .pe-card__title, .pe-card__link:focus-visible .pe-card__title,
.member:hover .member__open,          .member:focus-within .member__open {
  color: var(--primary-hover);
}
/* The banner's panel lifts instead of its picture: the picture is the whole
   card and it is clipped by the rounded corner, so moving it opens a gap. */
.pe-hero__panel { transition: translate 200ms ease-in-out; }
.pe-hero:hover .pe-hero__panel, .pe-hero:focus-visible .pe-hero__panel { translate: 0 -4px; }

/* --- the arrow, the quietest of the three -------------------------- */
.link-arrow { transition: border-color 200ms ease-in-out; }
.link-arrow img { transition: translate 200ms ease-in-out; }
.link-arrow:hover, .link-arrow:focus-visible,
.svc-post:hover .link-arrow, .pe-feature:hover .link-arrow, .pe-hero:hover .link-arrow {
  border-bottom-color: var(--primary);
}
.link-arrow:hover img, .link-arrow:focus-visible img,
.svc-post:hover .link-arrow img, .pe-feature:hover .link-arrow img,
.pe-hero:hover .link-arrow img { translate: 4px 0; }

/* --- the disclosure row --------------------------------------------- */
/* The mark is the affordance, so the mark is what answers: the plus sits in
   a mint disc when the pointer is on the row. It is also a bigger target
   than a bare glyph. */
/* background-COLOR, not background: the shorthand would wipe the two
   gradients that draw the mark itself. */
.faq__q::after {
  width: 2rem; height: 2rem; border-radius: 50%; background-color: transparent;
  transition: background-color 200ms ease-in-out, background-size 200ms ease-in-out;
}
/* THE SAME MISTAKE, THE SECOND TIME. The warning above was written for the
   base rule and this line still used the shorthand, so the mint disc arrived
   and took the plus away with it: hovering a row made its mark vanish. The
   plus is painted with background-image, so anything that touches this
   element's background has to name the longhand it means. */
.faq__q:hover::after, .faq__q:focus-visible::after { background-color: var(--accent); }

/* --- the small chrome ----------------------------------------------- */
/* Icons and a logo cannot underline and should not dim, so they lift, by
   half what a card lifts, because they are a quarter of the size. */
.chatbar__social a, .footer .row a, .nav__logo {
  display: inline-block; transition: translate 200ms ease-in-out;
}
.chatbar__social a:hover, .chatbar__social a:focus-visible,
.footer .row a:hover, .footer .row a:focus-visible,
.nav__logo:hover, .nav__logo:focus-visible { translate: 0 -2px; }
.chatbar__chip { transition: background-color 200ms ease-in-out, border-color 200ms ease-in-out; }
.chatbar__chip:hover, .chatbar__chip:focus-visible { border-color: var(--primary); }
/* An inactive slider dot is deliberately faint; on hover it is merely less
   faint, because it is still not the current one. */
.announce__dot:hover::before, .announce__dot:focus-visible::before { opacity: .6; }

/* --- PRESSED ---------------------------------------------------------
 * Nothing on this site answered a press. On a phone on mobile data that
 * means a tap gives no sign it landed until the next page arrives, which is
 * the one case where the fastest band actually matters (120ms: under the
 * threshold where a delay reads as lag rather than as motion).
 */
.btn, .pe-chip, .faq__q, .lightbox__btn, .booking-modal__close, .bio-modal__close,
.wa__close, .quote-modal__close {
  transition: translate 120ms ease-in-out, color 200ms ease-in-out,
              background-color 200ms ease-in-out, border-color 200ms ease-in-out;
}
.btn:active, .pe-chip:active, .lightbox__btn:active,
.booking-modal__close:active, .bio-modal__close:active,
.wa__close:active, .quote-modal__close:active { translate: 0 1px; }
/* A card presses back down: the picture it lifted returns to where it was. */
.svc-post:active .svc-post__media, .pe-feature:active .pe-feature__media,
.pe-mini:active .pe-mini__media, .gallery__open:active img,
.pe-hero:active .pe-hero__panel { translate: 0 0; transition-duration: 120ms; }

@media (prefers-reduced-motion: reduce) {
  .svc-post__media, .pe-feature__media, .pe-mini__media, .gallery__open img,
  .pe-hero__panel, .link-arrow img, .chatbar__social a, .footer .row a,
  .nav__logo,
  .svc-post__title, .pe-feature__title, .pe-mini__title, .pe-hero__title,
  .pe-card__title, .member__open, .btn, .pe-chip, .faq__q::after {
    transition: none;
  }
  .svc-post:hover .svc-post__media, .pe-feature:hover .pe-feature__media,
  .pe-mini:hover .pe-mini__media, .gallery__open:hover img,
  .pe-hero:hover .pe-hero__panel, .link-arrow:hover img,
  .chatbar__social a:hover, .footer .row a:hover, .nav__logo:hover,
  .btn:active, .pe-chip:active { translate: none; }
}

/* =========================================== HOW AN OVERLAY ARRIVES =====
 * TWO BEATS, AND THE REVERSE ON THE WAY OUT (Leke, 23 Sep 2026). Opening:
 * the scrim arrives first and the panel follows 140ms later, rising half a
 * rem as it fades up. Closing: the panel goes first and the scrim clears
 * after it.
 *
 * Written once, for both of the site's dialogs, because two overlays that
 * arrive differently is a system with a hole in it.
 *
 * Three things make it work, and all three are easy to leave out:
 *  - the TIMING LIVES IN TWO PLACES on purpose. The rule that applies while
 *    opening is the [open] one and the rule that applies while closing is
 *    the base one, so one shared transition cannot run the beats backwards.
 *  - `@starting-style` gives the panel something to move FROM. Until it
 *    opens the dialog is display:none, and a transition has nothing to
 *    start at.
 *  - `display` and `overlay` transition with `allow-discrete`, which is
 *    what keeps the dialog in the top layer while it leaves. Without it the
 *    close cuts both beats off at the first frame.
 */
.bio-modal, .lightbox, .booking-modal, .quote-modal {
  transition: display 500ms allow-discrete, overlay 500ms allow-discrete;
}
/* THE SCRIM, WRITTEN ONCE. It used to be declared three times, identically,
   once beside each dialog, which is three places to change one thing and
   exactly how the closing band ended up with two grounds. The choreography
   below already names all three, so the look belongs here too.
   BLURRED, NOT JUST DARKENED (Leke, 28 Sep 2026): the page behind a modal
   should stop competing for the eye. At 92% navy it was hidden rather than
   quietened, and a blur under a near-opaque scrim is invisible work. So the
   navy comes down to 76% and a 10px blur does the rest: the page is still
   there, still says where you are, and there is nothing on it left to
   read. The filter fades in with the scrim, because opacity on the
   pseudo-element composites the filtered result with it. */
.bio-modal::backdrop, .lightbox::backdrop, .booking-modal::backdrop,
.quote-modal::backdrop {
  background: color-mix(in srgb, var(--primary) 76%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  opacity: 0;
  transition: opacity 200ms ease-in-out 180ms;   /* leaves last */
}
.bio-modal[open]::backdrop, .lightbox[open]::backdrop, .booking-modal[open]::backdrop,
.quote-modal[open]::backdrop {
  opacity: 1;
  transition: opacity 220ms ease-in-out;          /* arrives first */
}
@starting-style {
  .bio-modal[open]::backdrop, .lightbox[open]::backdrop,
  .booking-modal[open]::backdrop, .quote-modal[open]::backdrop { opacity: 0; }
}
/* The lightbox's controls sit beside its frame rather than inside it, so
   they are named here too or they would appear on the first frame. */
.bio-modal__panel, .lightbox__frame, .lightbox__btn, .booking-modal__panel,
.quote-modal__panel {
  opacity: 0; translate: 0 .5rem;
  transition: opacity 200ms ease-in-out, translate 200ms ease-in-out;
}
.bio-modal[open] .bio-modal__panel,
.lightbox[open] .lightbox__frame, .lightbox[open] .lightbox__btn,
.booking-modal[open] .booking-modal__panel,
.quote-modal[open] .quote-modal__panel {
  opacity: 1; translate: 0 0;
  transition: opacity 260ms ease-in-out 140ms, translate 260ms ease-in-out 140ms;
}
@starting-style {
  .bio-modal[open] .bio-modal__panel,
  .lightbox[open] .lightbox__frame, .lightbox[open] .lightbox__btn,
  .booking-modal[open] .booking-modal__panel,
  .quote-modal[open] .quote-modal__panel {
    opacity: 0; translate: 0 .5rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .bio-modal, .lightbox, .booking-modal,
  .bio-modal::backdrop, .lightbox::backdrop, .booking-modal::backdrop,
  .bio-modal[open]::backdrop, .lightbox[open]::backdrop, .booking-modal[open]::backdrop,
  .bio-modal__panel, .lightbox__frame, .lightbox__btn, .booking-modal__panel,
  .bio-modal[open] .bio-modal__panel,
  .lightbox[open] .lightbox__frame, .lightbox[open] .lightbox__btn,
  .booking-modal[open] .booking-modal__panel,
  .quote-modal, .quote-modal::backdrop, .quote-modal[open]::backdrop,
  .quote-modal__panel, .quote-modal[open] .quote-modal__panel {
    transition: none; translate: none;
  }
}

/* --------------------------------------- the top of the scale ---
 * Leke, 29 Sep 2026: "on higher screens, the content should stop growing after
 * a certain point, I think it was 2200 pixels."
 *
 * Confirmed against docs/design/responsive-system.md §8, which is his own rule
 * from Negers Mawon on 21 Sep: a website scales up and then STOPS, because the
 * engine's top segment has no upper bound and simply keeps interpolating.
 * Measured here before writing this: the root was 18px at 1920 and 24.33px at
 * 3440, and the content column reached 2190px. That is not the design at a
 * larger size, it is the design zoomed in.
 *
 * CAPPING THE TYPE IS ONLY HALF A CAP. The bands bleed full width by design, so
 * freezing the root alone holds the words still while every coloured band and
 * every full-bleed photograph keeps widening, and the design ends up stranded
 * in a field of lilac. The page is capped too and the screen beyond it gets a
 * neutral ground.
 *
 * 19.1667px is not a taste: it is what the engine itself reaches at 2200, so
 * the scale stops without a step.
 *
 * The one thing outside the page's flow is the WhatsApp launcher, which is
 * fixed to the viewport, so it is pulled back to the capped edge on its own.
 *
 * NOTE for the responsive pass: the doc's rule of thumb is "about 15% above the
 * design width", which for a 1440 design points at roughly 1650 rather than
 * 2200. 2200 is the number Leke asked for and it is the one in the doc's worked
 * example. It is one line to move. */
@media screen and (min-width: 2200px) {
  html { font-size: 19.1667px; background: var(--surface-alt); }
  body {
    max-width: 2200px; margin-inline: auto;
    /* clip, not hidden: hidden makes this a scroll container and breaks
       position: sticky inside it, which the article aside relies on. */
    overflow-x: clip;
  }
  .wa { right: max(var(--s-5), calc(50% - 1100px + var(--s-5))); }
}

/* ------------------------------------------- spacing on a phone ---
 * Leke, 29 Sep 2026, on the home hero at phone width: "it looks kind of tight
 * and each of the elements don't have sufficient spacing. I think the spacing
 * needs to be rebalanced."
 *
 * Measured with tools/dev/spacing-audit.mjs before touching anything, and the
 * numbers said two different things were wrong, neither of them "too small":
 *
 * 1. THE GAPS WERE ALL THE SAME. Headline to buttons 31px, buttons to the
 *    sign-off 31px, sign-off to the picture 38px. When every gap is equal
 *    nothing belongs to anything, and a screen with no grouping reads as
 *    crowded even when the numbers are generous. The fix is the internal <=
 *    external rule (Gestalt proximity): three tiers that are VISIBLY
 *    different. Within a group ~8px, between groups ~31px, and where a
 *    desktop two-column layout stacks, the largest gap in the section,
 *    because that seam is the strongest division the design has.
 *
 * 2. THE INSETS STACKED. Every nested surface kept its desktop padding: page
 *    gutter, then panel padding, then card padding. The hero's words started
 *    57px from the edge of a 393 screen, the services list 70px. 29% and 36%
 *    of the phone was margin, which is the "tight" Leke was seeing: not a
 *    shortage of space but a shortage of WIDTH, so every heading wrapped an
 *    extra line and every block got taller and closer.
 *
 * The tiers are named once here and everything below uses them. */
:root { --gap-in: var(--s-2); --gap-group: var(--s-8); --gap-seam: var(--s-16); }

/* The hero CTA row's spacing lived in an inline style on two pages; it is a
   class now, so it can have a phone value at all. */
.hero-panel__cta { margin-top: var(--s-8); }

/* WHERE THE COLUMNS STACK, THE SEAM IS THE BIGGEST GAP. Below 768 the hero and
   the services card are one column, and their grid gap is what separates what
   used to sit side by side. It grows here rather than shrinking with the
   screen, which is the whole point: on desktop that division is 40px of
   horizontal white; stacked, it has to stay the strongest thing on the screen
   or the two columns read as one list. */
@media (max-width: 767px) {
  .hero-panel { gap: var(--gap-seam); }
  .hero-panel__cta { margin-top: var(--gap-group); }
  /* the sign-off belongs to the text column, so it tucks under the actions */
  .naija { margin-top: var(--s-6); }

  .services-card__grid { gap: var(--gap-seam); }
  /* a heading owns its list, so the list sits closer to it than the button
     does, and the button closes the column */
  .services-card .section-head { margin-bottom: var(--s-6); }
  .services-card__cta { margin-top: var(--gap-group); }

  /* The space between two cards has to beat the space inside one, or a title
     reads as the caption of the picture below it. At 600 a summary sat 21px
     from the next card's picture while its own picture was 14px above it. */
  .pe-list { row-gap: var(--gap-group); }

  /* THE BLOB IS ROTATED, AND A ROTATION OVERHANGS ITS OWN BOX. rotate(-100deg)
     on a 1.139:1 shape draws 13% of its width ABOVE where the layout thinks it
     starts, so the grid's seam was being eaten from the inside: 29px left at
     393 and 4px at 767, where the photograph sat on the Services button. A
     fixed margin cannot fix a gap that grows with the picture, so the margin
     grows with it: a percentage margin is measured against the column WIDTH,
     and 13% of an 80% blob is 10.4% of the column.
     BUT THE EYE DOES NOT SEE THE BOX. The blob is an irregular shape inside
     that rotated box, and its visible top sits about 30px below the box's
     top at 393. Compensating for the whole box put 93px of visible air under
     the buttons, half as much again as the seam anywhere else on the site.
     4% leaves the visible gap at about the seam token. spacing-audit.mjs
     measures boxes, so it carries this as a KNOWN rather than a pass. */
  .hero-panel .about-portrait { margin-top: 4%; }
}

/* The info card's two blocks (Contact us, Opening Hours) sit side by side from
   560 and stack below it, and stacked they were 23px apart while each title sat
   16px over its own lines: the hours read as more of the address. Stacked, the
   two blocks get the group gap. */
@media (max-width: 559px) { .quickinfo__card { gap: var(--gap-group); } }

/* ONE SURFACE DEEP ON A PHONE. Top-level text wants about 20px from the screen
   edge and text inside one card about 16px more. Two nested surfaces each
   keeping their desktop padding is what put the words at 57 and 70. */
@media (max-width: 479px) {
  /* the hero panel keeps its shape and its colour, and gives back its padding */
  .hero-panel { padding-inline: var(--s-5); }

  /* The white services card sits inside the mint panel, which sits inside the
     page gutter: three frames on a 393 screen. On a phone the card lets go of
     its own surface and the list sits on the mint, which is R0's COLLAPSE:
     less presence, same meaning. Desktop keeps the card, because there it
     crosses in front of the dentist and the white is doing a job. */
  .services-card {
    background: transparent; border: 0; box-shadow: none;
    padding: 0;
  }

  .quickinfo__card { padding: var(--s-5); }


  /* The About portrait was pulled UP into the buttons by a negative margin
     that was trying to save height for the fold. It landed 4px under
     "Services". The seam wins; the fold question is the promise list's. */

  /* The other hero shape (testimonial, thanks) had the same stacked inset. */
  .mint-panel--hero { padding-inline: var(--s-5); }

  /* On a phone the featured banner's text panel sits UNDER the picture, on
     white, so its 92% white fill is invisible and all that is left of it is a
     margin plus a padding: the words started 38px further in than the picture
     above them, two left edges a thumb's width apart. It lines up now. */
  .pe-hero__panel { margin: 0; padding: var(--s-5) 0 0; background: transparent; }

  /* R7: a row that wraps must wrap into equal units. Two hero buttons that do
     not fit side by side stack as full-width buttons, not as two different
     widths hung off the left edge. 6 of 6 phone screens in the Mobbin survey
     stacked their actions full width. */
  .hero-panel__cta { flex-direction: column; align-items: stretch; gap: var(--s-3); }
  .hero-panel__cta .btn { justify-content: center; text-align: center; }
}

/* Topic tags on the Patient Education cards (29 Sep 2026). The card is a
   flex column with its own gap, so the tag row drops the margins it carries
   elsewhere and sits the same distance from the title as the tags on the
   home page's cards. */
.pe-card .post__tags { margin: var(--s-2) 0 0; }
.pe-card .post__tags + .pe-card__title { margin-top: 0; }

/* THE WAY BACK (Leke, 29 Sep 2026: "there is no easy way to go back to the
   patient education page when you enter a patient education item"). The
   topic eyebrow already linked back, but to a filtered list and in a script
   face nobody reads as a control. This says what it does, first thing in the
   head, with the arrow pointing the way it goes. 44px tall for a thumb. */
.post-back {
  display: inline-flex; align-items: center; gap: var(--s-2);
  min-height: 44px; margin-bottom: var(--s-2);
  font-size: var(--t-body-sm); font-weight: 500; color: var(--primary);
  text-decoration: none;
}
.post-back::before {
  content: ''; width: .5em; height: .5em;
  border-left: 2px solid currentColor; border-bottom: 2px solid currentColor;
  rotate: 45deg; translate: .1em 0;
  transition: translate 200ms ease-in-out;
}
.post-back:hover, .post-back:focus-visible { text-decoration: underline; text-underline-offset: .2em; }
.post-back:hover::before, .post-back:focus-visible::before { translate: -.15em 0; }
@media (prefers-reduced-motion: reduce) { .post-back::before { transition: none; } }
