/* ============================================================
   SMILE STUDIO PR — design system
   Palette: monochrome. White / cool grey ramp / near-black. No hue.
   Type by role: Space Grotesk (display) + Instrument Sans (body) + system mono (data)
   ============================================================ */

:root {
  /* ---- GROUNDS ----------------------------------------------------
     Four steps of one cool-neutral ramp. The whole tonal rhythm of the
     site is built from these; no section introduces a hue. Ordered
     lightest to darkest so a section's ground is a value decision, not
     a colour decision. */
  --bone: #f5f6f7;      /* page ground — the default chapter */
  --ivory: #ffffff;     /* raised surfaces: cards, panels, inputs */
  --paper: #ffffff;     /* chapter that lifts BRIGHTER than the ground */
  --sand: #e8eaec;      /* chapter that sits DARKER than the ground */
  --ink: #0e0f11;       /* headline + logotype black */
  --char: #17191c;      /* dark chapters, footer */

  /* ---- THE ONE HUE ------------------------------------------------
     WhatsApp brand green, at the client's request (2 Sep 2026), and the
     only hue anywhere in the palette. It is scoped to the WhatsApp
     button alone so it reads as a channel badge rather than a brand
     colour the site does not otherwise have.

     Paired with --ink, never white: white on #25D366 is 1.98:1 and
     fails AA outright, which is the usual way this button gets built
     wrong. Ink on it is 9.67:1, and it matches how every other button
     already behaves on a dark chapter — light fill, ink label. */
  --wa: #25D366;
  --wa-hover: #1EBE5A;  /* 7.82:1 with --ink, so hover stays legible */

  /* ---- TEXT -------------------------------------------------------
     Both muted greys are AA-checked against the ground they run on:
     --stone 7.7:1 on --bone, --faint 8.9:1 on --char. */
  --stone: #585d64;     /* muted body text on light grounds */
  --faint: #a4aab2;     /* muted body text on dark grounds */

  /* ---- ACCENT -----------------------------------------------------
     In a monochrome system the accent IS contrast, so the three tokens
     that used to carry the brass hue now carry grey VALUES instead.
     Names kept so every downstream rule keeps working; only the role
     changed: hairline / text-on-light / text-on-dark. */
  --brass: #9aa0a8;      /* rules, ticks, hairline marks */
  --brass-ink: #474c53;  /* accent text on light grounds (AA) */
  --brass-light: #bcc2c9; /* accent text on dark grounds (AA on --char) */
  --alert: #ffffff;      /* form validation — monochrome: weight + copy carry it,
                            never colour alone (see .field-error) */
  --line: rgba(14, 15, 17, 0.12);
  --line-dark: rgba(255, 255, 255, 0.14);

  /* ROLE: display — headlines, section titles, the logotype.
     Space Grotesk: an engineered grotesque with squared terminals and a
     wide, measured skeleton. It reads as instrument panel, not as
     fashion masthead, which is the whole point on a clinical site. Set
     at 500, never 700: hierarchy comes from scale and colour, not from
     shouting. Variable 300..700, one request. */
  --display: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  /* --serif kept as an alias so any stray rule that still asks for it
     lands on the display face instead of falling back to Georgia. */
  --serif: var(--display);
  /* ROLE: body — paragraphs, buttons, inputs, nav. Instrument Sans is
     neutral and high-x-height; its job is to disappear under the
     display face. `font-size-adjust` on <body> pins the rendered
     x-height, so do not "fix" the scale by hand. */
  --sans: "Instrument Sans", -apple-system, "Helvetica Neue", sans-serif;
  /* ROLE: data — anything that forms a column or reads as a record:
     the hours table, phone numbers, the service index numerals, the
     method beat numerals, before/after labels. Broadening mono past
     the schedule is the single biggest "this is a clinic, not a
     boutique" move in the type system, and system mono costs zero
     extra requests on a site that already loads with font-display. */
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  /* There is no wordmark FONT role any more. The logotype used to be set
     as live text in Instrument Serif, standing in for the real mark; now
     that the actual lockup is an image in both the nav and the footer,
     that face is not requested at all. Two webfont families, not three. */

  --pad: clamp(1.25rem, 4vw, 3rem);
  --sect: clamp(4.5rem, 11vw, 8.5rem);
  /* ONE radius for the whole site. Clinical surfaces are rectilinear:
     2px is enough to stop an edge looking razor-cut on a retina screen
     and not enough to read as "friendly". Circles that are genuinely
     circular objects (rail ticks, icon discs, the drag handle) keep
     50%; nothing else curves. */
  --radius: 2px;
  --radius-btn: 2px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  background: var(--bone);
}

/* font-size-adjust pins the RENDERED x-height to Inter's x/em ratio
   (0.5459, measured with canvas TextMetrics) so swapping the body face
   never changes the optical size of a paragraph. Headlines opt out
   (font-size-adjust:none below) because there the size is reasoned
   directly in the clamp, not through x-height. */
body {font-size-adjust:0.5459;
  font-family: var(--sans);
  font-size: 1rem;
  line-height: 1.65;
  color: var(--ink);
  /* bone lives on <html> so the fixed ambient layers (z-index -1/-2)
     can float between the page background and the content */
  background: transparent;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  position: relative;
}

img { max-width: 100%; display: block; }

a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; background: none; border: 0; text-align: left; }

::selection { background: var(--ink); color: var(--bone); }

/* skip link — hidden until keyboard focus */
.skip {
  position: fixed;
  top: 0.75rem; left: 0.75rem;
  z-index: 400;
  background: var(--ink);
  color: var(--bone);
  padding: 0.8rem 1.4rem;
  border-radius: var(--radius-btn);
  font-weight: 600;
  font-size: 0.9rem;
  transform: translateY(-200%);
}
.skip:focus { transform: none; }

/* ---------- typography ---------- */

h1, h2, h3, .serif, .display-face {
  font-family: var(--display);
  font-weight: 500;
  font-size-adjust: none;
  line-height: 1.08;
  letter-spacing: -0.025em;
  text-wrap: balance;
}

/* Scale trimmed ~10% from the serif-era clamps. A grotesque at 500 has
   far more ink on the page than a high-contrast serif at the same
   size, so matching the old numbers would have read as shouting.
   Hierarchy is carried by scale + colour, never by weight 700. */
.display {
  font-size: clamp(2.15rem, 5.6vw, 4.4rem);
  letter-spacing: -0.035em;
}

h2, .h2 {
  font-size: clamp(1.65rem, 3.5vw, 2.75rem);
  letter-spacing: -0.03em;
}

h3, .h3 {
  font-size: clamp(1.1rem, 1.9vw, 1.45rem);
  line-height: 1.22;
  letter-spacing: -0.015em;
}

/* Eyebrows are rationed now (see the HTML: at most one per three
   sections). The ones that survive are set in the data face, because
   on this site a small caps label is a record heading, not a flourish.
   The decorative 2rem hairline that used to precede every one of them
   is gone with the rest of the ornament. */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--brass-ink);
  display: block;
  margin-bottom: 1.25rem;
}

.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  line-height: 1.6;
  color: var(--stone);
  max-width: 38em;
}
/* Until smile-makeover.html no section ever ran two ledes in a row, so
   nothing separated them and they set as one solid block. Margin on the
   second and later ones only, so every existing single-lede section keeps
   its spacing exactly as it was. */
.lede + .lede { margin-top: 1.1em; }

.muted { color: var(--stone); }

/* Emphasis inside a headline is weight in the SAME family. The old
   treatment swapped in a serif italic, which is mixed-family emphasis
   and reads as an accident rather than a decision. */
em.brand-em {
  font-style: normal;
  font-weight: 700;
}

/* ---------- layout ---------- */

.wrap {
  width: min(1180px, 100% - 2 * var(--pad));
  margin-inline: auto;
}

section { padding-block: var(--sect); position: relative; }

.section-head { max-width: 46em; margin-bottom: clamp(2.5rem, 6vw, 4.5rem); }
.section-head h2 { margin-bottom: clamp(1rem, 2vw, 1.5rem); }
.section-head h2:last-child { margin-bottom: 0; }

/* ---------- chapter grounds ----------
   Sections change by VALUE, never by hue. Index order reads:
   white-grey (hero, services) - near-black (method) - grey (studio) -
   white (faq) - near-black (booking, footer). Four steps of one ramp,
   so the page has rhythm without ever leaving the palette. */

.dark {
  color: var(--ivory);
  background: var(--char);
}
.dark .lede, .dark .muted { color: var(--faint); }
.dark .eyebrow { color: var(--brass-light); }

.chapter-paper { background: var(--paper); }
.chapter-brass { background: var(--sand); }
.chapter-paper .eyebrow,
.chapter-brass .eyebrow { color: var(--brass-ink); }
.chapter-paper h2,
.chapter-brass h2 { color: var(--ink); }

/* Chapter seams. The site used to cut every boundary with a 64px
   scalloped wave mask: an organic, spa-like device that fought the
   brief on every scroll. Boundaries are now square, and a chapter that
   meets a same-value neighbour gets a single hairline so the seam is
   still legible instead of invisible. --wave-top / --wave-bottom are
   accepted and ignored, so the per-section inline values in the HTML
   stay harmless. */
.edge { position: relative; }
.edge::before, .edge::after { content: none; }

/* hairline only where two light chapters meet and the value step alone
   would be too quiet to register as a boundary */
.chapter-paper + .chapter-brass,
.chapter-brass + .chapter-paper,
.chapter-paper + section:not(.dark):not(.chapter-brass),
.chapter-brass + section:not(.dark):not(.chapter-paper) { border-top: 1px solid var(--line); }

/* ---------- buttons (pill — echoes the clinic's 300px radius) ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 3.25rem;
  padding: 0.9rem 1.9rem;
  border-radius: var(--radius-btn);
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--bone);
  font-weight: 550;
  font-size: 0.95rem;
  letter-spacing: 0.01em;
  transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), background 0.25s;
  touch-action: manipulation;
}
.btn:hover { transform: translateY(-1px); box-shadow: 0 6px 16px -10px rgba(14, 15, 17, 0.4); }
.btn:active { transform: translateY(0); }
.btn svg { transition: transform 0.25s var(--ease); }
.btn:hover svg { transform: translateX(3px); }

.btn.ghost {
  background: transparent;
  color: var(--ink);
}
.btn.ghost:hover { background: rgba(14, 15, 17, 0.05); box-shadow: none; }

.dark .btn { background: var(--ivory); border-color: var(--ivory); color: var(--ink); }
.dark .btn.ghost { background: transparent; color: var(--ivory); border-color: rgba(255, 255, 255, 0.32); }
.dark .btn.ghost:hover { background: rgba(255, 255, 255, 0.08); }

/* demoted secondary CTA — one idea, one primary action per section */
.cta-alt-wrap { margin-top: 1rem; }
.cta-alt {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--stone);
  text-decoration: underline;
  text-decoration-color: var(--line);
  text-underline-offset: 4px;
  transition: color 0.25s;
}
.cta-alt:hover { color: var(--ink); text-decoration-color: var(--brass); }
.dark .cta-alt { color: var(--faint); text-decoration-color: var(--line-dark); }
.final-cta .cta-alt { color: var(--stone); text-decoration-color: var(--line); }
.dark .cta-alt:hover { color: var(--ivory); text-decoration-color: var(--brass); }
.final-cta .cta-alt:hover { color: var(--ink); text-decoration-color: var(--brass); }

:is(a, button, summary, input, select, textarea):focus-visible {
  /* the ring has to clear 3:1 against BOTH the light and the dark
     chapters, which no single mid grey does; ink + a white halo works
     on every ground the site has */
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px var(--ivory);
  border-radius: var(--radius);
}
body.on-dark :is(a, button, summary, input, select, textarea):focus-visible {
  outline-color: var(--ivory);
  box-shadow: 0 0 0 4px var(--ink);
}
.btn:focus-visible { border-radius: var(--radius-btn); }

/* ---------- nav ---------- */

.nav {
  position: fixed;
  top: clamp(0.6rem, 1.5vw, 1.1rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  width: min(1210px, 100% - 1.25rem);
  border-radius: var(--radius-btn);
  border: 1px solid rgba(14, 15, 17, 0.08);
  background: rgba(255, 255, 255, 0.72);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
  box-shadow: 0 14px 36px -22px rgba(14, 15, 17, 0.35);
  transition: background 0.35s, box-shadow 0.35s, border-color 0.35s;
}
.nav.scrolled {
  background: rgba(255, 255, 255, 0.88);
  box-shadow: 0 18px 44px -24px rgba(14, 15, 17, 0.42);
}
/* chapter-aware inversion — an IntersectionObserver toggles body.on-dark
   whenever a dark/ink section occupies the nav's band of the viewport */
body.on-dark .nav {
  background: rgba(14, 15, 17, 0.62);
  border-color: var(--line-dark);
  box-shadow: 0 14px 36px -22px rgba(0, 0, 0, 0.6);
}
body.on-dark .nav.scrolled { background: rgba(14, 15, 17, 0.78); }

/* ---------- brand lockup ----------
   The real identity: the twin-tooth mark with the wordmark across it.

   The asset is pure black plus an alpha channel, so it drops onto the
   light chapters as ink with no CSS at all, and the dark footer only has
   to invert it. That is deliberately NOT done with a CSS mask: a mask
   plus `background: currentColor` fails to a solid filled rectangle on
   any engine that cannot load the mask, which is a far worse failure
   than a plain <img> that simply renders.

   Source was a faded JPEG (nothing in it darker than mid-grey), so the
   tone curve was remapped to restore contrast before the white was keyed
   out. It is a raster. If a vector ever turns up, swap the file and drop
   the fixed width below. */
.brandmark { display: block; }
.brandmark img { width: 100%; height: auto; display: block; }

/* NAV. 124px wide = 56px tall. That number is measured, not picked: the
   wordmark sits in the middle band of a stacked lockup, so constraining
   the lockup by height shrinks the type inside it. Below ~56px the
   wordmark drops under the legibility of the live text it replaced. It
   pushes the bar from 64px to 74px, still inside the 80px ceiling. */
.nav .brandmark { width: 124px; min-height: 0; flex-shrink: 0; }
body.on-dark .nav .brandmark img { filter: invert(1); }

.foot-grid .brandmark { width: 190px; margin-bottom: 1.25rem; min-height: 0; }
footer .brandmark img { filter: invert(1); }

@media (max-width: 900px) { .nav .brandmark { width: 104px; } }
@media (max-width: 600px) { .foot-grid .brandmark { width: 158px; } }

/* ---------- language toggle + offer bar ----------
   The toggle is a plain link to the translated URL. It is set in mono and
   tracked so it reads as a control rather than a sixth nav destination, and it
   keeps a visible border so it is findable without hovering. */
.nav-links .nav-lang a {
  font-family: var(--mono);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  padding: 0.35rem 0.55rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--stone);
  transition: border-color 0.25s, color 0.25s, background 0.25s;
}
.nav-links .nav-lang a:hover { color: var(--ink); border-color: var(--ink); }
.nav-links .nav-lang a::after { display: none; }   /* no underline sweep — it is not a page link */
/* --faint is AA on the flat --char ground it was picked for, but over the
   film hero the nav band composites to about Y=114, where --faint measures
   1.9:1 — the chip all but disappears. The nav links next to it are --ivory
   for the same reason, and the chip has to match them. The border goes up
   too, or the box reads as a rendering artefact rather than a control. */
body.on-dark .nav-links .nav-lang a { color: var(--ivory); border-color: rgba(255, 255, 255, 0.42); }
body.on-dark .nav-links .nav-lang a:hover { color: var(--ink); background: var(--ivory); border-color: var(--ivory); }

/* The offer bar is deliberately NOT a modal. A full-screen language chooser on
   load is an intrusive interstitial — Google counts those against mobile
   rankings — and it would land on top of the film hero. This is a slim,
   dismissible strip that states the offer and gets out of the way. It only
   ever appears when the visitor's browser prefers the other language AND they
   have made no choice yet; site.js writes that choice to localStorage. */
.lang-offer {
  position: fixed;
  left: 50%;
  transform: translateX(-50%) translateY(120%);
  bottom: clamp(0.75rem, 2vw, 1.25rem);
  z-index: 120;
  display: flex;
  align-items: center;
  gap: 0.85rem;
  max-width: calc(100% - 2 * var(--pad));
  padding: 0.7rem 0.8rem 0.7rem 1.1rem;
  background: var(--char);
  color: var(--ivory);
  border: 1px solid var(--line-dark);
  border-radius: var(--radius);
  box-shadow: 0 20px 48px -24px rgba(0, 0, 0, 0.6);
  font-size: 0.9rem;
  transition: transform 0.5s var(--ease), opacity 0.4s;
  opacity: 0;
}
.lang-offer.in { transform: translateX(-50%) translateY(0); opacity: 1; }
.lang-offer p { margin: 0; }
.lang-offer .go {
  color: var(--ivory);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  white-space: nowrap;
}
.lang-offer .dismiss {
  flex-shrink: 0;
  width: 2rem; height: 2rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer;
  color: var(--faint); font-size: 1.1rem; line-height: 1;
  border-radius: var(--radius);
}
.lang-offer .dismiss:hover { color: var(--ivory); background: rgba(255, 255, 255, 0.1); }
@media (max-width: 560px) {
  .lang-offer { flex-wrap: wrap; font-size: 0.85rem; }
}
/* the sticky mobile action bar already owns the bottom of the screen */
@media (max-width: 760px) { .lang-offer { bottom: 5.25rem; } }

body.on-dark .nav-links a:not(.btn) { color: var(--ivory); }
body.on-dark .nav-links a:not(.btn)::after { background: var(--ivory); }
body.on-dark .nav .btn { background: var(--ivory); color: var(--ink); border-color: var(--ivory); }
body.on-dark .nav-toggle span,
body.on-dark .nav-toggle span::before,
body.on-dark .nav-toggle span::after { background: var(--ivory); }

.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1rem;
}
.nav .nav-inner {
  width: 100%;
  padding: 0.55rem clamp(1.1rem, 2.5vw, 1.6rem);
}
.logo {
  line-height: 1;
  transition: opacity 0.35s;
  display: inline-flex;
  align-items: center;
}
.logo:hover { opacity: 0.7; }

.nav-links {
  display: flex;
  align-items: center;
  /* Was a hard 2rem, which is what actually broke the bar between the burger
     breakpoint and ~900px: five items, the language chip and a button need
     675px there and only get 647px, so the CTA label wrapped onto two lines.
     iPad Pro 11" in landscape is 834px, so that band is a real device, not a
     gap between them. Letting the gap breathe recovers ~50px exactly where it
     is needed and leaves the desktop spacing untouched at its 2rem ceiling. */
  gap: clamp(1.25rem, 2.5vw, 2rem);
  list-style: none;
}
.nav-links a:not(.btn) {
  font-size: 0.85rem;
  font-weight: 550;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  position: relative;
  padding: 0.5rem 0;
  transition: color 0.35s;
}
.nav-links a:not(.btn)::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0.15rem;
  width: 100%;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.3s var(--ease), background 0.35s;
}
.nav-links a:not(.btn):hover::after,
.nav-links a[aria-current="page"]::after { transform: scaleX(1); transform-origin: left; }
.nav .btn { min-height: 2.75rem; padding: 0.6rem 1.4rem; font-size: 0.85rem; }

/* ---------- nav submenu — "What we do" ----------
   The parent is a <button>, not an <a>: it navigates nowhere on its own, and
   a link that only opens a menu lies to a screen reader. It repeats the
   .nav-links a:not(.btn) type rules rather than joining that selector list,
   because the underline sweep there is sized to a link box and this one
   carries a caret as well.

   Desktop: opens on hover AND on click/focus, so it works with a mouse, a
   keyboard and a touch laptop alike. Phone: the panel is already a full
   screen list, so the submenu renders inline underneath its parent and
   site.js opens it as the panel opens — the whole point of the group is that
   both children are visible. */
.nav-links .has-sub { position: relative; }
.nav-sub-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.42rem;
  font-family: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 550;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  position: relative;
  padding: 0.5rem 0;
  transition: color 0.35s;
}
.nav-sub-toggle::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0.15rem;
  width: 100%;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.3s var(--ease), background 0.35s;
}
.nav-sub-toggle:hover::after,
.has-sub.is-open > .nav-sub-toggle::after,
.has-sub.has-current > .nav-sub-toggle::after { transform: scaleX(1); transform-origin: left; }
.nav-sub-toggle svg { transition: transform 0.3s var(--ease); flex-shrink: 0; }
.has-sub.is-open > .nav-sub-toggle svg { transform: rotate(180deg); }
body.on-dark .nav-sub-toggle { color: var(--ivory); }
body.on-dark .nav-sub-toggle::after { background: var(--ivory); }

.nav-sub {
  position: absolute;
  top: 100%;
  left: -0.85rem;
  margin-top: 0.5rem;
  min-width: 13.5rem;
  list-style: none;
  display: grid;
  gap: 0.1rem;
  padding: 0.5rem;
  border: 1px solid rgba(14, 15, 17, 0.08);
  border-radius: var(--radius-btn);
  background: rgba(255, 255, 255, 0.94);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  backdrop-filter: blur(18px) saturate(1.5);
  box-shadow: 0 18px 44px -24px rgba(14, 15, 17, 0.42);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-0.35rem);
  transition: opacity 0.25s var(--ease), transform 0.25s var(--ease), visibility 0.25s;
}
.has-sub.is-open > .nav-sub { opacity: 1; visibility: visible; transform: none; }
body.on-dark .nav-sub {
  background: rgba(14, 15, 17, 0.9);
  border-color: var(--line-dark);
}
/* the panel items are a list, not nav rules — the sweeping underline from
   .nav-links a belongs on the bar itself and reads as a glitch in here */
.nav-links .nav-sub a:not(.btn) {
  display: block;
  padding: 0.62rem 0.7rem;
  font-size: 0.78rem;
  line-height: 1.25;
  border-radius: var(--radius);
  transition: background 0.25s, color 0.25s;
}
.nav-links .nav-sub a:not(.btn)::after { display: none; }
.nav-links .nav-sub a:not(.btn):hover,
.nav-links .nav-sub a[aria-current="page"] { background: var(--ink); color: var(--bone); }
body.on-dark .nav-links .nav-sub a:not(.btn):hover,
body.on-dark .nav-links .nav-sub a[aria-current="page"] { background: var(--ivory); color: var(--ink); }

.nav-toggle {
  display: none;
  width: 3rem;
  height: 3rem;
  align-items: center;
  justify-content: center;
}
.nav-toggle span,
.nav-toggle span::before,
.nav-toggle span::after {
  content: "";
  display: block;
  width: 26px;
  height: 2px;
  background: var(--ink);
  position: relative;
  transition: transform 0.3s var(--ease), opacity 0.2s, background 0.35s;
}
.nav-toggle span::before { position: absolute; top: -8px; }
.nav-toggle span::after { position: absolute; top: 8px; }
body.menu-open .nav-toggle span { background: transparent; }
body.menu-open .nav-toggle span::before { transform: translateY(8px) rotate(45deg); }
body.menu-open .nav-toggle span::after { transform: translateY(-8px) rotate(-45deg); }

/* ---------- section rail — wayfinding, recolours over ink ---------- */

.rail {
  position: fixed;
  right: clamp(0.9rem, 2vw, 1.75rem);
  top: 50%;
  transform: translateY(-50%);
  z-index: 90;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.9rem;
}
.rail-dot {
  position: relative;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
}
/* Ticks, not dots. This rail carries real state (which section you are
   in), so it stays; drawn as a measured scale it reads as an instrument
   rather than as the decorative dot-column every agency site ships. */
.rail-dot::before {
  content: "";
  width: 10px;
  height: 1px;
  background: rgba(14, 15, 17, 0.3);
  transition: background 0.3s, width 0.3s var(--ease);
}
.rail-dot.is-active::before { background: var(--ink); width: 20px; }
body.on-dark .rail-dot::before { background: rgba(255, 255, 255, 0.32); }
body.on-dark .rail-dot.is-active::before { background: var(--ivory); }
.rail-dot .rail-label {
  position: absolute;
  right: 130%;
  top: 50%;
  transform: translateY(-50%) translateX(6px);
  white-space: nowrap;
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.35rem 0.6rem;
  font-family: var(--mono);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s, transform 0.25s var(--ease);
}
.rail-dot:hover .rail-label,
.rail-dot:focus-visible .rail-label { opacity: 1; transform: translateY(-50%) translateX(0); }
@media (max-width: 900px) { .rail { display: none; } }

/* mobile progress rail — dot labels don't fit a touch screen, but the
   scroll still needs wayfinding, so phones get a continuous 3px fill
   instead of nothing */
.rail-mobile { display: none; }
@media (max-width: 900px) {
  .rail-mobile {
    display: block;
    position: fixed;
    right: 0.55rem;
    top: 5rem;
    bottom: 5.5rem;
    width: 2px;
    background: rgba(14, 15, 17, 0.12);
    z-index: 90;
    overflow: hidden;
    pointer-events: none;
    transition: background 0.3s;
  }
  body.on-dark .rail-mobile { background: rgba(255, 255, 255, 0.22); }
  .rail-mobile::after {
    content: "";
    position: absolute;
    left: 0; top: 0; width: 100%;
    height: var(--rail-progress, 0%);
    background: var(--ink);
  }
  body.on-dark .rail-mobile::after { background: var(--ivory); }
}

/* ---------- hero — full-bleed waiting-room film ----------
   Rebuilt 31 Aug 2026 to the client's own reference (apaaesthetic.com): the
   footage runs BEHIND the headline and the type sits low-left over it. The
   hero this replaced was an editorial cover — the headline was the picture,
   and the photograph was reduced to a tall column beside it. That version is
   in git at the commit before this one; nothing below reuses it, and the
   waiting-room band that used to sit under the cover is gone because this
   hero is now showing the same footage.

   The one honest difference from the reference: APA's film is a dark studio,
   so their white type needs no help. This room is white. Everything about
   .hero-film__scrim below exists to close that gap. */

.hero--film {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  /* type sits low over the film, as in the reference — not centred */
  justify-content: flex-end;
  /* the global `section` rule applies --sect padding-block; on a full-bleed
     film that shows as ink letterbox bars above and below the footage */
  padding-block: 0;
  padding-bottom: clamp(2.75rem, 8vh, 6rem);
  /* clears the fixed nav on the short/landscape viewports where the type
     block is tall enough to run up under it */
  padding-top: clamp(6rem, 14vh, 9rem);
  overflow: hidden;
  /* what shows for the instant before the poster paints */
  background: var(--ink);
  isolation: isolate;
}

.hero-film { position: absolute; inset: 0; z-index: 0; }
.hero-film__media {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* --op is only doing work on portrait viewports; see the override below */
  object-position: var(--op, 50% 50%);
  display: block;
}

/* THE SCRIM IS LOAD-BEARING, AND ITS STOPS ARE SOLVED, NOT STYLED.

   Measured over the whole loop at the rendered hero size, this room has no
   dark region anywhere: peak luminance runs Y=213..244 in EVERY horizontal
   band, mean Y=139..206. (APA's reference film is a dark studio, which is why
   their white type needs no help and ours needs all of this.)

   Working back from WCAG: white type needs the COMPOSITED ground at Y<=120 to
   clear 4.5:1, and Y<=149 to clear the 3:1 that large text and the inverted
   nav logo need. Against a Y=240 wall that is alpha 0.53 and 0.40 — so the
   scrim is not a vignette here, it is doing real work, and every stop below is
   the alpha some element needs at that height:

     0-8%    the nav band. The logo inverts to white over the film and needs
             3:1 as a graphic; the nav links are text and need 4.5:1.
     26%     nothing sits here, so it opens back up and the room reads as the
             bright room it is. The floor is 0.34 rather than the 0.16 the
             contrast budget would allow: against a flat white wall, a 0.36
             swing over 10% of the height renders as a visible horizontal seam
             across the mural. 0.34 into 0.52 is the widest swing this footage
             takes without banding.
     55-100% the type block. 0.52 by 66% carries the headline at 3:1, 0.62 by
             80% carries the lede, CTA and address at 4.5:1.

   The left-bias layer buys another ~0.34 under the type without touching the
   right half of the frame, which is why the reception desk stays bright.

   Verified by sampling every 6th frame of the loop at the measured position of
   each type box and compositing these exact stops. Re-run that check if the
   footage, the type sizes or the block's position change — the audit script is
   in smile-studio-pr-docs/recut-hero-film.sh. Lighten any stop and something
   stops passing. */
.hero-film__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to right,
      rgba(14, 15, 17, 0.34) 0%,
      rgba(14, 15, 17, 0) 55%),
    linear-gradient(to bottom,
      rgba(14, 15, 17, 0.58) 0%,
      rgba(14, 15, 17, 0.52) 10%,
      rgba(14, 15, 17, 0.34) 26%,
      rgba(14, 15, 17, 0.52) 40%,
      rgba(14, 15, 17, 0.56) 55%,
      rgba(14, 15, 17, 0.62) 75%,
      rgba(14, 15, 17, 0.70) 100%);
}

.hero-film__inner { position: relative; z-index: 1; }

/* The headline runs its own scale, but a smaller one than the cover hero it
   replaces (that one went to 7.2rem). On a film hero the picture is the
   subject and the type is the caption — sized past about 4.6rem it starts
   competing with the room instead of sitting in it. */
/* Deliberately smaller and narrower than the cover hero this replaced (that
   one ran to 7.2rem full-bleed). Two reasons, and neither is taste: on a film
   hero the picture is the subject and the type is the caption — the reference
   sets its heading at 18px — and every extra line of headline is more of the
   clinic the scrim above has to wash out to stay legible. 15ch wraps this to
   three short lines inside the left half of the frame, which is what lets the
   reception desk on the right stay bright. */
.hero--film .display {
  font-size: clamp(1.9rem, min(4.6vw, 8svh), 3.6rem);
  line-height: 1.04;
  letter-spacing: -0.03em;
  margin: 0;
  max-width: 15ch;
  color: var(--ivory);
}

/* .dark resolves the lede to --faint and the eyebrow to --brass-light, both
   of which are AA against the flat --char ground they were picked for and
   NEITHER of which passes over film: --faint on this scrim measures 3.4:1.
   These overrides are the fix, so do not "tidy" them back to the tokens. */
.hero--film .eyebrow { color: var(--ivory); margin-bottom: 1rem; }
.hero--film .lede { color: rgba(255, 255, 255, 0.94); max-width: 34ch; margin: 1.1rem 0 0; font-size: clamp(1rem, 1.35vw, 1.15rem); }
.hero--film .hero-meta { color: rgba(255, 255, 255, 0.74); }
.hero--film .cta-alt { color: rgba(255, 255, 255, 0.88); text-decoration-color: rgba(255, 255, 255, 0.42); }
.hero--film .cta-alt:hover { color: var(--ivory); text-decoration-color: var(--ivory); }

/* Insurance, not the mechanism — the scrim already carries the contrast. This
   only softens the edge where a bright wall passes under a thin stroke. */
.hero--film .eyebrow,
.hero--film .display,
.hero--film .lede,
.hero--film .hero-meta { text-shadow: 0 1px 20px rgba(0, 0, 0, 0.28); }

.hero-act {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1.6rem;
  align-items: center;
  margin-top: clamp(1.5rem, 3.5vh, 2.4rem);
}
.hero-act .cta-alt-wrap { margin: 0; }

/* The reference's CTA is a hairline box, not a filled button: uppercase,
   widely tracked, and it inverts to solid on hover. .dark .btn.ghost already
   gives transparent-with-white-text; this is the tracking and the invert. */
.hero--film .btn.ghost {
  border-color: rgba(255, 255, 255, 0.6);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.78rem;
  font-weight: 600;
  min-height: 3rem;
  padding: 0.85rem 1.7rem;
}
.hero--film .btn.ghost:hover {
  background: var(--ivory);
  border-color: var(--ivory);
  color: var(--ink);
}

.hero-meta {
  font-size: 0.8rem;
  margin: clamp(1.25rem, 3vh, 2rem) 0 0;
  padding-top: clamp(0.9rem, 2.2vh, 1.35rem);
  border-top: 1px solid rgba(255, 255, 255, 0.2);
  /* 46ch, not the 34ch the lede runs to: the address and the split hours are
     one unbroken line of fact, and wrapping "1pm to 5pm" onto its own line
     reads as a second, unrelated statement. */
  max-width: 46ch;
}

/* Pause control. Required, not decorative — WCAG 2.2.2 wants a stop for
   anything auto-playing past five seconds, and this loops forever. It is one
   aria-pressed toggle rather than two buttons so the accessible name stays
   put while the state changes underneath it. */
.hero-film__toggle {
  position: absolute;
  z-index: 2;
  right: clamp(0.9rem, 2vw, 1.6rem);
  bottom: clamp(0.9rem, 2vw, 1.6rem);
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-height: 2.25rem;
  padding: 0.4rem 0.85rem;
  font-size: 0.72rem;
  font-weight: 550;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ivory);
  background: rgba(14, 15, 17, 0.55);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--radius-btn);
  cursor: pointer;
  transition: background 0.25s, border-color 0.25s, opacity 0.25s;
}
.hero-film__toggle:hover { background: rgba(14, 15, 17, 0.78); border-color: rgba(255, 255, 255, 0.4); }
.hero-film__toggle .i-play { display: none; }
.hero-film__toggle[aria-pressed="true"] .i-play { display: block; }
.hero-film__toggle[aria-pressed="true"] .i-pause { display: none; }
/* fades back once the visitor leaves it alone, but never below the 3:1 a
   non-text control needs, and never while focused */
.hero--film:not(:hover) .hero-film__toggle:not(:focus-visible) { opacity: 0.72; }
/* no JS = no working toggle, and nothing is moving anyway */
html.no-js .hero-film__toggle { display: none; }

/* Over the film hero the nav carries no chrome of its own: the scrim's top stop
   already guarantees the inverted logo 3:1 and the links 4.5:1, so the frosted
   pill on top of it is a second, misaligned edge sitting on the picture. The
   backdrop-filter has to go with the background — left on, an 18px blur still
   frosts a rectangle of the film even where nothing is painted over it, which
   is the grey slab this rule exists to remove.

   Scope: .scrolled lands at 24px (site.js), so :not(.scrolled) means the very
   top of the page. The homepage film hero is the only first section on the site
   that is dark, so no other page ever sees this — and one scroll notch restores
   the normal pill everywhere, including here. */
body.on-dark .nav:not(.scrolled) {
  background: transparent;
  /* border:0, not border-color:transparent. A 1px transparent border still
     bounds the element's backdrop, and against a flat wall that renders as a
     ~4% step along the nav's bottom edge — measured, and visible as a faint
     rectangle across the film. box-sizing is border-box, so dropping the
     border does not move the bar. */
  border: 0;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

@media (max-width: 759.98px) {
  /* The portrait rendition is a 4:5 slice cut from x=0 of the square camera
     frame. A phone viewport is nearer 0.46, so `cover` takes another bite out
     of the sides — biasing the window left of centre is what keeps the leading
     "s" of the reception wordmark, which the pan settles on, inside the frame.
     Verified against the last frame of the loop at 390x844. */
  .hero--film { --op: 38% 50%; }
  .hero-film__scrim {
    /* Solved separately from the desktop scrim, against the PORTRAIT rendition,
       because two things change at this width: the left-bias layer is useless
       (the type block spans the full frame, so it would have to darken all of
       it anyway) and the 4:5 crop is brighter than the 16:9 one — it keeps more
       ceiling, and peaks at Y=253 rather than 244.

       That leaves the vertical ramp carrying everything: 0.50 at the top for
       the inverted logo and burger at 3:1, a window through the mural where no
       type sits, then 0.58 from 36% — where the eyebrow starts — down. */
    background: linear-gradient(to bottom,
      rgba(14, 15, 17, 0.52) 0%,
      rgba(14, 15, 17, 0.48) 10%,
      rgba(14, 15, 17, 0.36) 22%,
      rgba(14, 15, 17, 0.58) 36%,
      rgba(14, 15, 17, 0.58) 58%,
      rgba(14, 15, 17, 0.60) 78%,
      rgba(14, 15, 17, 0.66) 100%);
  }
  .hero--film .lede { font-size: 1rem; }
  .hero-act { gap: 0.75rem 1.2rem; }
  .hero-act .btn { width: 100%; }
}

/* A landscape phone has no room for a 100svh film plus five lines of type. */
@media (max-height: 520px) and (orientation: landscape) {
  .hero--film { min-height: 34rem; padding-top: 5.5rem; }
  .hero--film .lede, .hero--film .hero-meta { display: none; }
}

/* ---------- arch figures ----------
   The arch is retired as a shape: a 100rem dome on top of every portrait is a
   spa/boutique motif, and clinical photography sits in a plain rectangle. The
   class name is kept because index #studio and four figures on about.html
   still carry it. It lived inside the hero block until the film hero replaced
   that block on 31 Aug 2026; it is a general figure style, not hero-specific. */
.arch {
  border-radius: var(--radius);
  overflow: hidden;
  aspect-ratio: 4 / 5;
  background: var(--paper);
}
.arch img { width: 100%; height: 100%; object-fit: cover; }
/* wide arch — landscape figures cut to this exact ratio, so nothing is cropped */
.arch.arch-wide { aspect-ratio: 1748 / 1241; }

/* ---------- problem cards ---------- */

.cards-3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
}
.card {
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease);
}
.card:hover { transform: translateY(-2px); border-color: var(--ink); }
.card .k {
  font-family: var(--mono);
  font-size: 1.6rem;
  font-weight: 500;
  color: var(--brass-ink);
  line-height: 1;
  display: block;
  margin-bottom: 1.25rem;
}
.card .k svg { width: 2.4rem; height: 2.4rem; stroke: var(--brass-ink); }
.card h3 { margin-bottom: 0.75rem; }
.card p { color: var(--stone); font-size: 0.95rem; }

/* ---------- services ---------- */

.tag {
  font-family: var(--sans);
  font-size: 0.62rem;
  font-weight: 650;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* was bone-on-grey at 2.4:1, under AA for text this small. Solid ink
     reads as a stamp on a chart and clears AA by a mile. */
  color: var(--ivory);
  background: var(--ink);
  border-radius: var(--radius);
  padding: 0.3em 0.9em;
  vertical-align: middle;
  white-space: nowrap;
}

.icon-disc {
  width: 3.4rem;
  height: 3.4rem;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--bone);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}
.icon-disc svg { width: 1.5rem; height: 1.5rem; stroke: var(--brass-ink); fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }

/* homepage teaser cards */
.svc-mini-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
}
.svc-mini {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.5rem, 3vw, 2rem);
  transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease), border-color 0.35s;
}
.svc-mini:hover {
  transform: translateY(-6px);
  box-shadow: 0 24px 48px -28px rgba(14, 15, 17, 0.35);
  border-color: var(--brass);
}
.svc-mini .head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.svc-mini h3 { font-size: 1.3rem; }
.svc-mini p { color: var(--stone); font-size: 0.92rem; flex-grow: 1; }
.svc-mini .go {
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.svc-mini .go svg { transition: transform 0.3s var(--ease); }
.svc-mini:hover .go svg { transform: translateX(4px); }

/* homepage services — hover-fill row list (a style index, not a
   second card grid, so it doesn't repeat services.html's richer cards) */
.svc-row-list { display: flex; flex-direction: column; }
.svc-row {
  position: relative;
  display: grid;
  grid-template-columns: 2.4rem 1fr auto;
  gap: clamp(1rem, 3vw, 2rem);
  align-items: center;
  padding: clamp(1.1rem, 2.6vw, 1.5rem) 1.1rem;
  border-top: 1px solid var(--line);
  overflow: hidden;
}
.svc-row-list > .svc-row:last-child { border-bottom: 1px solid var(--line); }
.svc-row::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s var(--ease);
  z-index: 0;
}
.svc-row:hover::before, .svc-row:focus-visible::before { transform: scaleX(1); }
.svc-row > * { position: relative; z-index: 1; }
/* The index numerals used to be a serif italic in brass, repeated
   across three components as "the site's signature". Under the new
   system they are what they actually are: an index column. Tabular
   mono makes them line up, which a decorative italic never did. */
.svc-row .num { font-family: var(--mono); font-weight: 500; color: var(--brass-ink); font-size: 0.85rem; letter-spacing: 0.06em; font-variant-numeric: tabular-nums; transition: color 0.4s; }
.svc-row .body h3 { font-size: 1.15rem; margin-bottom: 0.3rem; transition: color 0.4s; display: block; }
.svc-row .body h3 .tag { margin-left: 0.6rem; }
.svc-row .body p { color: var(--stone); font-size: 0.92rem; transition: color 0.4s; max-width: 34em; }
.svc-row .go { color: var(--ink); transition: color 0.4s, transform 0.3s var(--ease); display: inline-flex; }
.svc-row:hover .num { color: var(--brass-light); }
.svc-row:hover .body h3, .svc-row:hover .go { color: var(--ivory); }
/* the row fills with ink on hover, so the ink tag has to invert with it
   or it disappears into the fill */
.svc-row:hover .tag, .svc-row:focus-visible .tag { background: var(--ivory); color: var(--ink); }
.svc-row:hover .body p { color: var(--faint); }
.svc-row:hover .go { transform: translateX(4px); }
@media (max-width: 700px) {
  .svc-row { grid-template-columns: 2rem 1fr; }
  .svc-row .go { display: none; }
}

/* services page — featured block */
.svc-feature {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: calc(var(--radius) * 1.4);
  padding: clamp(1.5rem, 4vw, 3.5rem);
}
.svc-feature-media {
  border-radius: var(--radius);
  overflow: hidden;
  aspect-ratio: 4 / 3;
}
.svc-feature-media img { width: 100%; height: 100%; object-fit: cover; }
.svc-feature h2 { margin-bottom: 1.1rem; }
.svc-feature p { color: var(--stone); margin-bottom: 1rem; max-width: 36em; }

/* ---------- services timeline — one brass spine, one card per treatment.
   Every stop is equal: veneers sits in the studio's own order with a Signature
   tag rather than getting a separate feature block above. The photo is the
   visual anchor of each card (~45% of it) but never crowds the copy out.
   --fill / --track are written by site.js. ---------- */
.svc-timeline { position: relative; --spine: 3.25rem; }
.svc-timeline::before,
.svc-timeline::after {
  content: "";
  position: absolute;
  left: calc(var(--spine) / 2);
  top: 0;
  width: 2px;
  transform: translateX(-50%);
  border-radius: 2px;
}
/* both bars stop at the LAST dot (--track) rather than the container bottom,
   else a chunk of track hangs past the final node and never fills */
.svc-timeline::before { height: var(--track, 100%); background: var(--line); }
.svc-timeline::after { height: var(--fill, 0%); background: var(--ink); }

.svc-stop {
  --card-pad: clamp(1.25rem, 2.4vw, 2rem);
  position: relative;
  display: grid;
  grid-template-columns: var(--spine) minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: center;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}
/* the card itself is a pseudo grid item spanning copy+photo, so the spine and
   its dot stay outside the panel instead of sitting on top of it */
.svc-stop::before {
  content: "";
  grid-column: 2 / -1;
  grid-row: 1;
  /* the row is align-items:center, and an empty pseudo has no intrinsic
     height — without this it collapses to 0 and only its border paints,
     as a stray hairline across the middle of the card */
  align-self: stretch;
  background: var(--ivory);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.stop-body { grid-column: 2; grid-row: 1; z-index: 1; padding: var(--card-pad); }
.stop-media {
  grid-column: 3;
  grid-row: 1;
  z-index: 1;
  margin: var(--card-pad);
  margin-left: 0;
  aspect-ratio: 4 / 3;
  border-radius: calc(var(--radius) * 0.75);
  overflow: hidden;
}
.stop-media img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* dot rides the spine, centred on the card, painted in the chapter colour so
   the track reads as passing behind it */
.stop-marker { grid-column: 1; grid-row: 1; align-self: stretch; position: relative; }
.stop-marker::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  background: var(--sand);
  box-shadow: 0 0 0 2px var(--sand), 0 0 0 3px var(--line);
  transition: background 0.45s var(--ease), box-shadow 0.45s var(--ease);
}
.svc-stop.is-reached .stop-marker::before {
  background: var(--ink);
  box-shadow: 0 0 0 2px var(--sand), 0 0 0 3px var(--ink), 0 0 0 9px rgba(14, 15, 17, 0.1);
}

.svc-stop .num {
  font-family: var(--mono);
  font-weight: 500;
  color: var(--brass-ink);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: 0.3rem;
}
.stop-body h2 {
  font-size: clamp(1.2rem, 2vw, 1.5rem);
  margin-bottom: 0.5rem;
  display: block;
}
.stop-body h2 .tag { margin-left: 0.6rem; }
.stop-body > p { color: var(--stone); font-size: 0.92rem; max-width: 40em; }
.stop-body .chips { margin-top: 0.85rem; }
.stop-body .book-link {
  margin-top: 1rem;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  white-space: nowrap;
  color: var(--brass-ink);
}
.stop-body .book-link svg { transition: transform 0.3s var(--ease); }
.svc-stop:hover .book-link svg { transform: translateX(4px); }

@media (prefers-reduced-motion: reduce) {
  .svc-timeline::after { height: 100%; }
  .stop-marker::before { transition: none; }
}

@media (max-width: 760px) {
  .svc-timeline { --spine: 2rem; }
  /* photo drops under the copy inside the same card; the spine stays so the
     page still reads as one continuous run */
  .svc-stop { grid-template-columns: var(--spine) minmax(0, 1fr); }
  .svc-stop::before { grid-column: 2; grid-row: 1 / 3; }
  .stop-body { grid-column: 2; grid-row: 1; padding-bottom: 0; }
  .stop-media {
    grid-column: 2;
    grid-row: 2;
    margin: var(--card-pad);
    margin-top: 1rem;
  }
}

/* ---------- before / after ---------- */

.ba-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr));
  gap: clamp(1.5rem, 3vw, 2.5rem);
}

.ba-case { display: flex; flex-direction: column; gap: 1.1rem; }

.ba {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  aspect-ratio: 8 / 5;
  border: 1px solid var(--line);
  user-select: none;
  --pos: 50%;
}
.ba-pane {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.ba-pane .ba-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--op, 50% 50%);
  /* --os zooms in (creates pan room in both axes even when the container's
     aspect ratio leaves object-position with nothing to do); --otx/--oty
     then pan within that room to register the seam between before/after */
  transform: scale(var(--os, 1)) translate(var(--otx, 0%), var(--oty, 0%));
  display: block;
}
.ba-pane .word {
  position: absolute;
  top: 0.9rem;
  /* a chart-room label on a clinical record, so: data face, caps */
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.7rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.4rem 0.85rem;
  border-radius: var(--radius-btn);
  background: rgba(14, 15, 17, 0.55);
  color: #f5f6f7;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.ba-before .word {
  left: 0.9rem;
  /* the Before pane's own clip-path shows only var(--pos)% of itself, so its
     label (anchored near the left edge) starts to get chopped rather than
     framed once --pos drops below ~20% — fade it out ahead of that point
     instead of letting the clip eat it mid-glyph */
  opacity: clamp(0%, calc((var(--pos) - 6%) * 7.14286), 100%);
  transition: opacity 0.2s linear;
}
.ba-after .word {
  right: 0.9rem;
  /* mirror of the above: the After pane sits unclipped but the Before pane
     visually covers all but the rightmost (100% - var(--pos)) of it, so its
     label fades out as --pos climbs back toward 100% */
  opacity: clamp(0%, calc((94% - var(--pos)) * 7.14286), 100%);
  transition: opacity 0.2s linear;
}
.ba-before {
  background:
    radial-gradient(120% 90% at 30% 20%, #787f87 0%, #585d64 55%, #4a4f56 100%);
  color: #e8eaec;
  clip-path: inset(0 calc(100% - var(--pos)) 0 0);
}
.ba-after {
  background:
    radial-gradient(120% 90% at 70% 25%, #ffffff 0%, #f0f2f4 60%, #dfe2e5 100%);
  color: var(--ink);
}
.ba-divider {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--pos);
  width: 2px;
  background: var(--ivory);
  box-shadow: 0 0 0 1px rgba(14, 15, 17, 0.2);
  pointer-events: none;
}
.ba-thumb {
  position: absolute;
  top: 50%; left: var(--pos);
  transform: translate(-50%, -50%);
  width: 2.9rem; height: 2.9rem;
  border-radius: 50%;
  background: var(--ivory);
  border: 1px solid var(--line);
  box-shadow: 0 10px 24px -8px rgba(14, 15, 17, 0.4);
  display: grid;
  place-items: center;
  pointer-events: none;
  color: var(--ink);
}
.ba-range {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  cursor: ew-resize;
  margin: 0;
  touch-action: pan-y;
}
.ba-meta { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.ba-meta h3 { font-size: 1.25rem; }
.ba-meta .muted { font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase; }
.ba-case > p { color: var(--stone); font-size: 0.95rem; }
.dark .ba-case > p { color: var(--faint); }

.gallery-note {
  font-size: 0.8rem;
  color: var(--stone);
  border: 1px dashed var(--line);
  border-radius: var(--radius-btn);
  padding: 0.6rem 1.3rem;
  display: inline-block;
  margin-bottom: 2.5rem;
}

/* ---------- signature moment: scroll-scrubbed before/after ---------- */

.ba-scrub-wrap { height: 260vh; position: relative; margin-bottom: clamp(3rem, 6vw, 5rem); }
.ba-scrub {
  position: sticky;
  top: clamp(6rem, 12vh, 9rem);
  max-width: 46rem;
  margin-inline: auto;
}
.ba-scrub .ba { aspect-ratio: 4 / 3; border: 1px solid var(--line); }
.ba-scrub .ba-meta { margin-top: 1.1rem; }
/* .ba-scrub-hint removed: a "Scroll" label at the bottom of a viewport
   tells the visitor something they already know. */
.ba-scrub-hint { display: none; }
@media (max-width: 900px) {
  .ba-scrub-wrap { height: 200vh; }
  /* same viewport-tall centring as the desktop .method rule, for the same
     reason — top:50% + translateY(-50%) let the pinned panel paint over the
     heading above it on the way in (this one governs smiles.html) */
  .ba-scrub {
    top: 0;
    height: 100svh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    transform: none;
  }
  /* max-height (not min-height) alongside a definite width: aspect-ratio
     can only shrink the box now, never force width past the container —
     the old min-height+aspect-ratio pairing let height win and blew the
     computed width out to 378px inside a 350px column. */
  .ba-scrub .ba {
    width: 100%;
    max-width: 100%;
    aspect-ratio: 4 / 5;
    max-height: 46svh;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ba-scrub-wrap { height: auto; }
  .ba-scrub { position: static; transform: none; }
}

/* ---------- signature spine: pinned Scan / Design / Reveal stage (index) ---------- */

/* Height minus one viewport IS the scrub travel (the sticky box is 100svh),
   so 240vh here buys 140vh of scrolling — about 47vh per beat across Scan /
   Design / Reveal. It was 340vh, which spent 80vh per beat and made the
   section the longest thing on the homepage by a wide margin. Do not drop
   below ~200vh: under ~33vh per beat the wipe reads as a jump cut. */
.ba-scrub-wrap.method { height: 240vh; }
/* Must come AFTER the rule above, not inside the max-width:900px block higher
   up the file: same specificity, so source order decides. An override written
   up there is silently dead. */
@media (max-width: 900px) {
  .ba-scrub-wrap.method { height: 220vh; }   /* 120vh of travel, ~40vh per beat */
}
.ba-scrub-wrap.method .ba-scrub {
  max-width: 64rem;
  /* Centre the pinned stage by making the sticky box itself viewport-tall
     and centring the stage inside it.

     Do NOT go back to `top: 50%; transform: translateY(-50%)`. Sticky
     resolves an element's position BEFORE transforms are applied, so that
     pairing painted the panel ~252px above its own flow position — on the
     approach to the section it sat straight on top of the section heading
     and lede (measured: 180px of overlap, over a 400px band of scroll).
     A viewport-tall sticky box starts at its flow position and can never
     rise above it, so the collision is structurally impossible. */
  top: 0;
  height: 100svh;
  display: flex;
  align-items: center;
  transform: none;
}
.ba-scrub-wrap.method .method-stage { width: 100%; }
@media (prefers-reduced-motion: reduce) {
  .ba-scrub-wrap.method { height: auto; }
  .ba-scrub-wrap.method .ba-scrub { position: static; top: auto; transform: none; }
}
.method-stage {
  display: grid;
  grid-template-columns: 1.05fr 0.72fr;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: center;
}
.method-visual { max-width: 38rem; }
.method-beats {
  display: flex;
  flex-direction: column;
  gap: clamp(1.5rem, 3vw, 2.25rem);
}
.method-beats .beat {
  border-left: 2px solid var(--line-dark);
  padding-left: 1.25rem;
  opacity: 0.4;
  transition: opacity 0.5s var(--ease), border-color 0.5s var(--ease);
}
.method-beats .beat.is-active { opacity: 1; border-color: var(--ivory); }
.method-beats .beat .num {
  font-family: var(--mono);
  font-weight: 500;
  color: var(--brass-light);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  display: block;
  margin-bottom: 0.3rem;
}
.method-beats .beat h3 { font-size: 1.3rem; margin-bottom: 0.5rem; color: var(--ivory); }
.method-beats .beat p { color: var(--faint); font-size: 0.92rem; max-width: 22em; }
@media (max-width: 900px) {
  .method-stage { grid-template-columns: 1fr; gap: 1.25rem; }
  .method-visual, .method-stage > * { min-width: 0; }
  /* pin from a fixed offset under the nav instead of centring the whole
     stage — at 3 stacked beats the centred inner ran 1038px tall in an
     862px viewport, so beat 03 (Reveal) never came on screen */
  /* height:auto/display:block undo the desktop rule's viewport-tall box —
     here the stage is pinned from a fixed offset instead of centred, because
     3 stacked beats ran taller than the viewport and clipped beat 03 */
  .ba-scrub-wrap.method .ba-scrub {
    top: 84px;
    height: auto;
    display: block;
    transform: none;
  }
  .method-visual .ba { max-height: 34svh; }
  /* only the active beat renders — one caption at a time, swapped by the
     same scroll progress that already drives is-active (see site.js) */
  .method-beats { flex-direction: column; gap: 0; margin-top: 0; }
  .method-beats .beat { display: none; flex: none; opacity: 1; border-color: var(--ivory); }
  .method-beats .beat.is-active { display: block; }
}

/* ---------- process / steps ---------- */

.steps {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.25rem;
  counter-reset: step;
}
/* The makeover page tells the same story in four beats rather than three, so
   the row needs a fourth track. NOTE the doubled class: the responsive
   fallbacks further down have to name `.steps.steps-4` too, because a
   single-class `.steps` rule inside a media query LOSES to this one on
   specificity and the four beats stay four columns down to 320px. */
.steps.steps-4 { grid-template-columns: repeat(4, 1fr); }
.step {
  border-top: 1px solid var(--line-dark);
  padding-top: 1.5rem;
}
.step h3 { margin-bottom: 0.7rem; }
.step p { color: var(--faint); font-size: 0.95rem; }

/* ---------- doctor band ---------- */

.doctor-grid {
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
}
/* ---------- tick list ----------
   For a plain enumeration whose items are too long to be chips. Two columns
   with hairline rules rather than bullet glyphs: the system has no ornament
   anywhere else, and a disc would be the only round thing on the page. */
.tick-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 clamp(1.5rem, 4vw, 3.5rem);
  max-width: 58rem;
  margin-top: clamp(2rem, 4vw, 2.75rem);
}
.tick-list li {
  position: relative;
  padding: 0.9rem 0 0.9rem 1.6rem;
  border-bottom: 1px solid var(--line);
  color: var(--stone);
  font-size: 0.95rem;
}
.tick-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.5em;
  width: 0.7rem;
  height: 1px;
  background: var(--ink);
}
.dark .tick-list li { border-bottom-color: var(--line-dark); color: var(--faint); }
.dark .tick-list li::before { background: var(--ivory); }
@media (max-width: 620px) { .tick-list { grid-template-columns: minmax(0, 1fr); } }

.chips { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 2rem; }
.chip {
  font-size: 0.78rem;
  font-weight: 550;
  border: 1px solid var(--line);
  border-radius: var(--radius-btn);
  padding: 0.5rem 1.1rem;
  background: var(--ivory);
  color: var(--stone);
}
.dark .chip { background: transparent; border-color: var(--line-dark); color: var(--faint); }

/* ---------- FAQ ---------- */

.faq { max-width: 46rem; }
.faq details {
  border-bottom: 1px solid var(--line);
}
.faq summary {
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1.5rem;
  padding: 1.4rem 0;
  cursor: pointer;
  font-family: var(--display);
  font-size: clamp(1.02rem, 1.7vw, 1.22rem);
  font-weight: 500;
  letter-spacing: -0.015em;
  min-height: 44px;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--brass-ink); }
.faq summary::after {
  content: "+";
  font-family: var(--sans);
  font-size: 1.4rem;
  font-weight: 300;
  color: var(--brass-ink);
  transition: transform 0.3s var(--ease);
  flex-shrink: 0;
}
.faq details[open] summary::after { transform: rotate(45deg); }
.faq .faq-a { padding: 0 0 1.5rem; color: var(--stone); max-width: 40em; }

/* ---------- patient reviews ----------
   These are EXCERPTS from real five-star reviews on the studio's Google
   listing, and the listing is linked from the section so any of them can be
   checked in one click.

   Two rules for anyone editing the markup:
   1. Every excerpt is a CONTIGUOUS, unedited span starting at a sentence
      boundary. Never stitch two non-adjacent sentences together and never
      tidy a patient's punctuation - that changes what they said, which is
      the whole thing a testimonial is not allowed to do. (This is also the
      one place on the site where an em-dash is allowed to survive: it is
      the reviewer's, not ours.)
   2. No decorative quote marks, no cards, no star glyphs. A hairline and a
      mono attribution read as a record, which is the register of the rest
      of the site.
   ---------- */
.quotes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.75rem, 3.5vw, 2.75rem) clamp(2rem, 5vw, 4rem);
}
.quote {
  border-top: 1px solid var(--line);
  padding-top: 1.35rem;
  margin: 0;
}
.quote blockquote { margin: 0; }
.quote p {
  color: var(--ink);
  font-size: clamp(0.98rem, 1.25vw, 1.08rem);
  line-height: 1.55;
  max-width: 34em;
}
/* Every patient excerpt on smiles.html is a single paragraph, so nothing ever
   separated two — and Dr. Doyle's two-paragraph quote on smile-makeover.html
   ran together as one block. Second-and-later only, so the excerpts are
   untouched. */
.quote blockquote p + p { margin-top: 0.9em; }
.quote figcaption {
  margin-top: 1rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brass-ink);
}
.dark .quote { border-top-color: var(--line-dark); }
.dark .quote p { color: var(--ivory); }
.dark .quote figcaption { color: var(--brass-light); }
@media (max-width: 720px) {
  .quotes { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- booking / contact ---------- */

.book-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  align-items: start;
}
/* The phone number is the single most important datum on the page, so
   it goes to the data face: tabular, spaced, unmistakably a number to
   dial rather than a headline that happens to contain digits. */
.book-phone {
  font-family: var(--mono);
  font-size: clamp(1.55rem, 3.2vw, 2.3rem);
  font-weight: 500;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  display: inline-block;
  margin: 0.75rem 0 1.5rem;
  border-bottom: 1px solid var(--line-dark);
  padding-bottom: 0.35rem;
}
.book-phone:hover { border-bottom-color: var(--ivory); }

/* WhatsApp CTA sitting under the phone number in #book.

   Deliberately NOT WhatsApp green. The entire site is monochrome by design,
   and a single brand-coloured button reads as a third-party widget bolted on
   to someone else's page. The glyph alone carries the recognition, which is
   the whole job — nobody needs the green to know what the mark means. */
.book-alt { margin: 0 0 1.75rem; }
.btn--wa,
.dark .btn--wa {
  background: var(--wa);
  border-color: var(--wa);
  color: var(--ink);
}
.btn--wa:hover,
.dark .btn--wa:hover { background: var(--wa-hover); border-color: var(--wa-hover); }

/* The nav CTA carries the WhatsApp glyph so it is visible that the button
   leaves the site. Between the burger breakpoint and ~900px the Spanish bar is
   at its tightest — five items, the language chip and a button — and the extra
   ~26px of glyph is exactly enough to wrap "Reservar cita" onto two lines.
   Drop just the glyph in that band; the label still says what the button does,
   and below 820px the burger menu has room for it again. */
@media (min-width: 821px) and (max-width: 899px) {
  .nav-links > li > .btn .wa-glyph { display: none; }
}
.btn--wa .wa-glyph { flex: none; }
/* .btn:hover svg slides its arrow to the right to suggest forward motion. A
   logo doing that just looks like a rendering fault, so it is exempt. */
.btn:hover .wa-glyph { transform: none; }
.hours { list-style: none; color: var(--faint); font-size: 0.95rem; }
.hours li { display: flex; justify-content: space-between; max-width: 18rem; padding: 0.45rem 0; border-bottom: 1px solid var(--line-dark); }
/* The studio closes 12-1pm, so weekdays are two ranges. They are two <li>s
   for screen-reader clarity, but the divider between them is suppressed so
   the pair reads as ONE weekday entry rather than two separate days. */
.hours li.hours-split { border-bottom: 0; padding-bottom: 0.1rem; }
.hours li.hours-cont { padding-top: 0.1rem; }
/* .hours no es sólo el horario: son tres filas etiqueta/valor
   (Lun–Vie · 8am–5pm, Sáb–Dom · Closed, Location · Condado, San Juan).
   Va en mono el VALOR de las tres, no sólo las que llevan cifras: lo que
   convierte eso en una tira de datos es que la columna derecha entera
   comparta tipografía. Mezclar mono en dos filas y Inter en la tercera
   se leería como error, no como criterio. */
.hours li span:last-child { color: var(--ivory); font-family: var(--mono); font-size: 0.875rem; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }

/* Teléfono pelado = dato. Los que llevan clase son la cifra hero
   (.book-phone) y un CTA (.cta-alt): ésos conservan su tratamiento. */
a[href^="tel:"]:not([class]) { font-family: var(--mono); letter-spacing: -.01em; font-variant-numeric: tabular-nums; }

.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.field { display: flex; flex-direction: column; gap: 0.45rem; }
.field.full { grid-column: 1 / -1; }
.field label { font-family: var(--mono); font-size: 0.7rem; font-weight: 500; letter-spacing: 0.14em; text-transform: uppercase; color: var(--brass-light); }
.field input, .field select, .field textarea {
  width: 100%;
  min-width: 0;
  font: inherit;
  color: var(--ivory);
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line-dark);
  border-radius: var(--radius); /* track the panel radius, not a rounder one of its own */
  padding: 0.85rem 1rem;
  min-height: 3.1rem;
  transition: border-color 0.25s;
}
.field ::placeholder { color: var(--faint); opacity: 1; }
.field textarea { min-height: 7rem; resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--ivory);
  outline: 1px solid var(--ivory);
  outline-offset: -2px;
}
.field select { appearance: none; }
.form-note { font-size: 0.8rem; color: var(--faint); margin-top: 1rem; }

/* inline validation — the form carries `novalidate` on purpose (the browser's
   own error bubbles are system-styled and clash with the studio's type), so
   the messages get drawn in site.js instead. .field is already a flex column,
   so the message just flows in under its input. */
/* Monochrome validation. With no hue left in the palette, an invalid
   field is marked by a doubled border weight plus a written reason
   below it; colour was never allowed to be the only signal anyway, so
   nothing is lost in accessibility terms by removing the terracotta. */
.field-error {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  color: var(--alert);
}
.field-error::before { content: "! "; font-weight: 700; }
.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: var(--alert); border-width: 2px; }

/* ---------- final CTA ---------- */

.final-cta { text-align: center; }
.final-cta .display { margin-bottom: 1.25rem; }
.final-cta .lede { margin-inline: auto; margin-bottom: 2.5rem; }
.final-cta .cta-alt-wrap { display: flex; justify-content: center; }

/* ---------- footer ---------- */

footer {
  background: var(--char);
  color: var(--faint);
  padding: clamp(3rem, 7vw, 5rem) 0 2rem;
  font-size: 0.9rem;
}
.foot-grid {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: 2.5rem;
  padding-bottom: 3rem;
  border-bottom: 1px solid var(--line-dark);
}
.foot-grid .logo { color: var(--ivory); margin-bottom: 1rem; }
.foot-grid h4 {
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ivory);
  margin-bottom: 1.1rem;
}
.foot-grid ul { list-style: none; display: grid; gap: 0.2rem; }
.foot-grid a { display: inline-flex; align-items: center; min-height: 44px; }
.foot-grid li:not(:has(a)) { display: flex; align-items: center; min-height: 44px; }
.foot-grid a:hover { color: var(--ivory); }
.foot-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1.8rem;
  font-size: 0.78rem;
}

/* ---------- interactive layer ---------- */

.card, .svc-mini, .svc-card-lg { position: relative; }
.glare::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--glare, 0);
  transition: opacity 0.35s;
  background: radial-gradient(480px circle at var(--gx, 50%) var(--gy, 50%), rgba(255, 255, 255, 0.5), transparent 46%);
}

/* ---------- hero depth ----------
   --hp is written on .hero by site.js and runs 0 (top of page) to 1 (hero
   fully scrolled past). It used to drive a light shaft and a warm bloom;
   those went with the boutique identity. Since 31 Aug 2026 it drives the film
   parallax instead: the footage drifts DOWN as the page scrolls up, so it
   lags the type rather than travelling with it, which is what gives a film
   hero its depth. The 5% slack above and below .hero-film is what the drift
   spends — without it the ink ground would show at the bottom edge.

   site.js pins --hp at 0 under prefers-reduced-motion, so the reduce rule
   below is belt-and-braces for a stale inline value. */
.hero .wrap { position: relative; z-index: 1; }

.hero-film {
  top: -5%;
  height: 110%;
  transform: translate3d(0, calc(var(--hp, 0) * 5%), 0);
  will-change: transform;
}
.hero-film__inner { transform: translateY(calc(var(--hp, 0) * -2.25rem)); }

@media (prefers-reduced-motion: reduce) {
  .hero-film { top: 0; height: 100%; transform: none; will-change: auto; }
  .hero-film__inner { transform: none; }
}

/* ---------- ambient layers: removed ----------
   Two fixed layers used to sit behind and above every page: a pair of
   drifting brass radial glows, and a full-viewport SVG grain at 5%
   overlay. Both were hue and texture on a site that is now neither,
   and the grain repainted on a fixed layer above scrolling content,
   which is the most expensive thing on the page for the least return.
   Deleted rather than recoloured. */

/* scroll progress hairline — real state (how far through the page you
   are), so it stays; recoloured to ink and thinned to 1px */
.scroll-progress {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: 0 50%;
  z-index: 160;
  pointer-events: none;
}
body.on-dark .scroll-progress { background: var(--ivory); }

/* The custom cursor is gone. A page that replaces the system pointer
   costs the visitor their own cursor settings and gains nothing a
   dental patient wants; the styles and the rAF loop that drove it are
   both deleted. */

/* staggered word reveal on the lead headline — gated behind the entrance
   ritual (html.preloader-done) on index, so the animation plays AFTER the
   curtain opens, not behind it. Pages with no preloader get the class
   added immediately (see site.js), so their fast entrance is unaffected. */
.display.split .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Fraunces' natural line box at this weight is ~1.23x font-size; the
     word-reveal mask below must be at least that tall or descenders
     (y, g, p) get sheared flat by the overflow:hidden clip rect. */
  height: 1.3em;
  line-height: 1.3;
}
.display.split .w > span {
  display: inline-block;
  transform: translateY(115%);
}
html.preloader-done .display.split .w > span {
  animation: word-up 0.9s var(--ease) forwards;
  animation-delay: calc(var(--wi) * 70ms + 0.12s);
}
@keyframes word-up { to { transform: none; } }

/* ---------- scroll reveal animations — gated behind html.js so a no-JS
   visitor (or a slow/failed script) always sees full content ---------- */

.rv { transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); transition-delay: var(--d, 0s); }
html.js .rv { opacity: 0; transform: translateY(26px); }
html.js .rv.in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  html.js .rv { opacity: 1; transform: none; transition: none; }
  .btn, .card, .svc-mini, .svc-card-lg { transition: none; }
  .display.split .w > span { animation: none; transform: none; }
}

/* The entrance preloader is gone. It held the page behind a curtain and
   asked the visitor to "Scroll to enter" before they had seen anything.
   On a clinic site that is a toll booth in front of a phone number. */

/* ---------- page hero (inner pages) ---------- */

.page-hero { padding-top: clamp(9rem, 18vw, 13rem); padding-bottom: clamp(3rem, 7vw, 5rem); }
.page-hero .display { margin-bottom: 1.25rem; }

/* ---------- about page ---------- */

.story {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2.5rem, 6vw, 6rem);
  align-items: center;
}
.story + .story { margin-top: var(--sect); }
/* Single-column variants, used where a photograph was removed rather than
   replaced. Both grids are two-column by default, so dropping the media child
   alone would leave the text sitting in the first column with the second one
   empty beside it — the block reads as broken rather than as prose. Introduced
   31 Aug 2026 when the client withdrew both portraits of Dr. Doyle Avellanet;
   delete these two rules if a new headshot ever goes back into either slot. */
.story--solo, .doctor-grid--solo { grid-template-columns: minmax(0, 1fr); }
/* Portrait + (bio and credentials). Top-aligned rather than the .story default
   of centre: the text column is much taller than the photograph, and centring
   would float the portrait halfway down beside the middle of a paragraph. The
   photograph takes the narrower track — it is one figure against a heading, two
   paragraphs and a six-item list. */
.story--bio { grid-template-columns: 0.8fr 1.2fr; align-items: start; }
/* doctor-portrait.webp is cut to exactly 3/5. Left alone it inherits
   `.story .arch { aspect-ratio: 4/4.6 }` and object-fit silently crops ~31% of
   its height away. The crop is not catastrophic — it lands as a headshot — but
   it is a crop nobody chose, applied to the client's own portrait, so the
   figure takes the file's ratio instead and shows the frame she approved.

   The selector needs `.story` in it: `.arch-portrait` alone is one class and
   loses to the two-class `.story .arch` above, which is exactly how this went
   unnoticed the first time. */
.story .arch.arch-portrait { aspect-ratio: 3 / 5; }
/* Single-column inside a half-width track; the default two-up would put one
   or two words per line. margin-top:0 because the eyebrow above it already
   sets the spacing. */
.tick-list--stack { grid-template-columns: minmax(0, 1fr); max-width: none; margin-top: 0; }
@media (max-width: 820px) { .story--bio { grid-template-columns: minmax(0, 1fr); } }
/* Story figures are portrait by default. .arch-wide had equal specificity
   and lost to this rule, so a landscape photo tagged wide was silently
   being cropped to a portrait anyway - which is what was quietly happening
   to the team shot that used to sit in the first block. Restore the
   opt-out. */
.story .arch { aspect-ratio: 4 / 4.6; }
.story .arch.arch-wide { aspect-ratio: 1748 / 1241; align-self: center; }
.story.flip > .story-media { order: 2; }
.story h2 { margin-bottom: 1.25rem; }
.story p { color: var(--stone); margin-bottom: 1rem; max-width: 34em; }
.story p:last-of-type { margin-bottom: 0; }

.values {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1.25rem;
}
.value {
  border-top: 1px solid var(--line);
  padding-top: 1.4rem;
}
.value h3 { font-size: 1.15rem; margin-bottom: 0.6rem; }
.value p { font-size: 0.9rem; color: var(--stone); }
.dark .value p { color: var(--faint); }

/* ============================================================
   Mobile — designed mobile-first in spirit; tuned breakpoints
   ============================================================ */

@media (max-width: 1024px) {
  .cards-3, .steps, .steps.steps-4 { grid-template-columns: 1fr 1fr; }
  .values, .foot-grid { grid-template-columns: 1fr 1fr; }
  .svc-mini-grid { grid-template-columns: 1fr 1fr; }
  .svc-feature { grid-template-columns: 1fr; }
  .svc-feature-media { aspect-ratio: 5 / 4; max-width: 32rem; }
}

@media (max-width: 820px) {
  /* The cover hero this was written for was a headline plus a tall photo
     column, which overran a phone at 100svh — hence min-height:auto. The film
     hero is the opposite case: filling the viewport IS the composition, so it
     keeps its own height and only trades some top padding for the shorter
     nav. Scoped with :not() rather than deleted because about/services still
     use the plain .hero on their page headers. */
  .hero:not(.hero--film) { padding-top: 6rem; min-height: auto; padding-bottom: 3.5rem; }
  .hero--film { padding-top: 5.5rem; }
  .doctor-grid, .book-grid, .story { grid-template-columns: 1fr; }
  .story.flip > .story-media { order: 0; }

  .nav-toggle { display: flex; }
  .nav {
    top: 0; left: 0;
    transform: none;
    width: 100%;
    border-radius: 0;
    border: 0;
    background: transparent;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
  }
  .nav.scrolled {
    background: rgba(245, 246, 247, 0.97);
    box-shadow: 0 1px 0 var(--line);
  }
  body.on-dark .nav.scrolled { background: rgba(14, 15, 17, 0.92); }
  .nav .nav-inner { padding: 0.85rem var(--pad); }
  .nav-links {
    position: fixed;
    inset: 0;
    background: var(--bone);
    flex-direction: column;
    justify-content: center;
    gap: 1.6rem;
    font-size: 1.2rem;
    transform: translateY(-102%);
    transition: transform 0.45s var(--ease);
    z-index: -1;
  }
  body.menu-open .nav-links { transform: none; }
  .nav-links a:not(.btn) { font-size: 1.05rem; min-height: 44px; display: inline-flex; align-items: center; }
  .nav-sub-toggle { font-size: 1.05rem; min-height: 44px; }
  /* In the burger menu every other item is set at 1.05rem, so the desktop
     language chip reads as a stray 11px box. Scale it to the menu it is in and
     give it a real tap target. */
  .nav-links .nav-lang a {
    font-size: 0.9rem;
    letter-spacing: 0.2em;
    padding: 0.6rem 1.2rem;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* phone: the panel is already a full screen of links, so the submenu sits
     inline under its parent rather than floating over it. site.js opens it
     with the panel — grouping the two pages is the point, hiding them behind
     a second tap would defeat it. */
  .nav-links .has-sub { display: flex; flex-direction: column; align-items: center; gap: 0.55rem; }
  .nav-sub {
    position: static;
    min-width: 0;
    padding: 0;
    margin: 0;
    gap: 0.55rem;
    justify-items: center;
    border: 0;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    box-shadow: none;
    transform: none;
    /* collapsed height animation would fight the panel's own slide — the
       submenu just isn't rendered until the panel is open */
    transition: opacity 0.3s var(--ease), visibility 0.3s;
  }
  .nav-links .nav-sub a:not(.btn) { font-size: 0.92rem; padding: 0.35rem 0.9rem; color: var(--stone); }
  .nav-links .nav-sub a:not(.btn):hover,
  .nav-links .nav-sub a[aria-current="page"] { background: none; color: var(--ink); text-decoration: underline; text-underline-offset: 0.3em; }

  /* ---- THE OPEN MENU IS A LIGHT SURFACE -----------------------------
     The panel that slides down is --bone, but body.on-dark is still set,
     because the film hero is sitting right behind it. Every on-dark rule
     then paints for a dark background that is no longer there: the links,
     the sub-toggle and the language chip all render --ivory on --bone at
     1.08:1, the logo is filter-inverted to white, the burger bars go white,
     and the submenu keeps the dark dropdown panel it uses on desktop.

     The client found this on her iPhone — the menu opened and read as
     almost entirely blank. It never showed up in testing because the inner
     pages have no film hero, so on-dark is never set there, and the
     homepage's observer does not fire in a headless pane.

     body.on-dark.menu-open is (0,4,x), so it beats every body.on-dark rule
     above regardless of source order. */
  body.on-dark.menu-open .nav-links a:not(.btn) { color: var(--ink); }
  body.on-dark.menu-open .nav-links a:not(.btn)::after { background: var(--ink); }
  body.on-dark.menu-open .nav-links .nav-sub a:not(.btn) { color: var(--stone); }
  body.on-dark.menu-open .nav-sub-toggle { color: var(--ink); }
  body.on-dark.menu-open .nav-sub-toggle::after { background: var(--ink); }
  body.on-dark.menu-open .nav-links .nav-lang a { color: var(--stone); border-color: var(--line); }
  body.on-dark.menu-open .nav-sub { background: none; border-color: transparent; }
  body.on-dark.menu-open .nav .brandmark img { filter: none; }
  /* the CTA goes back to the solid ink button the light menu uses, rather
     than the white-on-white it inherits from on-dark */
  body.on-dark.menu-open .nav .btn { background: var(--ink); color: var(--ivory); border-color: var(--ink); }
  body.on-dark.menu-open .nav-toggle span,
  body.on-dark.menu-open .nav-toggle span::before,
  body.on-dark.menu-open .nav-toggle span::after { background: var(--ink); }
  body.menu-open { overflow: hidden; }
}

@media (max-width: 560px) {
  .cards-3, .steps, .steps.steps-4, .values, .foot-grid, .form-grid { grid-template-columns: 1fr; }
  .svc-mini-grid, .svc-detail-grid { grid-template-columns: 1fr; }
  .hero-ctas .btn { width: 100%; }
  .ba { aspect-ratio: 4 / 3; }
  /* body already reserves 4.25rem for the sticky action bar; the footer only
     needs its normal breathing room on top of that. */
  footer { padding-bottom: 2rem; }
}


/* Titulares fuera del ajuste por altura de x — ver nota junto a body. */
h1,h2,h3,.serif,.display,.book-phone { font-size-adjust: none; }

/* ═══════════════════════════════════════════════════════════════
   STICKY MOBILE ACTION BAR
   On a phone the two things a prospective patient actually does are
   call and book. The header CTA scrolls away; this doesn't. Phone
   only — on desktop the nav "Book a visit" is always on screen, so a
   second permanent bar would just be clutter.
   ═══════════════════════════════════════════════════════════════ */
.mobile-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 90;
  display: none;                       /* enabled in the phone query below */
  gap: 0.55rem;
  padding: 0.65rem 0.8rem calc(0.65rem + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, 0.93);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-top: 1px solid var(--line);
  transform: translateY(110%);
  transition: transform 0.4s var(--ease);
}
.mobile-bar.is-on { transform: translateY(0); }
.mobile-bar a {
  flex: 1 1 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  min-height: 3rem;                    /* comfortably over the 44px target */
  font-family: var(--sans);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius-btn);
  transition: background 0.25s var(--ease), color 0.25s var(--ease);
}
.mobile-bar__call {
  color: var(--brass-ink);
  background: transparent;
  border: 1px solid var(--line);
}
.mobile-bar__call:active { background: rgba(14, 15, 17, 0.05); }
.mobile-bar__book {
  color: var(--ivory);
  background: var(--char);
}
.mobile-bar__book:active { background: var(--ink); }
.mobile-bar a:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.mobile-bar svg {
  width: 15px; height: 15px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The bar's own icons are line art, drawn with stroke and no fill. The WhatsApp
   mark is a solid glyph, so under that rule it renders as an invisible shape
   with its outline traced — swap the two properties back for it alone. */
.mobile-bar .wa-glyph { fill: currentColor; stroke: none; }

@media (max-width: 820px) {
  .mobile-bar { display: flex; }
  /* The bar sits over the page, so the footer needs room to clear it. */
  body { padding-bottom: 4.25rem; }
}
@media (prefers-reduced-motion: reduce) {
  .mobile-bar { transition-duration: 0.01ms; }
}

/* ---------- long-form legal prose (legal.html) ----------
   Deliberately plain: narrow measure, generous leading, no chapter colour.
   Nothing here should compete with the marketing pages. */
.legal-doc { max-width: 44rem; }
.legal-doc h2 {
  font-size: clamp(1.5rem, 3vw, 2.1rem);
  margin: clamp(3rem, 6vw, 4.5rem) 0 1.1rem;
  padding-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--line);
}
.legal-doc > h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.legal-doc h3 {
  font-family: var(--sans);
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--brass-ink);
  margin: 2.25rem 0 0.7rem;
}
.legal-doc p,
.legal-doc li {
  color: var(--stone);
  font-size: 0.98rem;
  line-height: 1.75;
}
.legal-doc p { margin-bottom: 1.1rem; }
.legal-doc ul { margin: 0 0 1.1rem 1.1rem; list-style: disc; }
.legal-doc li { margin-bottom: 0.5rem; padding-left: 0.25rem; }
.legal-doc a { color: var(--brass-ink); text-decoration: underline; text-underline-offset: 3px; }
.legal-doc a:hover { color: var(--ink); }
.legal-doc strong { color: var(--ink); font-weight: 600; }
.legal-updated {
  font-family: var(--sans);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 2.5rem;
}
/* Items the studio still has to supply before this page is final. Deleting
   the .todo spans is the whole handoff checklist — search for "todo". */
.legal-doc .todo {
  background: rgba(255, 255, 255, 0.16);
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.16);
  border-radius: 2px;
  color: var(--ink);
}
