/* ==========================================================================
   Dr Cameron Bagry — shared stylesheet
   One file for all pages (home, invisalign, book/consultation, thank-you/consultation).
   De-canvassed from Claude Design .dc.html exports — see design-source/.
   ========================================================================== */

/* ==========================================================================
   Colour tokens — every colour on the site is one of these, never a loose
   hex. Applies inside inline `style=""` attributes too (custom properties
   work anywhere CSS values do): `style="color: var(--ink)"`.
   Adding a new colour? Add it here first, then reference it — don't paste
   a new hex directly into a rule or a style attribute.
   ========================================================================== */
:root {
  /* Core brand */
  --ink: #17171A;          /* primary text / near-black surfaces */
  --paper: #FAF8F4;        /* page background / text-on-dark */
  --key-yellow: #E6FA3C;   /* brand accent — CTAs, active states, hover fills */
  --white: #FFFFFF;

  /* Text */
  --muted: #6E6A62;        /* secondary text on light backgrounds */
  --body-text: #3A3730;    /* body copy, slightly darker than --muted */
  --dark-muted: #A5A19A;   /* secondary text on dark backgrounds */
  --footer-text: #CFCAC0;  /* body text in the dark footer */
  --footer-label: #8F897C; /* uppercase eyebrow labels in the dark footer */
  --link-hover: #4A463D;   /* default <a>:hover colour */

  /* Surfaces + borders */
  --placeholder-bg: #E4DED4; /* image placeholder / loading background */
  --hairline: #E6E0D6;       /* light dividers/borders */
  --hairline-alt: #D8D2C6;
  --tab-bg: #EFEAE2;         /* inactive tab / pill background */
  --tab-bg-hover: #E7E2D8;
  --hover-bg-light: #F4F1EA;
  --dark-surface: #2A2A2E;   /* buttons/surfaces on dark sections */
  --dark-border: #2E2E33;
  --dark-border-alt: #4A4A50;
  --dark-border-alt-2: #2E2E34;
  --dark-hairline: #2C2C30;  /* dividers on dark sections */
  --dark-hairline-alt: #3A3A3F;
  --dark-hairline-alt-2: #2C2C31;
  --darker: #1E1E22;
  --pagination-hover: #B9B29F;

  /* Third-party brand colours. Only ever used to draw that company's own
     mark (the Google Maps pin on the Find us card) at its correct colours —
     never as a site colour. Apple's mark is monochrome, so it uses --ink. */
  --brand-google-blue: #4285F4;
  --brand-google-green: #34A853;
  --brand-google-yellow: #FBBC04;
  --brand-google-red: #EA4335;

  /* Decorative line strokes (background squiggles) */
  --decor-line-soft: #DED7C9;
  --decor-line-strong: #E8E2D6;

  /* One-off decorative tints (staggered card/step backgrounds, hover shades) */
  --wave-tint-1: #F0FF74;
  --wave-tint-2: #E9E8C6;
  --wave-tint-3: #E6EBA6;
  --wave-tint-4: #E4F072;
  --step-tint-1: #EDEBD2;
  --step-tint-2: #EBEFB6;
  --step-tint-3: #E9F485;
  --tint-a: #E4E1DA;
  --tint-b: #A9A49A;
  --tint-c: #EAE4D8;
  --tint-d: #E4DDCF;
  --placeholder-border: #C9C2B4; /* dashed border on "awaiting client copy" boxes */
  /* Sampled off uploads/headshot.JPG's own backdrop, not invented: the
     hero portrait is shot against near-black, and at mobile widths the
     card it sits in is extended upwards in this colour so the photo
     reads as continuing into the space the heading is written in. It is
     deliberately *not* --ink (#17171A), which is visibly lighter and
     bluer than the photograph and leaves a seam across the card. */
  --headshot-black: #100A10;
  /* Full-screen dimmer behind the privacy-policy lightbox. Derived from
     --ink (23,23,26) rather than pure black so the overlay reads as the
     same near-black the dark sections use. */
  --scrim: rgba(23, 23, 26, 0.78);

  /* Added-copy highlight (see the ".copy-added" block below). Deliberately
     a colour that appears nowhere else on the site, so a highlighted
     phrase is impossible to mistake for a design choice. */
  --copy-added-bg: transparent; /* off for client testing (2026-10-09); was rgba(255, 92, 92, 0.3) */
}

/* ==========================================================================
   Added copy — anything on the site that is NOT in a signed-off copy doc
   ==========================================================================
   Standing rule: copy on this site comes from the client's signed-off copy
   document. Where a design slot genuinely needs a line the doc does not
   supply (an interface instruction, a label the component cannot do
   without), the words go in with `class="copy-added"` around them and a
   comment in the markup saying what and why. The highlight makes every
   such phrase visible at a glance on a test build, so nothing
   unapproved reaches the client unnoticed.

   Rules:
   - One class, site-wide: `copy-added`. No variants, no per-page colours.
   - Wrap only the added words, not the whole element, so what is approved
     and what is not can be told apart inside one sentence.
   - Always leave an HTML comment beside it explaining why it was needed.

   TURNING IT OFF, at go-live or for a client screenshot: set
   --copy-added-bg above to `transparent`. That is the whole switch, one
   value in one file, and it covers every page at once. The classes stay
   in the markup on purpose — they are the record of which phrases were
   authored rather than supplied, they stay greppable, and the highlight
   can be turned back on the same way. Do not strip them.
   ========================================================================== */
.copy-added {
  background: var(--copy-added-bg);
  box-shadow: 0 0 0 3px var(--copy-added-bg);
  border-radius: 2px;
}

html, body {
  overflow-x: clip;
  /* Several sections resize elements in place while the user scrolls past
     them (e.g. .known-row expanding when it becomes active). Left to the
     browser's default scroll anchoring, that resize gets "compensated"
     by silently adjusting scrollTop to keep some nearby node visually
     still — which fights our own scroll-driven JS and can trap the user
     mid-section (scrolling up re-expands the same element, which shoves
     the page back down). We drive all of that ourselves, so anchoring is
     off everywhere rather than patched section by section. */
  overflow-anchor: none;
}
/* #page (the per-page wrapper div every page has) is the single source of
   truth for the site's horizontal-overflow containment — every page's
   max-width/overflow now lives here instead of repeated inline on each
   page. Two deliberate, non-obvious choices:

   1. position: relative. The QA pill layer inside it (js/qa-id-pills.js)
      is position:absolute; an absolutely-positioned box is only clipped by
      an ancestor's overflow when that ancestor is also its *containing
      block* — which requires the ancestor to be positioned. Without this,
      #page's overflow is simply skipped: the pill layer's containing block
      resolves further up (to the initial containing block), so it renders
      outside #page's box entirely, uncontained, regardless of #page's own
      overflow setting.

   2. overflow-x: clip, not hidden. hidden on only one axis forces the
      browser to silently upgrade the other axis (left at its default,
      visible) to auto — turning #page into its own scroll container. That
      breaks every position:sticky element inside it (the header included):
      sticky positioning resolves against the nearest scroll container, not
      the real viewport, once one exists. clip has no such side effect —
      it clips without ever creating a scroll container. (Same reasoning
      applies to the html, body rule above.) */
#page {
  position: relative;
  max-width: 100%;
  overflow-x: clip;
}
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}
* { box-sizing: border-box; }
a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--link-hover); }

/* Inline prose links — a link inside a sentence, as opposed to a nav/footer
   list item (.hv-*) or the circular .link-arrow component. Worth its own
   class because neither of those reads as a link mid-sentence, and .hv-1 in
   particular is a dark-footer hover (it resolves to --paper) so it is
   actively wrong in body copy on a light background. The underline is drawn
   in the text's own colour at partial strength, so the same class works on
   --paper, --white and --key-yellow without a variant. */
.copy-link {
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  transition: text-decoration-color 180ms ease;
}
.copy-link:hover { color: inherit; text-decoration-color: currentColor; }

/* ==========================================================================
   Site header / nav
   Every page's <header data-qa-id="global-nav"> uses this. Structure:
     .site-header                the sticky bar itself
       <logo>                    .site-logo — shrinks at narrower breakpoints
                                   instead of the nav ever being allowed to
                                   overflow or wrap onto its own line
       nav.site-nav               the white pill
         .nav-link-group          "Cosmetic solutions" / "About Cameron" —
                                   the text is a real link; the caret is a
                                   separate control that opens the dropdown
           .nav-link
           .nav-caret > .nav-caret-icon
           .nav-dropdown-panel
         .nav-item                plain links (Invisalign, Prices, …)
         .nav-icon-group          wraps Find us + call so they read as one
                                   related cluster, tucked close to the CTA
           .nav-icon-link         Find us / call — sit inside the pill so
                                   they survive down to the tablet layout
         a.nav-cta                Book a consultation / Book / calendar icon
         .nav-menu-toggle         hamburger — hidden until ≤1024px
       .mobile-menu-panel         the ≤1024px dropdown the hamburger opens
         .mobile-menu-group       "Cosmetic solutions" / "About Cameron" —
                                   same link-plus-caret pattern as desktop,
                                   expanding a .mobile-menu-submenu in place
   ========================================================================== */
.site-logo { height: 52px; width: auto; display: block; }
.site-header {
  position: sticky;
  top: 0;
  /* Deliberately above the QA pill overlay's z-index (2147483000, see
     css/qa-id-pills.css) — the nav must always be usable and legible even
     when a pill sits on top of it. */
  z-index: 2147483003;
  /* Even top and bottom, with room above the logo (DCB-7, 2026-10-07): it
     was 14px over 8px and the logo sat tight to the top of the screen. */
  padding: 20px 0;
  background: transparent;
  box-shadow: none;
  transition: background 320ms ease, box-shadow 320ms ease;
}
.site-header.is-scrolled {
  background: var(--paper);
  box-shadow: 0 12px 30px -20px rgba(23, 23, 26, 0.35);
}

/* ==========================================================================
   In-page anchors — how a #hash link travels, and where it lands
   --------------------------------------------------------------------------
   Almost every CTA on the site is an in-page link: eight "Request your
   consultation" buttons pointing at #form on /book/consultation, thirty-odd
   "Known for" rows pointing at #known on the homepage, "Find us" in every
   footer. Two things were wrong with all of them.

   1. **They jumped.** No easing, so travelling from the footer to the form at
      the top of the page simply replaced one screen with another, with nothing
      to say the two places were part of the same page.
   2. **They landed under the header.** The header is `position: sticky;
      top: 0`, so it covers the top ~95px of the viewport, but the browser
      scrolls a target to y=0 — behind it. On /book/consultation that hid the
      form card's first field, leaving "Email address" looking like the top of
      the form.

   `scroll-margin-top` fixes the second: it is the element's own answer to "how
   far down should I come to rest", and it applies however the visitor gets
   there — a click, a pasted URL with a hash, the back button, keyboard
   navigation. It does the same job as an absolutely-positioned offset element
   sitting above each target, without a phantom element per anchor that has to
   stay in sync with the markup around it, and without the offset element being
   what `:target` and the URL then refer to.

   One value covers every breakpoint because the header is one height: the nav
   pill sets it, not the logo (which does shrink), so it measures ~95px down to
   768px and ~87px on phones (DCB-7). The remaining ~27px is air, so a target
   reads as clearing the nav rather than tucking under it.
   ========================================================================== */
:root { --anchor-offset: 122px; }

html { scroll-behavior: smooth; }

/* Applied to anything with an id rather than to a list of the ids that happen
   to be anchor targets today: scroll-margin is inert on an element nothing
   ever scrolls to, and a list would quietly stop covering new sections as
   pages are added. */
[id] { scroll-margin-top: var(--anchor-offset); }

/* Same opt-out as the reveal animations further down — someone who has asked
   for less motion gets the corrected landing point without the travel. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

.nav-link-group { position: relative; display: flex; align-items: center; gap: 2px; }
.nav-link-group .nav-link { padding: 6px 0; color: var(--muted); font: inherit; }
.nav-caret {
  display: flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; flex: none;
  padding: 0; border: 0; background: none; cursor: pointer; color: var(--muted);
}
.nav-caret-icon { width: 9px; height: 6px; display: block; transition: transform 220ms ease; }
.nav-link-group:hover .nav-caret-icon,
.nav-link-group.is-open .nav-caret-icon { transform: rotate(180deg); }

.nav-dropdown-panel {
  position: absolute; top: 42px; left: -20px; width: 320px;
  background: var(--white); border: 1px solid var(--hairline); border-radius: 16px;
  padding: 10px 0; box-shadow: 0 24px 50px -30px rgba(23, 23, 26, 0.35);
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms;
}
.nav-link-group:hover .nav-dropdown-panel,
.nav-link-group.is-open .nav-dropdown-panel {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 220ms ease, transform 220ms ease, visibility 0s;
}
/* Text for screen readers only — e.g. telling them a link opens in a new tab,
   which sighted visitors read off the arrow icon beside it. */
.sr-only {
  position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* The privacy policy shown in a lightbox (README §1.5): the page adds
   html.is-framed when it finds itself in an iframe. Policy copy only. */
html.is-framed .site-header,
html.is-framed footer[data-qa-id="global-footer"] { display: none !important; }
html.is-framed #page { padding-bottom: 0; }

/* The current page. `npm run build` puts aria-current on any nav or footer link to the
   page it's written into (README §1.6), so this is the only place the
   current-item look lives. !important because the top-level items carry
   their colour inline. */
.site-nav a[aria-current="page"] { color: var(--ink) !important; }
.nav-dropdown-panel a[aria-current="page"],
.mobile-menu-sublink[aria-current="page"] { color: var(--ink); font-weight: 700; }
footer[data-qa-id="global-footer"] a[aria-current="page"] { font-weight: 700; }
/* The footer's own 110px margin is the only space above it (DCB-122,
   2026-10-08). 22 pages end on a section with 120px of bottom padding,
   which stacked with that margin into ~200px of empty page on a phone;
   the pages whose last section had none already sat 110px clear. */
section:has(+ footer[data-qa-id="global-footer"]) { padding-bottom: 0 !important; }
/* Bridges the gap between the link and the panel below it, so moving the
   mouse down to the panel doesn't lose :hover partway through. */
.nav-link-group::after { content: ""; position: absolute; left: 0; right: 0; top: 100%; height: 14px; }

/* Find us + call sit together as one cluster — tight gap between the two
   icons, then pulled in closer to the CTA than the pill's usual item gap. */
.nav-icon-group { flex: none; display: flex; align-items: center; gap: 2px; margin-right: -10px; }

.nav-icon-link {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; color: var(--ink);
  transition: transform 180ms ease, background 180ms ease;
}
.nav-icon-link:hover, .nav-icon-link:focus-visible { background: var(--tab-bg); }

/* The call icon opens a small menu of the two numbers rather than dialling
   (phone number changes, 2026-10-20): the practice line, where callers ask
   for Ellie, then Ellie's mobile. A click/keyboard toggle on every screen
   size (initNav() in js/site.js), not hover, since it is mostly tapped.
   The panel hangs off the pill's right edge (.site-nav is its containing
   block), so on a phone it opens under the pill instead of off-screen. */
.site-nav { position: relative; }
.nav-call-toggle { padding: 0; border: 0; background: none; cursor: pointer; font: inherit; }
.nav-call.is-open .nav-call-toggle { background: var(--tab-bg); }
.nav-call-panel {
  position: absolute; top: calc(100% + 10px); right: 0;
  width: min(300px, calc(100vw - 32px));
  background: var(--white); border: 1px solid var(--hairline); border-radius: 16px;
  padding: 8px 0; box-shadow: 0 24px 50px -30px rgba(23, 23, 26, 0.35);
  white-space: normal;
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity 220ms ease, transform 220ms ease, visibility 0s linear 220ms;
}
.nav-call.is-open .nav-call-panel {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 220ms ease, transform 220ms ease, visibility 0s;
}
.nav-call-option { display: flex; align-items: flex-start; gap: 12px; padding: 10px 22px; color: var(--ink); transition: background 180ms ease; }
.nav-call-icon { flex: none; display: flex; padding-top: 2px; }
.nav-call-option:hover, .nav-call-option:focus-visible { background: var(--tab-bg); }
.nav-call-label { display: block; font-size: 14px; font-weight: 600; }
.nav-call-detail { display: block; font-size: 13px; color: var(--muted); margin-top: 2px; }

.nav-cta { display: inline-flex; align-items: center; justify-content: center; }
.nav-cta .cta-short, .nav-cta .cta-icon { display: none; }

.nav-menu-toggle {
  display: none; flex: none; width: 34px; height: 34px; border-radius: 50%;
  border: 0; background: none; cursor: pointer; padding: 0;
  align-items: center; justify-content: center; flex-direction: column; gap: 4px;
}
.nav-menu-toggle:hover, .nav-menu-toggle:focus-visible { background: var(--tab-bg); }
.nav-menu-toggle-bar {
  display: block; width: 16px; height: 2px; background: var(--ink); border-radius: 2px;
  transition: transform 220ms ease, opacity 180ms ease;
}
.nav-menu-toggle.is-open .nav-menu-toggle-bar:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-menu-toggle.is-open .nav-menu-toggle-bar:nth-child(2) { opacity: 0; }
.nav-menu-toggle.is-open .nav-menu-toggle-bar:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.mobile-menu-panel {
  position: absolute; top: 100%; left: 0; right: 0;
  margin: 8px 20px 0; padding: 10px;
  background: var(--white); border: 1px solid var(--hairline); border-radius: 20px;
  box-shadow: 0 30px 60px -20px rgba(23, 23, 26, 0.5);
  display: flex; flex-direction: column;
  opacity: 0; visibility: hidden; transform: translateY(-10px);
  transition: opacity 240ms ease, transform 240ms ease, visibility 0s linear 240ms;
}
.mobile-menu-panel.is-open {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 240ms ease, transform 240ms ease, visibility 0s;
}
/* The open panel scrolls inside itself. It runs from under the header pill
   to the bottom of the *visible* screen: dvh, not vh, because on a phone
   100vh is the height with the browser's address bar retracted, so a
   vh-sized panel ran under the bar and its last links were unreachable.
   The vh line is the fallback for browsers without dvh. On a phone it also
   stops above the sticky booking bar, which stays on screen over it.
   overscroll-behavior stops a swipe that reaches the end of the list from
   carrying on into the page, and initNav() cancels any swipe that starts
   outside the panel, so the background no longer scrolls away under it.

   Never lock the page with overflow: hidden on html/body. Done while
   scrolled down, it drops the sticky header (and so this panel, which
   lives inside it) back to its place at the top of the page, off screen:
   the menu opened invisibly and the page froze (developer report,
   2026-10-02; reproduced in Chrome as well as Safari). */
.mobile-menu-panel {
  --menu-top: 92px;
  max-height: calc(100vh - var(--menu-top) - var(--menu-bottom, 16px));
  max-height: calc(100dvh - var(--menu-top) - var(--menu-bottom, 16px));
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
@media (max-width: 767px) {
  .mobile-menu-panel { --menu-bottom: calc(var(--sticky-cta-h, 0px) + 10px); }
}
.mobile-menu-link { padding: 14px 16px; font-size: 15px; color: var(--ink); border-radius: 12px; }
.mobile-menu-link:hover { background: var(--tab-bg); }

/* "Cosmetic solutions" / "About Cameron" inside the hamburger panel: the
   same link-plus-caret idea as the desktop .nav-link-group, except the
   caret expands a submenu in place (there's no hover on touch) instead of
   a floating panel. */
.mobile-menu-group-head { display: flex; align-items: center; }
.mobile-menu-group-head .mobile-menu-link { flex: 1 1 auto; }
.mobile-menu-caret {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 34px; height: 34px; margin-right: 6px; border-radius: 50%;
  border: 0; background: none; cursor: pointer; color: var(--muted);
}
.mobile-menu-caret:hover, .mobile-menu-caret:focus-visible { background: var(--tab-bg); }
.mobile-menu-caret .nav-caret-icon { transition: transform 220ms ease; }
.mobile-menu-group.is-open .mobile-menu-caret .nav-caret-icon { transform: rotate(180deg); }
.mobile-menu-submenu { display: none; flex-direction: column; padding: 2px 0 6px 12px; }
.mobile-menu-group.is-open .mobile-menu-submenu { display: flex; }
.mobile-menu-sublink { padding: 10px 16px; font-size: 13.5px; color: var(--muted); border-radius: 10px; }
.mobile-menu-sublink:hover { background: var(--tab-bg); }

/* ≤1024px: the white pill stays, but individual links give way to the
   hamburger — Find us / call / the CTA stay put since they sit inside the
   same pill regardless of breakpoint. The logo steps down here (and again
   below) so the pill never has to fight it for room and overflow/wrap. */
@media (max-width: 1024px) {
  .nav-item, .nav-link-group { display: none !important; }
  .nav-menu-toggle { display: flex !important; }
  .site-logo { height: 42px; }
}
/* ≤767px ("mobile landscape" and down): the CTA collapses from text to a
   single calendar icon (matching the icon-link buttons either side of it)
   so it doesn't eat the whole bar. */
@media (max-width: 767px) {
  .nav-cta .cta-full, .nav-cta .cta-short { display: none; }
  .nav-cta .cta-icon { display: inline-flex; }
  .nav-cta { width: 40px; height: 40px; padding: 0; border-radius: 50%; }
  /* As tall as the 54px pill beside it allows (DCB-15), so the header
     row keeps its height. Then 20% smaller (DCB-116, 2026-10-08): it was
     too big and tight to the top of the screen. */
  .site-logo { height: 40px; }
  .site-nav { gap: 12px; padding: 6px 6px 6px 16px; }
  .site-header { padding: 16px 0; }
  .mobile-menu-panel { --menu-top: 88px; }
  .nav-icon-group { margin-right: -6px; }
  /* Find us is in the menu and the footer; on a phone the pin only took
     room from the logo (DCB-14, 2026-10-07). */
  .nav-icon-link--find { display: none; }
  /* The open menu should feel like it spills past the site's own content
     width, not sit flush inside it — bleed past the page's side padding
     and sit a little clear of the pill above it. */
  .mobile-menu-panel { margin: 14px 6px 0; }
}
@media (max-width: 420px) {
  /* Then capped by width: the page's 16px gutters, the ~178px pill and a
     ~26px gap, over the lockup's 2.96:1 ratio. 50px at 393 wide, 42px at
     360 (DCB-15), both 20% smaller (DCB-116). */
  .site-logo { height: min(40px, calc((100vw - 236px) / 2.96 * 0.8)); }
  .site-nav { gap: 8px; padding: 5px 5px 5px 12px; }
}

/* ==========================================================================
   Scroll animation engine
   --------------------------------------------------------------------------
   One system for every "it appears as you scroll to it" effect on the site.

   Deliberately *not* CSS scroll-driven animations (`animation-timeline:
   view()`, which the home-002 steps and the home-003 photo used to use).
   Two reasons, both of which were visible on the built page: a scroll-driven
   animation's progress is the scroll position, so the element moves in
   lockstep with the wheel and reads as jumping rather than easing; and it
   only runs in Chromium, so most visitors saw no animation at all.

   Instead: every [data-reveal] carries one paused keyframe animation, and
   an IntersectionObserver un-pauses it once (initReveals() in js/site.js).
   It then plays over real time with real easing. Markup only ever carries
   attributes — never durations, never keyframes:

     data-reveal               up (the default)
     data-reveal="left"        in from the left
     data-reveal="right"       in from the right
     data-reveal-dist="120"    px it travels. Leave it off and initReveals()
                               sizes it against the element's own height,
                               which is nearly always what you want
     data-reveal-delay="240"   ms before this one starts — on-load
                               choreography only, see initReveals()
     data-reveal-dur="900"     ms it takes (default 800; read the note on
                               duration in README §1.17 before shortening one)

     data-reveal-group         reveal every [data-reveal] inside this element
                               together, when the *group* comes into view, so
                               the stagger is deterministic instead of each
                               child firing on its own position
     data-reveal-stagger="600" ms between the FIRST and the LAST child of the
                               group (not the gap between each pair)

   Anything horizontal collapses to a plain "up" at <=900px — a sideways
   entrance on a phone is mostly wasted, and the brief asks for it on
   home-003 specifically. #page's `overflow-x: clip` contains the off-screen
   start position at every width, so nothing can add a horizontal scrollbar.

   Everything below is gated on `.js` (set by the inline script in each
   page's <head>, so nothing ever paints in its final position and then
   jumps back), and switched off wholesale under prefers-reduced-motion.
   ========================================================================== */
:root {
  /* Measured, not eyeballed. The previous curve here — cubic-bezier(.22,
     .68, .24, 1) — was 59% of the way there after a fifth of its duration
     and 92% by halfway, so an 800ms reveal was really a ~200ms burst
     followed by half a second of movement too small to see. That reads as
     the element snapping into place, which is the same complaint the
     scroll-driven version got. This one is close to constant velocity
     through the middle with soft ends (19/37/53/67/77/85/91/96% at each
     tenth of the duration), so the whole 800ms is actually visible.
     If you change it, profile it — the shape matters far more than the
     duration does. */
  --rv-ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  /* Only the fallback for the split second before initReveals() measures
     the element and writes a distance suited to its size — see sizeTravel(). */
  --rv-dist: 34px;
}

/* ---- blank-page guard: the older .reveal/.jstep classes aren't used by
   this design, but the noscript guard + .js gate are kept per house style. ---- */
.reveal, .jstep { opacity: 1; transform: none; }
.js .reveal, .js .jstep { opacity: 0; transform: translateY(38px); }
.reveal.active, .jstep.on { opacity: 1; transform: none; }

/* The entrance is a *paused* keyframe animation rather than a transition,
   and that is load-bearing. Plenty of elements here carry their own inline
   `transition: transform …` for a hover nudge (the home-002 steps, the
   home-008 rows): an inline transition beats any stylesheet one, so a
   transition-based reveal would have run at the hover's 320ms with no fade
   at all. An animation is a separate layer — it overrides the element's own
   transform while it plays, then stops applying, leaving the hover
   (which is !important, and so outranks an animation anyway) untouched.

   Paused-with-fill means the `from` state is what paints from the very
   first frame, so nothing is ever briefly visible in its final position.
   initReveals() adds .is-in to start it, after whatever delay that element
   or its group calls for. */
.js [data-reveal] {
  animation-name: rvIn;
  animation-duration: var(--rv-dur, 800ms);
  animation-timing-function: var(--rv-ease);
  animation-fill-mode: both;
  animation-play-state: paused;
}
.js [data-reveal].is-in { animation-play-state: running; }

/* ...and once it has played, the animation is taken back off the element
   again (initReveals() adds .rv-done on animationend).

   `animation-fill-mode: both` above is doing two jobs, and the second one
   was a bug. Forwards-filling is what makes the *paused* state paint the
   `from` frame before the reveal fires — that part is essential. But a
   filling animation keeps applying its value to `transform` and `opacity`
   for the life of the page, long after it has finished, and **a property
   being supplied by an animation cannot be transitioned**. Every element
   on this site that reveals *and* has a hover nudge therefore had a dead
   `transition` on it: the home-002 / about-us-006 award cards, home-008's
   rows, the treatment cards. Measured on home-002 before this fix — the
   hover went 0px to 14px in a single frame, two distinct computed values
   over the whole 320ms, i.e. a jump. Reported by the developer 2026-09-22
   ("they do not have an ease animation between the hover and normal
   state"), and the same root cause as the find-us-002 buttons logged in §7
   on 2026-09-21, which was worked around there by dropping data-reveal.

   Removing the animation is safe precisely because @keyframes rvIn has
   only a `from`: its filled end state *is* the element's own computed
   style, so dropping it changes nothing visually on the frame it happens.
   It just hands `transform` and `opacity` back to the stylesheet, where
   the element's own `transition` can finally see them.

   Specificity (0,3,0) beats the (0,2,0) rule above, so no !important.

   The one thing to watch: this clears *all* animation on the element, so a
   [data-reveal] element must not carry a second CSS animation of its own.
   Checked across every page when this went in — none does, and the reveal
   is meant to be the only animation an element gets from this engine
   anyway. If one ever needs both, name the property instead of resetting
   the shorthand. */
.js [data-reveal].rv-done { animation: none; }

/* Only a `from` — the implicit `to` is whatever the element's own styles
   say, so an element keeps any transform of its own once this has played.

   The movement is on the individual `translate` / `scale` properties, NOT
   on `transform`, and that is load-bearing. Every hover nudge on the site
   (.hv-6:hover and friends) is `transform: … !important`, and an
   !important declaration outranks a running animation. So with the
   reveal on `transform`, any element that slid under a resting cursor
   mid-reveal — which is what a quick scroll does — lost its movement on
   the spot and snapped into place while it faded. Measured on home-008
   (2026-10-02): the hovered row's offset was 0px on every frame, its
   neighbours eased 44px → 0. `translate` is a separate property that
   composes with `transform`, so the two now stack instead of fighting.
   Don't move this back onto `transform`. */
@keyframes rvIn {
  from {
    opacity: 0;
    translate: var(--rv-x, 0px) var(--rv-y, var(--rv-dist));
    scale: var(--rv-scale, 1);
  }
}

.js [data-reveal="left"]  { --rv-x: calc(var(--rv-dist) * -1); --rv-y: 0px; }
.js [data-reveal="right"] { --rv-x: var(--rv-dist);            --rv-y: 0px; }
.js [data-reveal="up"]    { --rv-x: 0px; }

/* A photo that was still downloading when its block revealed (initReveals()
   waits up to 1.5s for it, then gives up) is held transparent and fades in
   on its own when it lands, instead of appearing in a single frame. An
   animation rather than a transition for the same reason as the reveal: an
   inline `transition` on the image would win and the fade would snap. */
.js img.rv-img-wait { opacity: 0; }
.js img.rv-img-wait.rv-img-in { opacity: 1; animation: rvImgIn 700ms var(--rv-ease) both; }
@keyframes rvImgIn { from { opacity: 0; } }

/* Word-by-word headings (home-005 "Cosmetic dentistry", home-009 "Design
   your own smile"). The words are wrapped in inline-block spans so they can
   move independently — the sentence itself is never broken up, re-ordered
   or duplicated, so it still reads and selects as one string. Below 900px
   initReveals() drops the stagger on a group named "words" and the whole
   heading arrives as one piece.

   The !important below is load-bearing: initReveals() writes --rv-x/--rv-y
   inline for any data-reveal-dist, and only an !important declaration can
   still flatten those back to a vertical entrance at phone widths. */
.rv-word { display: inline-block; }
@media (max-width: 900px) {
  .js [data-reveal] { --rv-x: 0px !important; --rv-y: var(--rv-dist) !important; }
}

/* ==========================================================================
   Parallax — initParallax() in js/site.js writes the transform. An element
   is never both [data-reveal] and [data-parallax]: they'd fight over the
   same property. Put the reveal on a wrapper and the parallax on the child.
   ========================================================================== */
.js [data-parallax]:not([data-parallax-mode="bg"]) { will-change: transform; }

/* ==========================================================================
   "You should" — written, not printed
   --------------------------------------------------------------------------
   The hero eyebrow is drawn left to right by a slanted wipe (a four-point
   polygon whose right edge leans over, the way a nib's angle would) with a
   small nib riding just ahead of the ink. The text is one plain string in
   one span — nothing is split into per-letter elements, so it stays
   selectable and reads normally to a screen reader.
   ========================================================================== */
.pen-write { position: relative; display: inline-block; }
.pen-nib { display: none; }
.js .pen-write > .pen-ink {
  display: inline-block;
  clip-path: polygon(0% -30%, 0% -30%, -16% 130%, -16% 130%);
}
.js .pen-write > .pen-nib {
  display: block;
  position: absolute;
  left: 0;
  top: 4%;
  width: 3px;
  height: 74%;
  margin-left: -1px;
  border-radius: 3px 3px 40% 40%;
  background: var(--ink);
  transform: rotate(13deg);
  transform-origin: 50% 100%;
  opacity: 0;
  pointer-events: none;
}
/* Near-linear on purpose: a pen moves at a fairly even speed, so the
   heavy ease-out used everywhere else would read as the word being
   stamped rather than written. */
.js .pen-write.is-writing > .pen-ink { animation: penInk var(--pen-dur, 1100ms) cubic-bezier(.3, .3, .7, .75) both; }
.js .pen-write.is-writing > .pen-nib { animation: penNib var(--pen-dur, 1100ms) cubic-bezier(.3, .3, .7, .75) both; }
@keyframes penInk {
  from { clip-path: polygon(0% -30%, 0% -30%, -16% 130%, -16% 130%); }
  to   { clip-path: polygon(0% -30%, 122% -30%, 106% 130%, -16% 130%); }
}
@keyframes penNib {
  0%   { left: -8%;  opacity: 0; }
  6%   { opacity: 0.85; }
  88%  { opacity: 0.85; }
  100% { left: 114%; opacity: 0; }
}

/* ==========================================================================
   Decorative background lines
   --------------------------------------------------------------------------
   The two-line flourish that runs through the site. It used to be a plain
   absolutely-positioned box filling each section (`inset: 0`), which meant
   its width was that section's *content* width — 1360px on a padded section
   — so on any wide screen the lines visibly started and stopped at the
   page's content boundary, drawing attention to exactly the edge they're
   meant to soften. And because the SVG stretched to the section's height
   with preserveAspectRatio="none", a short section squashed the same curve
   flat while a tall one drew it out.

   Now it's a free-floating element: wider than the viewport at every
   breakpoint (so both ends run off-screen and there is no visible edge),
   vertically centred on the section, and floored at a sensible height so
   short sections don't flatten the curve. Sections that own a background of
   their own (the white home-006 band) keep their `overflow: hidden` and so
   clip it to the band; sections that sit on the page's own paper let it
   bleed, which is the point.
   ========================================================================== */
.decor-lines {
  /* The wave's height is derived from its width, never from the section's.
     That is the whole fix for "tall and narrow on mobile": the old version
     stretched a fixed viewBox to fill whatever box the section happened to
     be, so a short wide section flattened the curve and a tall narrow one
     stood it on end — the same flourish looked like a different drawing on
     every page and at every breakpoint.

     The SVG is laid out at twice --decor-w with a 2400x240 viewBox, so this
     height is exactly the one that leaves the wave undistorted. Both axes
     then scale together, which is also why the stroke widths in the markup
     are in user units rather than pinned with non-scaling-stroke: the line
     thins on a phone in proportion with everything else. */
  --decor-w: 150vw;
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--decor-w);
  height: calc(var(--decor-w) * 2 * 240 / 2400);
  transform: translate(-50%, -50%);
  pointer-events: none;
  overflow: hidden;
  z-index: 0;
}

/* Lit slowly, once, the first time its section comes near the viewport —
   initWaves() adds .is-lit and never takes it off, so scrolling back up
   past a section doesn't fade it in again. Gated on .js so that with no
   JavaScript the waves are simply there rather than permanently invisible.
   A transition rather than a keyframe: there is nothing on this element to
   fight over the property, and a one-way transition can't re-run. */
.js .decor-lines { opacity: 0; transition: opacity 4s ease-out; }
.js .decor-lines.is-lit { opacity: 1; }

/* Two layers, each its own <svg>, drifting at different speeds — that
   difference is what reads as water rather than as a picture sliding
   sideways. The drift is a CSS transform on the <svg> element itself,
   which composites like any other element's; transforming the <path>
   inside one would instead repaint the whole SVG every frame, and a 14px
   stroke across 136vw is not a cheap repaint.

   The seam is the thing to be careful with. The wave has a period of 600
   user units, the viewBox is 2400 wide (four periods), and the element is
   laid out at 200% of its frame — so translating it by exactly -25% of
   its own width moves it precisely one period and the loop is invisible.
   Change any one of those three numbers and you have to change the
   others with it. */
.decor-wave {
  position: absolute;
  left: 0;
  top: 0;
  width: 200%;
  height: 100%;
  display: block;
  opacity: 0.3;
}
/* The animation is only attached while the section is anywhere near the
   viewport (initWaves() in js/site.js toggles .is-near). An element with a
   running transform animation is held on its own compositing layer, and
   these are deliberately far wider than the screen — eight of them on the
   homepage, all animating at once, would be a lot of texture memory to
   hold for something nobody is looking at. Re-attaching restarts the wave
   at phase zero, which is just another valid position on a continuous
   wave, and it always happens off-screen. */
.decor-lines.is-near .decor-wave--soft   { animation: waveDriftSoft 58s linear infinite; }
.decor-lines.is-near .decor-wave--strong { animation: waveDriftStrong 82s linear infinite; }

/* The vertical part is a slow swell of a few pixels, in opposite phase on
   the two layers so they close up and open out as they go. Horizontal
   travel stays exactly linear — it is what has to land on the seam. */
@keyframes waveDriftSoft {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-6.25%, 5px); }
  50%  { transform: translate(-12.5%, 0); }
  75%  { transform: translate(-18.75%, -5px); }
  100% { transform: translate(-25%, 0); }
}
@keyframes waveDriftStrong {
  0%   { transform: translate(0, 0); }
  25%  { transform: translate(-6.25%, -6px); }
  50%  { transform: translate(-12.5%, 0); }
  75%  { transform: translate(-18.75%, 6px); }
  100% { transform: translate(-25%, 0); }
}

.decor-lines--flip { transform: translate(-50%, -50%) scaleY(-1); }

/* The wave grows, in viewport units, as the screen narrows. A phone is tall
   and thin, so 150vw there would put nearly two whole periods across a
   375px screen and leave a 112px ribbon in the middle of a very tall
   section — small, busy, and nothing like what the same wave does on a
   desktop. Widening it instead means a phone sees rather less than one
   period: a single slow arc, which is the same gesture the desktop makes,
   just cropped closer.

   The stroke comes down to match. Because the whole wave scales with its
   own width, a fixed stroke would get proportionally twice as heavy on a
   phone; these keep the line at roughly 0.9% of the viewport width at
   every breakpoint. CSS wins over the stroke-width attribute in the
   markup, which carries the desktop value. */
@media (max-width: 1024px) {
  .decor-lines { --decor-w: 220vw; }
  .decor-wave--soft path { stroke-width: 0.8; }
  .decor-wave--strong path { stroke-width: 5; }
}
@media (max-width: 640px) {
  .decor-lines { --decor-w: 320vw; }
  .decor-wave--soft path { stroke-width: 0.55; }
  .decor-wave--strong path { stroke-width: 3.5; }
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { animation: none !important; }
  .decor-wave { animation: none !important; }
  .js .decor-lines { opacity: 1; transition: none; }
  .js .pen-write > .pen-ink { clip-path: none !important; animation: none !important; }
  .js .pen-write > .pen-nib { display: none !important; }
  /* The accordions still open and close — they just arrive there rather
     than travelling. `visibility` keeps its 0s timing either way. */
  .faq-a-wrap, .hero-tab-blurb, .hero-tab { transition: none !important; }
}

@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes iconPop { 0% { transform: rotate(-9deg) scale(0.82); } 55% { transform: rotate(-9deg) scale(1.14); } 100% { transform: rotate(-9deg) scale(1); } }
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* The video-review heading ("…who choose Cameron"). Its box is 34ch of the
   16px body text, ~300px, so at 100px the long words ("Telford's", "dentists",
   "Invisalign") are wider than the box. A line wider than its box doesn't
   centre, it spills out to the right, so those lines sat right of the short
   ones (DCB-20, DCB-80, 2026-10-07). As a centred flex column the heading is
   at least as wide as its longest word and any overflow is shared by both
   sides. Every page with this section. */
[data-m~="peerhead"] { display: flex; flex-direction: column; align-items: center; }

/* ==========================================================================
   Layout helper attributes (ported verbatim from the canvas [data-m] system)
   ========================================================================== */
@media (min-width: 1025px) and (max-width: 1120px) {
  header > div { flex-wrap: nowrap !important; gap: 18px !important; }
  header nav { gap: 8px 16px !important; font-size: 13px !important; }
  .site-logo { height: 46px; }
}
@media (max-width: 900px) {
  [data-m~="pad"] { padding-left: 20px !important; padding-right: 20px !important; }
  [data-m~="stack"] { grid-template-columns: minmax(0, 1fr) !important; }
  [data-m~="thumb"] { grid-template-columns: minmax(0, 1fr) !important; gap: 16px !important; padding-right: 60px !important; }
  [data-m~="thumb"] > div:first-of-type { aspect-ratio: 16 / 10 !important; max-width: none !important; }
  /* Ellie's face runs from about 20% to 72% of her headshot's height, more
     than a 16:10 strip can show, so it cut her off at the chin (DCB-130).
     Her step thumbnail gets a 4:3 strip, centred on her face. */
  [data-m~="thumb"] > div:first-of-type:has(img[src*="ellie-headshot"]) { aspect-ratio: 4 / 3 !important; }
  [data-m~="thumb"] img[src*="ellie-headshot"] { object-position: 50% 40% !important; }
  /* "thumb" reserves 60px on the right for the big ghost numeral that
     overhangs a step card. A card list with no numerals (the cosmetic
     dentistry hub's treatment cards) pairs it with "nonum" to give that
     strip back, or every card is inset from the right for nothing. */
  [data-m~="nonum"] { padding-right: 30px !important; }
  [data-m~="minh"] { min-height: 0 !important; }
  [data-m~="noml"] { margin-left: 0 !important; width: auto !important; }
  [data-m~="abs"] { position: static !important; transform: none !important; width: auto !important; left: auto !important; bottom: auto !important; margin-top: 14px !important; }
  .gal-viewport .swiper-slide, .peer-viewport .swiper-slide { width: calc(50% - 10px) !important; }

  /* The oversized watermark script (thank-you/consultation's "Thank you"
     behind the hero) scales up as the screen narrows. `:not(.rv-word)` is
     load-bearing: home-009's "your own" is also Ms Madi + nowrap, and was
     being caught by this and blown up to 190px beside a "smile" of 84px —
     a different sizing rule from the rest of its own heading, which is
     exactly what it looked like. Words inside a heading keep the clamp
     their markup gives them at every breakpoint. */
  [style*="Ms Madi"][style*="nowrap"]:not(.rv-word) { font-size: clamp(64px, 26vw, 190px) !important; }
  header > div { flex-wrap: nowrap !important; gap: 12px !important; }
  [data-m~="marrow"] { display: flex !important; }
  [data-m~="arrows"] > button { background: var(--key-yellow) !important; color: var(--ink) !important; cursor: pointer !important; width: 44px !important; height: 44px !important; margin-top: -22px !important; font-size: 16px !important; }
  [data-m~="arrows"] > button[style*="left: 16px"] { left: 6px !important; }
  [data-m~="arrows"] > button[style*="right: 16px"] { right: 6px !important; }
  [data-m~="strip2"] { grid-template-columns: minmax(0, 1fr) !important; }
  [data-m~="strip2"] > div { aspect-ratio: 16 / 10 !important; }
  [data-m~="strip2"] > div:nth-child(2), [data-m~="strip2"] > div:nth-child(4), [data-m~="strip2"] > div:nth-child(5) { display: none !important; }
  [data-m~="strip-duo"] { grid-template-columns: minmax(0, 1fr) !important; }
  [data-m~="strip-duo"] > div { aspect-ratio: 16 / 10 !important; }
  [data-m~="numrow"] { grid-template-columns: 50px minmax(0, 1fr) !important; padding-right: 0 !important; gap: 12px !important; }
  [data-m~="colM"] { flex-direction: column !important; align-items: flex-start !important; gap: 20px !important; }
  [data-m~="colM"] > p { max-width: none !important; }
  [data-m~="bigtitle"] > span:last-of-type { justify-content: flex-start !important; flex-wrap: wrap !important; gap: 10px !important; }
  [data-m~="bigtitle"] span { white-space: normal !important; }
  [data-m~="heroflow"] { display: flex !important; flex-direction: column !important; gap: 18px !important; align-items: stretch !important; }
  [data-m~="heroflow"] > [data-mo] { width: 100% !important; max-width: 100% !important; min-width: 0 !important; flex: 0 0 auto !important; }
  [data-m~="heroflow"] h1 { font-size: clamp(38px, 12.6vw, 66px) !important; }
  [data-m~="heroflow"] h1 span { white-space: normal !important; }
  [data-mo="2"] { margin-bottom: 0 !important; }
  [data-mo="3"] { margin-bottom: 0 !important; }
  [data-m~="flowM"] { display: flex !important; flex-direction: column !important; gap: 26px !important; }
  [data-m~="flowM"] > [data-mo] { margin-bottom: 0 !important; }
  [data-m~="eyebrowM"] { margin-bottom: -10px !important; }
  [data-mo="4"] { margin-top: 0 !important; }
  [data-m~="heroimgM"] { aspect-ratio: 4 / 3.7 !important; }
  [data-m~="stepM"]:first-of-type { margin-top: 14px !important; }
  [data-m~="stepM"] { width: auto !important; margin-left: 40px !important; padding-left: 44px !important; min-height: 0 !important; }
  [data-m~="stepM"] > img { left: -30px !important; width: 60px !important; height: 60px !important; margin-top: -30px !important; }
  [data-m~="contents"] { display: contents !important; }
  [data-mo="1"] { order: 1 !important; }
  [data-mo="2"] { order: 2 !important; }
  [data-mo="3"] { order: 3 !important; }
  [data-mo="4"] { order: 4 !important; }
  [data-mo="5"] { order: 5 !important; }
  [data-m~="tabrow"] { width: 100% !important; margin-left: 0 !important; text-align: left !important; }
  [data-m~="hideM"] { display: none !important; }
  [data-active="true"] [data-m~="tabimg"] { display: block !important; }
  [data-m~="pimg0"] { top: -30px !important; left: -6px !important; width: 104px !important; }
  [data-m~="pimg1"] { bottom: -184px !important; left: -4px !important; width: 96px !important; }
  [data-m~="pimg2"] { top: -24px !important; right: -6px !important; width: 100px !important; }
  [data-m~="pimg3"] { bottom: -190px !important; right: -4px !important; width: 104px !important; }
  [data-m~="peerhead"] { margin: 76px auto 250px !important; }
  /* Stacked, the arrow hangs off the peers block itself rather than the
     script word, which lands anywhere on the wrapped line: its centre sits
     70% across the screen, tucked behind the paragraph so it never covers the copy. */
  [data-m~="peerhead"] span:has(> [data-m~="curve"]) { position: static !important; }
  [data-m~="curve"] { left: calc(50% + 20vw - 34px) !important; right: auto !important; margin-left: 0 !important; top: auto !important; bottom: -56px !important; width: 68px !important; z-index: 0 !important; }
  [data-m~="peerhead"] > p { position: relative; z-index: 1; }
  /* book-invisalign-005's arrow hangs off the heading's last word, so
     stacked it lands on the line underneath. Every other peers heading
     carries it mid-sentence, where there is room below. */
  [data-qa-id="book-invisalign-005"] [data-m~="curve"] { display: none !important; }
  [data-m~="rev"] { display: flex !important; flex-direction: column-reverse !important; }
  footer { text-align: center !important; }
  footer [style*="flex-direction: column"] { align-items: center !important; }
  footer [style*="justify-content: space-between"] { justify-content: center !important; gap: 14px !important; }
  footer [style*="justify-content: flex-end"] { justify-content: center !important; }
  footer img { margin-left: auto !important; margin-right: auto !important; }
  footer [style*="display: grid"] { justify-items: center !important; }

  [style*="position: sticky"][style*="top: 120px"], [style*="position: sticky"][style*="top: 110px"] { position: static !important; }

  /* Invisalign hero + hero-shot specific mobile rules (harmless elsewhere) */
  [data-m~="heroshot"] { height: auto !important; overflow: visible !important; background: transparent !important; }
  [data-m~="heroshot"] .heroshot-media { position: relative !important; inset: auto !important; height: 300px !important; background: var(--placeholder-bg) !important; }
  /* The cards stack below the photo here (position: static, above), so the
     skirt would draw a strip of card colour into every gap between them. */
  .hero-tab::after { display: none !important; }
  [data-m~="heroshot"] [data-m~="abs"] { display: flex !important; flex-direction: column !important; align-items: stretch !important; gap: 8px !important; }
  [data-m~="heroshot"] [data-m~="abs"] > button { text-align: left !important; border-radius: 14px !important; min-height: 0 !important; box-shadow: none !important; }
  [data-m~="invhero"] h1 span { font-size: clamp(34px, 11.6vw, 62px) !important; text-align: left !important; }
  [data-m~="invhero"] > div:first-of-type > div { flex-direction: column !important; align-items: flex-start !important; margin-top: 14px !important; }
  [data-m~="invhero"] > div:first-of-type > div > p { text-align: left !important; margin-left: 0 !important; max-width: none !important; }
  /* The paragraph comes after the buttons in the markup (on desktop it sits
     to their right), so stacked it landed under them. It reads first. */
  [data-m~="invhero"] > div:first-of-type > div > p { order: -1; margin: 8px 0 6px !important; }
  [data-m~="tyhero"] h1 span { font-size: clamp(34px, 11.6vw, 62px) !important; }
}

/* ==========================================================================
   Tablet band — 768px to 1024px
   --------------------------------------------------------------------------
   The block above collapses everything to a single column from 900px down,
   which is the right answer for a phone and the wrong one for a tablet:
   between 768 and 900 there is still easily room for two columns, and a
   portrait photo given the full width of an iPad is enormous (728px wide,
   cropping Cameron's chin) rather than impressive.

   So this band re-establishes a real tablet layout for the sections where
   the phone stack reads badly. It sits *after* the ≤900 block on purpose —
   same specificity, later wins — and stops at 767px, where the phone layout
   (and §1.11's "mobile landscape and down" nav) legitimately takes over.
   ========================================================================== */
@media (min-width: 768px) and (max-width: 1024px) {
  /* home-001 hero. The eyebrow and the H1 sit in the left column with the
     portrait beside them; the yellow and dark cards run full width
     underneath both. [data-m~="contents"] is flattened here as well as at
     ≤900 so all four pieces are grid items of the same container — without
     it the cards are trapped inside the text column.

     Everything is scoped to home-001: book/consultation's hero carries the
     same [data-m~="heroflow"] and its own, unrelated [data-mo] numbering. */
  [data-qa-id="home-001"] [data-m~="heroflow"] {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) minmax(240px, 38%);
    /* The portrait is taller than the eyebrow and the H1 stacked, and it
       spans both their rows. With two auto rows the grid shares its extra
       height between them and prises the eyebrow off the top of the
       heading; a single 1fr row takes all of that slack instead, so the
       two stay locked together and settle against the foot of the photo. */
    grid-template-rows: 1fr auto auto;
    column-gap: 36px;
    row-gap: 0;
    align-items: start !important;
  }
  [data-qa-id="home-001"] [data-m~="contents"] { display: contents !important; }
  [data-qa-id="home-001"] [data-mo="1"] { grid-column: 1; grid-row: 1; align-self: end; }
  [data-qa-id="home-001"] [data-mo="2"] { grid-column: 1; grid-row: 2; }
  [data-qa-id="home-001"] [data-mo="3"] { grid-column: 2; grid-row: 1 / span 2; align-self: start; }
  [data-qa-id="home-001"] [data-mo="4"] { grid-column: 1 / -1; grid-row: 3; margin-top: 34px !important; }
  [data-qa-id="home-001"] [data-m~="heroflow"] h1 { font-size: clamp(44px, 6.4vw, 72px) !important; }
  /* 4/5, the desktop frame, rather than the phone's near-square 4/3.7 —
     at this width the short frame is what was cutting his chin off. */
  [data-qa-id="home-001"] [data-m~="heroimgM"] { aspect-ratio: 4 / 5 !important; }

  /* home-003. Its grid is repeat(auto-fit, minmax(320px, 1fr)), which holds
     two columns down to ~744px and then drops the portrait under the copy
     at full width — a 700px-wide, 930px-tall photo. Pin the two columns
     across the whole band; ≤767 below is where it is allowed to stack. */
  [data-qa-id="home-003"] > [data-reveal-group] {
    grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr) !important;
    gap: 36px !important;
  }
}

@media (max-width: 767px) {
  /* Note what is *not* here any more: the carousels used to drop to one
     slide per view at this breakpoint. They now keep the two-up they
     inherit from ≤900 and only go one-up at ≤560 (below) — a landscape
     phone is 667-930px wide, and a single slide left one card marooned in
     the middle of a very wide, very short screen.

     home-003 gives up its two columns here rather than at the ~744px its
     auto-fit grid happened to land on, so "stacked" starts at the same
     width the rest of the phone layout does. */
  [data-qa-id="home-003"] > [data-reveal-group] {
    grid-template-columns: minmax(0, 1fr) !important;
    gap: 30px !important;
  }
  /* Stacked, the 3/4 portrait frame is the full width of the page and half
     again as tall — it stops being a photograph beside a story and becomes
     a full-screen interruption in the middle of one. A landscape crop of
     the same picture keeps both people in frame at a fraction of the
     height. */
  [data-qa-id="home-003"] > [data-reveal-group] > [data-reveal="left"] { aspect-ratio: 4 / 3.2 !important; }

  /* book-consultation-005: the headshot/bullets pair stays as the desktop
     side-by-side grid through tablet (see .bc005-grid — it's no longer on
     the shared [data-m~="stack"] switch) and only stacks here, at mobile
     landscape and down. The headshot drops out of position: sticky (it was
     taking over the whole viewport) and the bullet rows become the sticky,
     overlapping "card stack" instead. */
  .bc005-grid { display: flex !important; flex-direction: column-reverse !important; }
  [data-qa-id="book-consultation-005"] [style*="position: sticky"][style*="top: 108px"],
  [data-qa-id="book-invisalign-003"] [style*="position: sticky"][style*="top: 108px"] { position: static !important; }

  /* book-consultation-001: stacked, the column's 18px flex gap already
     sits between the H1 and the line under it, so the H1's own 22px
     bottom margin made a 40px gap. Gap only (DCB-45, 2026-10-07). */
  [data-qa-id="book-consultation-001"] h1,
  [data-qa-id="book-invisalign-001"] h1 { margin-bottom: 0 !important; }

  /* The shared hero sits 64px under the nav on desktop and tablet (DCB-91,
     2026-10-07). On a phone it keeps its 30px, so the hero's buttons still
     land on the first screen. */
  [data-m~="invhero"] { padding-top: 30px !important; }

  /* Sticky card stack — every numbered list on the site (initStickyStacks()
     in site.js sets --stack-i and --stack-bg). Each row sticks 14px below
     the one before, so a sliver of every earlier row stays visible as the
     next slides over it. The trailing CTA, where a list has one, is the
     last card, so it never tucks under the rows. */
  .stack-card {
    position: sticky !important;
    top: calc(90px + var(--stack-i, 0) * 14px);
    z-index: calc(1 + var(--stack-i, 0));
    background: var(--stack-bg, var(--paper));
  }
  .stack-cta { padding: 24px 0 28px; margin-top: 0 !important; margin-bottom: 0 !important; }

  /* smile-makeovers-003 and -009: same underlying bug, found by the
     developer 2026-09-21 — both sections use a literal two-track
     `grid-template-columns: minmax(Apx, …) minmax(Bpx, …)`, not
     `repeat(auto-fit, minmax(...))`. A literal grid-template-columns never
     drops a track just because the viewport can't fit both minimums —
     auto-fit is what does that, and it isn't used here. -003's two column
     floors (300px + 260px) plus its 56px gap total 616px, comfortably
     wider than a phone; -009's (320+260+48=628px) has the same problem.
     Below 768px the second track was simply being pushed off past the
     right edge and clipped by the sitewide overflow-x: hidden fix — not
     removed, not visible, and the first track sat at its 300/320px floor
     rather than filling the phone's content width. Same base fix as
     .bc005-grid just above: stack to one column (flex's default
     align-items: stretch gives the single column full width for free,
     same as .bc005-grid needs no explicit width rule either), reverse the
     order so the second grid track reads first, and drop sticky to static
     since it makes no sense once nothing tall sits beside it. Unlike
     .bc005-grid, the more elaborate sticky-card-stack effect on
     book-consultation-005's own numrows (directly above) is not part of
     this fix — that is extra polish tailored to that section's 4-item
     count, not required here. */
  .sm003-grid { display: flex !important; flex-direction: column-reverse !important; }
  .sm003-sticky { position: static !important; }
  /* -009 has no sticky element to un-stick — its two tracks are the
     feature grid and a plain portrait image. column-reverse puts the
     portrait first, for the same "recognise the person before reading
     about them" reason the developer suggested. */
  .sm009-grid { display: flex !important; flex-direction: column-reverse !important; }
}
@media (max-width: 560px) {
  [data-m~="pad"] { padding-left: 16px !important; padding-right: 16px !important; }

  /* Portrait phone: one slide per view, inset evenly so the round arrow
     buttons overlap the card edges by two-thirds rather than sitting off in
     empty page margin on one side only. */
  .gal-viewport, .peer-viewport { padding: 0 21px; }
  .gal-viewport .swiper-slide, .peer-viewport .swiper-slide { width: 100% !important; }
}

/* ==========================================================================
   home-001 hero portrait at mobile landscape and down
   --------------------------------------------------------------------------
   Full width on a phone, the headshot read as a photograph of a man on a
   black background dropped into the page — the black stopped dead at a
   rounded rectangle just above his hair. So below 768px the card grows a
   band of that same black above the photo and the headline is written into
   it, in white, over his head: one continuous dark field that happens to
   have a face in the bottom of it, rather than a picture with a caption.

   Two things this depends on:

   - --headshot-black, sampled from the photo's own backdrop. --ink is
     visibly lighter and would draw a line across the card at the seam.
   - object-position 50% 0% with no scale. The desktop framing pushes down
     into the photo (and the near-square phone frame used to cut his chin);
     anchoring to the top keeps the strip of black above his hair, which is
     what lets the band and the photograph read as the same surface.

   The real <h1> is still the H1 — it keeps its place in the document for
   search engines and screen readers and only gives up its box here, and
   the white copy in the card is aria-hidden so neither is announced twice.
   ========================================================================== */
.hero-shot-title { display: none; }

@media (max-width: 767px) {
  /* Less of the page's own top padding, so the hero starts closer to the
     nav and the two cards below the portrait carry up into the first
     screen — the yellow card's "Book a consultation" was landing about
     30px under the fold on a 390x844 phone. */
  [data-qa-id="home-001"] { padding-top: 12px !important; }
  [data-qa-id="home-001"] [data-m~="heroflow"] { gap: 12px !important; }
  /* "You should" moves into the dark card, written above "Go and see"
     (.hero-shot-eyebrow, below). Its own line above the card cost ~70px
     of the first screen. The original stays in the document, visually
     hidden like the H1, so the sentence still reads in order. */
  [data-qa-id="home-001"] [data-mo="1"] {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px !important;
    padding: 0 !important;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* The yellow card is what has to reach the first screen: the Book button
     is in it. Benchmarked on an iPhone 17 (402px wide, ~760px of page
     visible under Safari's bars). */
  [data-qa-id="home-001"] [data-mo="4"] > div:first-child { padding: 22px 24px 24px !important; }
  [data-qa-id="home-001"] [data-mo="4"] > div:first-child > p { margin-bottom: 18px !important; font-size: 17px !important; }
  [data-qa-id="home-001"] [data-m~="heroimgM"] {
    aspect-ratio: auto !important;
    background: var(--headshot-black) !important;
    display: flex !important;
    flex-direction: column !important;
  }
  /* The photo is narrower than the card it sits in, and the black either
     side of it is the card's background rather than the photograph's.
     That only works because the two are the same colour — and it is what
     buys the extra head room: at full width, showing him down to the
     collarbone would make the card taller, not shorter. Sized this way
     the frame reaches past his shoulders and is still ~90px shorter.
     The frame is anchored to the photo's TOP. There is only ~5% of black
     above his hair, so a bottom-anchored frame any shorter than the photo
     cuts his head off (developer report, 2026-10-02 — never again). 1/1.35
     shows the top 90%: all of his hair, down past the chin to the black
     of his t-shirt, so the bottom edge still disappears into the card.
     56% wide (was 64%) is what keeps the yellow card's Book button on the
     first screen of an iPhone 17. */
  [data-qa-id="home-001"] [data-m~="heroimgM"] > img {
    width: 56% !important;
    height: auto !important;
    margin: 0 auto !important;
    aspect-ratio: 1 / 1.35 !important;
    object-position: 50% 0% !important;
    transform: none !important;
    /* His shoulders run off both sides of the photograph, so the frame's
       own edge cuts them in a straight vertical line. Fading the outer
       ~9% dissolves that cut into the card instead. The stops are set off
       the picture, not picked by eye: the head spans 13.8%-91.2% of the
       frame at its widest (his right ear is the far edge), so 9% and 93%
       are the widest fades that cannot touch him. Above the collar this
       is fading black into black and does nothing. */
    -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 9%, #000 93%, transparent 100%);
    mask-image: linear-gradient(to right, transparent 0%, #000 9%, #000 93%, transparent 100%);
  }
  .hero-shot-title {
    display: block;
    margin: 0;
    padding: 18px 24px 8px;
    color: var(--paper);
    font-size: clamp(38px, 12.6vw, 66px);
    line-height: 0.9;
    letter-spacing: -0.045em;
    font-weight: 700;
  }
  .hero-shot-title span { display: block; }
  .hero-shot-title .hero-shot-eyebrow {
    margin: 0 0 4px;
    padding-left: 4px;
    font-family: 'Ms Madi', cursive;
    font-weight: 400;
    font-size: clamp(32px, 9vw, 44px);
    line-height: 1;
    letter-spacing: 0;
    transform: rotate(-3deg);
    transform-origin: left bottom;
  }
  .hero-shot-title .hero-shot-eyebrow span { display: inline-block; }
  .hero-shot-title .pen-nib { background: var(--paper); }
  /* Visually hidden, not display:none — the heading stays rendered text. */
  [data-qa-id="home-001"] [data-m~="heroflow"] h1 {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ==========================================================================
   Global footer — the legal bar along the very bottom
   --------------------------------------------------------------------------
   .footer-legal is the row: the GDC registration line on one side, and
   .footer-legal-links (Complaints / Cookies / Privacy / Terms, the XD
   credit and the Last updated stamp) on the other. Both keep their layout
   in the markup at desktop; everything below is the narrow-screen story.
   ========================================================================== */
/* Footer breathing room from tablet down — the bottom bar sat almost on the
   fold of the page on anything smaller than a laptop. */
@media (max-width: 1024px) {
  footer [data-m~="pad"] { padding-bottom: calc(40px + 4rem) !important; }
}
/* The footer logo is not lazy-loaded (§1.14). Safari only fetches a lazy
   image once it is practically on screen, so on a phone the logo was the
   last thing in the footer to arrive and visibly popped in. It is 21KB,
   480px wide (2x its largest display size), and cached after the first
   page. Smaller on a phone, where 216px was most of the column. */
@media (max-width: 767px) {
  footer .footer-logo { width: 176px !important; }
}
/* Mobile landscape and down: the legal links stop being a row. Wrapped onto
   two or three ragged lines they read as a broken sentence rather than a
   list, so each one takes its own centred line, and the GDC line above them
   centres to match. */
@media (max-width: 767px) {
  .footer-legal { flex-direction: column !important; align-items: center !important; text-align: center; }
  .footer-legal-links {
    flex-direction: column !important;
    flex-wrap: nowrap !important;
    align-items: center !important;
    gap: 12px !important;
  }
}

/* ==========================================================================
   Nav dropdown ("Cosmetic solutions")
   ========================================================================== */
.nav-dropdown { display: none; }

/* ==========================================================================
   "Known for" tabbed accordion list (home / invisalign / book pages)
   ========================================================================== */
.known-row {
  position: relative;
  width: 76%;
  margin-left: auto;
  background: var(--tab-bg);
  color: var(--ink);
  border-radius: 20px;
  padding: 20px 26px;
  cursor: pointer;
  text-align: right;
  transition: width 420ms cubic-bezier(.25,.46,.45,.94), background 300ms ease, padding 420ms cubic-bezier(.25,.46,.45,.94);
}
@media (hover: hover) {
  .known-row:hover { background: var(--key-yellow) !important; }
}
.known-row.active {
  width: 100%;
  background: var(--key-yellow);
  text-align: left;
  padding: 26px 30px 28px;
}
.known-label { margin: 0; font-size: 19px; line-height: 1.15; letter-spacing: -0.02em; font-weight: 500; transition: font-size 300ms ease; }
.known-row.active .known-label { font-size: 27px; }
.known-expand { display: none; }
.known-row.active .known-expand { display: block; }
.known-expand p { margin: 12px 0 16px; font-size: 15px; line-height: 1.55; max-width: 40ch; }
/* A row whose copy is a list rather than a paragraph (composite bonding's
   "What it can change"). Same type size and measure as the paragraph above,
   so the rows still read as one set. */
.known-expand ul { margin: 12px 0 16px; padding-left: 20px; max-width: 40ch; display: grid; gap: 7px; font-size: 15px; line-height: 1.5; }
.known-expand li { margin: 0; }
.known-tabimg { display: none; position: relative; margin-top: 18px; border-radius: 14px; overflow: hidden; aspect-ratio: 4 / 3; background: var(--placeholder-bg); background-size: cover; background-position: center; }
@media (max-width: 900px) {
  .known-row.active .known-tabimg { display: block; }
  /* Tap-driven here, not scroll-driven (initKnown()), so there is no
     scroll position for the line to count down. */
  .known-row .row-progress { display: none; }
}
.known-image-panel { border-radius: 20px; overflow: hidden; background: var(--placeholder-bg); min-height: 580px; position: relative; background-size: cover; background-position: center; transition: opacity 300ms ease; }
/* A row that shows a video instead of a photo (home-018, DJ Simz). On
   desktop the panel drops its photo for the portrait .vid-card inside it,
   centred on ink; on a phone the open row shows the same video, portrait,
   in place of its photo. initKnown() adds .is-video to the panel. */
.known-image-panel .known-panel-video { display: none; position: absolute; top: 0; bottom: 0; left: 50%; transform: translateX(-50%); height: 100%; aspect-ratio: 62 / 100; }
.known-image-panel.is-video { background-image: none !important; background-color: var(--ink); }
.known-image-panel.is-video .known-panel-video { display: block; }
.known-tabimg.known-tabvid { aspect-ratio: 62 / 100; max-width: 340px; background: var(--placeholder-bg); }

/* ==========================================================================
   Row scroll-progress line — sits inside the far-right edge of the
   current row in a scroll-highlighted list (.known-row, [data-step]),
   filling top-to-bottom to show how far the user has scrolled through
   that specific item. See attachRowProgress()/updateRowProgress() in
   site.js. The row itself must be position:relative (both are).
   ========================================================================== */
.row-progress {
  position: absolute;
  top: 10px;
  bottom: 10px;
  right: 10px;
  width: 2px;
  background: var(--hairline-alt);
  border-radius: 2px;
  overflow: hidden;
  opacity: 0;
  transition: opacity 200ms ease;
  pointer-events: none;
}
.row-progress.current { opacity: 1; }
.row-progress-fill {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 0%;
  background: var(--ink);
  border-radius: 2px;
  transition: height 120ms linear;
}

/* Horizontal variant — the same component turned on its side, for a card
   that runs on a timer rather than on scroll position (the Invisalign hero
   USP cards). Half a rem up from the card's bottom edge, inset to the
   card's own side padding. */
.row-progress--bar {
  top: auto;
  bottom: 0.5rem;
  left: 18px;
  right: 18px;
  width: auto;
  height: 2px;
}
.row-progress--bar .row-progress-fill {
  height: 100%;
  width: 0%;
  transition: width 120ms linear;
}

/* Paused — the visitor is hovering the card, or has just clicked one, so
   the countdown is not running. The fill stops where it is and the whole
   bar drops back, which reads as held rather than broken. */
.row-progress.is-paused { opacity: 0.4; }
.row-progress.current.is-paused { opacity: 0.4; }
.row-progress.is-paused .row-progress-fill { transition: none; }

/* ==========================================================================
   FAQ tabs + accordion
   ========================================================================== */
/* The tab row deliberately does *not* set z-index. It used to carry
   z-index:2 inline, which made it a stacking context and lifted the whole
   row — inactive tabs included — above the white panel below it: each
   inactive tab's 4px of translateY showed as a strip of --tab-bg sitting
   on top of the white card. Leaving z-index alone here keeps the tabs and
   the panel in the same stacking context, where the per-tab z-index below
   can put the active one in front of the card and the inactive ones
   behind it, which is what a tab is meant to look like. */
/* The FAQ heading column stays with the questions.
   --------------------------------------------------------------------------
   Sitewide: every FAQ section is a two-column band — heading, one line of
   copy and a CTA on the left, the accordion on the right — and the accordion
   is always the taller of the two by a long way. Left static, the heading and
   its "book a consultation" scroll off within the first two questions and the
   visitor reads the rest of the answers next to nothing.

   Desktop only. Once the band stacks, the aside is a row above the questions
   rather than beside them, and sticking it would park it over the answers on
   the smallest screen we have. 901px is the site's own horizontal/vertical
   breakpoint (§1.17).

   Two things this needs from the markup, both already true on all five FAQ
   sections: the grid is `align-items: start`, or a stretched row leaves the
   column no height to travel in; and neither the column nor any ancestor of
   it carries [data-reveal] — a reveal writes transform, which takes the
   element out of its sticky containing block (§1.17). The reveals sit on the
   heading, the copy and the CTA instead. */
@media (min-width: 901px) {
  .faq-aside { position: sticky; top: 120px; }
}

.faq-tabs {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding-left: 4px;
}
.faq-tab {
  font: inherit;
  font-size: 14.5px;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  padding: 14px 26px 17px;
  margin-bottom: -1px;
  border: 1px solid var(--hairline);
  border-bottom: 0;
  border-radius: 14px 14px 0 0;
  cursor: pointer;
  white-space: nowrap;
  background: var(--tab-bg);
  color: var(--muted);
  transform: translateY(4px);
  /* z-index applies without position here — these are flex items. */
  z-index: 0;
  transition: background 220ms ease, color 220ms ease, transform 220ms ease;
}
.faq-tab.active { background: var(--white); color: var(--ink); transform: translateY(0); z-index: 2; }
.faq-group { position: relative; z-index: 1; background: var(--white); border: 1px solid var(--hairline); border-radius: 0 22px 22px 22px; padding: 6px 30px; }
.faq-item { border-bottom: 1px solid var(--tab-bg); }
/* The rule between two questions is a separator, so the last one has
   nothing to separate — it just draws a line across the bottom of the
   card, a few pixels above the card's own rounded edge. */
.faq-item:last-child { border-bottom: 0; }
.faq-q {
  width: 100%;
  text-align: left;
  font: inherit;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  background: none;
  border: 0;
  padding: 24px 0;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  color: var(--ink);
}
.faq-q-text { font-size: 17.5px; letter-spacing: -0.015em; }
.faq-sign { width: 34px; height: 34px; flex: none; border-radius: 50%; background: var(--key-yellow); color: var(--ink); display: flex; align-items: center; justify-content: center; font-size: 17px; }

/* Answers open on a height transition rather than on the `hidden`
   attribute, which cannot be eased — they used to snap. Same 0fr -> 1fr
   grid row as the hero USP cards: it is the only way to animate to a
   height the copy decides for itself, and it needs the one wrapper
   element around the <p> to have something to clip. `visibility` is what
   keeps a shut answer out of the accessibility tree, delayed to the end
   of the close so it does not cut the transition short. */
.faq-a-wrap {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  opacity: 0;
  transition: grid-template-rows 420ms var(--rv-ease), opacity 240ms ease,
              visibility 0s linear 420ms;
}
.faq-item.is-open .faq-a-wrap {
  grid-template-rows: 1fr;
  visibility: visible;
  opacity: 1;
  transition: grid-template-rows 420ms var(--rv-ease), opacity 300ms ease 120ms,
              visibility 0s;
}
.faq-a-wrap > .faq-a { overflow: hidden; min-height: 0; }
.faq-a { margin: 0; padding: 0 0 24px; max-width: 62ch; font-size: 15.5px; line-height: 1.62; color: var(--body-text); }

/* Mobile landscape and down: the tabs stack vertically, full width, like
   a pile of dividers, each tucked under the one below it (DCB-120,
   2026-10-08). On one line they no longer fit: "Cost and practicalities"
   was cut off at the screen edge on a 393px phone.

   The active tab always sits at the bottom of the pile (order: 1), so it
   is the one whose white fill runs into the panel, whichever was tapped:
   tapping a tab moves its label down to join the answers. The z-index
   ladder puts each tab in front of the one above it and the active tab in
   front of the panel's top border, so tab and panel read as one card. The
   panel loses its rounded top corners because the tab is now as wide as
   it is. DOM order, and so keyboard order, is unchanged. */
@media (max-width: 767px) {
  .faq-tabs { flex-direction: column; flex-wrap: nowrap; gap: 0; padding-left: 0; }
  /* No colour fade: the tabs swap places, and a fade made the old one
     look active for a beat in its new spot. */
  .faq-tab { width: 100%; text-align: left; padding: 14px 22px 24px; margin-bottom: -10px; transform: none; transition: none; }
  .faq-tab:nth-child(1) { z-index: 1; }
  .faq-tab:nth-child(2) { z-index: 2; }
  .faq-tab:nth-child(3) { z-index: 3; }
  .faq-tab.active { order: 1; z-index: 5; padding-bottom: 15px; margin-bottom: -1px; }
  .faq-group { border-radius: 0 0 22px 22px; padding: 6px 22px; }
}

/* ==========================================================================
   Treatment cards — one card per treatment on a hub page
   --------------------------------------------------------------------------
   The cosmetic dentistry hub (cosmetic-dentistry-003) is a catalogue: five
   subheadings, and under each of them the treatments it covers. Each
   treatment gets its own card, because each one is a real page with its own
   consultation behind it — packing three of them into one row made the most
   important information on the page read as a paragraph of small print.

   The whole card is the link. That is why the doc's sentence sits in the
   card as plain text with no inline <a> inside it: an anchor inside an
   anchor is invalid, and the card already goes exactly where that link went.

   No skirt under the lift, unlike .hero-tab (§1.22): these sit on paper with
   real gaps between them and own their own background, so there is no seam
   for the lift to open. That rule is about a card flush against something
   it does not own.
   ========================================================================== */
.treat-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
  align-items: stretch;
}

/* THE SLOT EXISTS SO THE CARD CAN BE HOVERED. Put [data-reveal] on the card
   itself and its hover lift snaps instead of easing — a revealed element
   carries a finished keyframe animation with fill:both, an animation outranks
   a plain declaration in the cascade, and a transition has no interpolable
   start value to work from. Marking the hover !important wins the cascade and
   still snaps, which is the version that looks like a bug.

   So: the slot reveals, the card hovers. One element, one owner of
   `transform` — the same rule as never putting data-reveal and data-parallax
   together (§1.17). Any hover-lifting card that also reveals needs this. */
.treat-card-slot { display: flex; }

.treat-card {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 20px;
  overflow: hidden;
  color: var(--ink);
  transition: transform 300ms var(--rv-ease), border-color 220ms ease, box-shadow 300ms var(--rv-ease), background 220ms ease;
}
.treat-card-media { aspect-ratio: 4 / 3; background: var(--placeholder-bg); overflow: hidden; }
.treat-card-media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform 520ms var(--rv-ease);
}
.treat-card-body { display: flex; flex-direction: column; flex: 1; padding: 18px 20px 18px; }
.treat-card-title { margin: 0 0 8px; font-size: 18px; font-weight: 700; letter-spacing: -0.025em; line-height: 1.16; }
.treat-card-text { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--muted); }
/* margin-top:auto pins the row to the bottom of every card in a row,
   whatever length the doc gave that treatment's sentence. */
.treat-card-go {
  margin-top: auto;
  padding-top: 18px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.treat-card-go-label { font-size: 13px; line-height: 1.3; letter-spacing: -0.01em; color: var(--muted); }
.treat-card-go-icon {
  width: 36px; height: 36px; flex: none; border-radius: 50%;
  background: var(--key-yellow); color: var(--ink);
  display: flex; align-items: center; justify-content: center;
  font-size: 16px; line-height: 1;
  transition: transform 200ms ease;
}
@media (hover: hover) {
  .treat-card:hover {
    transform: translateY(-4px);
    border-color: var(--hairline-alt);
    box-shadow: 0 20px 40px -24px rgba(23,23,26,0.45);
  }
  .treat-card:hover .treat-card-media img { transform: scale(1.045); }
  .treat-card:hover .treat-card-go-icon { transform: translateX(3px); }
}
.treat-card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* The soft variant — a card for a page that is not a treatment.
   Outline rather than fill, no photograph, centred: the bonding-vs-veneers
   comparison is a page you read to choose between two of the cards beside
   it, not a third thing you can book, and it should not compete with them
   for weight. It keeps the same footprint and the same grid track so the
   row still reads as one set. Filling on hover is what says it is live. */
.treat-card--soft {
  background: transparent;
  border-color: var(--hairline-alt);
  text-align: center;
}
.treat-card--soft .treat-card-body { justify-content: center; align-items: center; padding: 28px 26px; }
.treat-card--soft .treat-card-text { max-width: 30ch; }
.treat-card--soft .treat-card-go { margin-top: 20px; padding-top: 0; justify-content: center; }
@media (hover: hover) {
  .treat-card--soft:hover { background: var(--white); }
}

/* A category with one treatment in it fills its row rather than leaving two
   empty tracks — but it turns on its side to do it. Kept vertical it would
   be a ~970px card with a 4/3 image, which is a 730px-tall photograph of
   somebody's teeth; the horizontal version fills the same row at a sane
   height and reads as deliberate rather than as a gap.

   Keyed off :only-child, not a modifier class, because "this category has
   one treatment" is exactly the condition — and it stops being true on its
   own the moment a second treatment page is added to that category, with no
   markup to remember to change. The media takes its height from the body
   beside it, so the card is as tall as its copy needs and no taller. */
.treat-cards > .treat-card-slot:only-child { grid-column: 1 / -1; }
.treat-cards > .treat-card-slot:only-child > .treat-card {
  flex-direction: row;
  align-items: stretch;
  min-height: 210px;
}
.treat-cards > .treat-card-slot:only-child .treat-card-media {
  flex: 0 0 clamp(200px, 32%, 380px);
  aspect-ratio: auto;
}
.treat-cards > .treat-card-slot:only-child .treat-card-body { padding: 26px 30px 24px; }
.treat-cards > .treat-card-slot:only-child .treat-card-title { font-size: 21px; margin-bottom: 10px; }
.treat-cards > .treat-card-slot:only-child .treat-card-text { max-width: 62ch; }

@media (max-width: 640px) {
  .treat-cards { grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .treat-card-media { aspect-ratio: 16 / 9; }
  /* No room to be horizontal on a phone — back to the same card as the rest. */
  .treat-cards > .treat-card-slot:only-child > .treat-card { flex-direction: column; min-height: 0; }
  .treat-cards > .treat-card-slot:only-child .treat-card-media { flex: none; aspect-ratio: 16 / 9; }
  .treat-cards > .treat-card-slot:only-child .treat-card-body { padding: 18px 20px; }
  .treat-cards > .treat-card-slot:only-child .treat-card-title { font-size: 18px; margin-bottom: 8px; }
}

/* ==========================================================================
   Honest-limits list ("The bits nobody puts on the poster",
   composite-bonding-006)
   ==========================================================================
   A plain card of caveats — the one thing on the site that has to read as
   information rather than persuasion, so it deliberately borrows the
   quietest pattern already here (the .faq-group card: white, one hairline
   border, hairline rules between rows) rather than the dark numbered grid
   the "six reasons" sections use. Same card, no numbers, a --key-yellow
   marker instead, because these items are a set rather than a ranking.

   Reusable: any later treatment page with a "you should know this before
   you book" list uses this, not a new component.
   ========================================================================== */
.caveat-card {
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 22px;
  padding: 4px 30px;
}
.caveat-item {
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  padding: 20px 0;
  border-top: 1px solid var(--hairline);
}
.caveat-item:first-child { border-top: 0; }
.caveat-item p { margin: 0; font-size: 15.5px; line-height: 1.6; color: var(--body-text); }
/* The ring is what stops a 10px dot reading as a stray bullet at this
   type size — it gives the marker the weight of the yellow pills used
   elsewhere without being another filled circle. */
.caveat-dot {
  width: 10px;
  height: 10px;
  margin-top: 7px;
  border-radius: 50%;
  background: var(--key-yellow);
  box-shadow: 0 0 0 5px var(--hover-bg-light);
}
@media (max-width: 640px) {
  .caveat-card { padding: 2px 22px; }
  .caveat-item { grid-template-columns: 20px minmax(0, 1fr); gap: 12px; padding: 18px 0; }
}

/* Suits/does-not-suit variant: a tick or a cross in place of the plain dot,
   for a caveat-card list that is explicitly "yes" or "no" rather than a
   flat set of things to know (prep-less-veneers-003). Stays on-brand rather
   than reaching for a traffic-light green/red: the tick takes the same
   --key-yellow accent every positive/active state on the site already
   uses, the cross a neutral --tab-bg/--muted pairing. */
.caveat-mark {
  width: 20px;
  height: 20px;
  margin-top: 2px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
}
.caveat-mark svg { width: 11px; height: 11px; display: block; }
.caveat-mark--yes { background: var(--key-yellow); color: var(--ink); }
.caveat-mark--no { background: var(--tab-bg); color: var(--muted); }

/* Porcelain-vs-composite split card (prep-less-veneers-005). Mobile
   landscape and down (≤767px, this site's standing breakpoint — README
   §1.11): the two columns stack instead of squeezing side by side, and
   the divider between them turns from a vertical rule to a horizontal
   one so the "or" badge still sits on a line between the two stacked
   halves rather than as a leftover vertical sliver above them. */
@media (max-width: 767px) {
  .vs-card { grid-template-columns: 1fr !important; }
  .vs-divider { width: 100% !important; height: 1px !important; }
}

/* Find-us-003's travel-time/by-bus cards: a fixed 3-column grid (2 rows of
   3) rather than auto-fit, since auto-fit would show 4 or 5 across on
   wider viewports instead of the even 3x2 the six cards are designed for.
   Same ≤767px breakpoint as .vs-card above — the cards stack as a plain
   vertical flex column rather than squeezing into 1-2 columns. */
.find-us-loc-grid { display: grid; grid-template-columns: repeat(3, 1fr); }
@media (max-width: 767px) {
  .find-us-loc-grid { display: flex !important; flex-direction: column !important; }
}

/* ==========================================================================
   Price list — the from-price rows (prices-and-finance-004)
   ==========================================================================
   The same card the FAQ accordion and the honest-limits list already use —
   white, one hairline border, hairline rules between rows — because a list
   of prices is information rather than persuasion and should not be shouting
   at the treatments it is pricing.

   Two columns, not one line of prose per row: the whole point of the page is
   that you can run your eye down the figures without reading anything, which
   a comma-separated sentence per treatment does not let you do. The doc's own
   line splits at its own comma into the two cells and nothing is reworded —
   see the section comment in prices-and-finance/index.html.

   `align-items: baseline` is what keeps a two-line treatment name and its
   one-line price sitting on the same first line rather than the price
   floating to the middle of the row.
   ========================================================================== */
.price-card {
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 22px;
  padding: 4px 30px;
}
.price-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 6px 32px;
  align-items: baseline;
  padding: 22px 0;
  border-top: 1px solid var(--hairline);
}
.price-row:first-child { border-top: 0; }
.price-name {
  margin: 0;
  font-size: 17px;
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.price-value {
  margin: 0;
  font-size: 15px;
  line-height: 1.45;
  color: var(--muted);
  text-align: right;
}
/* Only the figure itself steps up — the "from" and the "per tooth" around it
   are qualifiers, and setting the whole cell at this size turns a price list
   into a wall of large type with nothing to scan. */
.price-figure {
  font-size: 21px;
  letter-spacing: -0.02em;
  color: var(--ink);
}
/* Below ~560px the two columns have nowhere to go: a long treatment name
   ("Layered ultra-natural composite bonding") wraps to three lines and
   squeezes the price into a column one word wide. Stacked, the name reads as
   a label and the price sits under it. */
@media (max-width: 560px) {
  .price-card { padding: 2px 22px; }
  .price-row { grid-template-columns: 1fr; padding: 18px 0; }
  .price-value { text-align: left; }
}

/* "What is not on this page" — the housekeeping line that closes the price
   section. It is one heading and one sentence, and given a full-width band of
   its own (heading left, paragraph right, the shape every other section on
   the page uses) it read as a section with nothing in it. As a single tinted
   strip at the foot of the prices it reads as what it is: a footnote to the
   list above it. Reusable for any other "and here is what this list does not
   cover" line. */
.price-note {
  display: grid;
  grid-template-columns: minmax(180px, 0.44fr) minmax(280px, 1fr);
  gap: 10px 48px;
  align-items: start;
  background: var(--tab-bg);
  border-radius: 22px;
  padding: 30px 34px;
}
.price-note h3 {
  margin: 0;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.015em;
  line-height: 1.3;
  color: var(--ink);
}
.price-note p {
  margin: 0;
  font-size: 15.5px;
  line-height: 1.62;
  color: var(--body-text);
  max-width: 62ch;
}
@media (max-width: 767px) {
  .price-note { grid-template-columns: 1fr; padding: 26px 24px; }
}



/* ==========================================================================
   Swiper carousels — the standard pattern for every carousel on this site.
   Uses the Swiper library (css/js loaded from cdnjs, pinned version — see
   the <head>/footer of any page). Arrow buttons keep their own existing
   classes/styles below; only wiring (navigation/pagination) comes from JS.
   To add a new carousel: give its slides container class="swiper", wrap
   slides in .swiper-wrapper > .swiper-slide, add two prev/next buttons
   styled however this page needs, and call
   initSwiperCarousel(viewportSelector, prevSelector, nextSelector) in
   js/site.js. Everything below (dots, disabled-arrow dimming, slide gap
   sizing) comes for free.
   ========================================================================== */
.swiper-slide { height: auto; }

/* Dot pagination: a static list *below* the carousel, not Swiper's default
   overlay-on-top-of-the-last-slide placement. */
.swiper-pagination {
  position: static !important;
  margin-top: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
}
.swiper-pagination-bullet {
  width: 9px;
  height: 9px;
  margin: 0 !important;
  border-radius: 50%;
  background: var(--decor-line-soft);
  opacity: 1;
  cursor: pointer;
  transition: background 200ms ease, transform 200ms ease;
}
.swiper-pagination-bullet:hover { background: var(--pagination-hover); transform: scale(1.2); }
.swiper-pagination-bullet-active { background: var(--key-yellow); }

/* Dimmed, inert arrow buttons at the start/end of a carousel — Swiper adds
   this class to whatever custom prevEl/nextEl was passed in. */
.swiper-button-disabled { opacity: 0.35; cursor: default; pointer-events: none; }

/* watchOverflow (js/site.js initSwiperCarousel) adds .swiper-button-lock
   to prevEl/nextEl when there's nowhere to scroll, and Swiper's own
   bundled CSS sets `display: none` for it — but every arrow button here
   (.gal-prev/.gal-next/.peer-prev/.peer-next) also carries its own
   `display: flex`/`display: absolute` positioning rule at the same
   specificity, loaded after Swiper's stylesheet, so it was winning the
   cascade and the lock never visibly took effect. One override, covering
   every carousel's arrows site-wide rather than a per-carousel fix. */
.swiper-button-lock { display: none !important; }

/* Edge fade: bleed the viewport slightly wider than its arrows wrapper and
   fade opacity across that bleed, so slides look like they fade into the
   page rather than getting clipped by a hard edge. The bleed is what
   widens — the fully-visible carousel area stays the same size as before.

   Currently switched OFF (no visual effect) — parked here on purpose. The
   class stays applied on every carousel's markup; only the declarations
   below are commented out. To re-enable, uncomment the two blocks below. */
.swiper-edge-fade {
  /* margin-left: -40px;
  margin-right: -40px;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 40px, #000 calc(100% - 40px), transparent); */
}
@media (max-width: 900px) {
  .swiper-edge-fade {
    /* margin-left: -24px;
    margin-right: -24px;
    -webkit-mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, transparent, #000 24px, #000 calc(100% - 24px), transparent); */
  }
}

/* ==========================================================================
   Before/after gallery carousel
   ========================================================================== */
/* Slides run on past the carousel's own box and are cut off by the edge of
   the screen, not by the 1360px content column, so the next card shows
   part-way across the margin (DCB-34, DCB-39, 2026-10-07). Swiper's own CSS
   clips .swiper; this undoes it for both carousels (.peer-viewport too).
   The page still never scrolls sideways: html, body and #page carry
   overflow-x: clip, which (unlike hidden) leaves position: sticky working. */
.gal-viewport, .peer-viewport { overflow: visible; }
.gal-viewport .swiper-slide { width: 30.6%; }
.gal-prev, .gal-next {
  position: absolute; top: 50%; margin-top: -28px; z-index: 3;
  width: 56px; height: 56px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--key-yellow); color: var(--ink); font-size: 19px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 14px 34px rgba(23,23,26,0.28); transition: transform 220ms ease;
}
.gal-prev { left: 16px; }
.gal-next { right: 16px; }
.gal-prev:hover { transform: translateX(-3px); }
.gal-next:hover { transform: translateX(3px); }
/* A before/after slide carries its treatment tag under the photo
   (2026-10-20), about 40px, so lift the arrows half that to keep them
   centred on the photo rather than on photo and tag together. */
.gal-arrows-wrap:has(.case-card-tags) > .gal-prev,
.gal-arrows-wrap:has(.case-card-tags) > .gal-next { margin-top: -48px !important; }

/* ==========================================================================
   Before/after case grid + treatment filter (/before-and-afters/)
   --------------------------------------------------------------------------
   The one page that shows every case at once, so a grid rather than the
   carousel above: a visitor comparing cases wants to see them side by side,
   not one at a time. initCaseFilter() in js/site.js does the filtering.

   Markup contract:
     [data-case-filter]               the section holding both halves
       .case-filter > button[data-filter="slug"]   "all" is the reset
       .case-grid > .case-card[data-treatments="slug slug"]

   A filter button whose treatment matches no case is hidden by the JS, not
   removed from the markup. The doc names ten filters and the practice has
   cases for five of them today; the other five come back on their own the
   moment a case tagged with them is added, with no change here.

   Hiding a card is `hidden`, so it leaves the grid and the accessibility
   tree together. On a phone the bar becomes a dropdown (below) rather than
   wrapping onto four rows above the thing it filters.
   ========================================================================== */
.case-filter {
  display: flex; flex-wrap: wrap; gap: 10px;
  margin: 0 0 30px; padding: 0; list-style: none;
}
.case-filter-btn {
  appearance: none; border: 1px solid var(--hairline-alt); background: var(--white);
  color: var(--ink); font: inherit; font-size: 14px; line-height: 1;
  padding: 12px 18px; border-radius: 100px; cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 8px;
  transition: background 200ms ease, border-color 200ms ease, color 200ms ease;
}
.case-filter-btn:hover { background: var(--tab-bg); border-color: var(--hairline-alt); }
.case-filter-btn[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.case-filter-btn[hidden] { display: none; }
.case-filter-count { font-size: 12px; color: var(--muted); transition: color 200ms ease; }
.case-filter-btn[aria-pressed="true"] .case-filter-count { color: var(--key-yellow); }

.case-grid {
  display: grid; gap: 24px;
  grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr));
}
.case-card { margin: 0; }
.case-card[hidden] { display: none; }
.case-card-media {
  position: relative;
  border-radius: 18px; overflow: hidden; background: var(--placeholder-bg);
  aspect-ratio: 1 / 1;
}
.case-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.case-card-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 0; padding: 0; list-style: none; }
.case-card-tag {
  background: var(--key-yellow); color: var(--ink); font-size: 12px; font-weight: 500;
  padding: 6px 12px; border-radius: 100px; white-space: nowrap;
}

/* The phone dropdown initCaseFilter() builds from the buttons (DCB-123,
   DCB-127, 2026-10-08). The sideways-scrolling row it replaces hid most of
   the filters off the edge of the screen. The X clears back to "all" and
   is `hidden` while nothing is filtered. */
.case-filter-select { display: none; }

@media (max-width: 767px) {
  .case-filter { display: none; }
  .case-filter-select { display: flex; align-items: center; gap: 10px; margin: 0 0 26px; }
  .case-filter-clear {
    flex: none; width: 46px; height: 46px; border-radius: 50%; border: 0; cursor: pointer;
    background: var(--ink); color: var(--paper); font: inherit; font-size: 22px; line-height: 1;
    display: flex; align-items: center; justify-content: center;
  }
  .case-filter-clear[hidden] { display: none; }
  .case-filter-dropdown {
    flex: 1; min-width: 0; height: 46px; appearance: none; -webkit-appearance: none;
    border: 1px solid var(--hairline-alt); border-radius: 100px; cursor: pointer;
    color: var(--ink); font: inherit; font-size: 16px; padding: 0 46px 0 20px;
    background: var(--white); text-overflow: ellipsis;
  }
  /* The chevron, drawn on the wrapper because a <select> takes no
     pseudo-elements. */
  .case-filter-select { position: relative; }
  .case-filter-select::after {
    content: ""; position: absolute; right: 22px; top: 50%; width: 8px; height: 8px; margin-top: -6px;
    border-right: 1.6px solid var(--ink); border-bottom: 1.6px solid var(--ink);
    transform: rotate(45deg); pointer-events: none;
  }
  .case-grid { gap: 28px; }
}

/* ==========================================================================
   Peer testimonial video carousel + parallax photos
   ========================================================================== */
.peer-viewport .swiper-slide { width: 30.6%; }
.peer-prev, .peer-next {
  position: absolute; top: 50%; margin-top: -26px; z-index: 3;
  width: 52px; height: 52px; border-radius: 50%; border: 0; cursor: pointer;
  background: var(--key-yellow); color: var(--ink); font-size: 18px;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 14px 34px rgba(23,23,26,0.28);
  transition: background 220ms ease, transform 220ms ease;
}
.peer-prev { left: 16px; }
.peer-next { right: 16px; }
.peer-prev:hover { transform: translateX(-3px); }
.peer-next:hover { transform: translateX(3px); }
.peer-prev[disabled], .peer-next[disabled] { cursor: default; }
/* Invisalign-only: arrows hidden on desktop, shown at mobile widths via [data-m~="marrow"] */
[data-m~="marrow"].peer-prev, [data-m~="marrow"].peer-next { display: none; }

.vid-play-btn[hidden] { display: none !important; }

/* Hover-preview loop: a short, muted, looping clip layered over a video
   testimonial's poster so hovering hints there's video behind it before
   you click play. JS plays/pauses it on mouseenter/mouseleave; it's only
   ever shown on hover-capable devices (touch just sees the static poster),
   and it hides itself once the real video is actually playing. */
.vid-card { position: relative; }
.vid-hover-loop {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; pointer-events: none; opacity: 0;
  transition: opacity 260ms ease;
}
@media (hover: hover) {
  .vid-card:hover .vid-hover-loop { opacity: 1; }
}
.vid-card.is-playing .vid-hover-loop { opacity: 0 !important; }

/* book/consultation-001's new-enquiries video and the form beside it
   (George, 2026-10-20). The video is shot 9:16. On a phone it stays a
   340px 9:16 card; from 768px up a 9:16 card that fits on screen looked
   too thin, so it widens and becomes a 4:5 centre crop, which still shows
   the whole face. 768-900px is the stacked layout, so it is capped there
   rather than running the full width of a tablet. */
.consult-vid { max-width: 340px; }
.consult-vid .vid-card { aspect-ratio: 9 / 16; }
@media (min-width: 768px) {
  .consult-vid { max-width: 520px; }
  .consult-vid .vid-card { aspect-ratio: 4 / 5; }
}
@media (min-width: 901px) {
  .consult-vid { max-width: none; }
}
/* Two columns from 901px: the form column is shorter than the copy and
   video beside it, so the form sticks under the header while the left
   column scrolls past instead of leaving a blank column. Only on screens
   tall enough to show the whole form under the header, so its submit
   button can never be stuck below the fold. */
@media (min-width: 901px) and (min-height: 820px) {
  .consult-form-col { align-self: stretch; }
  .consult-form-col > #form { position: sticky; top: 120px; }
}

/* ==========================================================================
   "How it works" / "What happens next" scroll-highlighted steps
   ========================================================================== */
[data-step] {
  background: var(--tab-bg);
  transform: scale(1);
  transition: transform 420ms cubic-bezier(.25,.46,.45,.94), background 420ms ease;
}
[data-step].active { background: var(--tab-bg-hover); transform: scale(1.025); }
.step-num { color: rgba(23,23,26,0.13); transition: color 420ms ease; }
[data-step].active .step-num { color: var(--key-yellow); }

/* ==========================================================================
   Invisalign hero USP accordion
   ========================================================================== */
/* The hero photo's clipping frame. It exists so the photo can parallax
   inside the hero without the drift showing at the top or bottom edge —
   the hero itself cannot do that job on a phone, where it has to go
   overflow: visible so the USP cards can drop out of it and stack below. */
.heroshot-media { position: absolute; inset: 0; overflow: hidden; }
/* A photo that can't be cropped to the band's width (DCB-184): shown
   whole, with a blurred copy of itself filling the sides. */
.heroshot-media--fit .heroshot-fill { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: blur(28px) brightness(0.88); }
/* On a phone the 300px band is nearly the photo's own shape: fill it. */
@media (max-width: 480px) { .heroshot-media--fit .heroshot-photo { object-fit: cover !important; object-position: 50% 30% !important; } }

.hero-tab {
  position: relative;
  text-align: left; font: inherit; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  border: 0; cursor: pointer; background: rgba(250,248,244,0.94); backdrop-filter: blur(6px);
  border-radius: 16px 16px 0 0; padding: 16px 18px 20px; min-height: 88px; color: var(--ink);
  box-shadow: 0 -6px 20px rgba(23,23,26,0.10);
  transition: transform 260ms var(--rv-ease), background 220ms ease;
}

/* The skirt. These cards sit flush on the bottom edge of the hero photo,
   so lifting one on hover used to open a 6px window onto the photo behind
   it — the card read as a loose chip floating over the image rather than
   as a card being pulled up out of the stack. The pseudo-element hangs
   below the card in the card's own colour (`background: inherit` tracks
   both states, including mid-transition), and the hero's own
   `overflow: hidden` clips whatever of it is not needed. So at rest there
   is nothing to see, and on hover the card slides up out of more of
   itself. Height only has to beat the lift distance. */
.hero-tab::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 24px;
  background: inherit;
  pointer-events: none;
}
/* Stacked under the photo (≤900px) the cards ran the full width of the
   screen, edge to edge. From mobile landscape down they sit at 98%, with a
   sliver of paper either side, on every page that uses this hero. */
@media (max-width: 767px) {
  [data-m~="heroshot"] [data-m~="abs"] { width: 98% !important; margin-left: auto !important; margin-right: auto !important; }
}
.hero-tab:hover { transform: translateY(-6px); }
.hero-tab.active { background: var(--key-yellow); }
.hero-tab-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; min-height: 48px; }
.hero-tab-title { font-size: 21px; font-weight: 700; letter-spacing: -0.035em; line-height: 1.1; }
.hero-tab-dot {
  width: 26px; height: 26px; flex: none; border-radius: 50%;
  background: var(--key-yellow); color: var(--ink);
  display: flex; align-items: center; justify-content: center; font-size: 15px; line-height: 1;
  transition: background 220ms ease, color 220ms ease;
}
.hero-tab.active .hero-tab-dot { background: var(--ink); color: var(--key-yellow); }

/* Opening a card used to be display:none -> block, which nothing can ease:
   the blurb appeared and the card jumped to its new height in one frame.
   The 0fr -> 1fr grid row is the one way to animate to a height the content
   decides for itself. `visibility` keeps a shut card's copy out of the
   accessibility tree without cutting the transition short — 0s delayed to
   the end of the close, 0s immediately on the way open. */
.hero-tab-blurb {
  display: grid;
  grid-template-rows: 0fr;
  padding-top: 0;
  opacity: 0;
  visibility: hidden;
  font-size: 13.5px; line-height: 1.45; color: var(--body-text);
  transition: grid-template-rows 420ms var(--rv-ease), padding-top 420ms var(--rv-ease),
              opacity 240ms ease, visibility 0s linear 420ms;
}
.hero-tab-blurb > span { overflow: hidden; min-height: 0; }
.hero-tab.active .hero-tab-blurb {
  grid-template-rows: 1fr;
  padding-top: 8px;
  opacity: 1;
  visibility: visible;
  transition: grid-template-rows 420ms var(--rv-ease), padding-top 420ms var(--rv-ease),
              opacity 300ms ease 120ms, visibility 0s;
}

/* ==========================================================================
   Find us — "open this in your maps app" buttons
   --------------------------------------------------------------------------
   Sat over the bottom of the map image on the Find us card, which appears
   on the homepage, the Invisalign page and the consultation page. Two
   buttons rather than one, and both carrying their own brand mark, because
   the answer to "which maps app?" is the visitor's, not ours: a link that
   guesses wrong sends an iPhone to a web page instead of to turn-by-turn
   directions. The map image's own wrapper carries position: relative.
   ========================================================================== */
.map-actions {
  position: absolute;
  left: 20px;
  right: 20px;
  bottom: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.map-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  background: var(--white);
  color: var(--ink);
  font-size: 14px;
  line-height: 1;
  padding: 12px 18px;
  border-radius: 100px;
  white-space: nowrap;
  box-shadow: 0 10px 26px -12px rgba(23,23,26,0.55);
  transition: transform 180ms ease, box-shadow 180ms ease;
}
.map-btn:hover {
  color: var(--ink);
  transform: translateY(-2px);
  box-shadow: 0 16px 32px -14px rgba(23,23,26,0.6);
}
.map-btn-icon { width: 17px; height: 17px; flex: none; display: block; }

@media (max-width: 480px) {
  .map-actions { left: 14px; right: 14px; bottom: 14px; gap: 8px; }
  .map-btn { font-size: 13px; padding: 11px 15px; gap: 8px; }
}

/* ==========================================================================
   style-hover -> real hover rules
   320 attributes across the 4 source pages -> 25 unique values -> 25 rules.
   Every rule below needs !important: the elements that carried style-hover
   also carry an inline style attribute, which otherwise always wins.
   ========================================================================== */
/* style-hover="color: var(--paper);"  x92 */
.hv-1:hover { color: var(--paper) !important; }
/* style-hover="background: var(--hover-bg-light);"  x40 */
.hv-2:hover { background: var(--hover-bg-light) !important; }
/* style-hover="transform: translateY(-2px);"  x33 */
.hv-3:hover { transform: translateY(-2px) !important; }
/* hv-4, hv-7, hv-8 and hv-10 (border-color/transform+background+colour combos)
   have been consolidated into the .link-arrow component below — every
   instance that used them was the same "icon circle + underlined text"
   pattern, just with different default colours. See that block for the
   equivalent rules. */
/* style-hover="color: var(--ink);"  x16 */
.hv-5:hover { color: var(--ink) !important; }
/* style-hover="background: var(--darker); transform: translateX(6px);"  x16 */
.hv-6:hover { background: var(--darker) !important; transform: translateX(6px) !important; }
/* style-hover="transform: translateX(3px);"  x10 (also: the .link-arrow-icon--nudge variant below) */
.hv-9:hover { transform: translateX(3px) !important; }
/* style-hover="background: var(--key-yellow); color: var(--ink); border-color: var(--key-yellow);"  x8 */
.hv-11:hover { background: var(--key-yellow) !important; color: var(--ink) !important; border-color: var(--key-yellow) !important; }
/* style-hover="transform: translateX(-3px);"  x6 */
.hv-12:hover { transform: translateX(-3px) !important; }
/* style-hover="transform: scale(1.05);"  x5 */
.hv-13:hover { transform: scale(1.05) !important; }
/* style-hover="transform: translateY(-4px); background: var(--tab-bg-hover);"  x4 */
.hv-14:hover { transform: translateY(-4px) !important; background: var(--tab-bg-hover) !important; }
/* style-hover="border-color: var(--paper);"  x4 */
.hv-15:hover { border-color: var(--paper) !important; }
/* style-hover="background: var(--dark-surface); border-color: var(--link-hover);"  x4 */
.hv-16:hover { background: var(--dark-surface) !important; border-color: var(--link-hover) !important; }
/* style-hover="background: var(--ink); color: var(--key-yellow);"  x4 */
.hv-17:hover { background: var(--ink) !important; color: var(--key-yellow) !important; }
/* style-hover="transform: scale(1.06);"  x3 */
.hv-18:hover { transform: scale(1.06) !important; }
/* style-hover="background: var(--key-yellow);"  x3 */
.hv-19:hover { background: var(--key-yellow) !important; }
/* style-hover="transform: translateY(-6px);"  x1 */
.hv-20:hover { transform: translateY(-6px) !important; }
/* style-hover="transform: translateY(-2px); background: var(--dark-surface);"  x1 */
.hv-21:hover { transform: translateY(-2px) !important; background: var(--dark-surface) !important; }
/* style-hover="transform: translateX(14px); background: var(--wave-tint-1);"  x1 */
.hv-22:hover { transform: translateX(14px) !important; background: var(--wave-tint-1) !important; }
/* style-hover="transform: translateX(14px); background: var(--wave-tint-2);"  x1 */
.hv-23:hover { transform: translateX(14px) !important; background: var(--wave-tint-2) !important; }
/* style-hover="transform: translateX(14px); background: var(--wave-tint-3);"  x1 */
.hv-24:hover { transform: translateX(14px) !important; background: var(--wave-tint-3) !important; }
/* style-hover="transform: translateX(14px); background: var(--wave-tint-4);"  x1 */
.hv-25:hover { transform: translateX(14px) !important; background: var(--wave-tint-4) !important; }
/* footer "Website by Xcelerator Dental" credit link  x9 */
.hv-26:hover { color: #FFD102 !important; }

/* ==========================================================================
   Link-arrow component
   A text link with a small circular arrow icon next to it — "See the full
   gallery", "More about Cameron", "Dentist referrals", etc. Used ~35 times
   across the site; every instance is this same shape, previously scattered
   across four different numbered hv-N utility classes with no shared name.
   One component now: `.link-arrow` on the <a>, `.link-arrow-icon[--variant]`
   on the circle, `.link-arrow-text[--variant]` on the underlined text.
   Hovering ANYWHERE on the link — the text included, not just the icon —
   triggers the icon's colour change and nudge, via the .link-arrow:hover
   descendant selectors below (each icon/text also keeps its own self-hover
   rule, for the rare case one is used on its own outside a .link-arrow).
   Icon variants (what the circle looks like before you hover it):
     .link-arrow-icon            ink circle              -> turns yellow
     .link-arrow-icon--yellow    yellow circle            -> turns ink
     .link-arrow-icon--invert    paper circle (dark section) -> turns yellow
     .link-arrow-icon--nudge     ink circle, no colour swap -> just shifts
   Text variants:
     .link-arrow-text            default -> underline turns yellow
     .link-arrow-text--invert    used on yellow-background cards -> turns paper
   ========================================================================== */
.link-arrow:hover .link-arrow-icon,
.link-arrow-icon:hover {
  transform: translateX(3px) !important;
  background: var(--key-yellow) !important;
  color: var(--ink) !important;
}
.link-arrow:hover .link-arrow-icon--yellow,
.link-arrow-icon--yellow:hover {
  transform: translateX(3px) !important;
  background: var(--ink) !important;
  color: var(--paper) !important;
}
.link-arrow:hover .link-arrow-icon--invert,
.link-arrow-icon--invert:hover {
  transform: translateX(3px) !important;
  background: var(--key-yellow) !important;
}
.link-arrow:hover .link-arrow-icon--nudge,
.link-arrow-icon--nudge:hover {
  transform: translateX(3px) !important;
}
.link-arrow:hover .link-arrow-text,
.link-arrow-text:hover {
  border-color: var(--key-yellow) !important;
}
.link-arrow:hover .link-arrow-text--invert,
.link-arrow-text--invert:hover {
  border-color: var(--paper) !important;
}

/* ==========================================================================
   home-006 — "I will never sell you treatment you do not need"
   --------------------------------------------------------------------------
   The section was ~35% shorter and the copy column sat in a lot of dead
   space with nothing to explain it. It's now deliberately tall, and the
   heading parallaxes (data-parallax on the <h2>) as you scroll through it,
   so the space reads as room for the movement rather than as a gap. Both
   halves collapse back to ordinary padding once the grid stacks.
   ========================================================================== */
.h006-panel { min-height: 890px; }
.h006-copy { padding: 150px 0 175px; }
@media (max-width: 900px) {
  .h006-panel { min-height: 0 !important; }
  .h006-copy { padding: 56px 0 72px; }
}

/* The yellow-panel band (home-006 and the six sections built on it,
   marked [data-m~="panelM"]). Stacked, the panel used to come first, so
   the visitor met its supporting copy and CTA before the heading it
   supports. Once the band stacks, the heading and paragraph lead and the
   panel follows, still sitting flush on the band's bottom edge as it does
   beside them on desktop. The copy column's own bottom padding was sized
   for the panel being beside it, so it gives way to an ordinary gap. */
@media (max-width: 900px) {
  [data-m~="panelM"] { gap: 36px !important; }
  [data-m~="panelM"] > :first-child { order: 2; }
  [data-m~="panelM"] > :last-child { padding-bottom: 0 !important; }
}

/* The dark numbered-list band (home-008 "Six reasons" and the twenty
   sections built on it, all 96/104px in the markup). On a phone the list
   ends with the CTA row, so the full 104px under it read as the section
   running on after it had finished. */
@media (max-width: 767px) {
  [data-m~="pad"][style*="padding: 96px 40px 104px"] { padding-top: 76px !important; padding-bottom: 64px !important; }
}

/* ==========================================================================
   ActiveCampaign embedded forms — `.ac-form`
   --------------------------------------------------------------------------
   The lead form on /book/consultation is a real ActiveCampaign form (account
   `cameronbagry`, form id 23), injected at runtime by
   `https://cameronbagry.activehosted.com/f/embed.php?id=23`. We do not own
   its markup: AC writes the whole <form>, its fields, its error tooltips and
   its own <style> block into `<div class="_form_23">`.

   Everything below re-skins that markup as the dark card the page was
   designed with (see design-source/Book a consultation.dc.html) — same input
   sizes, radii, colours, radio treatment and yellow pill button the
   hand-built placeholder form used before it.

   Two things about AC's stylesheet dictate how these rules are written, and
   neither is optional:

   1. **Specificity.** Every AC declaration is `#_form_6AAC0C2659835_ … !important`
      — an id. An id outranks any number of classes, so `.ac-form ._form input`
      can never win, `!important` or not. Each rule here is therefore anchored
      on `#form .ac-form` (the card's own id + the wrapper class), which is one
      class *above* AC's own selectors, and repeats `!important`.
   2. **Source order.** AC's <style> is injected into the body at runtime, so it
      lands *after* this file. Ties go to AC — which is the other reason the
      selectors below are deliberately one step stronger rather than equal.

   AC's CSS also references `--_form-properties---*` custom properties that only
   exist inside AC's own designer. Undefined here, so those declarations resolve
   to `unset` — which for `color` means *inherit*. That is why placeholder colour
   is set explicitly below: left alone, placeholder text inherits the input's own
   colour and reads as a filled-in value.

   The wrapper class is `.ac-form` so a second AC form (the other /book/ pages in
   §1.2) can reuse all of this by dropping the same wrapper inside its own
   `#form` card.
   ========================================================================== */

/* AC sets Outfit (pulled from fonts.bunny.net by an @import inside its own
   injected <style>) on the form, its labels, its disclaimer and its error
   tooltips. One rule puts the site's face back on all of it, including any
   element AC adds to the form later. */
#form .ac-form ._form,
#form .ac-form ._form * {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}

/* The form, and the shell AC wraps it in. AC pads and centres both; the card
   around them already provides the padding. */
#form .ac-form ._form,
#form .ac-form ._form-content,
#form .ac-form ._form-inner {
  margin: 0 !important;
  padding: 0 !important;
  max-width: none !important;
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  color: var(--paper) !important;
  font-size: 15.5px !important;
  line-height: 1.5 !important;
}

/* One field per row, 22px apart — the gap the placeholder form's flex column
   used. `._form_element` rows that hold nothing but a hidden input (AC ships
   six: the five UTM fields plus its spam honeypot) would otherwise each add an
   empty 22px row. */
#form .ac-form ._form-content {
  display: flex !important;
  flex-direction: column !important;
  gap: 22px !important;
}
#form .ac-form ._form ._form_element {
  margin: 0 !important;
  padding: 0 !important;
  max-width: none !important;
}
#form .ac-form ._form ._form_element:has(> input[type="hidden"]) {
  display: none !important;
}

/* AC prints each group's title twice: once as an `._html-code` heading above
   the fieldset, once as the fieldset's own <legend>. Keep the legend — it is
   the one a screen reader announces with the group — and drop the heading and
   its wrapper row. */
#form .ac-form ._form ._form_element:has(> ._html-code > .crm_form-header) {
  display: none !important;
}

/* Text inputs — the placeholder form's exact spec. AC forces height:60px and
   20px side padding; both are overridden here. The textarea selector carries
   `._form_element` because AC's own
   `#_form_… ._form_element :is(textarea, input[type="text"], …)` sets the text
   colour, and with a bare `textarea` the two tie on specificity and AC's,
   injected later, wins: the referral form's address box came out navy on
   the dark card. The input selectors already outrank it through `[type]`. */
#form .ac-form ._form input[type="text"],
#form .ac-form ._form input[type="email"],
#form .ac-form ._form input[type="tel"],
#form .ac-form ._form ._form_element textarea {
  display: block !important;
  width: 100% !important;
  height: auto !important;
  font-size: 15.5px !important;
  font-weight: 400 !important;
  letter-spacing: normal !important;
  color: var(--paper) !important;
  background: var(--dark-border-alt-2) !important;
  border: 1px solid var(--dark-border-alt) !important;
  border-radius: 12px !important;
  padding: 15px 16px !important;
  margin: 0 !important;
  outline: none !important;
}
#form .ac-form ._form input[type="text"]:focus,
#form .ac-form ._form input[type="email"]:focus,
#form .ac-form ._form input[type="tel"]:focus,
#form .ac-form ._form ._form_element textarea:focus {
  border-color: var(--key-yellow) !important;
}
/* Date + time pairs (AC form 9, the team's phone-call form). AC renders them
   as a native date and time input side by side at 69% / 29% with its own
   borders, which on this dark card came out near-invisible. Here they take
   the text inputs' box exactly, sit in a two-column row with the same gap
   the card uses, and `color-scheme: dark` turns the browser's own calendar
   and clock icons light so they show on the dark fill. The two-column row is
   keyed on the time input, so a date on its own (the referral form's date of
   birth) keeps the full width. */
#form .ac-form ._form ._form_element:has(> ._field-wrapper > input[type="time"]) > ._field-wrapper {
  display: grid !important;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: 10px;
}
#form .ac-form ._form input[type="date"],
#form .ac-form ._form input[type="time"] {
  display: block !important;
  width: 100% !important;
  min-width: 0 !important;
  height: auto !important;
  min-height: 52px;
  font: inherit !important;
  font-size: 15.5px !important;
  font-weight: 400 !important;
  letter-spacing: normal !important;
  color: var(--paper) !important;
  background: var(--dark-border-alt-2) !important;
  border: 1px solid var(--dark-border-alt) !important;
  border-radius: 12px !important;
  padding: 13px 16px !important;
  margin: 0 !important;
  outline: none !important;
  color-scheme: dark;
  -webkit-appearance: none;
  appearance: none;
}
#form .ac-form ._form input[type="date"]:focus,
#form .ac-form ._form input[type="time"]:focus {
  border-color: var(--key-yellow) !important;
}
#form .ac-form ._form input[type="date"]::-webkit-calendar-picker-indicator,
#form .ac-form ._form input[type="time"]::-webkit-calendar-picker-indicator {
  opacity: 0.85;
  cursor: pointer;
}

/* Without this, placeholder text inherits --paper (see the header note) and a
   blank field looks like a completed one. */
#form .ac-form ._form input::placeholder,
#form .ac-form ._form ._form_element textarea::placeholder {
  color: var(--dark-muted) !important;
  font-weight: 400 !important;
  letter-spacing: normal !important;
  opacity: 1 !important;
}

/* Per-field labels. The design uses placeholders, not visible labels, so these
   are hidden the accessible way — AC's own rule is `display: none`, which takes
   them away from screen readers too. */
#form .ac-form ._form ._form_element > label._form-label {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip-path: inset(50%) !important;
  white-space: nowrap !important;
  border: 0 !important;
}

/* Group label (<legend>) — the small tinted caption that sat above the
   "How would you prefer to be contacted?" radios in the original design. */
#form .ac-form ._form ._form-fieldset > legend._form-label {
  display: block !important;
  position: static !important;
  float: none !important;
  width: auto !important;
  height: auto !important;
  clip-path: none !important;
  overflow: visible !important;
  font-size: 13px !important;
  font-weight: 400 !important;
  letter-spacing: 0.02em !important;
  color: var(--tint-b) !important;
  padding: 0 !important;
  margin: 0 0 10px !important;
}
#form .ac-form ._form .field-required {
  color: var(--key-yellow) !important;
}

/* A date field has no placeholder to say what it is (the browser fills the box
   with dd/mm/yyyy), so its label stays visible, as a caption like the one
   above. Only the team's phone-call form (AC form 9) has these. */
#form .ac-form ._form ._form_element:has(input[type="date"]) > label._form-label {
  display: block !important;
  position: static !important;
  width: auto !important;
  height: auto !important;
  clip-path: none !important;
  overflow: visible !important;
  white-space: normal !important;
  font-size: 13px !important;
  font-weight: 400 !important;
  letter-spacing: 0.02em !important;
  color: var(--tint-b) !important;
  padding: 0 !important;
  margin: 0 0 10px !important;
}

/* Radio groups. AC lays its options out in a 2-column grid and swaps the real
   radio for a pair of pseudo-element circles; the design uses native radios at
   18px with accent-color, wrapping inline. Both AC pseudo-elements are removed
   rather than restyled. */
#form .ac-form ._form ._form-fieldset {
  display: block !important;
  border: 0 !important;
  padding: 0 !important;
  margin: 0 !important;
}
#form .ac-form ._form ._row._checkbox-radio {
  display: inline-flex !important;
  align-items: center !important;
  gap: 10px !important;
  width: auto !important;
  margin: 0 26px 12px 0 !important;
  font-size: 15px !important;
  cursor: pointer !important;
}
#form .ac-form ._form ._row._checkbox-radio input[type="radio"],
#form .ac-form ._form ._row._checkbox-radio input[type="checkbox"] {
  display: inline-block !important;
  flex: none !important;
  width: 18px !important;
  height: 18px !important;
  min-width: 0 !important;
  padding: 0 !important;
  margin: 0 !important;
  border: 0 !important;
  border-radius: 0 !important;
  background: none !important;
  accent-color: var(--key-yellow);
  cursor: pointer !important;
}
#form .ac-form ._form ._row._checkbox-radio span,
#form .ac-form ._form ._row._checkbox-radio label {
  display: inline !important;
  width: auto !important;
  margin: 0 !important;
  font-size: 15px !important;
  font-weight: 400 !important;
  line-height: 1.4 !important;
  color: var(--paper) !important;
  cursor: pointer !important;
}
#form .ac-form ._form ._row._checkbox-radio label::before,
#form .ac-form ._form ._row._checkbox-radio input[type="radio"]:checked + span > label::after {
  content: none !important;
  display: none !important;
}

/* Treatment options (`field[13]`, 28 of them in the AC account) are hidden here
   so the list never flashes at full length, and js/site.js's `initAcForm()`
   reveals the handful this page offers, in the order it lists them, by adding
   `.is-shown`. That list is the single source of truth — this rule only stops
   the flash. A form with no `data-treatments` (the team's /book/phone-calls/)
   shows every option AC has, so the rule only applies where a list exists. */
#form .ac-form[data-treatments] ._form ._row._checkbox-radio:has(input[name="field[13]"]):not(.is-shown) {
  display: none !important;
}

/* A page about one treatment (`data-fixed-treatment`, /book/invisalign/)
   does not ask: the whole picker is hidden here, before it can flash, and
   `fixTreatment()` in js/site.js ticks the page's treatment so it is still
   sent to AC. A hidden radio submits like any other. */
#form .ac-form[data-fixed-treatment] ._form ._form_element:has(input[name="field[13]"]) {
  display: none !important;
}

/* Two-step forms (the dentist referral form, `initAcSteps()` in js/site.js).
   AC ships each step with a "STEP 1 / STEP 2" marker and a section title
   ("Patient's Details", "Dentist's Details") as part of the form. The marker
   becomes a small caption with the current step in yellow; the titles become
   the card's subheadings. Step 1 fades out before step 2 is shown. */
#form .ac-form [data-ac-step][hidden] { display: none !important; }
#form .ac-form [data-ac-step] { transition: opacity 300ms var(--rv-ease); }
#form .ac-form [data-ac-step].is-leaving { opacity: 0; }
#form .ac-form [data-ac-step="2"]:not([hidden]) { animation: acStepIn 400ms var(--rv-ease); }
@keyframes acStepIn { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  #form .ac-form [data-ac-step] { transition: none; animation: none !important; }
}
#form .ac-form ._form .step-item-wrapper {
  display: flex !important;
  align-items: center !important;
  gap: 8px !important;
  margin: 0 !important;
  font-size: 12px !important;
  letter-spacing: 0.16em !important;
  text-transform: uppercase !important;
  color: var(--dark-muted) !important;
  white-space: nowrap !important;
}
/* "STEP 1" broke onto two lines on a phone (DCB-124, 2026-10-08): its
   wrapper was a flex item allowed to shrink below its text. */
#form .ac-form ._form .step-item-wrapper > * { flex: none !important; width: auto !important; white-space: nowrap !important; }
#form .ac-form ._form .step-item-wrapper h6,
#form .ac-form ._form .step-item-wrapper p {
  margin: 0 !important;
  font-size: 12px !important;
  font-weight: 400 !important;
  line-height: 1.4 !important;
  letter-spacing: 0.16em !important;
  color: var(--dark-muted) !important;
}
#form .ac-form ._form .step-item-wrapper .p-step-process-is-focal {
  color: var(--key-yellow) !important;
  font-weight: 700 !important;
}
#form .ac-form ._form ._form-title {
  margin: 0 !important;
  font-size: clamp(22px, 2.2vw, 28px) !important;
  font-weight: 700 !important;
  line-height: 1.1 !important;
  letter-spacing: -0.02em !important;
  color: var(--paper) !important;
}
/* Space above any title after the first, so "Dentist's Details" reads as a
   new section of step 1 rather than one more field. */
#form .ac-form ._form ._form_element:has(> ._form-title) ~ ._form_element:has(> ._form-title) {
  margin-top: 18px !important;
}

/* Privacy-policy consent line, immediately above the button. */
#form .ac-form ._form p.p-disclaimer {
  margin: 0 !important;
  font-size: 13px !important;
  line-height: 1.55 !important;
  color: var(--tint-b) !important;
}
#form .ac-form ._form p.p-disclaimer a {
  color: var(--paper) !important;
  text-decoration: none !important;
  border-bottom: 1px solid var(--dark-border-alt) !important;
  cursor: pointer;
}
#form .ac-form ._form p.p-disclaimer a:hover {
  opacity: 1 !important;
  border-bottom-color: var(--key-yellow) !important;
}

/* Submit — the same yellow pill, including the 2px hover lift the rest of the
   site's CTAs use (.hv-3). */
#form .ac-form ._form ._button-wrapper {
  margin: 0 !important;
  padding: 0 !important;
}
#form .ac-form ._form ._submit {
  width: 100% !important;
  margin: 12px 0 0 !important;
  font-size: 16px !important;
  font-weight: 400 !important;
  text-transform: none !important;
  letter-spacing: normal !important;
  background: var(--key-yellow) !important;
  color: var(--ink) !important;
  border: 0 !important;
  border-radius: 100px !important;
  padding: 19px 26px !important;
  cursor: pointer !important;
  transition: transform 180ms ease, opacity 180ms ease !important;
}
/* AC ships the button with its account-wide label ("REQUEST YOUR APPOINTMENT").
   `initAcForm()` rewrites it to this page's wording and adds `.is-labelled`;
   until it does, the text is transparent rather than wrong. The button is
   full-width, so nothing moves when the real label lands. */
#form .ac-form ._form ._submit:not(.is-labelled) {
  color: transparent !important;
}
#form .ac-form ._form ._submit:hover {
  opacity: 1 !important;
  transform: translateY(-2px) !important;
}
#form .ac-form ._form ._submit:disabled {
  transform: none !important;
  opacity: 0.55 !important;
  cursor: not-allowed !important;
}

/* A float clear, from AC's pre-flexbox layout. Here it is a flex item, so it
   adds one more 22px row under the button and pushes the note away from it. */
#form .ac-form ._form ._clear-element {
  display: none !important;
}

/* AC's success message, shown in place of the fields if the redirect below
   ever fails to fire. */
#form .ac-form ._form ._form-thank-you {
  font-size: 16px !important;
  line-height: 1.6 !important;
  color: var(--paper) !important;
}

/* AC's validation tooltips are left as AC draws them (pale red, dark text) —
   they are legible on the dark card and restyling them risks making a failed
   submission invisible. They are absolutely positioned against `._form_element`,
   so only the anchor needs asserting. */
#form .ac-form ._form ._form_element {
  position: relative !important;
}
/* The one exception. A radio group's message is not wrapped in the positioned
   `._error` its field messages use — AC drops a bare `._error-inner` in as the
   row's first child, which in a row laid out as a flex line takes its place in
   the line and shoves the first option sideways. Taken out of flow it behaves
   like every other AC message: it sits over what follows and goes as soon as an
   option is picked. */
#form .ac-form ._form ._row._checkbox-radio > ._error-inner {
  position: absolute !important;
  top: calc(100% + 8px) !important;
  left: 0 !important;
  z-index: 3 !important;
  white-space: nowrap !important;
}

/* ==========================================================================
   Privacy-policy lightbox — `.pp-lightbox`
   --------------------------------------------------------------------------
   The consent line under the form opens /policy/privacy-policy-lightbox (the
   policy with no nav or footer, §1.5) in an overlay instead of navigating
   away, so a half-completed form is never thrown away to read it. Built by
   js/site.js; the iframe's src is only set on first open.
   ========================================================================== */
.pp-lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: var(--scrim);
}
.pp-lightbox[hidden] { display: none; }
.pp-lightbox iframe {
  display: block;
  width: min(900px, 100%);
  height: min(90vh, 100%);
  border: 0;
  border-radius: 16px;
  background: var(--paper);
  box-shadow: 0 40px 90px -40px rgba(23, 23, 26, 0.6);
}
.pp-lightbox-close {
  position: absolute;
  top: 18px;
  right: 18px;
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 180ms ease, background 180ms ease;
}
.pp-lightbox-close:hover {
  transform: scale(1.06);
  background: var(--key-yellow);
}
@media (max-width: 700px) {
  .pp-lightbox { padding: 16px; }
  .pp-lightbox iframe { height: 100%; }
  .pp-lightbox-close { top: 8px; right: 8px; width: 38px; height: 38px; }
}

/* ==========================================================================
   Sticky booking bar — `.sticky-cta`  (§1.21)
   --------------------------------------------------------------------------
   A fixed call + book bar along the bottom of every page, phones only, sliding
   in once the visitor is past the top of the page. The markup is built by
   js/site.js's `initStickyCta()` from one config object — it is a global
   component in the §1.6 sense, and hand-copying it into nine pages is how the
   navbar and footer ended up able to drift.

   **≤767px only.** "Mobile landscape and down" — the same breakpoint the rest
   of this file already treats as phone territory. Above it the header's own
   "Request your consultation" pill is on screen the whole time, so a second
   permanent CTA would be arguing with it.

   The `display: none` default is outside the media query on purpose: at wider
   widths the bar is not laid out at all, so none of its fixed positioning,
   shadow or transition costs anything on the desktop the page is designed on.
   ========================================================================== */
.sticky-cta { display: none; }

@media (max-width: 767px) {
  /* The bar's own height: 1px border + 10px + the 48px button + 10px, plus
     whatever the device keeps for its home indicator. On :root because the bar
     is appended to <body>, so #page is not inside it and could not read a
     variable declared on the bar itself. */
  :root { --sticky-cta-h: calc(69px + env(safe-area-inset-bottom, 0px)); }

  .sticky-cta {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    /* Above the header (2147483003) and so above the hamburger menu that
       opens out of it, so call and book are reachable with the menu open
       (developer instruction, 2026-10-02) — and above the QA pill overlay
       (2147483000, css/qa-id-pills.css), so a pill can never sit on top of
       the only booking button on the screen. */
    z-index: 2147483004;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    /* Home-indicator gap on iOS. Without it the bar's buttons sit in the strip
       the system swipes from, which eats the taps. */
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    background: var(--paper);
    border-top: 1px solid var(--hairline);
    box-shadow: 0 -14px 34px -22px rgba(23, 23, 26, 0.45);
    /* Hidden state. `visibility` rather than opacity alone so a bar that is
       off-screen is also out of the tab order and out of a screen reader's
       way — same pattern as .nav-dropdown-panel above. */
    transform: translateY(100%);
    visibility: hidden;
    transition: transform 320ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
                visibility 0s linear 320ms;
  }
  .sticky-cta.is-visible,
  html.menu-open .sticky-cta {
    transform: none;
    visibility: visible;
    transition: transform 320ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
                visibility 0s;
  }

  /* Reserve exactly the bar's own height at the end of the page. By the time a
     visitor reaches the bottom of any page the bar is showing, so without this
     the footer's last line sits permanently underneath it — and a round number
     instead of the real height leaves either a strip of paper above the bar or,
     on a phone with a home indicator, the same covered line it was meant to
     free. Hence one expression, used by both. */
  #page { padding-bottom: var(--sticky-cta-h); }

  .sticky-cta-call {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: var(--ink);
    color: var(--paper);
    transition: transform 180ms ease;
  }
  .sticky-cta-call:hover { color: var(--paper); transform: translateY(-2px); }
  .sticky-cta-call svg { width: 20px; height: 20px; display: block; }

  .sticky-cta-book {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    min-height: 48px;
    padding: 12px 22px;
    border: 0;
    border-radius: 100px;
    background: var(--key-yellow);
    color: var(--ink);
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 15px;
    line-height: 1.2;
    text-align: center;
    cursor: pointer;
    transition: transform 180ms ease;
  }
  .sticky-cta-book:hover { color: var(--ink); transform: translateY(-2px); }
  .sticky-cta-caret { display: flex; }
  .sticky-cta-caret svg { width: 10px; height: 7px; display: block; transition: transform 220ms ease; }
  .sticky-cta.is-open .sticky-cta-caret svg { transform: rotate(180deg); }
  .sticky-cta-ext { display: flex; }
  .sticky-cta-ext svg { width: 14px; height: 14px; display: block; }

  /* The booking menu, for a practice that takes bookings more than one way.
     It opens upwards, out of the bar — there is nothing below it to open into. */
  .sticky-cta-menu {
    position: absolute;
    left: 16px;
    right: 16px;
    bottom: calc(100% - 4px);
    padding: 10px;
    background: var(--white);
    border: 1px solid var(--hairline);
    border-radius: 20px;
    box-shadow: 0 30px 60px -20px rgba(23, 23, 26, 0.5);
    display: flex;
    flex-direction: column;
    opacity: 0;
    visibility: hidden;
    transform: translateY(10px);
    transition: opacity 240ms ease, transform 240ms ease, visibility 0s linear 240ms;
  }
  .sticky-cta.is-open .sticky-cta-menu {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity 240ms ease, transform 240ms ease, visibility 0s;
  }
  .sticky-cta-menuitem {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 14px 16px;
    border-radius: 12px;
    font-size: 15px;
    color: var(--ink);
  }
  .sticky-cta-menuitem:hover { background: var(--tab-bg); color: var(--ink); }
}

/* Someone who has asked for less motion still gets the bar — it just appears
   rather than travelling. Same opt-out as the reveals (§1.17) and the anchor
   scrolling (§1.20). */
@media (prefers-reduced-motion: reduce) {
  .sticky-cta,
  .sticky-cta.is-visible,
  .sticky-cta-menu,
  .sticky-cta.is-open .sticky-cta-menu { transition: none; }
}

/* ======================================================================== *
 * Policy pages (/policy/privacy-policy/, cookies-policy, terms-of-service)
 * ------------------------------------------------------------------------
 * Long legal copy, so: a readable measure, numbered clauses with the number
 * hanging in its own column, and lettered sub-items laid out the same way.
 * The letter/number is written into the markup rather than generated by CSS
 * counters, so the clause numbering always matches the supplied document.
 * ======================================================================== */
.policy-body { font-size: 15.5px; line-height: 1.7; color: var(--body-text); }
.policy-body h2 {
  margin: 44px 0 14px; font-size: clamp(20px, 2.2vw, 26px); font-weight: 700;
  letter-spacing: -0.025em; line-height: 1.2; color: var(--ink);
}
.policy-body h2:first-child { margin-top: 0; }
.policy-body p { margin: 0 0 14px; }
.policy-body a { color: var(--ink); border-bottom: 1px solid currentColor; overflow-wrap: anywhere; }
.policy-body a:hover { color: var(--link-hover); }
.policy-clause { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 0 10px; margin: 0 0 6px; }
.policy-num { font-weight: 700; color: var(--ink); }
.policy-clause-body > :last-child { margin-bottom: 14px; }
.policy-list { list-style: none; margin: 0 0 14px; padding: 0; }
.policy-list li { display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 0 6px; margin: 0 0 8px; }
.policy-marker { color: var(--muted); }
/* Details still to come from the client — same visible-placeholder convention
   as [[ AWAITING CLIENT COPY ]] (README §6.2), so they can be grepped at go-live. */
.policy-todo { background: var(--key-yellow); color: var(--ink); padding: 0 4px; border-radius: 4px; font-weight: 700; }

/* ======================================================================== *
 * Awaiting-confirmation box
 * ------------------------------------------------------------------------
 * Where a signed-off copy doc leaves a slot as a build instruction rather
 * than copy ("[Insert the confirmed list of what the plan covers…]"), the
 * slot is built and holds this box instead of invented wording. It is the
 * policy pages' "[[ AWAITING CLIENT COPY ]]" box made a component, first
 * needed on /prices-and-finance/cosmetic-warranty-plan/, whose doc says the
 * page must not be published until those slots are filled. The label says
 * what is missing; the line under it is the doc's own instruction, verbatim,
 * so the reviewer sees exactly what was asked for.
 *
 * `--dark` is the same box for an --ink band. Delete the box, not the class,
 * when the confirmed copy arrives: `grep -rn copy-pending` lists every one
 * left, and §4 says none may survive to go-live.
 * ======================================================================== */
.copy-pending {
  border: 1px dashed var(--placeholder-border);
  border-radius: 14px;
  padding: 22px 24px;
  background: var(--hover-bg-light);
  color: var(--ink);
}
.copy-pending-label {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.copy-pending-note {
  margin: 10px 0 0;
  font-size: 14.5px;
  line-height: 1.62;
  color: var(--muted);
  max-width: 70ch;
}
.copy-pending--dark {
  border-color: var(--dark-border-alt);
  background: var(--darker);
  color: var(--paper);
}
.copy-pending--dark .copy-pending-note { color: var(--dark-muted); }

/* ==========================================================================
   Dental education articles (/about-us/dental-education/<slug>/)
   --------------------------------------------------------------------------
   The blog template. One page per article, all five built from the same
   markup, so a change here is a change to every article at once.

     .post-hero                  breadcrumb + tags, H1, standfirst, byline,
                                 then the wide photograph
     .post-layout                the article beside the consultation form
       .post-body                the article itself, 68ch measure
       .post-aside > #form       book-consultation-001's form card, sticky
     .post-cards > .post-card-slot > a.post-card
                                 "Keep reading" on every article, and the
                                 hub's filterable grid (dental-education-003)

   THE ASIDE STICKS ON DESKTOP ONLY. At ≤1024px — the same width the nav
   turns into the hamburger — the layout drops to one column and the form
   sits under the article, unstuck. Sticking it beside a column that narrow
   would leave the article at a phone's measure on a tablet.

   The form card is taller than some laptop screens (AC writes the fields,
   so its height is not ours). initPostAside() in js/site.js sets the
   aside's `top` so it sticks with its *bottom* in view when it cannot fit
   whole — it scrolls with the page until the submit button is on screen,
   then holds there — rather than pinning its top and hiding the button
   below the fold for the length of the article. With JS off it falls back
   to the 120px set here, the FAQ asides' offset (§1.26).

   Same two rules as every other sticky column on the site: the grid is
   `align-items: start`, and neither the aside nor anything above it
   carries data-reveal (§1.17) — the reveals are on the card inside it and
   on the article's own blocks.
   ========================================================================== */
.post-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; margin: 0 0 34px; }
.post-crumb {
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 14px; line-height: 1; color: var(--ink);
}
.post-crumb-icon {
  width: 34px; height: 34px; flex: none; border-radius: 50%;
  border: 1px solid var(--hairline-alt); background: var(--white);
  display: flex; align-items: center; justify-content: center; font-size: 14px;
  transition: transform 200ms ease, background 200ms ease, border-color 200ms ease;
}
@media (hover: hover) {
  .post-crumb:hover .post-crumb-icon { transform: translateX(-3px); background: var(--key-yellow); border-color: var(--key-yellow); }
}
.post-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
.post-tag {
  display: inline-block; background: var(--key-yellow); color: var(--ink);
  font-size: 12px; font-weight: 500; line-height: 1;
  padding: 7px 12px; border-radius: 100px; white-space: nowrap;
  transition: background 200ms ease, color 200ms ease;
}
@media (hover: hover) {
  a.post-tag:hover { background: var(--ink); color: var(--paper); }
}
.post-tag--quiet { background: var(--tab-bg); }

.post-byline { display: flex; align-items: center; gap: 14px; margin: 0; }
.post-byline img {
  width: 48px; height: 48px; flex: none; border-radius: 50%;
  object-fit: cover; object-position: 50% 18%; display: block;
  background: var(--placeholder-bg);
}
.post-byline-text { margin: 0; font-size: 14.5px; line-height: 1.5; color: var(--muted); max-width: 52ch; }
.post-byline-text strong { color: var(--ink); font-weight: 500; }
.post-readtime {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 14px; color: var(--muted); white-space: nowrap;
}
.post-readtime::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--key-yellow);
  box-shadow: 0 0 0 1px var(--ink);
}

.post-heroimg {
  position: relative; overflow: hidden; border-radius: 24px;
  background: var(--placeholder-bg); aspect-ratio: 21 / 9;
}
.post-heroimg img { width: 100%; height: 100%; object-fit: cover; display: block; }

.post-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(340px, 420px);
  gap: clamp(48px, 6vw, 104px);
  align-items: start;
}
.post-aside { position: sticky; top: 120px; }

.post-body { max-width: 68ch; font-size: 18px; line-height: 1.72; color: var(--body-text); }
.post-body > p { margin: 0 0 1.1em; }
.post-body > p:first-child::first-line { color: var(--ink); }
.post-body h2 {
  margin: 1.9em 0 0.55em; color: var(--ink);
  font-size: clamp(28px, 2.7vw, 42px); line-height: 1.02; letter-spacing: -0.035em; font-weight: 700;
}
.post-body h2:first-child { margin-top: 0; }
.post-script { font-family: 'Ms Madi', cursive; font-weight: 400; font-size: 1.24em; letter-spacing: 0; line-height: 0.7; }
.post-body ul {
  margin: 0 0 1.3em; padding: 26px 30px; list-style: none;
  display: grid; gap: 12px;
  background: var(--tab-bg); border-radius: 20px;
  font-size: 17px; line-height: 1.55; color: var(--body-text);
}
.post-body li { display: flex; align-items: flex-start; gap: 14px; }
.post-body li::before {
  content: ""; width: 14px; height: 2px; flex: none; margin-top: 0.78em; background: var(--ink);
}
/* An in-copy link (the doc's underlined words): a highlighter stroke in the
   brand yellow under ink text, filling on hover. Yellow on paper is ~1.1:1,
   so the stroke is decoration only — the text itself stays --ink, and the
   underline that tells you it is a link is the text-decoration under it. */
.post-body a:not([class]) {
  color: var(--ink);
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
  background: linear-gradient(var(--key-yellow), var(--key-yellow)) 0 88% / 100% 0.42em no-repeat;
  transition: background-size 220ms ease;
}
@media (hover: hover) {
  .post-body a:not([class]):hover { background-size: 100% 100%; color: var(--ink); }
}
.post-body .link-arrow { margin: 0.6em 0 0; }

.post-close {
  margin: 56px 0 0; padding: 34px 36px;
  background: var(--white); border: 1px solid var(--hairline); border-radius: 22px;
  display: grid; gap: 22px;
}
.post-close p { margin: 0; font-size: 17px; line-height: 1.62; color: var(--body-text); max-width: 56ch; }

.post-aside-title {
  margin: 0 0 6px; color: var(--paper);
  font-size: clamp(24px, 2vw, 30px); line-height: 1.04; letter-spacing: -0.03em; font-weight: 700;
}

@media (max-width: 1024px) {
  .post-layout { grid-template-columns: minmax(0, 1fr); gap: 64px; }
  .post-aside { position: static; top: auto !important; }
  .post-body { max-width: none; }
}
@media (max-width: 767px) {
  .post-heroimg { aspect-ratio: 4 / 3; border-radius: 18px; }
  .post-body { font-size: 17px; }
  .post-body ul { padding: 22px 22px; font-size: 16px; }
  .post-close { padding: 28px 24px; }
  .post-crumbs { margin-bottom: 26px; }
}

/* Article cards. Same build as .treat-card (white, hairline, 20px, the
   whole card is the link) and the same slot rule: the slot reveals, the
   card hovers, so the lift eases instead of snapping (§1.17). On the hub
   the slot also carries .case-card, which is what initCaseFilter() hides
   and shows — one filter component for the site, not two. */
.post-cards {
  display: grid; gap: 24px;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
}
.post-card-slot { display: flex; }
.post-card-slot[hidden] { display: none; }
.post-card {
  flex: 1; display: flex; flex-direction: column;
  background: var(--white); border: 1px solid var(--hairline); border-radius: 20px;
  overflow: hidden; color: var(--ink);
  transition: transform 300ms var(--rv-ease), border-color 220ms ease, box-shadow 300ms var(--rv-ease);
}
.post-card-media { aspect-ratio: 3 / 2; background: var(--placeholder-bg); overflow: hidden; }
.post-card-media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 520ms var(--rv-ease); }
.post-card-body { display: flex; flex-direction: column; flex: 1; gap: 14px; padding: 18px 20px 18px; }
.post-card-title { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -0.025em; line-height: 1.18; }
.post-card-go { margin-top: auto; padding-top: 6px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.post-card-go-icon {
  width: 36px; height: 36px; flex: none; border-radius: 50%;
  background: var(--key-yellow); color: var(--ink);
  display: flex; align-items: center; justify-content: center; font-size: 16px; line-height: 1;
  transition: transform 200ms ease;
}
@media (hover: hover) {
  .post-card:hover { transform: translateY(-4px); border-color: var(--hairline-alt); box-shadow: 0 20px 40px -24px rgba(23,23,26,0.45); }
  .post-card:hover .post-card-media img { transform: scale(1.045); }
  .post-card:hover .post-card-go-icon { transform: translateX(3px); }
}
.post-card:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* ==========================================================================
   Map photo on a phone (DCB-118, 2026-10-08)
   The practice map is one 1280x692 screenshot on 22 pages. At phone width it
   showed a ~210px-tall strip of the whole neighbourhood, too small to read.
   Taller box, scaled in around the pin, so it reads street by street. Phones
   only; the desktop crop is unchanged.
   ========================================================================== */
@media (max-width: 767px) {
  div:has(> img[alt^="Map showing"]) { overflow: hidden; }
  img[alt^="Map showing"] {
    height: 380px !important;
    object-position: 48% 49%;
    transform: scale(1.4);
    transform-origin: 48% 49%;
  }
}

/* ==================================================================== *
   Landing pages — `<body data-landing>` (/book/invisalign/)
   --------------------------------------------------------------------
   A campaign landing page keeps the visitor on the page (George,
   2026-10-09). Same nav and footer partials as every other page, so
   nothing drifts; this only hides what does not belong. The header keeps
   the logo, the call icon and "Request a consultation" (which the build
   points at the page's own #form). The footer keeps only the phone
   number and the CTA: no columns, no social links, no legal bar. */
[data-landing] .site-nav > :not(.nav-icon-group):not(.nav-cta),
[data-landing] .nav-icon-link--find,
[data-landing] .mobile-menu-panel {
  display: none !important;
}
[data-landing] .site-nav { padding-left: 12px; }

[data-landing] .footer-legal { display: none !important; }
[data-landing] .footer-main {
  display: flex !important;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 14px !important;
  padding: 56px 0 16px !important;
}
/* The two links sit three or four levels deep in different columns.
   Every wrapper that holds one stops being a box of its own, so the two
   links become items of the flex row above; everything else goes. */
[data-landing] .footer-main div:has(.footer-call, .footer-cta) { display: contents !important; }
[data-landing] .footer-main :is(div, p, img, a):not(:has(.footer-call, .footer-cta)):not(.footer-call, .footer-cta) {
  display: none !important;
}
[data-landing] .footer-call,
[data-landing] .footer-cta { font-size: 15px !important; padding: 15px 26px !important; }
