/* ============================================
   PHYSIO SAMOS — Design System
   ============================================ */

/* JuliaMono — self-hosted Latin+Greek subset (SIL OFL, see fonts/JuliaMono-LICENSE.txt).
   Not on Google Fonts, and the full family is far too large to ship whole.
   The Medium face covers 500-700 explicitly so the browser never synthesises a faux
   bold from Regular for the uppercase labels. */
@font-face {
  font-family: "JuliaMono";
  src: url("../../fonts/JuliaMono-Regular-latin-greek.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JuliaMono";
  src: url("../../fonts/JuliaMono-Medium-latin-greek.woff2") format("woff2");
  font-weight: 500 700;
  font-style: normal;
  font-display: swap;
}

/* Literata + Manrope — self-hosted (SIL OFL, see fonts/Literata-LICENSE.txt and
   fonts/Manrope-LICENSE.txt). These are the same variable files Google Fonts served, downloaded
   on 2026-09-05 and declared the same way: one face per script subset with a unicode-range, so a
   Greek page fetches the greek + latin files and nothing else. Two things are load-bearing:
   - Literata's weight is the RANGE 200 900, not a list. VariableProximity animates the headings
     through font-variation-settings 'wght', which does nothing against pinned instances.
   - Every file carries its axes: Literata opsz 7-72 + wght 200-900, Manrope wght 200-800
     (verified with fontTools). font-optical-sizing: auto picks the optical cut per size.
   Manrope's 800 exists solely for the footer wordmark (.footer-mega).
   They moved off fonts.googleapis.com for the same reason every image did — this origin serves
   everything — and because a Google Fonts request sends each visitor's IP to Google before any
   consent (LG München I, 3 O 17493/20). No visitor data now leaves this origin for a font. */
@font-face {
  font-family: "Literata";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-italic-greek-ext.woff2") format("woff2");
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: "Literata";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-italic-greek.woff2") format("woff2");
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: "Literata";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-italic-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Literata";
  font-style: italic;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-italic-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Literata";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-normal-greek-ext.woff2") format("woff2");
  unicode-range: U+1F00-1FFF;
}
@font-face {
  font-family: "Literata";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-normal-greek.woff2") format("woff2");
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: "Literata";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-normal-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Literata";
  font-style: normal;
  font-weight: 200 900;
  font-display: swap;
  src: url("../../fonts/Literata-normal-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../../fonts/Manrope-normal-greek.woff2") format("woff2");
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../../fonts/Manrope-normal-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("../../fonts/Manrope-normal-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


:root {
  /* Palette — warm cream + sage + dark teal + terracotta */
  --bg: #f5f1ea;
  --bg-2: #ede7dc;
  --bg-3: #e3dcce;
  --ink: #1a1f1e;
  /* The ground the service cards mix their accent into. Deliberately **not** overridden in the
     dark block: those cards are dark coloured blocks carrying cream copy in both themes, the
     same arrangement `.hero-full` uses. Mixing into `var(--ink)` looked right and was the bug —
     in dark mode `--ink` is #f1ece2, so every card ground came out *light* (luminance 0.39-0.53)
     under cream text, measured at 1.29:1 to 1.71:1. If you add another always-dark ground,
     mix into this, never into `--ink`. */
  --svc-ink: #1a1f1e;
  --ink-2: #2d4a47;
  --ink-3: #4a5e5b;
  --muted: #7a8784;
  --line: #d8d0c1;
  --line-2: #c8bea9;
  --sage: #68a29b;
  --sage-dark: #2d4a47;
  --sage-light: #a8c8c2;
  --terra: #d97757;
  --terra-dark: #b85d3f;
  --paper: #ffffff;
  /* Nav control states. Mixed from `--ink` into `--paper` rather than pointing at a fixed
     token, for the same reason `--line-strong` is: one declaration then inverts with the
     theme and no dark-mode override is needed. The hover used to be `--bg-2`, which in dark
     mode is **the same colour as `--paper`** (both `rgb(24,34,31)`) — contrast 1.000, so the
     hover was a literal no-op there and only a faint one in light. Measured deltas now:
     hover 21/20 (light/dark), selected 41/39. */
  --ctl-hover: color-mix(in srgb, var(--ink) 9%, var(--paper));
  --ctl-sel: color-mix(in srgb, var(--ink) 18%, var(--paper));
  --ctl-ring: color-mix(in srgb, var(--ink) 32%, transparent);
  --band: #1a1f1e;
  --on-band: #f5f1ea;
  /* Footer: a half-step off --bg rather than the dark slab it used to be, so it reads as
     the end of the page instead of a separate block. Its own tokens (not --bg-2 directly)
     because every colour inside it — links, rules, social buttons, the ghost wordmark —
     has to move with it, in both themes. */
  --footer-bg: #ebe5d9;
  --footer-ink: #1a1f1e;
  --footer-ink-2: #4a5e5b;
  /* The strong hairline. Mixed from --ink so it inverts with the theme, and a clear step
     up from --line/--line-2, which were tuned against a flat slab and disappear over the
     animated backdrop. Anything drawing a rule on a below level wants this one. */
  --line-strong: color-mix(in srgb, var(--ink) 26%, transparent);
  --footer-line: var(--line-strong);
  --footer-ghost: rgba(26,31,30,0.07);
  --shadow: 0 1px 2px rgba(26,31,30,0.04), 0 8px 24px rgba(26,31,30,0.06);
  --shadow-lift: 0 4px 12px rgba(26,31,30,0.08), 0 24px 48px rgba(26,31,30,0.12);
  /* Same lift, bounded: the negative spreads keep it from reaching more than 6px sideways
     and 30px down, which is what lets the services scrollport pad for it without hanging
     past the page gutter. Change these and re-check .svc-track's padding. */
  --shadow-card-lift: 0 6px 14px -6px rgba(26,31,30,0.14), 0 22px 40px -16px rgba(26,31,30,0.30);

  /* Type — Literata (display + italic accents), Manrope (text), JuliaMono (labels).
     All three carry Greek; the previous display and accent faces did not, so every
     Greek heading on this site was silently falling back to the system UI font. */
  --font-display: "Literata", ui-serif, Georgia, serif;
  --font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif;
  --font-serif: "Literata", ui-serif, Georgia, serif;
  --font-mono: "JuliaMono", ui-monospace, SFMono-Regular, monospace;

  /* Spacing */
  /* The sticky nav sits in flow above the hero, so the hero has to subtract it to
     add up to exactly one viewport. 72px of .nav-inner plus its 1px bottom border. */
  --nav-h: 72px;
  --pad-x: clamp(20px, 4vw, 64px);
  --section-y: clamp(64px, 9vw, 140px);
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 24px;
  --radius-xl: 40px;
}

:root[data-theme="dark"] {
  /* !important so dark mode wins over inline palette vars set by the Tweaks panel */
  --bg: #111917 !important;
  --bg-2: #18221f !important;
  --bg-3: #20302d !important;
  --ink: #f1ece2 !important;
  --ink-2: #d6ded9 !important;
  --ink-3: #a6b1ac !important;
  --muted: #859089 !important;
  --line: #29332f !important;
  --line-2: #38443f !important;
  --sage: #74b0a8 !important;
  --sage-dark: #9cc6bf !important;
  --sage-light: #b3d2cc !important;
  --terra: #e0855f !important;
  --terra-dark: #c96a48 !important;
  --paper: #18221f !important;
  --shadow: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.55) !important;
  --shadow-lift: 0 4px 12px rgba(0,0,0,0.5), 0 24px 48px rgba(0,0,0,0.65) !important;
  --shadow-card-lift: 0 6px 14px -6px rgba(0,0,0,0.55), 0 22px 40px -16px rgba(0,0,0,0.7) !important;
  --footer-bg: #161f1d !important;
  --footer-ink: #f1ece2 !important;
  --footer-ink-2: #a6b1ac !important;
  --line-strong: color-mix(in srgb, var(--ink) 26%, transparent) !important;
  --footer-line: var(--line-strong) !important;
  --footer-ghost: rgba(241,236,226,0.055) !important;
}

* { box-sizing: border-box; }
html, body {
  margin: 0;
  padding: 0;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 17px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
/* Only the root paints the page ground. Within the root stacking context, in-flow block
   backgrounds paint *after* negative-z-index positioned elements, so a background on
   <body> would cover the fixed Grainient backdrop entirely — which it did. */
html { background: var(--bg); }
body { background: transparent; }
/* `overflow-x: clip`, not `hidden`, wherever the browser knows it. `hidden` turns <body> into a
   scroll container in WebKit, and a sticky element inside it then sticks to a box that never
   scrolls: the nav scrolled away with the page on every iPhone and iPad (owner's report,
   2026-09-05) while it stuck fine on the desktop. `clip` clips the same horizontal overflow
   without creating a scroll container, so `position: sticky` resolves against the viewport. */
body { overflow-x: hidden; }
@supports (overflow: clip) { body { overflow-x: clip; } }
img { max-width: 100%; display: block; }
/* Every photo ships as a picture element wrapping a WebP source and a JPEG fallback.
   display:contents keeps the inner image as the real layout child, so the sizing rules
   written against it still apply. */
picture { display: contents; }

/* Silent looping clip layered over its own still — see the Loop component. */
.ps-loop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 900ms ease;
  pointer-events: none;
}
.ps-loop[data-ready="1"] { opacity: 1; }
/* No reduced-motion rule here on purpose: the components never mount a clip for those
   visitors, so nothing is downloaded rather than merely hidden. */

/* Clinic page — three portrait clips of the rooms in use. */
.motion-grid {
  margin-top: 40px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}
.motion-cell { margin: 0; }
/* The no-op filter is load-bearing. A <video> is composited on its own GPU layer, and
   Chromium leaves those out of the snapshot `backdrop-filter` reads — so the nav's blur had
   nothing to blur where a clip was behind it and the video stayed razor sharp under the bar
   while the poster stills blurred correctly. Any filter on an ancestor folds the video back
   into the parent's rasterisation, and the nav can see it again. Verified by screenshot: the
   clips' top strip is soft under the nav with this, sharp without it. Do not "clean it up". */
.motion-frame { filter: opacity(1); }
.motion-frame {
  position: relative;
  aspect-ratio: 9 / 16;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-3);
}
.motion-frame > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 720px) {
  /* Two-up, not one: these are 9/16, so four stacked clips is ~2400px of scrolling on a
     phone. Paired, the strip stays one screen and the clips stay big enough to read. */
  .motion-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
}

/* ============================================
   Preloader
   ============================================
   Shown once per session, before any JS runs. .ps-skip (set pre-paint from
   sessionStorage) hides it instantly for every later page, so in-site navigation
   never flashes it. While visible the page cannot scroll. */
html.ps-loading { overflow: hidden; }
/* Hidden unless the pre-paint script opted this page load in: a no-JS visitor (whose
   inline script never ran) must not get a cream wall that nothing will ever remove. */
.ps-loader { display: none; }
/* The boot cover on later pages: the same element as the first-visit preloader, but it is a
   **transition curtain, not a loader**, and the difference is what it shows.
   `.ps-loader__inner` — the wordmark and the progress bar — is hidden here. With the logo up,
   every navigation replayed the site's opening screen, so moving between four pages of one
   site looked like loading the site four times. A plain panel that *moves* reads as a
   transition; a logo that sits still reads as a wait.
   It is lifted, not deleted: `PS.mountApp` adds `.is-lifting` and removes the element when
   that animation ends, so the page is revealed by the curtain sliding off rather than by the
   cover blinking out of existence. `ps-booting` has to survive until then — it is what makes
   the element `display: flex` and `position: fixed` at all. */
html.ps-booting { overflow: hidden; }
html.ps-booting .ps-loader__inner { display: none; }
/* The curtain lifts with an animation rather than the 750ms transition the first-visit loader
   uses, so mountApp can hang its cleanup off a single `animationend` instead of guessing which
   `transitionend` is the real one. The `transition: transform` further down still applies to
   this element and is simply never triggered — a running animation beats a transition on the
   same property, and nothing else changes its transform. (Declaring `transition: none` here
   does not work and was tried: it is the same specificity as the rule below and loses on
   source order.) */
.ps-loader.is-lifting { animation: ps-curtain-up 440ms cubic-bezier(0.65, 0, 0.35, 1) both; }
@keyframes ps-curtain-up {
  from { transform: translateY(0); }
  to   { transform: translateY(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  .ps-loader.is-lifting { animation: none; }
}
html.ps-booting .ps-loader,
html.ps-loading .ps-loader {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 750ms cubic-bezier(0.72, 0, 0.2, 1);
}
.ps-loader.is-done { transform: translateY(-100%); }
.ps-loader__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 26px;
  transition: opacity 300ms ease, transform 400ms ease;
}
.ps-loader.is-done .ps-loader__inner { opacity: 0; transform: translateY(-14px); }
.ps-loader__logo { height: 56px; width: auto; }
.ps-loader__logo--dark { display: none; }
:root[data-theme="dark"] .ps-loader__logo--light { display: none; }
:root[data-theme="dark"] .ps-loader__logo--dark { display: block; }
.ps-loader__bar {
  width: 148px;
  height: 2px;
  background: var(--line);
  border-radius: 2px;
  overflow: hidden;
}
.ps-loader__bar i {
  display: block;
  height: 100%;
  background: var(--sage);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 220ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .ps-loader, .ps-loader__inner, .ps-loader__bar i { transition: none; }
}

/* Featured block: the big photo takes its height from the right-hand column, so the
   three tiles bottom out together. Falls back to a ratio when stacked. */
@media (min-width: 901px) {
  .g--featured { align-items: stretch; }
}
@media (max-width: 900px) {
  .featured-left { aspect-ratio: 16 / 10; }
}

/* Home services: snap carousel, three cards per view on desktop. Each card carries its
   own muted accent through the --svc custom property. */
.svc-carousel { position: relative; }
.svc-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - 48px) / 3);
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* A scroll container clips both axes, so the cards' hover lift and shadow have to fit
     inside the scrollport or they get sliced. The padding below is derived from
     --shadow-card-lift, not guessed: that shadow reaches 6px sideways, 30px down and
     nothing up, plus the 4px lift. 20px/48px clears all of it with room to spare, and
     the negative margins pull the padding back out so the layout does not shift.
     The horizontal figure also has to stay within --pad-x's 20px floor, or the track
     would hang past the page gutter on a phone. */
  /* Top padding also has to clear the active card's 16px lift and its shadow, hence 40
     rather than the 20 the hover lift alone needed. */
  padding: 40px 20px 48px;
  margin: -40px -20px -32px;
  scroll-padding-left: 20px;
  scrollbar-width: none;
  outline: none;
}
.svc-track::-webkit-scrollbar { display: none; }
.svc-card {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* `start`, not `center`. The scrollport clips at the track's padding box, which is 20px
     wider on each side than the three-card row; centring one card leaves the row unaligned to
     that box, so a neighbour drifts into the 20px and shows as a sliver at the edge. Aligned
     to start, the previous card's right edge lands 4px outside the padding box and is
     clipped. The active card is still found by nearest-to-centre in JS, which is unaffected. */
  scroll-snap-align: start;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 30px 28px;
  border-radius: var(--radius-lg);
  /* Bold ground in the service's own accent. The raw accents are mid-tone — cream on them
     measures 2.4:1 to 4.4:1, all of it failing — so each is pulled toward --ink until the
     worst of the seven clears 4.5:1. Mixing rather than picking new hex values keeps every
     service's hue and keeps SVC_ACCENTS the single source. */
  background: color-mix(in srgb, var(--svc, var(--sage)) 55%, var(--svc-ink));
  color: #f5f1ea;
  border: 0;
  min-height: 260px;
  transition: transform 420ms cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 420ms ease;
}
/* The oversized glyph. Still cropped by the card so it reads as texture rather than an icon
   someone forgot to size, but pulled much closer to the middle and turned on all three axes,
   so it sits in the card like an object rather than a flat sticker. The perspective() has to
   be part of this transform: the card sets `transform-style: preserve-3d` but no perspective
   of its own, and without one rotateX/rotateY are an orthographic squash with no depth. */
.svc-card__mark {
  position: absolute;
  right: -6%;
  bottom: -10%;
  z-index: 0;
  color: #f5f1ea;
  opacity: 0.07;
  pointer-events: none;
  line-height: 0;
  transform-origin: 60% 60%;
  transform: perspective(680px) rotateX(16deg) rotateY(-24deg) rotateZ(-10deg);
}
.svc-card > *:not(.svc-card__mark) { position: relative; z-index: 1; }

/* The countdown to the next card. Present on every card as a faint rule so the top edge is
   consistent; only the active one fills. */
.svc-card__bar {
  display: block;
  height: 3px;
  border-radius: 2px;
  background: rgba(245,241,234,0.22);
  overflow: hidden;
}
.svc-card__bar i {
  display: block;
  height: 100%;
  width: 0;
  border-radius: inherit;
  background: #f5f1ea;
}
.svc-card.is-active .svc-card__bar i {
  animation: svc-countdown var(--svc-dur, 5200ms) linear both;
}
/* Hovering the carousel holds both the timer and its bar, so the bar never claims time that
   is not passing. */
.svc-carousel.is-paused .svc-card__bar i { animation-play-state: paused; }
@keyframes svc-countdown { from { width: 0; } to { width: 100%; } }

/* The centre card stands out of the row. Transform only — changing its height would reflow
   the whole track on every step. */
.svc-card.is-active {
  transform: translateY(-16px);
  box-shadow: 0 26px 50px -24px rgba(26,31,30,0.55);
}
@media (hover: hover) { .svc-card:hover { transform: translateY(-20px); box-shadow: var(--shadow-card-lift); } }

.svc-card__icon {
  width: 46px; height: 46px; border-radius: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(245,241,234,0.14);
  color: #f5f1ea;
}
.svc-card__title { font-size: 21px; line-height: 1.15; margin: 0; max-width: 15ch; color: #f5f1ea; }
/* 0.9, not the 0.85 this started at: against the gold accent (#6f633d once mixed) the
   lighter value measured 4.34:1, under the 4.5 floor. Measure this again if SVC_ACCENTS
   changes — the seven backgrounds are not equally dark. */
.svc-card__desc { font-size: 14px; line-height: 1.55; color: rgba(245,241,234,0.9); flex: 1; max-width: 30ch; }
.svc-card__more { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(245,241,234,0.92); }
@media (prefers-reduced-motion: reduce) {
  .svc-card.is-active .svc-card__bar i { animation: none; width: 100%; }
  .svc-card, .svc-card.is-active { transition: none; }
}
.svc-nav { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; }
.ctl-round {
  width: 42px; height: 42px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line-2); background: var(--paper); color: var(--ink);
  font-size: 16px; cursor: pointer;
  transition: background 160ms ease, color 160ms ease, border-color 160ms ease;
}
@media (hover: hover) { .ctl-round:hover { background: var(--ink); color: var(--bg); border-color: var(--ink); } }
@media (max-width: 900px) {
  .svc-track { grid-auto-columns: 78%; }
}

/* Team preview: the service cards' language — accent tick, mono role, muted accent per
   person — worn by the section's own portraits-beside-copy layout rather than by cards. */
.team-portrait {
  position: relative;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-3);
}
/* z-index clears .img-overlay's scrim, which sits at z-index 1. */
.team-portrait__tick { position: absolute; left: 18px; bottom: 18px; z-index: 2; }
.team-tick { display: block; width: 40px; height: 3px; border-radius: 2px; background: var(--svc, var(--sage)); }
.team-list { margin-top: 30px; display: flex; flex-direction: column; gap: 22px; }
/* The name carries the person's accent; the role under it stays neutral, so the colour
   marks who rather than decorating every line. */
.team-row__name { font-family: var(--font-display); font-size: 20px; font-weight: 600; color: var(--svc, var(--ink)); }
.team-row__role {
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--ink-3); margin-top: 4px;
}

/* The "Why choose us" headline sits in the narrow 0.8fr track of a 0.8fr/1.7fr row, so
   the global h2 clamp — sized for full-width headings — overshoots it. At 66px the German
   "Wissenschaft." measures ~470px against a ~366px column and broke mid-word. The ceiling
   here is set from that word: 13 characters of Literata 700 have to fit the track at every
   width down to 901px, where the row stacks and the heading gets the full container. */
.values-title { font-size: clamp(26px, 3.1vw, 44px); }

/* The premises gallery lives beside the "Why choose us" headline. */
/* Stretches with the label column, so it is exactly as tall as the eyebrow-to-lead block
   on the left. */
.values-gallery {
  position: relative;
  height: 100%;
  min-height: 420px;
}
@media (min-width: 901px) {
  /* Only while the section is two columns. Below that the row stacks and .values-gallery
     is auto-height, so `height: 100%` would resolve against nothing — it collapsed the
     gallery to 0 at 768px and to its content height on a phone. Under 901px the component
     keeps its own height instead. Scoped by a min-width query rather than overridden in a
     max-width one, because `.values-gallery .accordion-gallery` outranks the component's
     own `.accordion-gallery` rules whatever the source order. */
  .values-gallery .accordion-gallery { height: 100%; }
}
@media (max-width: 900px) {
  .values-gallery { height: auto; min-height: 0; }
}

/* AccordionGallery — ported from React Bits, see assets/js/accordion-gallery.jsx. */
.accordion-gallery {
  --ag-accent: #ffffff;
  --ag-overlay: #060010;
  --ag-text: #ffffff;
  --ag-gap: 10px;
  --ag-radius: 16px;
  --ag-media-size: 320px;
  --ag-height: 460px;

  display: flex;
  flex-direction: row;
  gap: var(--ag-gap);
  width: 100%;
  max-width: 100%;
  /* Height via a custom property, not the inline style upstream uses: an inline height
     cannot be overridden by the media query below. */
  height: var(--ag-height);
  perspective: 1400px;
  perspective-origin: 50% 50%;
}
.accordion-gallery--vertical { flex-direction: column; }

.ag-panel {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  border-radius: var(--ag-radius);
  cursor: pointer;
  display: block;
  text-decoration: none;
  outline: none;
  transform-style: preserve-3d;
  transform-origin: center center;
  background: var(--bg-3);
  box-shadow: 0 10px 30px -18px rgba(26,31,30,0.55);
  will-change: flex-grow, transform;
  -webkit-tap-highlight-color: transparent;
}
.ag-panel:focus-visible {
  box-shadow: 0 0 0 2px var(--ag-accent), 0 10px 30px -18px rgba(26,31,30,0.55);
}
.ag-panel__frame { position: absolute; inset: 0; overflow: hidden; border-radius: inherit; }
.ag-panel__media {
  --ag-gray: 1;
  --ag-dim: 0.35;
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--ag-media-size);
  height: 100%;
  filter: grayscale(var(--ag-gray));
  will-change: transform, filter;
}
.accordion-gallery--vertical .ag-panel__media { width: 100%; height: var(--ag-media-size); }
.ag-panel__media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  user-select: none; -webkit-user-drag: none;
}
.ag-panel__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, transparent 48%, color-mix(in srgb, var(--ag-overlay) 64%, transparent) 100%),
    color-mix(in srgb, var(--ag-overlay) calc(var(--ag-dim, 0.35) * 100%), transparent);
}
.ag-panel__label {
  position: absolute;
  left: 20px; bottom: 20px; right: 20px;
  display: flex;
  align-items: center;
  gap: 12px;
  pointer-events: none;
  z-index: 2;
}
.ag-panel__bar {
  flex: 0 0 auto;
  width: 3px; height: 26px;
  border-radius: 3px;
  background: var(--ag-accent);
  opacity: 0;
  box-shadow: 0 0 12px color-mix(in srgb, var(--ag-accent) 60%, transparent);
}
.ag-panel__text {
  color: var(--ag-text);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1rem, 1.4vw, 1.35rem);
  letter-spacing: 0.01em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  text-shadow: 0 2px 14px rgba(0,0,0,0.55);
}
@media (max-width: 560px) {
  /* A row of five panels has nothing left to give at phone widths, so it becomes a
     column. Upstream sets `height: auto` here, which leaves flex-grow no free space to
     distribute and freezes every panel at its min-height — the accordion stops
     accordioning. A definite height keeps the expansion working. */
  .accordion-gallery {
    flex-direction: column;
    perspective: none;
    /* The height is --ag-height, which the component computes for the column: the open
       panel at the photographs' 16:9 plus the four strips at their min-height plus the gaps
       (412px on a 375 phone). Flex-grow lands the open panel on exactly that height because
       the strips clamp at their floor and it takes the rest — so the floor below and the
       open panel's ratio are the two numbers that decide the layout. It was a width-keyed
       clamp before, and the open panel's box then had nothing to do with the photo's shape. */
  }
  .ag-panel { min-height: 46px; transform: none !important; }
  .accordion-gallery .ag-panel__media { width: 100%; height: var(--ag-media-size); }
}
@media (prefers-reduced-motion: reduce) {
  .ag-panel, .ag-panel__media { will-change: auto; }
}

/* Home hero: one full-bleed photograph with the copy weighted to the lower left, where
   the studio wall is calm enough to carry white type. The photo is a CSS background
   (not <picture>) because it is decorative and must sit under a scrim; the width swap
   is a media query rather than JS, the same pattern as the About hero. */
/* The three cream page heroes. Their slab is pulled up behind the nav so the ground runs to
   the top of the window, so the bar's height comes back here as padding — the eyebrow and
   heading start exactly where they did, with no seam above them. Padding, never margin: a
   top margin would move the slab's ground down with the content and put the gap back. */
/* The FAQ's left column: heading at the top, MagnetLines pinned to the bottom of whatever
   height the accordion beside it sets. `margin-top: auto` rather than a fixed offset, so the
   grid stays in the corner however long the questions list gets. */
.faq-intro { display: flex; flex-direction: column; height: 100%; }
/* Takes every pixel the heading and lead leave, across the full column width. flex:1 with
   min-height:0 rather than a fixed box, so it grows with the questions list beside it. */
.faq-lines { margin-top: auto; padding-top: 48px; flex: 1 1 auto; min-height: 160px; }
.faq-lines .magnet-lines { width: 100%; height: 100%; }
@media (max-width: 900px) {
  /* Stacked, there is no corner left to fill and it would only push the questions down. */
  .faq-lines { display: none; }
}

.page-hero {
  position: relative;
  padding-top: calc(80px + var(--nav-h));
  padding-bottom: 40px;
}
/* The aurora band. It runs the full width and off the top of the section — the slab is pulled
   up behind the nav, so the band starts under the bar and the blurred nav sits on it. Masked
   away toward the bottom rather than ending on a line. */
.page-hero__aurora {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 78%;
  min-height: 260px;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: 0.55;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 38%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 38%, transparent 100%);
}
:root[data-theme="dark"] .page-hero__aurora { opacity: 0.42; }
.aurora { width: 100%; height: 100%; }
/* The copy rides above it. */
.page-hero > .wrap { position: relative; z-index: 1; }

/* Centred section header — same idea as the hero, one step down. The clinic's sections use
   it so the page reads as one centred column from top to bottom. The ch-based cap does the
   same job here: it is characters per line, not pixels, that decide the line count. */
.sec-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  margin-bottom: 56px;
}
.sec-head__title {
  margin: 18px 0 0;
  max-width: 20ch;
  font-size: clamp(24px, 5.4vw, 64px);
  text-wrap: balance;
}
.sec-head__lead {
  margin: 18px 0 0;
  max-width: 56ch;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-3);
}

/* The three page heroes are one centred column. */
.page-hero__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.page-hero__title {
  margin: 24px 0 0;
  /* The whole "one or two lines at any width" rule lives in this max-width, and it is in
     `ch` on purpose: ch scales with the font size, so what it caps is the number of
     *characters* per line, which is what decides the line count — a px or vw cap would let
     the count drift with the viewport. 21ch is set from the longest heading any language
     produces here: the clinic's 39 characters, which need ~20 a line to land on two.
     The clamp minimum is then whatever keeps 21ch inside a 375px column. */
  max-width: 21ch;
  /* Every number here is measured, not guessed. The minimum is set by the narrowest viewport
     worth supporting (320px, where the column is 280px) against the longest line any language
     produces — the Greek "Πλήρως εξοπλισμένος." — and the coefficient is then raised to 7.2vw
     so the common phone widths do not sit at that floor: 24px only at 320, 27px at 375, 31px
     at 430. Re-run the line-count sweep if any hero heading gets longer. Below 900px this is
     only the no-JS baseline: fit-headings.js sizes the heading from what fits (see the block
     at the end of this file). */
  font-size: clamp(24px, 7.2vw, 92px);
  /* Two lines of roughly equal length rather than a long one and an orphan. */
  text-wrap: balance;
}
.page-hero__lead {
  margin: 26px 0 0;
  max-width: 54ch;
  font-size: 18px;
  line-height: 1.6;
  color: var(--ink-3);
}
.page-hero__actions {
  margin-top: 32px;
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
}

.hero-full {
  position: relative;
  /* A full viewport, not a viewport minus the nav: the slab this sits in is pulled up under
     the nav (see `.nav + .lvl-above:has(> .hero-full)` below), so the photo starts at y=0
     and the transparent nav floats on it. The hero alone is therefore exactly one screen
     and its buttons never sit under the fold; `.hero-full__content` keeps the --nav-h of
     top padding that holds the headline clear of the bar. svh keeps mobile URL bars
     honest. */
  height: 100vh;
  height: 100svh;
  min-height: 520px;
  padding: 0;
  overflow: hidden;
  isolation: isolate;
  background: #0d1614;
  /* Fixed cream, not var(--bg): the photo is dark in both themes, and in dark mode
     --bg is near-black, which made the headline and primary button disappear. */
  color: #f5f1ea;
  display: flex;
  align-items: center;
}
.hero-full__bg {
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Anchored to the photo's bottom edge, so the floor and the reformer's base are
     never cropped away, whatever the viewport ratio. */
  background: url("../../media/hero-treatment-room-1200.jpg") center bottom / cover no-repeat;
  /* The render is bright and airy; grade it down and let the dark stage show through so
     cream type stays legible, instead of leaning on a heavy scrim. */
  filter: saturate(0.82) contrast(1.06) brightness(0.92);
  opacity: 0.68;
  /* No drift animation: the scale would re-crop the bottom we just anchored. */
}
@media (min-width: 900px) {
  /* The render is 1376px native and the build never upscales; 1376 is the ceiling. */
  .hero-full__bg { background-image: url("../../media/hero-treatment-room-1264.jpg"); }
}
.hero-full__scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  background:
    /* Regraded 2026-09-03 for the treatment-room photograph, which is far brighter than the
       render it replaced — cream walls and a window falling exactly where the left-aligned
       copy sits. At the old 0.62/0.30/0.06 the review widget's muted line measured 3.90:1,
       under the 4.5 floor for 14px text. Measured by rasterising the real composite (image →
       the bg filter → 0.68 over #0d1614 → both gradients) and sampling every pixel under each
       run, not from the tokens. Re-measure the same way if the photograph changes again. */
    linear-gradient(90deg, rgba(13,22,20,0.78) 0%, rgba(13,22,20,0.40) 50%, rgba(13,22,20,0.08) 100%),
    linear-gradient(180deg, rgba(13,22,20,0.18) 0%, rgba(13,22,20,0.00) 40%, rgba(13,22,20,0.62) 100%);
}
/* SideRays (WebGL) sits between the scrim and the copy. */
.hero-full__rays {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: screen;
}
.side-rays-container {
  width: 100%;
  height: 100%;
  position: relative;
  pointer-events: none;
  overflow: hidden;
}
.hero-full__content {
  position: relative;
  z-index: 4;
  width: 100%;
  /* Vertically centred by the flex parent; this clears the nav the photo now runs behind. */
  padding-top: var(--nav-h);
  /* No max-width here: `.wrap` is declared later and would win. The headline and lead
     carry their own limits instead, so the copy stays on the calm left of the photo. */
}
/* Two sentences, one per line: wide enough for 21 Greek characters at the top size. */
.hero-full h1 { max-width: 22ch; }
.hero-full h1 {
  margin-top: 0;
  color: #f5f1ea;
  font-size: clamp(36px, 5.6vw, 84px);
  line-height: 1.04;
}
.hero-full__lead {
  margin-top: 24px;
  max-width: 560px;
  font-size: 18px;
  line-height: 1.55;
  color: rgba(245,241,234,0.82);
}
.hero-full__actions {
  display: flex;
  gap: 12px;
  margin-top: 32px;
  flex-wrap: wrap;
}
/* Button colours are defined for a cream page; invert them on the photograph. */
/* Hero social proof: overlapping reviewer initials, the rating, and the aggregate.
   No ground and no border — it sits directly on the photograph. That means its legibility is
   carried entirely by `.hero-full__scrim`, which is graded for the headline, so **re-measure
   this if the scrim or the hero photograph changes**; it is the small text on this page with
   the least margin. Measured over the actual backdrop at the widget's own position, cream at
   0.82 alpha clears AA.

   It takes its colour from `.hero-full` by inheritance rather than restating a hex, so it
   follows whatever scheme the hero uses. Note that scheme is fixed dark in *both* themes on
   purpose: `.hero-full` hardcodes `#0d1614` and `#f5f1ea` because the photograph does not
   change with the theme, and in dark mode `--bg` is near-black, which made the headline and
   the primary button disappear. Reaching for `--ink` here would put dark text on a dark
   photograph in light mode. */
.hero-proof {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  /* Sits above the headline, so the space it owns is below it. */
  margin-bottom: 26px;
  /* The row wraps rather than overflowing when a long count string meets a phone. */
  flex-wrap: wrap;
  max-width: 100%;
}
.hero-proof__avatars { display: inline-flex; flex-shrink: 0; }
.hero-proof__avatars > span {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 13px;
  line-height: 1;
  color: #fff;
  /* No ring. The overlap is smaller to compensate: without a border it was the ring that
     kept two adjacent discs from reading as one smear, so the discs now barely touch and
     their own colours do that job. */
  margin-left: -7px;
  /* Optically centres the initial. `align-items: center` centres the *line box*, and the
     baseline sits (fontAscent - fontDescent)/2 below that centre, so the glyph itself lands
     low — measured at 0.95px in a 30px disc, which is visible. The correction is the gap
     between the ink centre and the font's own centre:
       (inkAscent - inkDescent)/2 - (fontAscent - fontDescent)/2
     which for Literata 600 is 4.55 - 5.5 = -0.95px at 13px, i.e. -0.073em. Applied as
     padding rather than a transform because the letter is a bare text node with nothing to
     transform, and padding-bottom shifts a centred child up by half its value. Re-measure if
     the display face changes; the four initials' ink metrics agree to within 0.1px, so one
     value serves all of them. */
  padding-bottom: 0.146em;
}
.hero-proof__avatars > span:first-child { margin-left: 0; }
/* Both colours come off `.hero-full` by inheritance rather than restating its cream, so the
   widget cannot drift from the hero the way a second copy of a hex would. The mute goes on
   `__meta` and not on `__text`, because a <strong> inside a muted parent resolves
   `currentColor` to the muted value and quietly stops standing out. */
.hero-proof__text {
  font-size: 14px;
  line-height: 1.3;
}
.hero-proof__text strong { font-weight: 700; }
.hero-proof__meta { color: color-mix(in srgb, currentColor 82%, transparent); }

@media (max-width: 600px) {
  /* Tighter. No padding or radius to step down any more — there is no pill. */
  .hero-proof { gap: 10px; margin-bottom: 20px; }
  .hero-proof__avatars > span { width: 26px; height: 26px; font-size: 12px; margin-left: -9px; }
  .hero-proof__text { font-size: 13px; }
}

.hero-full .btn-primary { background: #f5f1ea; color: #1a1f1e; }
.hero-full .btn-primary::after { background: var(--sage-light); }
.hero-full .btn-ghost { color: #f5f1ea; }
.hero-full .btn-ghost::after { background: #f5f1ea; }
@media (hover: hover) { .hero-full .btn-ghost:hover { color: #1a1f1e; border-color: #f5f1ea; } }
@media (max-width: 900px) {
  .hero-full { min-height: 520px; }
  /* The plinth and the wall bars sit right of centre; keep them in the portrait crop, which
     shows only ~31% of the width at 375. */
  .hero-full__bg { background-position: 58% bottom; }
  .hero-full__scrim {
    background:
      linear-gradient(180deg, rgba(13,22,20,0.30) 0%, rgba(13,22,20,0.20) 30%, rgba(13,22,20,0.86) 100%);
  }
  /* "Αποκατάσταση" is 12 unbreakable characters: let the headline use the whole column
     and start a step smaller, or the word splits in two. */
  .hero-full h1 { max-width: none; font-size: clamp(34px, 9.5vw, 56px); }
  /* Tightened for short phones. Splitting the headline into per-letter spans for
     VariableProximity gives up the kerning between them, which widens each line enough to
     cost a wrap on a 375px column — at 375x667 that put the buttons 4px under the fold.
     These three reclaim 20px between them; the taller phones keep the breathing room. */
  .hero-proof { margin-bottom: 14px; }
  .hero-full__lead { margin-top: 18px; }
  .hero-full__actions { margin-top: 24px; }
}

/* A short phone window. The hero is exactly one viewport, so on a 640px-tall screen the
   heading is what has to give — not the buttons, which are the only reason the hero exists.
   Keyed on height, because that is the actual constraint: chasing it with width breakpoints
   fixes one device and breaks the next. */
@media (max-width: 900px) and (max-height: 700px) {
  .hero-full h1 { font-size: clamp(26px, 7.4vw, 40px); --fit-max: 40px; }
  .hero-full__lead { margin-top: 14px; font-size: 16px; }
  .hero-proof { margin-bottom: 10px; }
  .hero-full__actions { margin-top: 18px; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-full__bg { animation: none; }
}
/* ============================================
   Responsive grids
   ============================================
   The page grids were authored as inline styles, and an inline style beats any
   stylesheet rule, so a media query could never collapse them — which is why the site
   kept its desktop columns on a phone and overflowed sideways.

   The fix keeps each grid's desktop template exactly where it was written, but as a
   --cols custom property instead of the property itself:

     <div className="g g--stack" style={{ "--cols": "1.2fr 0.8fr", gap: 24 }}>

   The class supplies grid-template-columns from that property, which leaves the
   property free for the media queries below to override. Desktop is unchanged. */
.g { display: grid; grid-template-columns: var(--cols, 1fr); min-width: 0; }
.g > * { min-width: 0; }

/* Four rooms need more width per column than three did: at 1000px a quarter is ~230px and the
   longest name ("Αίθουσα αποκατάστασης & γυμναστήριο") wrapped to four lines against two for
   its neighbours. Two-up carries them from where that starts down to the single column the
   900px block sets below.
   **It has to live after `.g`, and that is the whole reason it is here.** `.g--rule` is ONE
   class, so it is (0,1,0) — exactly the same specificity as `.g { grid-template-columns:
   var(--cols) }`. Source order decides between them, and written earlier in the file it lost
   silently: the rule was in the stylesheet, its media condition matched, and the grid stayed
   at four columns. Same trap the contact-side map rule documents. */
@media (max-width: 1150px) and (min-width: 901px) {
  .g--rule { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  /* Two-column text/media pairs simply stack. */
  .g--stack { grid-template-columns: minmax(0, 1fr); }

  /* Card rows go two-up on a tablet before stacking outright. */
  .g--cards { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }

  /* Columns separated by hairlines rather than gaps: the dividers move from the sides
     to between the stacked rows. !important is deliberate — the borders and padding
     being overridden are themselves inline styles. */
  .g--rule { grid-template-columns: minmax(0, 1fr); }
  .g--rule > * { padding: 26px 0 !important; border: 0 !important; }
  .g--rule > * + * { border-top: 1px solid var(--rule, var(--line)) !important; }

  /* Services spotlight: three fixed-height tiles become full-width landscape ones. */
  .g--spotlight { grid-template-columns: minmax(0, 1fr); height: auto !important; }
  /* Taller than the desktop letterbox: full-width on a phone these are only ~210px
     high at 16/10, which is not enough for a caption carrying a long Greek service name
     over three lines. */
  .g--spotlight > * { aspect-ratio: 4 / 3; }

  /* Clinic gallery: keep the lead cell double-width, everything else two-up. */
  .g--gallery { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-auto-rows: 150px !important; }
  .g--gallery > * { grid-column: auto; grid-row: auto; }
  .g--gallery > *:first-child { grid-column: span 2; grid-row: span 2; }

  /* Bio cards: portrait above the biography, and nothing sticks to the viewport. */
  .g--bio { grid-template-columns: minmax(0, 1fr); }
  /* Not `position: static`: the accent rule is absolutely positioned against this box, so
     taking away its containing block would resolve it against the level instead. The
     un-sticking is done by `relative` + `top: auto` in the .bio-photo block further down,
     which is also where the offset shrinks. */
  .bio-photo { order: -1; }

  .g--stats { gap: 24px !important; }

  /* Price rows: name, note and figure stack, and the figure stops hanging right. */
  .g--price { grid-template-columns: minmax(0, 1fr); gap: 6px !important; }
  .g--price > :last-child { text-align: left !important; }
}

@media (max-width: 620px) {
  .g--cards { grid-template-columns: minmax(0, 1fr); }
  .g--gallery { grid-auto-rows: 132px !important; }
}

/* Contact details. The email is one unbreakable 21-character token, so at a fixed 32px
   it was wider than a phone's text column. Above 727px this still computes to exactly
   32px, so the desktop rendering is unchanged. */
.contact-value {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(22px, 4.4vw, 32px);
  letter-spacing: -0.015em;
  overflow-wrap: anywhere;
}

/* The stats sit as one centred group, each figure centred over its label. */
.g--stats { justify-content: center; justify-items: center; text-align: center; }
/* Three `auto` tracks never collapse, so on a phone the widest label decides the row —
   and German ("Behandlungsprotokolle") and Greek ("Πρωτόκολλα θεραπείας") are both single
   long words that cannot share 91px. Stack them instead of hoping they fit. */
@media (max-width: 560px) {
  .g--stats { grid-template-columns: minmax(0, 1fr) !important; gap: 28px !important; }
}
.g--stats > * { min-width: 0; }
.g--stats > * > * { overflow-wrap: break-word; }

/* Stat figures scale with the viewport instead of sitting at a fixed 56px, which
   overflowed three-up on a phone. */
.stat-num {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1;
  color: var(--ink);
  font-size: clamp(30px, 7vw, 56px);
}
/* Tabular figures while the number is counting: with proportional ones every frame that
   swaps a 1 for a 4 changes the string's width, and the suffix beside it twitches all the
   way up. Literata carries tnum; a face that does not simply ignores this. */
.count-up { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }

/* Headings — Literata display */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.06;
  margin: 0;
  color: var(--ink);
  text-wrap: balance;
  overflow-wrap: break-word;
}
h1 { font-size: clamp(33px, 7vw, 108px); font-weight: 700; letter-spacing: -0.022em; }
h2 { font-size: clamp(26px, 4.6vw, 66px); letter-spacing: -0.016em; }
h3 { font-size: clamp(20px, 2.6vw, 40px); }
h4 { font-size: clamp(18px, 1.4vw, 22px); }

.serif { font-family: var(--font-serif); font-style: italic; font-weight: 400; letter-spacing: -0.01em; }
.mono { font-family: var(--font-mono); letter-spacing: 0; }
.eyebrow {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
  font-weight: 500;
}

p { margin: 0; text-wrap: pretty; }
p + p { margin-top: 1em; }

/* Layout */
.wrap { max-width: 1440px; margin: 0 auto; padding-left: var(--pad-x); padding-right: var(--pad-x); }
.wrap-wide { max-width: 1600px; margin: 0 auto; padding-left: var(--pad-x); padding-right: var(--pad-x); }
section { padding-top: var(--section-y); padding-bottom: var(--section-y); }
/* Two sections stacked inside one level each pay a full `--section-y`, so the gap between
   them is double the gap the rhythm intends — measured at 277px between the clinic gallery's
   last photograph and the rooms heading at 1440. The second of a pair takes half its top
   padding, which closes that to ~212px without touching the space around a level's first and
   last sections, where the full step is what separates the slab from its neighbours.
   Affects every adjacent pair on the site: home (stats + values, featured + services
   preview), services (hero + spotlight), about (hero + reviews), clinic (hero + gallery,
   gallery + rooms). */
.lvl-above > section + section,
.lvl-below > section + section { padding-top: calc(var(--section-y) * 0.5); }

/* Nav.
   Three columns: brand, centred links, controls. The links were a free-floating flex row
   with an underline; they are now pills so the active page reads at a glance. Every
   control on the right is exactly 40px tall, so nothing can sit off the shared centre. */
.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  /* No border and no ground of its own. Both live on the layer below, which fades them out
     down the bar so nothing draws a hard line where the nav ends. Keeping `backdrop-filter`
     off `.nav` itself also stops it being the containing block for `position: fixed`
     descendants — the trap that made the mobile sheet resolve `inset: 0` against the 72px
     nav strip. The sheet is portalled to <body> regardless; this just removes the trap. */
}
.nav::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  /* Taller than the bar, and that is the whole trick. A blur layer the same height as the
     nav has to reach zero exactly at its bottom edge, which reads as a blur that was cut
     off; giving the mask another 38px below the bar lets the falloff finish in open page,
     so it reads as a gradient. Same construction as the reference: layer 1.35x the bar. */
  height: calc(var(--nav-h) + 38px);
  z-index: -1;
  pointer-events: none;
  /* No tint. The bar is not coloured at all — blur alone, so whatever is behind stays its
     own colour and the nav has no ground of any kind. */
  background: none;
  /* Plain blur, no saturate(): saturate shifts the colour of what is behind, which is a
     tint by another name. */
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  /* Opaque across the logo and links, then off to nothing 38px below the bar. */
  mask-image: linear-gradient(to bottom, #000 0%, #000 40%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 40%, transparent 100%);
}
.nav-inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 24px;
  height: 72px;
  padding: 0 var(--pad-x);
}
.nav-logo {
  display: flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  flex-shrink: 0;
}
.nav-logo img { height: 40px; width: auto; }
.nav-logo-text {
  display: flex;
  flex-direction: column;
  line-height: 1;
}
.nav-logo-text strong {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 18px;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.nav-logo-text span {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-top: 4px;
}
.nav-links {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
.nav-links a {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 14px;
  border-radius: 999px;
  font-size: 14.5px;
  font-weight: 500;
  /* The same token the hover fill uses (.pill-circle, and the active tab's ground). The
     label and the shape that rises behind it are one colour, so the hover reads as the
     label being filled in rather than swapped for something else. */
  color: var(--ink);
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.nav-links a.active { background: var(--ink); color: var(--bg); }
.nav-links .pill-circle {
  position: absolute;
  /* Out past the clip on all three sides, the same -1px `.btn::after` takes and for the
     neighbouring reason. The parent clips with `overflow: hidden` + `border-radius: 999px`,
     and while the dome is mid-transform it is on its own composited layer, so the rounded
     clip is applied to it as a mask. With the dome's own edge sitting exactly on that
     boundary the mask's antialiased pixel is a blend of dome and whatever is *behind* the
     nav, which showed as a mis-coloured hairline down one end of the pill for the length of
     the animation. Pushing the dome a pixel past the boundary means that pixel is dome on
     both sides of the blend and only the clip's own antialiasing remains.
     It reads as one-sided because it is: every nav link has fractional edges and the two
     ends land on different subpixel offsets (measured at 1200px — Υπηρεσίες is left .75,
     right .563), so the seam is far more visible at one end than the other, and which end
     changes with the viewport width and the language. Fixing both ends is the only version
     of this that stays fixed. */
  left: -1px;
  right: -1px;
  bottom: -1px;
  height: 200%;
  border-radius: 50% 50% 0 0 / 30% 30% 0 0;
  background: var(--ink);
  z-index: -1;
  transform: scaleY(0);
  transform-origin: 50% 100%;
  transition: transform 460ms cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: none;
}
/* The current page's tab is already a filled pill, so it takes no hover effect at all. */
.nav-links a.active .pill-circle { display: none; }
.nav-links a.active .pill-label-hover { display: none; }
.nav-links a.active:focus-visible .pill-label { transform: none; }
@media (hover: hover) { .nav-links a.active:hover .pill-label { transform: none; } }
.nav-links a:focus-visible .pill-circle { transform: scaleY(1); }
@media (hover: hover) { .nav-links a:hover .pill-circle { transform: scaleY(1); } }
.nav-links a:active .pill-circle { transform: scaleY(0.92); transition-duration: 120ms; }
/* Two copies of the label, one sliding out as the other slides in. */
.nav-links .label-stack {
  position: relative;
  display: block;
  overflow: hidden;
  height: 1.25em;
  line-height: 1.25em;
}
.nav-links .pill-label,
.nav-links .pill-label-hover {
  display: block;
  transition: transform 460ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.nav-links .pill-label-hover {
  position: absolute;
  inset: 0;
  color: var(--bg);
  transform: translateY(110%);
}
.nav-links a:focus-visible .pill-label { transform: translateY(-110%); }
@media (hover: hover) { .nav-links a:hover .pill-label { transform: translateY(-110%); } }
.nav-links a:focus-visible .pill-label-hover { transform: translateY(0); }
@media (hover: hover) { .nav-links a:hover .pill-label-hover { transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .btn::after, .nav-links .pill-circle,
  .nav-links .pill-label, .nav-links .pill-label-hover { transition: none; }
}
.nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-self: end;
}
/* The booking CTA is the one cream-page button whose height must match the controls. */
.nav-right > .btn { height: 40px; padding: 0 18px; font-size: 13.5px; }
.lang-toggle {
  display: inline-flex;
  background: var(--bg-2);
  border-radius: 999px;
  padding: 4px;
  border: 1px solid var(--line);
  gap: 2px;
}
.lang-toggle button {
  background: transparent;
  border: 0;
  padding: 6px 12px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--ink-3);
  border-radius: 999px;
  transition: all 160ms ease;
}
.lang-toggle button.active {
  background: var(--ink);
  color: var(--bg);
}

/* Theme + language share one segmented pill, 40px tall like everything beside it. */
.nav-controls {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 40px;
  padding: 3px;
  border-radius: 999px;
  border: 1px solid var(--line-2);
  background: var(--paper);
}
.ctl-circle {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--ink);
  padding: 0; overflow: hidden; flex-shrink: 0;
  transition: background 160ms ease;
}
.ctl-circle:focus-visible { background: var(--ctl-hover); }
@media (hover: hover) { .ctl-circle:hover { background: var(--ctl-hover); } }
.ctl-circle svg { width: 17px; height: 17px; }
.ctl-circle--flag { width: 28px; height: 28px; margin: 2px; }
/* The flag fills its button. This used to be an inline style on the svg; it lives here so the
   coarse-pointer block below can hold the flag at 28px while the button grows around it.
   The selector is TWO classes on purpose: `.ctl-circle svg` above sets every control icon to
   17px at (0,1,1), and a bare `.flag-svg` at (0,1,0) loses to it — which shrank the flag to a
   17px square inside the 28px circle for a day (2026-09-05). The radius is on the flag itself,
   not only on the button, so it stays a circle when the coarse block grows the button to 40px
   around a 28px flag. */
.ctl-circle--flag .flag-svg { width: 100%; height: 100%; display: block; border-radius: 50%; }
/* A ring, not a background. The flag SVG is 28x28 inside a 28x28 button — it covers the
   button completely, so a background hover is painted underneath it and can never be seen,
   whatever colour it is. There is 6px of room around the flag inside the pill and
   `.nav-controls` does not clip, so a 2px ring sits clear of the pill's own border. */
.ctl-circle--flag { transition: box-shadow 160ms ease; }
.ctl-circle--flag:focus-visible { box-shadow: 0 0 0 2px var(--ctl-ring); }
@media (hover: hover) { .ctl-circle--flag:hover { box-shadow: 0 0 0 2px var(--ctl-ring); } }
/* The wrapper was a block div inside a flex row, which is what pushed the flag 3px
   above the other controls: it grew to the line box and the button sat at its top. */
.lang-flag { position: relative; display: flex; align-items: center; }
.lang-menu {
  position: absolute; right: 0; top: calc(100% + 10px);
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow-lift);
  padding: 6px; display: flex; flex-direction: column; gap: 2px;
  z-index: 120; min-width: 190px;
}
.lang-menu button {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 12px; border: 0; background: transparent;
  border-radius: 10px; text-align: left; color: var(--ink);
  font-family: var(--font-sans); font-size: 14px; font-weight: 500;
  transition: background 140ms ease;
}
/* Three distinct steps. Hover and `.active` were both `--bg-2`, so hovering the selected
   language changed nothing and hovering any other made it look selected. */
.lang-menu button:focus-visible { background: var(--ctl-hover); }
@media (hover: hover) { .lang-menu button:hover { background: var(--ctl-hover); } }
.lang-menu button.active { background: var(--ctl-sel); font-weight: 700; }
.lang-menu button.active:focus-visible {
  background: color-mix(in srgb, var(--ink) 24%, var(--paper));
}
@media (hover: hover) { .lang-menu button.active:hover {
  background: color-mix(in srgb, var(--ink) 24%, var(--paper));
} }
.lang-menu .flag-circle {
  width: 22px; height: 22px; border-radius: 50%; overflow: hidden;
  flex-shrink: 0; border: 1px solid var(--line); display: block;
}
.lang-menu .flag-circle svg { width: 100%; height: 100%; display: block; }

/* Mobile nav toggle */
.nav-mobile-btn {
  display: none;
  width: 40px; height: 40px; border-radius: 50%;
  align-items: center; justify-content: center;
  background: var(--paper);
  border: 1px solid var(--line-2);
  padding: 0;
  color: var(--ink);
}
.nav-mobile-btn svg { width: 20px; height: 20px; }

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  /* Centred, so a button stretched to full width (the mobile sheet's booking button) keeps
     its label in the middle; on an auto-width button the content fills the row and this is
     a no-op (owner caught the left-parked label, 2026-09-05). */
  justify-content: center;
  gap: 10px;
  --btn-py: 14px;
  --btn-px: 22px;
  padding: var(--btn-py) var(--btn-px);
  border-radius: 999px;
  font-weight: 600;
  font-size: 15px;
  border: 1px solid transparent;
  transition: color 260ms ease, border-color 260ms ease, transform 200ms cubic-bezier(0.2, 0.7, 0.2, 1);
  white-space: nowrap;
  text-decoration: none;
  /* PillNav's hover: a circle rises from the bottom edge and fills the pill. It is a
     negative-z-index pseudo-element inside an isolated stacking context, which paints
     above the button's own background but below its text — buttons here contain bare
     text nodes, and a text node cannot be lifted with z-index. */
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
/* A dome, not a circle. PillNav sizes its circle per pill in JS so the arc passes through
   the bottom corners; a fixed circle in CSS cannot, because the required diameter depends
   on the pill's aspect ratio — at `bottom: 0` the circle is a single point on the bottom
   edge, so the bottom corners never fill, and the ratio that covers a 156x40 nav pill
   still leaves a 245x38 full-width button short. This shape is full-width below its dome
   at every size, so it always covers, and the curve still sweeps up as it rises. */
.btn::after {
  content: "";
  position: absolute;
  /* Out past the edges by the border width: an absolutely positioned child resolves
     against the padding box, which left a 1px hairline of border showing round the fill. */
  left: -1px;
  right: -1px;
  bottom: -1px;
  height: 200%;
  border-radius: 50% 50% 0 0 / 30% 30% 0 0;
  z-index: -1;
  transform: scaleY(0);
  transform-origin: 50% 100%;
  transition: transform 460ms cubic-bezier(0.2, 0.7, 0.2, 1);
  pointer-events: none;
}
/* Entering, the recolour waits for the fill to get behind the text; leaving, it snaps
   back with the fill so the label is never stranded in its hovered colour on the idle
   background — which on the hero's ghost button showed as dark text on a dark button. */
.btn:focus-visible { transition-delay: 150ms, 150ms, 0s; }
@media (hover: hover) { .btn:hover { transition-delay: 150ms, 150ms, 0s; } }
.btn:focus-visible::after { transform: scaleY(1); }
@media (hover: hover) { .btn:hover::after { transform: scaleY(1); } }
/* Pressing eases it back a little, so a click reads as a press rather than nothing. */
.btn:active::after { transform: scaleY(0.92); transition-duration: 120ms; }
/* The filled buttons carry no border. They used to inherit `.btn`'s 1px transparent one,
   and `overflow: hidden` clips the rising hover fill to the *padding* box — so that 1px ring
   kept showing the button's own background around the fill, reading as an outline that
   appeared on hover. They take the 1px back as padding so a filled button and an outline one
   side by side are still exactly the same size. */
.btn-primary, .btn-accent {
  border: 0;
  padding: calc(var(--btn-py) + 1px) calc(var(--btn-px) + 1px);
}
.btn-primary {
  background: var(--ink);
  color: var(--bg);
}
.btn-primary::after { background: var(--sage-dark); }
@media (hover: hover) { .btn-primary:hover { transform: translateY(-1px); } }
.btn-accent {
  background: var(--terra);
  color: white;
}
.btn-accent::after { background: var(--terra-dark); }
@media (hover: hover) { .btn-accent:hover { transform: translateY(-1px); } }
/* Outline buttons take their border from their text: `currentColor`, so setting the colour
   is the only thing a variant has to do and the two can never drift apart. Hover is the one
   place it is set explicitly — by then the pill is filled, so the ring has to match the fill
   rather than the (now inverted) label. */
.btn-ghost {
  background: transparent;
  color: var(--ink);
  border-color: currentColor;
}
.btn-ghost::after { background: var(--ink); }
@media (hover: hover) { .btn-ghost:hover { color: var(--bg); border-color: var(--ink); } }
.btn-sm { --btn-py: 10px; --btn-px: 16px; font-size: 13px; }
.btn-lg { --btn-py: 18px; --btn-px: 28px; font-size: 16px; }

.arrow {
  display: inline-block;
  transition: transform 200ms ease;
}
@media (hover: hover) { .btn:hover .arrow { transform: translateX(4px); } }

/* Tag pill */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: var(--bg-2);
  border: 1px solid var(--line);
  color: var(--ink-3);
  font-weight: 500;
}
.tag.dot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--sage);
}

/* Footer */
.footer {
  background: var(--footer-bg);
  color: var(--footer-ink);
  /* No top border. The level boundary above already draws the edge — the slab's shadow
     falls onto the footer's level — so a rule here read as a second, competing seam. */
  padding: 80px var(--pad-x) 40px;
  /* So .footer-mega can size itself from this content box in cqw. Measuring in vw instead
     overshoots by the scrollbar's width, which the content box does not have. */
  container-type: inline-size;
}
.footer-grid {
  display: grid;
  /* Navigation and Contact are the same width and stay side by side; the brand block is
     the one that gives way, moving above them rather than squeezing them apart. */
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: 48px;
  max-width: 1440px;
  margin: 0 auto;
}
.footer h4 {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--footer-ink-2);
  margin-bottom: 20px;
}
.footer-col p, .footer-col a {
  color: var(--footer-ink);
  font-size: 15px;
  line-height: 1.6;
  display: block;
  margin-bottom: 10px;
}
@media (hover: hover) { .footer-col a:hover { color: var(--sage-dark); } }
/* A glyph and its label on one row, for both the navigation and the contact columns.
   The selectors carry `.footer-col` and the element because `.footer-col p, .footer-col a`
   sets `display: block` at (0,1,1) — the same trap `.footer-social` documents below. The
   icon does not shrink, so a long value (the email) wraps against it rather than squashing
   it, and it sits at the top of a wrapped row rather than drifting to the middle. */
.footer-col a.footer-link,
.footer-col p.footer-link {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 10px;
}
.footer-col .footer-link svg {
  flex: 0 0 auto;
  /* Nudged down to sit on the text's optical centre rather than its box top: the line box is
     1.6 x 15px and the glyph is 16px, so without this it rides high against the first line. */
  margin-top: 4px;
  color: var(--footer-ink-2);
  transition: color 160ms ease;
}
@media (hover: hover) { .footer-col a.footer-link:hover svg { color: var(--sage-dark); } }
/* The email is one unbreakable token and the contact column is the narrowest one, so
   without this it is what pushes the footer sideways on a phone. */
.footer-col a[href^="mailto:"] { overflow-wrap: anywhere; }
/* Selector carries .footer-col because `.footer-col p, .footer-col a` sets
   `display: block; margin-bottom: 10px` and outranks a bare `.footer-social` — the button
   was never a flex container, so its icon sat 11px left and 10px above the circle's
   centre, and the 10px margin pushed the row off the block below it. */
.footer-col a.footer-social {
  width: 40px; height: 40px; border-radius: 50%;
  /* Filled with the mark's own brand colour and a white glyph — the same treatment
     `.contact-social__mark` gives these two on the clinic page, so the site has one format
     for a social button rather than an outlined circle here and a colour block there. The
     border is gone with it: a ring round a filled disc is a second edge saying nothing. */
  border: 0;
  background: var(--brand, var(--ink));
  display: flex; align-items: center; justify-content: center;
  margin: 0; color: #fff;
  transition: transform 160ms ease, filter 160ms ease;
}
/* Instagram's mark is a gradient, not a flat colour. Same stops as the clinic tiles. */
.footer-col a.footer-social--ig {
  background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%);
}
/* The clinic tiles lift on hover; a filled disc has no border to recolour, so it lifts and
   brightens instead of changing its brand colour, which would be the one thing not to do. */
@media (hover: hover) { .footer-col a.footer-social:hover { transform: translateY(-2px); filter: brightness(1.08); } }
.footer-mega {
  /* Manrope — the same face the body copy and every paragraph use, not the display serif.
     There are no `font-optical-sizing` / `opsz` declarations here any more: those pinned
     Literata's optical-size axis so its advance-to-font-size ratio could not drift with the
     viewport, and Manrope has no such axis for them to act on. */
  font-family: var(--font-sans);
  /* Sized from the footer's own usable width rather than a clamp, so the wordmark spans
     it exactly at every viewport instead of being cropped. 100cqw is the footer's content
     box — vw would overshoot by the scrollbar. The divisor is the measured advance width
     of this string at 1em **in Manrope 800 at this tracking** — 7.752 — plus ~1% of
     headroom; `min()` caps it where the footer's own max-width stops it growing.

     The ratio is specific to the face *and the weight*, and neither is portable: this string
     measures 8.0756 in Literata 700, 7.6052 in Manrope 700 and 7.752 in Manrope 800, so the
     divisor has gone 8.17 -> 7.69 -> 7.84 as those changed. Re-measure whenever the string,
     the face, the weight or the tracking moves, and measure after `document.fonts.ready`
     plus a settle or you measure the fallback. */
  --mega-ratio: 7.84;
  font-size: min(100cqw / var(--mega-ratio), 1440px / var(--mega-ratio));
  /* 800, Manrope's heaviest cut. The Google Fonts request in all four HTML files had to gain
     `;800` for this to be a real face — a weight the request does not list is served as
     nothing and the browser silently renders the next one down, which is exactly what 700
     was already doing when this was asked for. Change the request and this together. */
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
  max-width: 1440px;
  margin: 44px auto 0;
  color: var(--footer-ghost);
  white-space: nowrap;
  /* Kept from the Literata version, but it is now belt and braces rather than load-bearing:
     that fallback serif set ~8% *wider* than the ratio assumed and put horizontal scroll on
     the page for the length of the swap, whereas Manrope's `ui-sans-serif` fallback measures
     7.5272 — narrower than Manrope itself, so the swap can only undershoot. Left in place
     because the guard costs nothing and the next face swap may go the other way. */
  overflow-x: clip;
}
.footer-bottom {
  max-width: 1440px;
  margin: 60px auto 0;
  padding-top: 24px;
  border-top: 1px solid var(--footer-line);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--footer-ink-2);
}

/* Marquee */
.marquee {
  overflow: hidden;
  background: var(--band);
  color: var(--on-band);
  padding: 18px 0;
  /* No borders. They were 1px of --sage-dark, which on the fixed-dark --band is very nearly
     the band's own colour in light mode, and the marquee is the last child of an above slab
     on both pages that use it — so the slab's 40px radius and `overflow: clip` cut the
     bottom rule off at each corner while the band curved away from it. The band is dark
     against cream; that contrast is the edge, and it follows the corner for free. */
}
.marquee-track {
  display: flex;
  white-space: nowrap;
  /* One copy of three, so the loop point lands exactly on a copy boundary. */
  animation: marquee 25s linear infinite;
  width: max-content;
}
.marquee-item {
  /* The spacing between items is a margin, not the track's `gap`. With `gap` the track is
     3 copies plus 29 gaps rather than 3 whole periods, so no percentage translate can be a
     whole number of copies and the loop stutters by the width of one gap. As a margin every
     item carries its own trailing space and the track is exactly 3 × one period. */
  margin-right: 40px;
  font-family: var(--font-display);
  font-size: clamp(20px, 2vw, 28px);
  font-weight: 500;
  letter-spacing: -0.01em;
  display: inline-flex;
  align-items: center;
  gap: 40px;
}
.marquee-item::after {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--terra);
  display: inline-block;
}
@keyframes marquee {
  from { transform: translateX(0); }
  /* One third: the track holds three copies of the list, so a copy is 33.333% of it. It ran
     at -50% — one and a half copies — which put the loop point half a copy out of phase and
     jumped the band every cycle. */
  to { transform: translateX(-33.3333%); }
}

@media (prefers-reduced-motion: reduce) {
  /* An endlessly scrolling band is exactly what this setting is asking us not to do. */
  .marquee-track { animation: none; }
}

/* Booking modal */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(26,31,30,0.6);
  backdrop-filter: blur(8px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  opacity: 1;
  animation: fadeIn 240ms ease both;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal {
  background: var(--bg);
  border-radius: var(--radius-lg);
  /* The dialog is its own scroll container, and Chromium draws a scroll container's bar in
     the square padding box — so when the content is taller than the dialog the bar poked past
     the rounded corners (owner's report, 2026-09-06). A clip-path in the same rounded shape
     clips the scrollbar with the rest; border-radius alone never did. */
  clip-path: inset(0 round var(--radius-lg));
  max-width: 920px;
  width: 100%;
  max-height: 90vh;
  overflow: auto;
  display: grid;
  grid-template-columns: 1fr 1.2fr;
  opacity: 1;
  animation: modalIn 320ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes modalIn {
  from { transform: translateY(20px) scale(0.98); opacity: 0; }
  to { transform: translateY(0) scale(1); opacity: 1; }
}
.modal-side {
  background: var(--band);
  color: var(--on-band);
  padding: 40px 36px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 480px;
}
.modal-main {
  padding: 40px 36px;
  position: relative;
}
.modal-close {
  position: absolute;
  top: 20px;
  right: 20px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  transition: all 160ms ease;
}
@media (hover: hover) { .modal-close:hover { background: var(--ink); color: var(--bg); } }
.modal-contact-method {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 18px 0;
  border-bottom: 1px solid var(--line);
}
.modal-contact-method:last-child { border-bottom: 0; }
/* The middle column carries the only shrinkable content in the row, and a flex item's default
   `min-width: auto` would stop it going under its longest word — which is what kept the email
   from ever wrapping. The icon chip and the trailing arrow keep their size. */
.modal-contact-method > div { min-width: 0; }
/* Same treatment the contact card gives it, and for the same reason: one long unbreakable
   token that has to be able to break on a narrow column. */
.modal-contact-method .value { overflow-wrap: anywhere; }
.modal-contact-method .icon {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: var(--bg-2);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  color: var(--sage-dark);
}
.modal-contact-method .label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}
.modal-contact-method .value {
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
@media (hover: hover) { .modal-contact-method .value:hover { color: var(--sage-dark); } }
/* The "this goes somewhere" arrow at the end of each row. Every one of these rows leaves the
   page — a tel:, a mailto:, or Google Maps in a new tab — and nothing said so; the rows read
   as a list of facts rather than as things you can press. `margin-left: auto` puts it at the
   far edge without the middle column needing a width, and `align-self: center` keeps it on
   the row's optical centre whether that row is two lines or three (the parent is
   `align-items: flex-start`, for the icon chip's sake). The nudge runs up and to the right,
   the direction the glyph itself points. */
.modal-contact-method__go {
  margin-left: auto;
  align-self: center;
  flex-shrink: 0;
  display: flex;
  color: var(--muted);
  transition: color 200ms ease, transform 200ms ease;
}
.modal-contact-method:focus-visible .modal-contact-method__go {
  color: var(--sage-dark);
  transform: translate(2px, -2px);
}
@media (hover: hover) { .modal-contact-method:hover .modal-contact-method__go {
  color: var(--sage-dark);
  transform: translate(2px, -2px);
} }
@media (prefers-reduced-motion: reduce) {
  .modal-contact-method__go { transition: color 200ms ease; }
  .modal-contact-method:hover .modal-contact-method__go { transform: none; }
}
.modal-hours {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ink-3);
  margin-top: 4px;
  letter-spacing: 0.04em;
}

/* Service drawer */
.drawer-overlay {
  position: fixed;
  inset: 0;
  background: rgba(26,31,30,0.5);
  backdrop-filter: blur(6px);
  z-index: 90;
  animation: fadeIn 200ms ease both;
}
.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(640px, 100vw);
  background: var(--bg);
  z-index: 91;
  overflow-y: auto;
  animation: drawerIn 320ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
  box-shadow: -20px 0 60px rgba(0,0,0,0.2);
}
@keyframes drawerIn {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

/* Accordion */
.acc-cat-tabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  margin-bottom: 32px;
  background: var(--bg-2);
  padding: 5px;
  border-radius: 999px;
  border: 1px solid var(--line);
  width: fit-content;
  max-width: 100%;
}
.acc-cat-tabs button {
  background: transparent;
  border: 0;
  padding: 9px 16px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-3);
  transition: all 200ms ease;
  white-space: nowrap;
}
.acc-cat-tabs button.active {
  background: var(--ink);
  color: var(--bg);
}
.acc-item {
  /* --line-strong, not --line: the services accordion sits on a below level, over the
     gradient, where the plain hairline vanishes. Same rule the values grid draws. */
  border-bottom: 1px solid var(--line-strong);
  transition: background 200ms ease;
}
.acc-item:first-child { border-top: 1px solid var(--line-strong); }
.acc-row {
  display: grid;
  grid-template-columns: 64px 1fr auto auto;
  gap: 24px;
  align-items: center;
  padding: 28px 0;
  cursor: pointer;
  /* Hover slides the row on transform, not padding: the old padding animation forced
     a grid relayout on every frame of the hover. The hairline stays put because the
     border lives on .acc-item, not the row. */
  transition: transform 200ms ease;
}
@media (hover: hover) { .acc-row:hover { transform: translateX(10px); } }
.acc-num {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.acc-title {
  font-family: var(--font-display);
  font-size: clamp(20px, 2.4vw, 38px);
  font-weight: 500;
  letter-spacing: -0.012em;
  line-height: 1.12;
}
.acc-subtitle {
  display: block;
  font-family: var(--font-sans);
  font-size: 14px;
  font-weight: 400;
  color: var(--ink-3);
  margin-top: 6px;
  letter-spacing: 0;
}
.acc-chip {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--bg-2);
  color: var(--ink-3);
  border: 1px solid var(--line);
  white-space: nowrap;
}
.acc-icon {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--sage);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 200ms ease;
}
.acc-item.open .acc-icon { transform: rotate(45deg); background: var(--terra); }

/* Accordion without number column (new layout) */
.acc-row.no-num {
  grid-template-columns: 1fr auto auto;
  gap: 32px;
}
.acc-body {
  padding: 0 0 36px 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 40px;
}

/* Mobile navigation sheet. Portalled to <body> rather than rendered inside .nav — that
   element's backdrop-filter makes it the containing block for fixed descendants, so in
   place this panel's inset:0 resolved to the 72px nav strip and its background covered
   only that. */
.nav-sheet {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--bg);
  color: var(--ink);
  padding: 24px var(--pad-x) 32px;
  display: flex;
  flex-direction: column;
  gap: 24px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
.nav-sheet__top { display: flex; justify-content: space-between; align-items: center; }
.nav-sheet__close {
  background: transparent; border: 0; color: var(--ink);
  cursor: pointer; padding: 8px; margin: -8px; line-height: 0;
}
.nav-sheet__links { display: flex; flex-direction: column; gap: 18px; margin-top: 24px; }
.nav-sheet__links a {
  font-family: var(--font-display);
  /* Greek nav labels are long compounds with no break opportunities, so the floor here
     is set by the longest word rather than by what looks good on a laptop. */
  font-size: clamp(28px, 8vw, 40px);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  overflow-wrap: break-word;
}
.nav-sheet__links a.active { color: var(--sage-dark); }
.nav-sheet__foot { margin-top: auto; padding-top: 24px; }

/* Both halves of the contact block end level: the cards grid fills the row, and on the
   right the map absorbs whatever the note and the social tiles leave over. */
/* Three columns: a quarter for phone + email, a quarter for the opening hours, a half for
   the map. Each is a direct grid child; there is no wrapper around the two card columns any
   more, because a wrapper would have made them one grid cell and they need to be two.

   **The ratio lives here, not in an inline style, and that is the whole reason it can
   respond.** A custom property set inline outranks every stylesheet rule, so a `--cols`
   written on the element could never be narrowed by the query below — the same trap the
   "Never put `grid-template-columns` in an inline style" convention is about, one level up.

   A quarter column has a hard floor: the hours card has to hold one day and its time on a
   single line. Measured across all five dictionaries the binding case is German
   ("Samstag / Nach Vereinbarung") at 192px of text plus 56px of card padding — 248px. With
   two gaps to pay for now, a quarter is `(grid - 48) / 4`, which clears 248 from about a
   1144px viewport; the switch stays at 1180 for headroom.

   Below that the map moves to its own full-width row and the two card columns split the
   width in half, which is far more than either needs. Re-measure if a day name or an hours
   string gets longer in any language. */
.contact-grid { align-items: stretch; --cols: 1fr 1fr 2fr; }
@media (max-width: 1179px) {
  .contact-grid { --cols: 1fr 1fr; }
  /* Three children into two tracks: the map takes a row of its own underneath rather than
     being squeezed into a third of the width it needs. */
  .contact-grid > .contact-side { grid-column: 1 / -1; }
}
/* Each column is its own flex stack, so a card is never dragged to the height of a
   neighbour in another column — which is what a plain grid would do, its rows being shared.
   Every card is `fill`, so each column distributes whatever the tallest column leaves over
   across its own cards instead of pooling it all at the bottom of one. */
.contact-cards__col { min-width: 0; display: flex; flex-direction: column; gap: 16px; }
.contact-cards__col > .contact-card { flex: 0 0 auto; }
.contact-cards__col > .contact-card--fill { flex: 1 1 auto; }
@media (max-width: 900px) {
  /* Stacked, there is no other column to align to. */
  .contact-cards__col > .contact-card--fill { flex: 0 0 auto; }
}
.contact-side { display: flex; flex-direction: column; }
.contact-side .clinic-map {
  /* aspect-ratio would fight a flex height, so in this column the map is sized by the
     space left instead, with a floor so it never collapses to a sliver.

     **The basis is 0, not `auto`, and that is what lets the card column set the row's
     height.** At `auto` the map contributes its own intrinsic height to this column, which
     made the *right* column the taller of the two — so the row was pinned at 1043px and
     shortening a card on the left only handed the slack back to a `fill` card. With a 0
     basis this column's natural height is the note, the tiles and the map's floor, which is
     well under the cards', so the cards drive the row and the map takes what they leave.
     Measured: the row went 1043 -> 965 and the whole section 1510 -> 1432. */
  flex: 1 1 0;
  aspect-ratio: auto;
  min-height: 300px;
  align-self: stretch;
}
/* Full-width on its own row between 901 and 1179, where there is no column to match, so it
   has to size itself. This rule has to sit *after* the base `.contact-side .clinic-map`
   above: both are (0,2,0), so source order decides, and written up beside `--cols` it lost
   silently — the map came out at the base 300px floor, which is only ~76px of visible street
   once the two overlays take their ~224px. */
@media (min-width: 901px) and (max-width: 1179px) {
  /* A banner ratio, not `auto`. With `aspect-ratio: auto` inherited from the base rule and
     nothing to stretch against, the map resolved square — 829px tall at 901 and 1085 at
     1179, which is a map the height of the whole viewport. 5/2 keeps it a band, and the
     420px floor stops it going under ~196px of visible street once the address and button
     overlays take their ~224px. Measured: 420 at 901 (196 clear), 434 at 1179 (210 clear). */
  .contact-side .clinic-map { flex: 0 0 auto; aspect-ratio: 5 / 2; min-height: 420px; }
}
@media (max-width: 900px) {
  /* Stacked, there is no row to fill, so the map goes back to its own proportions — but it
     now carries the address overlay at the top and the button at the bottom, and those two
     cost ~216px between them. At 375 a 4/3 box is 251px tall, which left 35px of visible
     street. The floor is what fixes that, not a taller ratio: a ratio tuned for a phone
     makes the map absurdly tall on a 700px tablet, whereas the floor simply stops applying
     once 4/3 is taller than it. 420px leaves ~204px of map at 375 and does nothing at 560+. */
  .contact-side .clinic-map { flex: 0 0 auto; aspect-ratio: 4 / 3; min-height: 420px; }
}

/* Contact cards (About page). Same language as the service cards — accent chip, mono
   label, hover lift — so the block reads as part of the site rather than a form. */
/* Same construction as the home services cards, minus the photograph: accent tick, icon
   chip tinted from the same --svc, hover lift on the shared shadow token. */
.contact-card {
  /* The hairline the schedule's row rules and the footnote's top edge both draw with. It is
     declared HERE, not on `.contact-hours`, because the footnote is that element's sibling —
     scoped to the schedule the variable did not reach it and the border silently fell back to
     `currentColor`, drawing a rule about four times too strong.
       Not `--line`: against this card's `--paper` ground that token is 39 channel steps in
     light but only 17 in dark, where the two nearly coincide, and the rules all but vanished.
     Mixed from `--ink` instead, one declaration holds the same step in both themes with no
     dark override — the same reasoning as `--line-strong` and the `--ctl-*` states. */
  --hours-rule: color-mix(in srgb, var(--ink) 17%, var(--paper));
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 30px 28px;
  border-radius: var(--radius-lg);
  background: var(--paper);
  border: 1px solid var(--line);
  transition: transform 200ms ease, box-shadow 200ms ease;
}
@media (hover: hover) { .contact-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-card-lift); } }
.contact-card::before {
  content: "";
  width: 44px;
  height: 3px;
  border-radius: 2px;
  background: var(--svc, var(--sage));
}
.contact-card__icon {
  width: 46px; height: 46px; border-radius: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--svc, var(--sage));
  background: color-mix(in srgb, var(--svc) 14%, transparent);
}
.contact-card__label {
  font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  /* Mixed into `--ink`, not the raw accent. The three accents are mid-tones chosen to sit on
     a white card as a 3px tick and a tinted icon chip, and at 11px as text they measured
     3.06:1 (olive), which is under the 4.5:1 floor — the card's own title was the least
     legible thing on it. Mixing keeps each card's hue while borrowing the ink's weight, and
     because `--ink` inverts with the theme one declaration serves both: the same rule that
     lightens the label on a dark card darkens it on a white one. Same technique the service
     cards use for their grounds. */
  color: color-mix(in srgb, var(--svc, var(--sage)) 42%, var(--ink));
}
/* The footnote at the foot of the hours card. It sits inside `.contact-card__body`, after
   the schedule — `.contact-hours` is `flex: 1 1 auto`, so it grows and pushes this to the
   bottom without needing a margin. Not mono and not uppercase: this is a sentence, and the
   two registers above it (the mono label and the mono times) are both labels or figures. */
.contact-hours__note {
  /* No top margin: the rule has to land exactly where the eighth row's border would have
     been, so it reads as one more separator in the schedule rather than a second idea. The
     space above it is the last row's own bottom half, the same as every other rule gets.
     `.contact-card__body`'s 4px flex gap is zeroed for this card below, or it would push the
     rule 4px past that line. */
  margin-top: 0;
  padding-top: 12px;
  border-top: 1px solid var(--hours-rule);
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--ink-3);
  display: flex;
  gap: 5px;
}
/* The marker keeps its own column so a wrapped second line aligns under the first word
   rather than under the asterisk. */
.contact-hours__note > span:first-child { flex: 0 0 auto; }
.contact-card__body { display: flex; flex-direction: column; gap: 4px; flex: 1; }
/* The hours card's body is the schedule plus its footnote, and the two must meet on the
   footnote's rule with nothing between them — see `.contact-hours__note`. The other two
   cards keep the 4px. */
.contact-card__body:has(.contact-hours) { gap: 0; }
.contact-card__value {
  font-family: var(--font-display);
  font-size: 26px; font-weight: 600; letter-spacing: -0.015em; line-height: 1.1;
  color: var(--ink); display: block;
}
/* The email is one unbreakable token, so it sets its own step and is allowed to break. */
.contact-card__value--sm { font-size: 17px; overflow-wrap: anywhere; }
.contact-card__sub { font-size: 13px; color: var(--ink-3); display: block; }
.contact-card__action { margin-top: auto; padding-top: 4px; }
.contact-card__action .btn { width: 100%; justify-content: center; }
/* The schedule is ONE grid, not seven rows that each lay themselves out. That is the whole
   design: four of the seven days carry an identical pair of ranges, and they only *read* as
   identical if their times start at the same x. Rows are `display: contents`, so every day
   name and every time block lands in the same two columns of the same grid — the day column
   sizes itself to the longest name in whichever language is active, automatically, and the
   times align across all seven rows for free.

   Measured at the tightest card (quarter column at the 1180 breakpoint: 259px wide, 203px of
   content): a day name plus two fixed 73px time columns needs ~237px and cannot fit. So the
   ranges are a flex row that wraps inside its cell instead — side by side where there is
   room, stacked where there is not, and left-aligned at the same x either way. No container
   query and no breakpoint: the content decides, and the alignment survives both states. */
.contact-hours {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  /* 10px, not 14: the two ranges only sit side by side if the time cell clears
     2 x 80px + their own gap, and at a 318px card (262px of content, ~75px of day name for
     the longest weekday in any of the five languages) every pixel of this gap comes out of
     that budget. Measured: 170px needed against 177px available. */
  column-gap: 10px;
  /* `stretch`, not `baseline`. Baseline was right while rows were `display: contents` and the
     day/time spans were themselves the grid items; now the ROW is the item, and baseline-
     aligning it leaves an 18.6px box floating inside a 45.75px track — the rule then draws at
     the box's edge instead of the track's, and the rhythm collapses. Each row now fills its
     track and does its own baseline alignment internally. */
  align-items: stretch;
  /* The card is `fill`, so it absorbs whatever the taller columns leave over, and that slack
     goes into the ROWS rather than into the gaps between them. `align-content: space-between`
     was tried first and is wrong now that the rows are ruled: it pools every spare pixel in
     the gaps, so each rule ended up 9px under one day and 22.8px above the next. Equal-height
     rows with their content centred put every rule exactly halfway between the two days it
     separates. On a card with no slack to spend (stacked, below 900px) the height is
     indefinite, `1fr` resolves to the content's own height, and the list is simply compact. */
  grid-auto-rows: 1fr;
  flex: 1 1 auto;
  row-gap: 0;
  /* The separator between the card's label and the schedule. It lives on the list rather than
     under `.contact-card__label`, because that label is shared with the phone and email cards
     and neither of those wants a rule under it. */
  border-top: 1px solid var(--hours-rule);
  /* No top padding: the rows centre their own content, which already puts ~14px under this
     rule — the same air the six day rules carry, so the first day joins their rhythm instead
     of starting 28px down. The margin is what separates the rule from the label above it, and
     it is the larger of the two gaps on purpose: this divide is label-from-schedule, a step up
     from day-from-day. */
  margin-top: 6px;
  padding-top: 0;
}
/* Rows were `display: contents` — no box, so nothing to draw a rule on. `subgrid` gives each
   row a real box while it still takes its column tracks from `.contact-hours`, so the day
   column keeps sizing itself to the longest name in the active language and the times stay
   aligned across all seven rows. Without subgrid this would need a hardcoded day-column
   width, which breaks the moment a language has a longer weekday. */
.contact-hours__row {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  /* `center`, not `baseline`. Baseline pinned the day to the FIRST line of its times, so on
     the four days that carry two shifts the label sat at the top of the row while its value
     ran on below it. Centred, the day sits against the middle of whatever the time cell is —
     one line or two — which is what makes the four identical pairs read as a set. */
  align-items: center;
  /* Centres the row's own track inside the stretched row, which is what puts the rule at the
     midpoint. That is a different axis from `align-items` above: this positions the track,
     that positions the day and its times within it. */
  align-content: center;
  /* The floor for the no-slack case. Stacked below 900px the card has nothing spare, `1fr`
     resolves to the content's own height, and without this the rule drew 1.7px under the day
     name with nothing above the next — a ruled list with no air in it. With slack the padding
     simply sits inside the centred result and changes nothing (measured identical at 1440). */
  padding: 7px 0;
  border-bottom: 1px solid var(--hours-rule);
}
/* The last day closes the list; a rule there would be a line to nowhere above the card edge. */
.contact-hours__row:last-child { border-bottom: none; }
/* Not mono. Times are measurement and earn a monospace face; a weekday is a label, and
   JuliaMono uppercase on it was the face worn as a costume. */
.contact-hours__day {
  font-size: 13px;
  /* Bold, so the seven days carry the scan and the times read as their values. Weight is
     what widens the `max-content` day column, so re-check that no day name or time range
     wraps at the 1180 breakpoint (the 259px card) after changing it. */
  font-weight: 700;
  line-height: 1.3;
  color: var(--ink-2);
}
.contact-hours__time {
  display: flex;
  flex-wrap: wrap;
  /* Flushed right, so the seven days read as a column of values against the card's edge.
     When a day's two ranges wrap to their own lines they still end on that same edge, which
     is what keeps the four identical pairs looking identical. */
  justify-content: flex-end;
  text-align: right;
  gap: 2px 10px;
  font-family: var(--font-mono);
  /* 12px is the size at which two ranges fit beside each other in a quarter-column card.
     It is the site's existing mono step (the gallery counter and the card labels sit at
     11-12px), so the trade buys the side-by-side reading without leaving the type scale. */
  font-size: 12px;
  line-height: 1.3;
  color: var(--ink);
  /* Declared even though JuliaMono is already fixed-pitch: these are figures in a column and
     the intent should not depend on the face surviving a future swap. */
  font-variant-numeric: tabular-nums;
}
/* Each range stays whole — an en dash is a break opportunity, so without this the browser
   breaks inside a time rather than between the two. */
.contact-hours__time > span { white-space: nowrap; }
/* Closed days recede so the open ones carry the scan. Colour only — no badge and no claim
   about whether the clinic is open right now, which the site has no way to know. */
.contact-hours__row.is-closed .contact-hours__day,
.contact-hours__row.is-closed .contact-hours__time { color: var(--ink-3); }
/* Clinic map. Drawn from OSM vector data (tools/build-map.py) rather than embedded from
   a tile server, so it can use the site's own tokens and follow the theme. Four colours:
   land, minor street, major street, marker. Nothing responds to the pointer. */
.clinic-map {
  position: relative;
  /* width first, then height from the ratio. In a stretched grid row the item's height
     becomes definite and aspect-ratio then derives the WIDTH from it, which on the clinic
     page pushed the map 309px past its column and over the card beside it. */
  width: 100%;
  min-width: 0;
  align-self: start;
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-2);
  border: 1px solid var(--line);
  pointer-events: none;
  user-select: none;
}
/* Fills the space its container leaves instead of holding a 4/3 box. The SVG already uses
   `preserveAspectRatio: slice`, so it crops to whatever shape it is given. */
.clinic-map--fill {
  align-self: stretch;
  aspect-ratio: auto;
  flex: 1 1 140px;
  min-height: 140px;
  margin: 24px 0;
}
.clinic-map svg {
  display: block; width: 100%; height: 100%;
  /* The SVG is centred on its own box, but the top bar covers the upper part of that box, so
     the marker landed above the middle of the space actually left. Sliding the drawing down by
     half the bar's height puts it in the centre of what you can see. `--cmap-top-h` is written
     by `ClinicMap` from a ResizeObserver, because the bar's height is content-driven.

     A transform, deliberately, not `top`/`height`: changing the SVG's box changes what
     `preserveAspectRatio: slice` scales against, which would zoom the map — at 375 the scale
     would jump from 0.42 to 0.55 and crop away a third of the streets. A transform moves the
     paint and leaves the box alone. The strip it uncovers at the top is half the bar's height,
     so it is always behind the bar. */
  transform: translateY(calc(var(--cmap-top-h, 0px) / 2));
}
.cmap-water    { fill: none; stroke: var(--line-2); stroke-width: 9; stroke-linecap: round; opacity: 0.55; }
.cmap-building { fill: var(--line); stroke: none; opacity: 0.5; }
.cmap-path     { fill: none; stroke: var(--line-2); stroke-width: 2.5; stroke-dasharray: 7 6; opacity: 0.7; }
.cmap-minor    { fill: none; stroke: var(--line-2); stroke-width: 4.5; stroke-linecap: round; stroke-linejoin: round; }
.cmap-major    { fill: none; stroke: var(--ink-3); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; }
/* Labels reuse the major-street colour rather than introducing a fifth. */
.cmap-label {
  fill: var(--ink-3);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bg-2);
  stroke-width: 4;
  stroke-linejoin: round;
}
.cmap-pin__halo { fill: var(--terra); opacity: 0.2; }
.cmap-pin__dot  { fill: var(--terra); }
/* The clinic's own name on the map, above its pin. Sized in viewBox units like the street
   labels, so it scales with the `slice` crop instead of drifting off the marker: at 30 units
   it renders ~27px on the clinic page's map and ~13px on a phone's. It carries the same
   painted halo the street names use, and a heavier one (8 against their 4) in proportion to
   its size — without it the name lands on whatever street happens to run under it. */
.cmap-pin__label {
  fill: var(--ink);
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bg-2);
  stroke-width: 8;
  stroke-linejoin: round;
}
/* The map's overlays. Both are `pointer-events: none` because `.clinic-map` is inert by
   design; the button opts back in on its own. Each carries a scrim, since the map underneath
   is drawn from tokens and its lightness moves with the theme. */
.clinic-map__top,
.clinic-map__bottom {
  position: absolute;
  left: 0; right: 0;
  z-index: 1;
  pointer-events: none;
}
/* The scrim is deliberately faint, and that is measured rather than guessed. It used to run
   0.96 -> 0.92, i.e. very nearly opaque, because the figure it was sized against assumed a
   solid `--ink-3` major street directly under the text — the sub-line is `--ink-3` too, so
   that pairing is 1:1 and needs a heavy scrim to survive.

   That case does not occur here. The map is static: fixed coordinates, no panning, so what
   sits under the address is deterministic, and it is land and minor streets rather than a
   major one. Rasterising the map's own SVG with the 8px blur applied and sampling every
   pixel under each text run gives, at **zero scrim**, 12.69:1 for the address and 5.11:1 for
   the sub-line — both already clear of AA before any scrim at all. The blur is doing most of
   the work: it dilutes any dark stroke that does pass nearby.

   So the stops below are a light touch for margin, not a ground. Re-measure by rasterising
   rather than from token values if the map's centre, the crop or the type moves — the token
   worst case is far more pessimistic than the real one and will talk you into an opaque
   scrim again. */
.clinic-map__top {
  top: 0;
  /* Rounded to match the map's own corner. `.clinic-map` clips with `border-radius` +
     `overflow: hidden`, which handles a plain background fine — but the blur layer below
     carries a `backdrop-filter`, and that establishes its own backdrop root whose filtered
     output the ancestor's rounded clip does not apply to. Left square, the blur's corners
     visibly poked outside the map's radius. The map's 1px border insets this box by 1px, so
     its inner radius is one pixel smaller. */
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
  display: flex;
  /* A row now: the address on the left, the maps button on the right, sharing one line.
     `flex-start` rather than `center` so the button stays level with the first line of the
     address instead of drifting down when the street name wraps. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  /* Symmetric now. The old 34px bottom padding existed to give the gradient somewhere to fade
     out; with a border the bar has a real edge instead, and the extra 18px was dead height
     over the map. */
  padding: 16px 18px;
  /* The bar reads as a panel on the map rather than a wash into it, so it ends on a line and
     the gradient no longer runs to transparent — it would have left a fade dying just above a
     hard rule, which reads as two different ideas about where the bar stops. */
  border-bottom: 1px solid var(--line);
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--bg) 42%, transparent) 0%,
    color-mix(in srgb, var(--bg) 30%, transparent) 100%);
}
.clinic-map__addr {
  font-family: var(--font-display);
  font-size: 19px;
  font-weight: 600;
  line-height: 1.15;
  color: var(--ink);
}
.clinic-map__sub { font-size: 13px; color: var(--ink-3); }
.clinic-map__addr-block { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* The top overlay blurs the map as well as scrimming it, built the way `.nav::before` builds
   its progressive blur:

   * **The filter is on a pseudo-element, not on `.clinic-map__top`.** An element with a
     `backdrop-filter` becomes the containing block for `position: fixed` descendants — the
     trap that once resolved the mobile nav sheet's `inset: 0` against the 72px nav strip
     instead of the viewport. Keeping it on `::before` means the overlay stays an ordinary
     absolutely positioned box.
   * **The layer runs 28px past the overlay's own bottom edge.** A blur whose mask reaches
     zero exactly where the layer ends reads as a blur that was *cut off*; the extra strip
     lets the falloff finish in open map, which is what makes it read as a gradient.
   * **Plain `blur()`, no `saturate()`** — saturate shifts the colour of what is behind, which
     is a tint by another name, and this map is drawn from the site's own tokens.
   * `z-index: -1` puts it under the address and the button but over the map. The parent's
     gradient is painted before it and so is part of what gets blurred; that costs nothing,
     because a smooth vertical ramp is unchanged by a blur, and the measured contrast below
     is unaffected. */
.clinic-map__top::before {
  content: "";
  position: absolute;
  /* `inset: 0` resolves against the bar's *padding* box, so the blur stops just inside the
     border rather than under it. It used to run 28px past the bar with a mask fading it out,
     which is what you do when a blur has no edge to end on; the border is that edge now, so
     the fade would only have blurred the map below a line that says the bar has ended. */
  inset: 0;
  /* The corner that actually leaked — see `.clinic-map__top` above. */
  border-radius: calc(var(--radius-lg) - 1px) calc(var(--radius-lg) - 1px) 0 0;
  z-index: -1;
  pointer-events: none;
  background: none;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
/* Both overlays are inert by design, so anything pressable inside one has to opt back in. */
.clinic-map__top .btn,
.clinic-map__bottom .btn { pointer-events: auto; }
/* The button holds its size under a long street name; the address wraps instead. */
.clinic-map__top .btn { flex: 0 0 auto; }
/* Nothing passes `bottom` any more — the clinic map's button moved up into the top row —
   but `ClinicMap` still offers the slot, so its styling stays with it. */
.clinic-map__bottom {
  bottom: 0;
  display: flex;
  justify-content: center;
  /* The extra bottom padding keeps whatever sits here clear of the OpenStreetMap credit. */
  padding: 40px 18px 30px;
  background: linear-gradient(to top,
    color-mix(in srgb, var(--bg) 92%, transparent) 0%,
    color-mix(in srgb, var(--bg) 60%, transparent) 60%,
    transparent 100%);
}

.cmap-credit {
  position: absolute;
  /* Above the overlays: the attribution must never end up behind one. */
  z-index: 2;
  right: 10px; bottom: 8px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  opacity: 0.65;
}
/* The map is a stack of tokens, so dark mode needs no rules of its own beyond lifting
   the streets off the darker ground. */
:root[data-theme="dark"] .cmap-minor { opacity: 0.75; }
:root[data-theme="dark"] .cmap-building { opacity: 0.65; }
/* The note and the social tiles share one shape: a full-height coloured block on the
   left, the two lines of text beside it. */
.contact-note, .contact-social {
  display: flex;
  align-items: stretch;
  gap: 0;
  border-radius: var(--radius);
  background: var(--paper);
  border: 1px solid var(--line);
  overflow: hidden;
  text-decoration: none;
  color: var(--ink);
  min-height: 62px;
}
.contact-note { margin-bottom: 16px; }
.contact-social { transition: border-color 160ms ease, transform 160ms ease; }
@media (hover: hover) { .contact-social:hover { border-color: var(--brand, var(--sage)); transform: translateY(-2px); } }
.contact-note__icon, .contact-social__mark {
  flex: 0 0 auto;
  width: 58px;
  display: flex; align-items: center; justify-content: center;
  align-self: stretch;
  color: #fff;
}
.contact-note__icon { background: var(--sage); }
.contact-social__mark { background: var(--brand, var(--ink)); }
/* Instagram's mark is a gradient, not a flat colour, so the tile carries the real one. */
.contact-social--ig .contact-social__mark {
  background: radial-gradient(circle at 30% 107%, #fdf497 0%, #fdf497 5%, #fd5949 45%, #d6249f 60%, #285AEB 90%);
}
@media (hover: hover) { .contact-social--ig:hover { border-color: #d6249f; } }
.contact-note__text, .contact-social__text {
  display: flex; flex-direction: column; justify-content: center; gap: 2px;
  padding: 12px 16px; min-width: 0;
}
.contact-note__title { font-family: var(--font-display); font-size: 16px; font-weight: 600; }
.contact-note__body { font-size: 13px; color: var(--ink-3); line-height: 1.45; }
.contact-social__text b { font-family: var(--font-display); font-size: 15px; font-weight: 600; }
.contact-social .mono { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--sage-dark); }

/* Values list: the plain terracotta dot now carries the value's icon, and is a step
   larger to give the glyph room. */
.value-dot {
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--terra); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  margin-bottom: 24px;
}

/* Testimonials. The column scrolls itself and can also be scrolled; the fade at both
   ends is a mask rather than two gradient overlays, so it works over the band colour
   without anything sitting on top of the cards. */
.rcol {
  position: relative;
  height: clamp(420px, 56vh, 560px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding-right: 4px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 64px, #000 calc(100% - 64px), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 64px, #000 calc(100% - 64px), transparent 100%);
  /* The mask is what fades the ends, so it needs nothing painted behind it. */
}
.rcol { scrollbar-width: none; }
.rcol::-webkit-scrollbar { display: none; }
.rcol:focus-visible { outline: 2px solid var(--sage); outline-offset: 6px; border-radius: var(--radius); }
.rcard {
  flex: 0 0 auto;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 22px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.rcard__top { display: flex; align-items: center; gap: 12px; }
.rcard__avatar {
  width: 40px; height: 40px; border-radius: 12px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display); font-weight: 600; font-size: 17px; color: #fff;
  line-height: 1;
  /* Same optical correction as .hero-proof__avatars, but a different value, and the reason
     is worth knowing: browsers round the font's own ascent/descent metrics to integers, so
     the ratio that centres a 13px glyph does not centre a 17px one. 0.146em left this 0.29px
     low; measured, this size wants 0.182em. Re-measure rather than copy if the size changes. */
  padding-bottom: 0.182em;
}
.rcard__who { min-width: 0; flex: 1; }
.rcard__name { font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--ink); }
.rcard__when { font-size: 12px; color: var(--muted); margin-top: 1px; }
.rcard__quote { color: var(--sage); flex-shrink: 0; }
.rcard__text { font-size: 14.5px; line-height: 1.6; color: var(--ink-3); margin: 0; }
/* The aggregate moves under the heading now that the column owns the right half. */
.rscore {
  display: inline-flex; align-items: center; gap: 20px; margin-top: 32px;
  padding: 18px 24px; border-radius: var(--radius);
  background: var(--paper); border: 1px solid var(--line);
}
.rscore__num {
  font-family: var(--font-display); font-size: 56px; font-weight: 700;
  line-height: 1; letter-spacing: -0.015em; color: var(--ink);
}
.rscore__meta { display: flex; flex-direction: column; gap: 6px; }
.rscore__count {
  font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--ink-3);
}
@media (max-width: 900px) {
  .rcol { height: clamp(380px, 70vh, 480px); }
  .rscore { margin-top: 24px; }
}

/* The clinic gallery: a bento of photographs with a BorderGlow hover -----------------------
   The layout is the original four-column bento — five photographs fill it exactly, the first
   spanning 2x2, edge to edge in their cells. The hover is React Bits' BorderGlow, ported in
   `assets/js/border-glow.jsx`: as the pointer approaches an edge, a ring and a halo light up
   along *that* edge, masked to a cone pointing at the cursor.

   **Each photograph glows in its own palette accent** — sage-dark, terra, sage, terra-dark,
   sage-light — so no two tiles light the same colour. `--gal-accent` is the only per-card
   value; everything else is shared. */
.gal-card { position: relative; }
.gal-card__photo {
  position: relative;
  height: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-2);
}
.gal-card__photo img { width: 100%; height: 100%; object-fit: cover; }

/* BorderGlow — see assets/js/border-glow.jsx. The class is only applied where the effect
   actually runs (fine pointer, motion allowed), so on a phone none of this is even parsed
   against an element. */
.bg-card {
  --edge-proximity: 0;
  --cursor-angle: 45deg;
  /* How near an edge the pointer has to be before anything shows. Lower = the glow starts
     further into the card and is stronger everywhere: at half-way out this is 0.53 opacity
     where 30 gave 0.43, and at 40% out it is 0.30 against 0.14.
     This lives here and not as a component default on purpose — BorderGlow writes its
     props as inline custom properties, which outrank this block, so a default there
     would make retuning this line do nothing. */
  --edge-sensitivity: 14;
  /* Half-width of the lit arc, as a percentage of the full turn — 18 lights ~130deg, an edge
     and both its corners. It was hardcoded at ~72deg and the variable was dead. */
  --cone: 18;
  --glow-pad: 34px;
  /* The ring and its halo, mixed from the tile's own accent so one declaration serves both
     themes — the same reasoning as `--line-strong`. */
  --bg-glow: var(--gal-accent, var(--sage-dark));
  --bg-glow-75: color-mix(in srgb, var(--bg-glow) 75%, transparent);
  --bg-glow-48: color-mix(in srgb, var(--bg-glow) 48%, transparent);
  --bg-glow-26: color-mix(in srgb, var(--bg-glow) 26%, transparent);
  --bg-glow-14: color-mix(in srgb, var(--bg-glow) 14%, transparent);
  position: relative;
  border-radius: var(--radius);
  isolation: isolate;
}
.bg-edge {
  position: absolute;
  /* Larger than the card, so the halo has somewhere to fall. It overlaps the neighbouring
     tiles' cells, which is fine — only one card is ever hovered. */
  inset: calc(var(--glow-pad) * -1);
  pointer-events: none;
  z-index: 2;
  /* The cone is the whole effect: the ring only exists on the side the pointer is on. Driven
     by --cone now rather than fixed stops, so the arc is tunable in one place. */
  -webkit-mask-image: conic-gradient(from var(--cursor-angle) at center,
    #000 0%, #000 calc(var(--cone) * 0.35%), transparent calc(var(--cone) * 1%),
    transparent calc((100 - var(--cone)) * 1%),
    #000 calc(100% - var(--cone) * 0.35%), #000 100%);
  mask-image: conic-gradient(from var(--cursor-angle) at center,
    #000 0%, #000 calc(var(--cone) * 0.35%), transparent calc(var(--cone) * 1%),
    transparent calc((100 - var(--cone)) * 1%),
    #000 calc(100% - var(--cone) * 0.35%), #000 100%);
  /* Nothing until the pointer is genuinely near an edge, then it ramps to full at the frame. */
  opacity: calc((var(--edge-proximity) - var(--edge-sensitivity)) / (100 - var(--edge-sensitivity)));
  transition: opacity 250ms ease-out;
}
.bg-card:not(:hover) .bg-edge { opacity: 0; transition: opacity 700ms ease-in-out; }
.bg-edge::before {
  content: "";
  position: absolute;
  /* Back to the card's own box: the parent is inflated only so the outer shadows are not
     clipped by it. */
  inset: var(--glow-pad);
  border-radius: var(--radius);
  /* A 2px ring, then the bloom inward over the photograph's own edge and outward onto the
     slab. The innermost spread stops well short of the middle so the picture stays a
     picture — the glow is an edge, not a wash. */
  box-shadow:
    inset 0 0 0 2px var(--bg-glow),
    inset 0 0 6px 0 var(--bg-glow-75),
    inset 0 0 18px 0 var(--bg-glow-48),
    inset 0 0 38px 2px var(--bg-glow-26),
    0 0 6px 0 var(--bg-glow-75),
    0 0 16px 0 var(--bg-glow-48),
    0 0 34px 4px var(--bg-glow-26),
    0 0 60px 6px var(--bg-glow-14);
}

/* MagnetLines — see assets/js/magnet-lines.jsx. Strokes carry their colour from the
   container's `color`, so the caller sets one token and every stroke follows it. */
.magnet-lines {
  display: grid;
  justify-items: center;
  align-items: center;
  pointer-events: none;
}
.magnet-lines span {
  display: block;
  background: currentColor;
  border-radius: 999px;
  transform-origin: center;
  transform: rotate(var(--rotate));
  will-change: transform;
  transition: transform 260ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* VariableProximity — see assets/js/variable-proximity.jsx. The letters are split into
   inline-block spans so each can carry its own font-variation-settings; nothing else about
   the heading changes, so this class only has to undo what splitting costs. */
.variable-proximity { display: inline; }
/* The split letters are aria-hidden, so this carries the heading's accessible name. Clipped
   rather than display:none, which would take it out of the accessibility tree too. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ============================================
   Scrollbars
   ============================================
   Two halves of one look. The page's own bar is drawn by assets/js/scrollbar.js as an
   overlay, because the native one cannot be given a rounded thumb, a token colour or a
   fade; the four inner scrollers keep their native bars and are only restyled, because
   reimplementing a scrollbar inside a modal buys nothing.

   `.ps-custom-scrollbar` is added to <html> by the script, never written here. If the
   script does not run — it stands down on coarse pointers — the browser's own bar has to
   still be there, so hiding it unconditionally would strand those visitors. */
html.ps-custom-scrollbar { scrollbar-width: none; }
html.ps-custom-scrollbar::-webkit-scrollbar { width: 0; height: 0; }

.ps-scrollbar {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 12px;
  /* Above the sticky nav (50), below every overlay: the drawer (90/91), the modal (100),
     the language menu (120), the mobile sheet (200) and the preloader (9999) all have to
     cover it — a page scrollbar floating on top of an open modal reads as a bug. */
  z-index: 60;
  padding: 6px 3px;
  display: flex;
  justify-content: center;
  opacity: 0.35;
  transition: opacity 260ms ease;
}
.ps-scrollbar.is-active { opacity: 1; }
.ps-scrollbar.is-idle-hidden { opacity: 0; pointer-events: none; }

/* Two elements, because two different things move. The thumb is translated down the track
   by JS on every scroll frame; the grip inside it widens on hover. Doing both on one
   element meant transitioning `width` — a layout property — on the element being written
   to sixty times a second, so each hover invalidated layout under a running transform.
   The grip scales instead, which stays on the compositor. */
.ps-scrollbar__thumb {
  width: 8px;
  cursor: grab;
}
.ps-scrollbar.is-dragging .ps-scrollbar__thumb { cursor: grabbing; }

.ps-scrollbar__grip {
  width: 100%;
  height: 100%;
  border-radius: 999px;
  background: var(--ink-3);
  opacity: 0.55;
  /* Resting width is 6 of the thumb's 8. The pill caps go very slightly elliptical at
     0.75, by well under a pixel at this size. */
  transform: scaleX(0.75);
  transform-origin: 50% 50%;
  transition: background 200ms ease, opacity 200ms ease, transform 160ms ease;
}
.ps-scrollbar.is-dragging .ps-scrollbar__grip {
  transform: scaleX(1);
  opacity: 1;
  background: var(--sage-dark);
}
@media (hover: hover) { .ps-scrollbar:hover .ps-scrollbar__grip {
  transform: scaleX(1);
  opacity: 1;
  background: var(--sage-dark);
} }

/* A drag must not select the copy it passes over, and must not hand the pointer to
   anything underneath the bar. */
body.ps-scrollbar-dragging { user-select: none; cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
  .ps-scrollbar, .ps-scrollbar__grip { transition: none; }
}

/* The inner scrollers — modal, drawer, mobile nav sheet, reviews column — keep native
   scrolling and only take the colours. `scrollbar-width: thin` is the standard property
   (Firefox, and Chromium since 121); the ::-webkit rules cover Safari and older Chromium.
   Both are listed because neither covers the whole set on its own.

   `.rcol` is deliberately NOT in this list. It scrolls itself, and a visible bar on a column
   that is already moving reads as chrome on a carousel. It hides its scrollbar further up;
   adding it here silently overrode that, because this block comes later. */
.modal, .drawer, .nav-sheet {
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}
.modal::-webkit-scrollbar,
.drawer::-webkit-scrollbar,
.nav-sheet::-webkit-scrollbar { width: 8px; height: 8px; }
.modal::-webkit-scrollbar-track,
.drawer::-webkit-scrollbar-track,
.nav-sheet::-webkit-scrollbar-track { background: transparent; }
.modal::-webkit-scrollbar-thumb,
.drawer::-webkit-scrollbar-thumb,
.nav-sheet::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border-radius: 999px;
  /* The border is transparent and the background clipped to the padding box, which is how
     you inset a webkit thumb — it has no margin. */
  border: 2px solid transparent;
  background-clip: padding-box;
}
@media (hover: hover) { .modal::-webkit-scrollbar-thumb:hover, .drawer::-webkit-scrollbar-thumb:hover, .nav-sheet::-webkit-scrollbar-thumb:hover {
  background: var(--sage-dark);
  background-clip: padding-box;
} }

/* ============================================
   Two levels: slabs above, backdrop below
   ============================================
   Every page is a stack of `.lvl-above` sections — opaque slabs with rounded corners and a
   shadow — over one fixed Grainient backdrop. `.lvl-below` sections carry no background of
   their own, so the backdrop shows through them. A page can alternate as often as it likes;
   there is still only one WebGL context. */
.page-backdrop {
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background: var(--bg);
}
.grainient-container { position: relative; width: 100%; height: 100%; overflow: hidden; }

.lvl-above {
  position: relative;
  z-index: 1;
  background: var(--bg);
  border-radius: var(--radius-xl);
  box-shadow: 0 30px 70px -35px rgba(26,31,30,0.5);
  /* `clip`, not `hidden`: the corners must cut the hero photograph and any section that
     paints its own ground, but `hidden` would make this a scroll container and break the
     `position: sticky` elements inside (the bio portraits, the contact map). */
  overflow: clip;
}
/* A slab has one ground. Sections inside it stop painting their own, so the tonal steps
   that used to separate them are now carried by the levels instead. */
.lvl-above > section { background: transparent !important; }
/* A run of consecutive slabs reads as one: only the first rounds its top and only the last
   rounds its bottom, so there is no seam where two above-sections meet. */
.lvl-above + .lvl-above { border-top-left-radius: 0; border-top-right-radius: 0; }
.lvl-above:has(+ .lvl-above) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
/* The hero slab starts flush under the nav rather than curving away from it. Selected by
   what precedes it, not by `:first-of-type` — `#root`'s first div child is `.page-backdrop`,
   so `.lvl-above:first-of-type` matches nothing and the first slab kept its top corners. */
.nav + .lvl-above { border-top-left-radius: 0; border-top-right-radius: 0; }
/* Every first slab runs up behind the nav, so its ground — the hero photograph on home, the
   cream slab everywhere else — reaches the top of the window and the uncoloured nav sits on
   it. Without this the nav's own strip showed the animated backdrop instead, and the seam
   between that and the slab read as two different pages stacked. The sections themselves
   give the nav's height back as padding (`.hero-full__content`, `.page-hero`) rather than as
   margin, so nothing lands underneath the bar. */
.nav + .lvl-above { margin-top: calc(var(--nav-h) * -1); }

/* Below-level sections let the backdrop through; anything that set its own ground has to
   stand down, which is why these are !important — several sections set it inline. The
   tonal steps those grounds used to carry are the levels' job now, on both sides. */
.lvl-below { position: relative; z-index: 0; background: transparent !important; }
.lvl-below > section { background: transparent !important; }
.lvl-below .footer { background: transparent !important; }

:root[data-theme="dark"] .lvl-above { box-shadow: 0 30px 70px -35px rgba(0,0,0,0.75); }

@media (max-width: 900px) {
  .lvl-above { border-radius: var(--radius-lg); }
}
@media (prefers-reduced-motion: reduce) {
  /* No backdrop is rendered, so the slabs would float on nothing. */
  .lvl-below { background: var(--bg-2) !important; }
}

/* SVG icon helpers. Lucide draws on a 24px grid at stroke-width 2; 1.6 is a deliberate
   lightening to sit with Manrope rather than shout over it. Both classes exist because
   the sets differ: Lucide glyphs are strokes, Simple Icons brand marks are filled. */
.icon-stroke {
  stroke: currentColor;
  stroke-width: 1.6;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.icon-brand { fill: currentColor; stroke: none; }
/* A stroke glyph drawn solid: the star keeps its stroke so the points stay rounded
   rather than going needle-sharp, and fills with the same colour. */
.icon-fill { fill: currentColor; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }

/* Mobile */
@media (max-width: 900px) {
  .nav-links { display: none; }
  :root { --nav-h: 64px; }
  .nav-inner { grid-template-columns: auto auto; height: 64px; }
  .nav-mobile-btn { display: inline-flex; }
  /* `minmax(0, 1fr)`, not a bare `1fr`. A bare `1fr` is `minmax(auto, 1fr)`, so the track
     refuses to go below its content's min-content — and the widest thing in here is
     the contact address set at 22px, one unbreakable token. The stacked modal was
     therefore 399px wide inside a 328px dialog at 375, scrolling sideways by 64px. */
  .modal { grid-template-columns: minmax(0, 1fr); }
  /* Stacked, the booking panel comes first and the map side second (owner's call,
     2026-09-06): the reason the dialog opened is the phone number, not the map. */
  .modal-side { order: 2; }
  /* Headers that sit beside their content on the desktop stack above it here — the values
     head, the team head, the reviews head, the FAQ intro — and a left-aligned header over a
     centred column read as a different page from the .sec-head sections. Centred like them,
     with the lead and the score box following the text (owner's call, 2026-09-06). The team
     list under its head keeps its own left alignment: it is content, not a header. */
  .split-head { text-align: center; }
  .split-head > p { margin-left: auto; margin-right: auto; }
  .split-head .team-list { text-align: left; }
  /* The bio cards' button pair centres under its (left-aligned) text, like the hero's pair. */
  .bio-actions { justify-content: center; }
  /* The map's address bar stacks: the two address lines, then the maps button under them,
     instead of the address wrapping beside the button in a 118px bar (owner's call). The button
     then runs the full width of the bar (owner's call, 2026-09-07) — stretched by the column's
     align-items, which needs `flex: 0 0 auto` from the desktop rule undone, since that pins it
     to its content width on the cross axis too. `.btn` is already `justify-content: center`, so
     the label stays centred once it is wide. */
  .clinic-map__top { flex-direction: column; align-items: stretch; gap: 12px; }
  .clinic-map__top .btn { flex: 0 0 auto; width: 100%; }
  .footer-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 24px; }
  .footer-brand { grid-column: 1 / -1; }
  .acc-row { grid-template-columns: 48px 1fr auto; gap: 16px; padding: 22px 0; }
  .acc-row.no-num { grid-template-columns: 1fr auto; gap: 16px; }
  .acc-body { grid-template-columns: 1fr; gap: 24px; }
  .acc-chip { display: none; }
}
@media (max-width: 600px) {

  /* The header cannot fit the logo, the controls and the booking CTA at this width —
     it was the CTA pushing the whole document past the viewport edge. The mobile menu
     carries its own full-width booking button, so nothing is lost by dropping it here. */
  .nav-right > .btn { display: none; }
}

/* The corner wash: one cool-to-warm gradient bloom in the top-left of every photograph on
   the site. It is the same two hues everything else here sweeps between — `--sage` into
   `--terra` — so a photo picks up the palette without being tinted wholesale: both stops are
   out of the picture by 60% of the box, which leaves the subject and the whole bottom half
   untouched.

   z-index 1 puts it above the photograph and below `.photo-cap` (2). It is deliberately the
   `::before` so it paints *under* `.img-overlay::after`, the dark bottom scrim the caption
   reads against — the wash must never lighten the ground beneath the text. `.gal-card__photo` is
   a clinic gallery tile, which has no caption but is still a photograph. */
.img-overlay::before,
.gal-card__photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  background:
    radial-gradient(68% 56% at 0% 0%,
      color-mix(in srgb, var(--sage) 54%, transparent) 0%, transparent 72%),
    radial-gradient(46% 38% at 24% 0%,
      color-mix(in srgb, var(--terra) 32%, transparent) 0%, transparent 74%);
}

/* Featured image overlay (for dark text-on-photo) */
.img-overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(26,31,30,0.95) 0%, rgba(26,31,30,0.7) 25%, rgba(26,31,30,0.2) 60%, rgba(26,31,30,0) 80%);
  pointer-events: none;
  z-index: 1;
}
/* The clips carry the same scrim colour and the same bottom alpha as the photographs above,
   but their own stops, and the reason is the box shape rather than taste. `.img-overlay`
   boxes are wide and short; `.motion-frame` is 9/16, so the photo gradient's percentages
   cover a completely different amount of picture — reaching zero at 80% of a tall frame put
   0.34 alpha across the clip's midpoint, dimming the whole thing where the old bespoke
   caption scrim had dimmed nothing above its own 54px box. These stops darken the same band
   of pixels the caption occupies and are clear of the picture by 42%. Judge this one on the
   clip, not on matching the numbers above it. */
.motion-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(26,31,30,0.95) 0%, rgba(26,31,30,0.75) 12%, rgba(26,31,30,0.15) 30%, rgba(26,31,30,0) 42%);
  pointer-events: none;
  z-index: 1;
}
/* The bottom scrim exists to give a caption something to read against, and the bio portraits
   no longer carry one — their subject is named by the h2 beside them. Left on, it was
   dimming the lower quarter of both photographs for nothing. The corner wash stays: that is
   the site's treatment for every photograph, caption or not, which is why the captionless
   gallery tiles (`.gal-card__photo`) take `::before` and never took this. Turned off explicitly
   rather than with `:has(.photo-cap)` on purpose — if that selector ever failed to match,
   every caption on the site would lose its ground, and this way the failure runs the safe
   direction. */
.bio-photo .img-overlay::after { display: none; }

/* --- The bio portraits ---------------------------------------------------------------
   A mounted print rather than a bare photograph. The picture sits on a paper mat with a
   hairline edge, and a second hairline — in that therapist's own accent — is offset behind
   it, leaning away from the text column so the two bios mirror each other.

   The accent is not a new colour. `--sage-dark` for the first woman and `--terra` for the
   second is the pair that already runs down her half of the page: her eyebrow, the italic
   half of her name, her quote mark and her credentials chips in the section below all carry
   it. The frame joins the portrait to that run instead of decorating it, which is also why
   it is a hairline and not a filled border — at any real weight it would be a fifth object
   competing with those four.

   Both rules are mixed from tokens (`--line-strong` is `--ink` at 26%), so the mat inverts
   with the theme without a dark override. */
.bio-photo {
  position: sticky;
  top: 100px;
  align-self: start;
  padding: 10px;
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: calc(var(--radius-lg) + 10px);
}
.bio-photo::before {
  content: "";
  position: absolute;
  inset: 0;
  border: 1px solid var(--bio-accent, var(--sage-dark));
  border-radius: inherit;
  transform: translate(var(--bio-offset, 16px), 16px);
  /* Behind the mat's own ground, so the rule reads only where it escapes it. `.bio-photo`
     is sticky with `z-index: auto` and so is not a stacking context — this sinks below the
     mat but stays inside the level, not behind the page. */
  z-index: -1;
}
@media (max-width: 900px) {
  /* Nothing sticks to the viewport in the stacked layout, and the offset comes in: the
     photo is the full width of `.wrap` here, so 16px would put the rule 4px from the
     screen edge inside a 20px gutter. */
  .bio-photo { position: relative; top: auto; }
  .bio-photo::before { transform: translate(calc(var(--bio-offset, 16px) * 0.625), 10px); }
}
/* The one in-photo caption. z-index clears the scrim and the corner wash, both at 1.
   The shape is `.contact-note`'s: a full-height accent on the left, the labels stacked to the
   right, capped at two lines. The chip that used to sit above the title is gone — inside a
   photograph, a chip plus a title plus a subtitle was three type registers competing in a box
   already carrying an image. */
.photo-cap {
  position: absolute;
  left: 24px; right: 24px; bottom: 24px;
  z-index: 2;
  color: #fff;
  display: flex;
  align-items: stretch;
  gap: 14px;
  --cap-accent: 4px;
}
/* `align-self: stretch` on a flex child, so the bar is exactly as tall as the two lines
   beside it however they wrap — the one thing a fixed height here could not do. */
.photo-cap__accent {
  flex: 0 0 auto;
  width: var(--cap-accent);
  align-self: stretch;
  border-radius: 999px;
  background: var(--terra);
}
.photo-cap__text {
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
}
.photo-cap__title {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 600;
  line-height: 1.15;
  /* Greek compounds offer no break opportunities, and these sit in narrow photo boxes. */
  overflow-wrap: break-word;
}
.photo-cap__sub {
  font-size: 12px;
  line-height: 1.5;
  color: rgba(255,255,255,0.85);
  max-width: 44ch;
}
.photo-cap--sm { left: 20px; right: 20px; bottom: 20px; gap: 12px; --cap-accent: 3px; }
.photo-cap--sm .photo-cap__title { font-size: 20px; }
.photo-cap--lg { left: 32px; right: 32px; bottom: 32px; gap: 18px; --cap-accent: 5px; }
/* Clamped, not fixed: these tiles go full-width on a phone but the type was sized for a
   narrow desktop column, so the long Greek service names broke mid-word. */
.photo-cap--lg .photo-cap__title { font-size: clamp(24px, 2.6vw, 34px); }
.photo-cap--lg .photo-cap__sub { font-size: 14px; }
@media (max-width: 900px) {
  /* Below this the "large" tiles are full-width anyway, so the extra step buys nothing
     and only costs vertical room the caption needs. */
  .photo-cap--lg { left: 20px; right: 20px; bottom: 20px; gap: 14px; --cap-accent: 4px; }
  .photo-cap--lg .photo-cap__sub { font-size: 13px; }
}

/* Reveal animations — opt-in by adding `data-reveal` to a section.
   Without JS or before observer fires, content is fully visible. */
/* ============================================
   Page transitions
   ============================================
   The outgoing half, driven by `assets/js/page-exit.js`: an internal link click is
   intercepted, the curtain sweeps up over the page while it dims, and the navigation is
   issued when the sweep completes. The incoming half is unchanged — the pre-paint script
   sets `ps-booting`, so the new document's first frame is the same cream curtain, and
   `PS.mountApp` lifts it once the page is mounted. One continuous movement across two
   documents, stitched by colour rather than by snapshot.

   **This replaced cross-document View Transitions, and the reason is who it never worked
   for.** `@view-transition { navigation: auto }` runs nowhere in Gecko — Firefox and every
   Firefox-derived browser got no outgoing animation at all, only the lift, which read as
   "half the transition is missing" (it was) — and even in Chromium it raced the incoming
   document's first render and was skipped whenever that lost. A 480ms transform on one fixed
   element is something every browser this site supports can run identically, so the JS owns
   it everywhere and there is exactly one engine. Do not add `@view-transition` back
   alongside it: Chromium would then snapshot the already-swept curtain and replay a second,
   invisible sweep over it, hiding the real DOM behind snapshots for another half second.

   **It is a curtain, not a cross-fade, and the shape is dictated by the load profile.** The
   HTML transpiles its JSX in the browser, so `#root` is empty at the next document's first
   render. Anything that dissolves the old page into the new one is dissolving into an empty
   page; an opaque panel that covers, holds, and lifts is the honest shape. The two halves
   are independently timed on purpose: the sweep is fixed at 480ms, the lift happens whenever
   the mount actually finishes — a slow page holds the curtain rather than showing a blank.

   z-index 9990: over the drawer (90), the modal (100), the language menu (120) and the
   mobile sheet (200) — a navigation covers whatever UI it was started from — and under the
   preloader (9999), which on a first visit is the same colour anyway. */
.ps-exit-curtain {
  position: fixed;
  inset: 0;
  z-index: 9990;
  background: var(--bg);
  transform: translateY(102%);
  pointer-events: none;
  visibility: hidden;
}
html.ps-leaving .ps-exit-curtain {
  visibility: visible;
  animation: ps-curtain-in 480ms cubic-bezier(0.65, 0, 0.35, 1) both;
}
@keyframes ps-curtain-in {
  from { transform: translateY(102%); }
  to   { transform: translateY(0); }
}
/* The page dims as the curtain covers it — the depth cue the old ::view-transition-old
   recede provided. Opacity only, never transform: a transform on #root would make it the
   containing block for the fixed `.page-backdrop` inside it, which would re-resolve
   `inset: 0` against the whole document box and resize the WebGL canvas mid-exit. */
html.ps-leaving #root {
  opacity: 0.45;
  transition: opacity 420ms cubic-bezier(0.65, 0, 0.35, 1);
}
@media (prefers-reduced-motion: reduce) {
  /* page-exit.js does not intercept at all under reduced motion, so navigation is native
     and instant; this is the backstop for a preference that flips mid-exit. */
  html.ps-leaving .ps-exit-curtain { animation: none; visibility: hidden; }
  html.ps-leaving #root { transition: none; opacity: 1; }
}

/* ============================================
   Entrances
   ============================================
   One keyframe for every first-appearance on the site. BlurText draws it per word, and
   VariableProximity reuses it per letter for the page headings — those letters are already
   split for the hover effect and cannot be split a second time, so the entrance rides on the
   spans that exist rather than nesting one component inside the other. Same animation either
   way, so headings read as one family.

   The three steps are upstream BlurText's own from/to snapshots — blur 10 -> 5 -> 0, opacity
   0 -> 0.5 -> 1 — with the travel scaled from its 50px down to something a 144px heading can
   carry without appearing to drop in from off screen. */
@keyframes ps-blur-in {
  0%   { opacity: 0;   filter: blur(10px); transform: translateY(var(--bi-from, -14px)); }
  55%  { opacity: 0.5; filter: blur(5px);  transform: translateY(var(--bi-mid, 3px)); }
  100% { opacity: 1;   filter: blur(0);    transform: translateY(0); }
}

/* The hidden start state is scoped to a class the scripts add to <html> themselves. If the
   JS never runs, nothing is ever hidden — the failure mode of an entrance animation must be
   "no animation", never "no text". This project has already lost one reveal system to that. */
/* `.bi-unit` marks whatever is actually being staggered — a letter, normally. It has to be
   the same element that carries `--bi-delay`, and that is the whole point of the class: these
   selectors used to target `> span`, which is the *word wrapper*, while the delay was set on
   the letters inside it. Every delay resolved to the default 0ms and the heading animated as
   one block. Anything with a per-item delay must carry this class on the same element. */
html.ps-anim .blur-text .bi-unit,
html.ps-anim .variable-proximity.bi-pending .bi-unit { opacity: 0; }

.blur-text.is-in .bi-unit,
.variable-proximity.is-in .bi-unit {
  animation: ps-blur-in var(--bi-dur, 620ms) cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: var(--bi-delay, 0ms);
}
/* No `will-change` here: a heading is 20-40 letters and the site has nine of them, so
   promoting every one would cost hundreds of layers for a 620ms one-shot. */
.bi-unit { display: inline-block; }

/* Section bodies: everything that is not a heading rises once as its section arrives. Only
   opacity and transform — a filter here would make the container a containing block for the
   fixed and sticky things some sections hold. */
html.ps-anim [data-reveal]:not(.is-in) { opacity: 0; transform: translateY(18px); }
[data-reveal] { transition: opacity 620ms ease, transform 620ms cubic-bezier(0.2, 0.7, 0.2, 1); }

@media (prefers-reduced-motion: reduce) {
  /* The scripts do not add .ps-anim at all under reduced motion, so nothing is hidden and
     nothing animates. These are belt and braces for a preference that flips mid-session. */
  .blur-text.is-in .bi-unit, .variable-proximity.is-in .bi-unit { animation: none; }
  [data-reveal] { transition: none; }
  html.ps-anim .blur-text .bi-unit,
  html.ps-anim .variable-proximity.bi-pending .bi-unit,
  html.ps-anim [data-reveal]:not(.is-in) { opacity: 1; transform: none; }
}

/* The grain is all that survives the retired About hero: the home hero still layers it
   over its photograph. Everything else that hung off `.th` went with that hero, which is
   now the same plain header the other non-home pages use. */
.th__grain {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0.10;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
}

/* ============================================
   Services page: the ScrollStack
   ============================================
   Cards pin in turn and stack behind one another. The component is in
   `assets/js/scroll-stack.jsx`; these are the styles it needs plus this site's card design.

   **The cards are opaque, and that is not decoration.** The whole effect is one card occluding
   the one behind it — a transparent card shows the page and the card below through itself and
   the stack reads as overlapping text rather than as depth. So the request for "no background"
   is answered by the ground being the service's own accent rather than a generic card surface:
   there is no card chrome, no border, no shadow-on-white, just the coloured block. */
.scroll-stack { position: relative; }
.scroll-stack-inner { position: relative; }
/* The runway the last card needs to release its pin *before* the next level arrives.
   The component releases every card at `endTop - vh/2`, and this spacer is what pushes the
   following slab far enough down that it is still below the fold at that moment. At 24vh it
   was not: measured at 1213x788 the pin held to scroll 4866 while the next `.lvl-above`
   entered the viewport at 4770, so the cream slab slid up over the pinned card and cut it.
   The requirement is `spacer + section padding >= vh/2`; 40vh clears it at every viewport
   height with room to spare. Upstream's equivalent is a flat 50rem of padding. */
.scroll-stack-end { width: 100%; height: 40vh; pointer-events: none; }
/* Under reduced motion the script never runs, so it never sets the inline margin either. */
.scroll-stack.is-static .scroll-stack-card { margin-bottom: 24px; }

.svc-stack__card {
  position: relative;
  /* The same mix as `.svc-card` on the home page, for the same reason: the seven raw accents
     measure 2.40:1 to 4.43:1 against cream and every one of them fails. Pulling each toward
     `--ink` until the worst clears 4.5:1 is what makes a bold ground usable. Re-measure all
     seven if SVC_ACCENTS changes — they are not equally dark. */
  background: color-mix(in srgb, var(--svc) 55%, var(--svc-ink));
  color: #f5f1ea;
  border-radius: var(--radius-lg);
  /* `clip`, not `hidden`: both cut the oversized glyph to the corner radius, but `hidden`
     would make every card a scroll container. */
  overflow: clip;
  display: grid;
  grid-template-columns: var(--cols, 1.1fr 0.9fr);
  gap: 40px;
  align-items: start;
  padding: 44px;
  min-height: 420px;
}
/* The oversized glyph, turned on all three axes so it sits in the card like an object rather
   than a flat sticker. The perspective() has to live in this transform: the card sets no
   perspective of its own, and without one rotateX/rotateY are an orthographic squash. */
.svc-stack__mark {
  position: absolute;
  right: -4%;
  bottom: -14%;
  z-index: 0;
  color: #f5f1ea;
  opacity: 0.07;
  pointer-events: none;
  line-height: 0;
  transform-origin: 60% 60%;
  transform: perspective(760px) rotateX(18deg) rotateY(-26deg) rotateZ(-9deg);
}
/* The pointer spotlight (assets/js/spotlight-card.jsx). `::after`, not upstream's `::before`,
   and that is the whole stacking argument: the glyph watermark is a z-index 0 positioned
   child, the copy is z-index 1, and among equal z-indexes paint order is DOM order. A
   `::before` would paint *under* the glyph; `::after` puts the glow over the glyph and still
   under the text, which is where a light source belongs.

   **The gradient is inverted against upstream, and that is an accessibility fix.** Upstream
   paints light *at* the cursor fading to transparent. These cards are cream copy on
   `color-mix(--svc 55%, --ink)`, and that 55% exists because it is the point where the worst
   accent just clears AA at 4.64:1 — so any lightening overlay eats the whole margin. Measured
   on the gold accent (#B49B57, the binding one): a cream glow at 0.16 takes the body copy to
   **3.41:1**, the card's own accent at 0.15 to 4.11:1, and buying the headroom back by
   darkening the grounds would cost 27% of their chroma and break the match with the home
   cards. So this runs the other way — transparent at the cursor, `--ink` toward the edges.
   It still reads as a light following the pointer, because the card dims everywhere the
   pointer is not, but it can only ever *darken*: contrast holds at 4.64:1 under the cursor
   and rises to ~6.2:1 away from it. Do not "fix" this back to a light centre. */
.card-spotlight {
  --mouse-x: 50%;
  --mouse-y: 50%;
  --spotlight-color: rgba(26, 31, 30, 0.28);
}
.card-spotlight::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(circle at var(--mouse-x) var(--mouse-y),
    transparent 0%, var(--spotlight-color) 80%);
  opacity: 0;
  transition: opacity 500ms ease;
}
.card-spotlight:focus-within::after { opacity: 1; }
@media (hover: hover) { .card-spotlight:hover::after { opacity: 1; } }

.svc-stack__body, .svc-stack__aside { position: relative; z-index: 1; min-width: 0; }
/* The home services card's head, exactly: the accent tick, then the glyph in its chip below
   it. `.contact-card::before` and `.svc-card__bar` are the same 44x3 rounded line — cream
   here rather than `var(--svc)`, because this card's ground is already mixed from `--svc`
   and the tick would disappear into it. */
.svc-stack__head {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 14px; margin-bottom: 22px;
}
.svc-stack__tick {
  display: block;
  width: 44px; height: 3px;
  border-radius: 2px;
  background: #f5f1ea;
}
.svc-stack__icon {
  width: 46px; height: 46px; border-radius: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(245,241,234,0.14);
  color: #f5f1ea;
  flex: 0 0 auto;
}
.svc-stack__title {
  font-family: var(--font-display);
  font-size: clamp(24px, 2.6vw, 36px);
  font-weight: 600; line-height: 1.12; margin: 0;
  /* `color: inherit` is load-bearing. The global `h1, h2, h3, h4` rule sets
     `color: var(--ink)` *explicitly*, and an explicit colour beats the cream this heading
     would otherwise inherit from `.svc-stack__card` — so on the coloured ground every card
     title rendered near-black. Same trap as the services process block, inverted: there a
     section kept `--on-band` after losing its dark ground, here a heading keeps `--ink`
     after gaining one. Any heading placed on a dark ground needs this. */
  color: inherit;
  /* Greek compounds offer no break opportunities and these titles are long. */
  overflow-wrap: break-word;
}
.svc-stack__sub {
  margin: 12px 0 0;
  font-family: var(--font-serif); font-style: italic;
  font-size: 17px; line-height: 1.45;
  color: rgba(245,241,234,0.92);
}
/* 0.9 alpha, not 0.85: at 0.85 this measured 4.34:1 against the gold accent. */
.svc-stack__text {
  margin: 16px 0 0;
  font-size: 15px; line-height: 1.65;
  color: rgba(245,241,234,0.9);
}
.svc-stack__cta { margin-top: 26px; }
/* The card's button is cream on a dark ground, so the dome has to be *darker* than the button
   to be seen at all — it was `rgba(245,241,234,0.72)`, near-identical to the button's own
   cream, which is why hovering appeared to do nothing. It fills with the same fixed dark the
   ground is mixed from, and the label flips to cream as it arrives (`.btn` already transitions
   colour, 150ms entering and none leaving). */
.svc-stack .btn-primary { background: #f5f1ea; color: var(--svc-ink); }
.svc-stack .btn-primary::after { background: var(--svc-ink); }
.svc-stack .btn-primary:focus-visible { color: #f5f1ea; }
@media (hover: hover) { .svc-stack .btn-primary:hover { color: #f5f1ea; } }

.svc-stack__tags { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
/* Frosted, not flat. What they are over is the card's solid accent plus the oversized glyph
   drifting behind them, so the blur reads mainly where the glyph passes under a chip — which
   is exactly where a flat translucent panel looked like a sticker laid on top. */
.svc-stack__tags li {
  font-size: 13px; line-height: 1.3;
  padding: 9px 14px; border-radius: 999px;
  background: rgba(245,241,234,0.12);
  border: 1px solid rgba(245,241,234,0.18);
  color: rgba(245,241,234,0.95);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
/* Two columns, because the two services that use subgroups have four and seven of them. In
   one column the seven-group card measured 830px against ~440px for the other six, and a card
   twice its neighbours' height reads as a mistake in a stack where they sit on top of each
   other. */
.svc-stack__groups {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 12px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.svc-stack__groups li {
  padding: 14px 16px; border-radius: var(--radius);
  background: rgba(245,241,234,0.10);
  border: 1px solid rgba(245,241,234,0.16);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
/* A trailing odd panel spans both columns rather than sitting alone next to a gap. Written as
   `:last-child:nth-child(odd)` so it applies only when the count is odd — the four-subgroup
   card is untouched, the seven-subgroup one closes on a full-width row. */
.svc-stack__groups li:last-child:nth-child(odd) { grid-column: 1 / -1; }
.svc-stack__groups b {
  display: block; font-family: var(--font-display);
  font-size: 13px; font-weight: 600; color: #f5f1ea;
}
.svc-stack__groups span {
  display: block; margin-top: 3px;
  font-size: 12px; line-height: 1.45; color: rgba(245,241,234,0.86);
}

@media (max-width: 900px) {
  /* One column, and the aside follows the copy. The card also loses its floor: seven bullets
     stacked vertically are taller than 420px on a phone anyway. */
  .svc-stack__card { --cols: minmax(0, 1fr); gap: 24px; padding: 28px; min-height: 0; }
  .svc-stack__groups { grid-template-columns: minmax(0, 1fr); }
  .svc-stack__mark { right: -12%; bottom: -18%; }
  .svc-stack__sub { font-size: 15px; }
}

/* The About page's closing credentials block. Same construction as `.contact-card` — accent
   tick, paper ground — and it themes normally: unlike the service cards this is a light card
   in light mode and a dark one in dark, so every colour here is a token. */
.cred-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 30px 28px;
  border-radius: var(--radius-lg);
  background: var(--paper);
  border: 1px solid var(--line);
}
.cred-card::before {
  content: "";
  width: 44px;
  height: 3px;
  border-radius: 2px;
  background: var(--svc, var(--sage));
}
.cred-card__name {
  margin: 0;
  font-size: clamp(22px, 2.2vw, 28px);
  line-height: 1.15;
}
.cred-card__role {
  margin: 0;
  font-size: 14px;
  color: var(--ink-3);
}
.cred-card__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 8px;
}
.cred-card__list li {
  font-size: 13px;
  line-height: 1.3;
  padding: 8px 13px;
  border-radius: 999px;
  color: var(--ink-2);
  background: color-mix(in srgb, var(--svc) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--svc) 26%, transparent);
}

/* ============================================
   DriftWall — the certificate wall
   ============================================
   Component in `assets/js/drift-wall.jsx`. Upstream's CSS is written for a near-black page; the
   two colours that carried that (`#0b0b12` tile ground, `#060010` overlay) are tokens here, and
   the tiles contain rather than cover — see the component header for why. */
.drift-wall {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* The cursor lives on the wall, not on the tile. Hit-testing a stack of 3D-transformed
     quads is not watertight — measured, only about half the points over the wall resolve to a
     `.drift-wall__tile`, the rest fall through to the track, column, plane or wall behind it.
     With `cursor: pointer` only on the tile, a drifting card dragged that boundary under a
     stationary cursor and it flickered between hand and arrow several times a second. Whatever
     the hit test returns, it is inside the wall, so the wall is where the cursor belongs. */
  cursor: pointer;
  perspective: var(--dw-perspective, 1200px);
  perspective-origin: 50% 50%;
  --dw-tile-w: 200px;
  --dw-tile-h: 132px;
  --dw-gap: 18px;
  --dw-radius: 14px;
  --dw-lift: 64px;
  --dw-dim: 0.55;
  --dw-gray: 0;
  --dw-overlay: var(--bg);
  --dw-edge: 40%;
  /* Upstream's vertical mask is `to top`, which dissolves the top and leaves the bottom edge
     cut clean off. Both ends fade here, and the bottom fade is the longer of the two (starts at
     52%, gone by 100%) so the wall sinks into the slab instead of ending on a line. */
  -webkit-mask-image:
    radial-gradient(ellipse 84% 88% at 50% 44%, #000 var(--dw-edge), transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 52%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-image:
    radial-gradient(ellipse 84% 88% at 50% 44%, #000 var(--dw-edge), transparent 100%),
    linear-gradient(to bottom, transparent 0%, #000 14%, #000 52%, transparent 100%);
  mask-composite: intersect;
}
.drift-wall__plane {
  position: absolute;
  top: 50%; left: 50%;
  display: flex;
  flex-direction: row;
  transform-style: preserve-3d;
  transform-origin: 50% 50%;
  will-change: transform;
}
.drift-wall__col {
  position: relative;
  width: calc(var(--dw-tile-w) + var(--dw-gap));
  transform-style: preserve-3d;
}
.drift-wall__track {
  display: flex;
  flex-direction: column;
  will-change: transform;
  transform-style: preserve-3d;
}
.drift-wall__tile {
  position: relative;
  display: block;
  width: 100%;
  height: calc(var(--dw-tile-h) + var(--dw-gap));
  flex: 0 0 auto;
  outline: none;
  transform-style: preserve-3d;
}
.drift-wall__inner {
  position: absolute;
  inset: calc(var(--dw-gap) / 2);
  display: block;
  border-radius: var(--dw-radius);
  overflow: hidden;
  /* A document sits on paper, not on upstream's near-black. */
  background: var(--paper);
  border: 1px solid var(--line);
  opacity: var(--dw-dim);
  transform: translateZ(0);
  pointer-events: none;
  transition:
    transform 420ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 420ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.drift-wall__tile img {
  width: 100%;
  height: 100%;
  /* `cover` is safe *because* each tile is sized to its own certificate's ratio — there is
     nothing to crop. It was `contain` while tiles were a fixed shape, which letterboxed the
     six portrait scans inside landscape boxes. If tiles ever go back to a fixed height this
     has to go back to `contain`, or the documents get cropped into unreadable fragments. */
  object-fit: cover;
  display: block;
  filter: grayscale(var(--dw-gray)) saturate(0.92);
  transition: filter 420ms cubic-bezier(0.22, 1, 0.36, 1);
  user-select: none;
  -webkit-user-drag: none;
}
/* Resting tiles are knocked back twice — once by `--dw-dim` on the tile and again by this
   wash — so it is easy to end up with certificates too faint to recognise. The wash is the
   lighter of the two levers; keep it low and let `--dw-dim` carry the effect. */
.drift-wall__overlay {
  position: absolute;
  inset: 0;
  background: var(--dw-overlay);
  opacity: 0.16;
  pointer-events: none;
  transition: opacity 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.drift-wall__tile.is-active .drift-wall__inner,
.drift-wall__tile:focus-visible .drift-wall__inner {
  opacity: 1;
  transform: translateZ(var(--dw-lift));
  box-shadow: 0 24px 60px -18px rgba(26, 31, 30, 0.45);
}
.drift-wall__tile.is-active img,
.drift-wall__tile:focus-visible img { filter: grayscale(0) saturate(1.05); }
.drift-wall__tile.is-active .drift-wall__overlay,
.drift-wall__tile:focus-visible .drift-wall__overlay { opacity: 0; }
.drift-wall__tile:focus-visible .drift-wall__inner {
  box-shadow: 0 24px 60px -18px rgba(26,31,30,0.45), 0 0 0 2px var(--sage);
}
@media (prefers-reduced-motion: reduce) {
  .drift-wall__plane, .drift-wall__track { will-change: auto; }
}

/* The wall's box on the About page, and the line that attributes it. */
.cert-wall {
  height: 680px;
  /* Carries the bottom space the attribution line used to provide. */
  margin: 0;
}
@media (max-width: 900px) {
  .cert-wall { height: 460px; }
}

/* Set by PS.lockScroll() while an overlay is open. Both elements, because which one owns the
   viewport scrollbar differs, and `overflow: hidden` here stops *native* scrolling while the
   wheel handler in smooth-scroll.js stops the lerp — neither alone is enough. */
html.ps-scroll-locked,
html.ps-scroll-locked body { overflow: hidden; }
/* On touch screens the lock also pins <body> (PS.lockScroll); the sheet and the menu keep their
   own scroll chaining to themselves so a flick inside them never reaches the page. */
.nav-sheet, .lang-menu { overscroll-behavior: contain; }

/* The certificate preview. z-index 100, the modal step: above the nav (50) and the custom
   scrollbar (60), below the mobile sheet (200) and the preloader (9999). */
.cert-view {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px;
  background: rgba(13, 22, 20, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: certIn 220ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
/* The exit. `DIALOG_EXIT_MS` in about-page.jsx holds the unmount for exactly this long — keep
   the two in step or the dialog either vanishes mid-animation or lingers as a dead overlay. */
.cert-view.is-closing { animation: certOut 220ms cubic-bezier(0.4, 0, 0.6, 1) both; }
.cert-view.is-closing .cert-view__img { animation: certImgOut 220ms cubic-bezier(0.4, 0, 0.6, 1) both; }
@keyframes certIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes certOut {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* The scan itself rises and settles rather than just fading, so opening reads as the
   certificate coming forward off the wall. */
@keyframes certImgIn {
  from { opacity: 0; transform: scale(0.94) translateY(12px); }
  to   { opacity: 1; transform: none; }
}
@keyframes certImgOut {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(0.96) translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  /* No travel and no scale — the dialog simply appears. The JS skips its exit wait to match. */
  .cert-view, .cert-view.is-closing,
  .cert-view__img, .cert-view.is-closing .cert-view__img { animation: none; }
}
.cert-view__img {
  animation: certImgIn 300ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
  max-width: min(1000px, 92vw);
  max-height: 86vh;
  width: auto;
  height: auto;
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: 0 40px 90px -30px rgba(13, 22, 20, 0.7);
}
/* Filled cream, not a translucent outline. At 0.12 alpha it read as a faint ring that was
   easy to miss against the blurred page behind — the one control in the dialog should not need
   looking for. `position: fixed`, so it stays at the top of the viewport rather than tracking
   a flex-centred box. */
.cert-view__close {
  position: fixed;
  top: 24px;
  right: 24px;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 0;
  background: #f5f1ea;
  color: #1a1f1e;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: 0 10px 30px -10px rgba(13, 22, 20, 0.6);
  transition: transform 160ms ease, background 160ms ease;
}
@media (hover: hover) { .cert-view__close:hover { transform: scale(1.06); background: #fff; } }
.cert-view__close:focus-visible { outline: 2px solid #f5f1ea; outline-offset: 3px; }
@media (max-width: 600px) {
  .cert-view { padding: 16px; }
  .cert-view__close { top: 12px; right: 12px; }
}


/* --- Selection and dragging -----------------------------------------------------------
   Off site-wide, owner's request (2026-09-04). Two mechanisms, and both are needed:

   `-webkit-user-drag` is non-standard and **Firefox does not implement it**, so on its own
   the photographs would still drag out of a Gecko browser — which is the owner's own. The
   HTML `draggable="false"` attribute is the standard one and is set at every image call
   site; this block covers selection and the iOS long-press callout.

   **This is a deterrent, not protection.** Right-click "Save image", the network panel, and
   a direct request to /media/<slug>-<width>.jpg all still return the file. Nothing served
   over HTTP can be made uncopyable; the honest description of this rule is that it stops the
   accidental drag and the stray highlight.

   Form controls keep selection. The only ones on the site are in the tweaks panel, which
   renders null in production — but a text field you cannot select inside is broken rather
   than protected, so the exemption stands for whatever is added next. */
html {
  -webkit-user-select: none;
  user-select: none;
}
input, textarea, select, [contenteditable="true"] {
  -webkit-user-select: text;
  user-select: text;
}
img, picture, video, svg {
  -webkit-user-drag: none;
  -webkit-user-select: none;
  user-select: none;
  /* Suppresses the iOS long-press "Save Image" sheet. Scoped to media on purpose: on a link
     that same callout carries Open/Copy, which is navigation rather than copying. */
  -webkit-touch-callout: none;
}

/* --- Touch ------------------------------------------------------------------------------
   Every top-level `:hover` rule above is wrapped in `@media (hover: hover)` (done 2026-09-04,
   32 of them, by script — the remaining two are `.bg-card:not(:hover)` and a `transform: none`
   inside the reduced-motion block, both of which are correct as they are). On a touch screen a tap leaves an element in `:hover` until the next tap
   lands elsewhere, so an unguarded lift or recolour reads as the card being stuck. The
   `:focus-visible` halves of those rules are deliberately NOT guarded — keyboard focus has to
   keep its feedback everywhere. `.bg-card:not(:hover)` is the one hover selector left as it
   was: guarding it would invert it, and BorderGlow never renders on a coarse pointer anyway.

   Below: the controls that measured under 44px at 375, grown to a full touch target on coarse
   pointers only. The visuals inside them do not change — the toggle's icon stays 17px and the
   flag stays 28px; it is the pressable box that grows. */
@media (pointer: coarse) {
  .nav-controls { height: 44px; padding: 2px; }
  .ctl-circle { width: 40px; height: 40px; }
  .ctl-circle--flag { width: 40px; height: 40px; margin: 0; }
  .ctl-circle--flag .flag-svg { width: 28px; height: 28px; }
  .nav-mobile-btn { width: 44px; height: 44px; }
  /* `.btn-sm` measured 42px tall; two more pixels of padding make it a full target. */
  .btn-sm { --btn-py: 11px; }
  .lang-menu button { min-height: 44px; }
  .ctl-round { width: 44px; height: 44px; }
  .footer-col a.footer-social { width: 44px; height: 44px; }
}

/* ---------- Crawlable static fallback (tools/build-seo.js) ----------
   #ps-static is a plain-HTML copy of each page that the generator writes into #root, so a
   crawler that runs no JavaScript — and a visitor with it off — gets the page's text, its links
   and the clinic's contact facts instead of an empty root. React clears it on its first commit,
   and with scripts on it only ever sits under the preloader or the boot curtain, so this block
   styles the no-JS reading alone. Tokens only; nothing here is decorative. */
#ps-static { max-width: 760px; margin: 0 auto; padding: 48px var(--pad-x) 80px; color: var(--ink); font-family: var(--font-sans); font-size: 16px; line-height: 1.6; }
#ps-static h1, #ps-static h2, #ps-static h3 { font-family: var(--font-display); letter-spacing: 0; color: var(--ink); }
#ps-static h1 { font-size: clamp(30px, 5vw, 48px); line-height: 1.1; margin: 0 0 16px; }
#ps-static h2 { font-size: clamp(22px, 3vw, 30px); line-height: 1.2; margin: 40px 0 12px; }
#ps-static h3 { font-size: 18px; line-height: 1.3; margin: 20px 0 6px; }
#ps-static p, #ps-static blockquote { margin: 0 0 12px; }
#ps-static blockquote { font-family: var(--font-serif); font-style: italic; padding-left: 14px; border-left: 2px solid var(--line-strong); }
#ps-static ul, #ps-static ol { padding-left: 20px; margin: 0 0 12px; }
#ps-static li { margin: 0 0 6px; }
#ps-static nav ul { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 8px 20px; }
#ps-static a { color: var(--sage-dark); }
#ps-static .ps-static__brand { font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.08em; font-size: 12px; color: var(--ink-3); margin: 0 0 8px; }
#ps-static dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 0 0 12px; }
#ps-static dt { font-weight: 700; }
#ps-static dd { margin: 0; overflow-wrap: anywhere; }
#ps-static footer { margin-top: 48px; padding-top: 20px; border-top: 1px solid var(--line-strong); font-size: 14px; color: var(--ink-2); }

/* ---------- Legal page (/legal) ----------
   A document, not a marketing page: a sticky contents column beside one measured reading
   column, plain h2/h3 (no BlurText — a privacy notice sweeping in letter by letter would be
   theatre), the business details as a definition list. Every colour is a token. The section
   ids are the deep-link targets the footer and _redirects point at, hence scroll-margin-top. */
.legal-meta { margin-top: 18px; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
.legal__wrap { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 48px; max-width: 1080px; }
.legal__toc { position: sticky; top: calc(var(--nav-h) + 24px); align-self: start; }
.legal__toc .line-sidebar { margin-top: 14px; }
.legal__doc { max-width: 68ch; font-size: 16px; line-height: 1.65; color: var(--ink-2); }
.legal__doc section { scroll-margin-top: calc(var(--nav-h) + 24px); }
.legal__doc section + section { margin-top: 44px; padding-top: 36px; border-top: 1px solid var(--line); }
.legal__doc h2 { font-family: var(--font-display); font-size: clamp(24px, 2.6vw, 32px); line-height: 1.15; letter-spacing: 0; margin: 0 0 18px; color: var(--ink); }
.legal__doc h3 { font-family: var(--font-display); font-size: 19px; line-height: 1.3; letter-spacing: 0; margin: 26px 0 8px; color: var(--ink); }
.legal__doc p { margin: 0 0 14px; }
.legal__doc ul { margin: 0 0 14px; padding-left: 20px; }
.legal__doc li { margin: 0 0 6px; }
.legal__doc a { color: var(--sage-dark); }
.legal-dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px 20px; margin: 0; }
.legal-dl dt { font-weight: 700; color: var(--ink); }
.legal-dl dd { margin: 0; overflow-wrap: anywhere; }
.legal__binding { margin-top: 40px; font-family: var(--font-serif); font-style: italic; color: var(--ink-3); }
@media (max-width: 900px) {
  .legal__wrap { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .legal__toc { position: static; }
  .legal-dl { grid-template-columns: minmax(0, 1fr); gap: 2px 0; }
  .legal-dl dd { margin-bottom: 10px; }
}
/* Footer: the copyright line (bold) and the legal links share the bottom row, copyright left and
   links right. When the two cannot sit side by side the row takes .is-stacked from the Footer
   component's measurement (CSS cannot see a wrap) and the pair stacks, centred. The children
   keep their intrinsic width — flex-shrink 0 — so the measurement compares real widths, capped
   at the row so a long link list wraps inside itself rather than overflowing before JS runs. */
.footer-bottom { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 8px 24px; }
.footer-bottom > * { flex: 0 0 auto; max-width: 100%; min-width: 0; }
.footer-bottom > span { font-weight: 700; }
.footer-bottom.is-stacked { flex-direction: column; align-items: center; text-align: center; gap: 12px; }
.footer-legal { display: flex; flex-wrap: wrap; gap: 6px 18px; }
.footer-bottom.is-stacked .footer-legal { justify-content: center; }
.footer-legal a { color: inherit; text-decoration: none; }
@media (hover: hover) { .footer-legal a:hover { color: var(--ink); text-decoration: underline; } }
/* A deep link (/clinic#contact, /legal#privacy) lands via PS.mountApp's scrollIntoView, and the
   nav is fixed over the page — without this the first 72px of the target sit under the bar. */
.lvl-above > section[id], .lvl-below > section[id] { scroll-margin-top: calc(var(--nav-h) + 8px); }

/* ---------- Home FAQ: rows that move on hover, answers that open and close ----------
   The list used to render the answer only while open (a fadeIn on mount, nothing on unmount)
   and hover did nothing. Now every answer stays mounted and the open state is a class:
   - Height animates through grid-template-rows 0fr -> 1fr on .faq-a, with the inner element
     clipped (min-height 0 + overflow hidden). A node that unmounts cannot animate shut.
   - The text fades and settles a beat after the height starts, so the row opens as one motion.
   - On hover the whole row moves: the label steps right, the button steps left (position only —
     its colour and size belong to the open/closed state), and the + keeps its 45deg turn while open. Guarded by (hover: hover) like every hover on the site.
   Reduced motion drops every transition; the state changes still apply. */
.faq-list { display: block; }
.faq-item { border-bottom: 1px solid var(--line-strong); }
.faq-item:first-child { border-top: 1px solid var(--line-strong); }
.faq-q {
  width: 100%; background: transparent; border: 0; padding: 22px 0; margin: 0;
  text-align: left; display: flex; justify-content: space-between; align-items: center; gap: 16px;
  color: inherit; font: inherit; cursor: pointer;
}
.faq-q:focus-visible { outline: 2px solid var(--sage-dark); outline-offset: 4px; border-radius: 6px; }
.faq-q__label {
  font-family: var(--font-display); font-size: 20px; font-weight: 500; letter-spacing: -0.015em;
  color: var(--ink); min-width: 0; overflow-wrap: break-word;
  transition: transform 280ms cubic-bezier(0.2, 0.7, 0.2, 1), color 200ms ease;
}
.faq-q__icon {
  width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  transition: transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1), background 220ms ease,
              color 220ms ease, border-color 220ms ease;
}
.faq-item.is-open .faq-q__icon { background: var(--terra); color: #fff; border-color: transparent; transform: rotate(45deg); }
@media (hover: hover) {
  .faq-q:hover .faq-q__label { transform: translateX(8px); color: var(--sage-dark); }
  /* The button only moves on hover — no tint, no scale (owner's call, 2026-09-05). Its colour
     is the open/closed state's to change, not the pointer's. */
  .faq-q:hover .faq-q__icon { transform: translateX(-4px); }
  .faq-item.is-open .faq-q:hover .faq-q__icon { transform: translateX(-4px) rotate(45deg); }
}
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 380ms cubic-bezier(0.2, 0.7, 0.2, 1); }
.faq-item.is-open .faq-a { grid-template-rows: 1fr; }
.faq-a__inner { min-height: 0; overflow: hidden; }
.faq-a__text {
  padding-bottom: 24px; color: var(--ink-3); line-height: 1.6; font-size: 15px; max-width: 640px;
  opacity: 0; transform: translateY(-6px);
  transition: opacity 240ms ease, transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
.faq-item.is-open .faq-a__text { opacity: 1; transform: none; transition-delay: 70ms; }
@media (prefers-reduced-motion: reduce) {
  .faq-q__label, .faq-q__icon, .faq-a, .faq-a__text { transition: none; }
}

/* ---------- LineSidebar (React Bits, ported in line-sidebar.jsx) — the legal contents list ----------
   Every derived property reads the per-item --effect (0..1) that the component's rAF loop eases,
   so label colour, shift, marker length and tick length move as one; there are no CSS transitions
   here on purpose. Colours are tokens passed in as custom properties: the color-mix() resolves the
   var() inside them. The <a> is the label (upstream uses a plain span in a clickable li). */
.line-sidebar {
  --accent-color: var(--sage-dark); --text-color: var(--ink-2); --marker-color: var(--line-strong);
  --marker-length: 60px; --marker-gap: 0px; --tick-scale: 0.5; --max-shift: 30px;
  --item-gap: 20px; --font-size: 1.1rem; --smoothing: 100ms;
  position: relative; display: flex; justify-content: flex-start;
}
.line-sidebar--markers { padding-left: calc(var(--marker-length) + var(--marker-gap)); }
.line-sidebar__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--item-gap); }
.line-sidebar__item { position: relative; }
/* No widened hit box (upstream extends each item 48px sideways with a ::before): the pointer
   area is the whole component, and an entry lighting up before the cursor is on it felt wrong. */
.line-sidebar__label {
  position: relative; display: inline-flex; align-items: baseline;
  font-family: var(--font-sans); font-size: var(--font-size); line-height: 1.25; text-decoration: none;
  color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--text-color));
  transform: translateX(calc(var(--effect, 0) * var(--max-shift)));
}
.line-sidebar__label:focus-visible { outline: 2px solid var(--sage-dark); outline-offset: 4px; border-radius: 4px; }
.line-sidebar__index { font-family: var(--font-mono); margin-right: 0.6rem; font-size: 0.85em; opacity: calc(0.55 + var(--effect, 0) * 0.45); }
.line-sidebar__marker {
  position: absolute; top: 50%; left: calc(-1 * var(--marker-length) - var(--marker-gap));
  height: 1px; width: var(--marker-length);
  background-color: color-mix(in srgb, var(--accent-color) calc(var(--effect, 0) * 100%), var(--marker-color));
  transform-origin: left center; transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.5));
}
/* Short tick centred in the gap between two entries */
.line-sidebar--markers .line-sidebar__item:not(:last-child)::after {
  content: ""; position: absolute; top: calc(100% + var(--item-gap) / 2);
  left: calc(-1 * var(--marker-length) - var(--marker-gap));
  height: 1px; width: calc(var(--marker-length) * var(--tick-scale));
  background-color: var(--marker-color); opacity: 0.5; transform: translateY(-50%);
}
/* When enabled, the in-between ticks grow with cursor proximity too */
.line-sidebar--scale-tick .line-sidebar__item:not(:last-child)::after {
  transform-origin: left center; transform: translateY(-50%) scaleX(calc(0.7 + var(--effect, 0) * 0.6));
}

/* ---------- Headings fit the viewport below 900px (assets/js/fit-headings.js) ----------
   Each display heading declares the ceiling it may grow to on a phone or tablet; the script
   finds the largest size up to it at which no word overflows and the line budget holds
   (--fit-lines: 4 for an h1, 3 for an h2, plus any <br>). Above 900px the clamps own the size
   and the script clears its inline value, so nothing here touches the desktop. A heading
   without --fit-max is left alone — the legal document's h2s, the service cards' titles. */
/* Two ceilings: a tablet column (561-900px) can carry the larger one, a phone column cannot —
   at 375px a 60px title runs to four lines and outweighs the lead under it. Below the ceiling
   the width still decides: the Greek home headline fits at 43px on a 375 phone, German at 52. */
.hero-full h1 { --fit-max: 64px; }
.page-hero__title { --fit-max: 64px; }
.sec-head__title { --fit-max: 48px; }
.values-title { --fit-max: 44px; }
.bio-name { --fit-max: 56px; }
.reviews-title, .team-title, .faq-title { --fit-max: 48px; }
@media (max-width: 560px) {
  .hero-full h1, .page-hero__title { --fit-max: 52px; }
  .sec-head__title, .reviews-title, .team-title, .faq-title { --fit-max: 40px; }
  .values-title { --fit-max: 36px; }
  .bio-name { --fit-max: 48px; }
}
/* The four h2s that used to carry their size inline (an inline font-size would beat the
   script's own inline write only by accident of order — it is CSS now, like every other). */
.bio-name { font-size: clamp(32px, 6vw, 88px); }
.faq-title { font-size: clamp(28px, 5vw, 72px); }
