/* ==========================================================================
   Jewel Wellness Bangkok — public site
   ==========================================================================

   §40 asks for premium, calm, warm and trustworthy, and lists what to avoid:
   excessive gold, nightclub styling, fake luxury, low contrast, small fonts.
   The palette below is therefore a deep teal (calm, clinical enough to read as
   professional), a warm sand (the warmth, without gold), and one restrained
   brass accent used only for emphasis — never as a background.

   §41 and §42: system fonts so there is no web-font request on first paint,
   16px minimum body size, 44px minimum tap targets, visible focus rings,
   reduced-motion honoured, and AA contrast on every text/background pair used.

   Thai and English share the stack. Thai needs a taller line-height than Latin
   because its vowel and tone marks stack above and below the consonant line —
   1.4 on a Thai paragraph is cramped and clips the marks on some renderers,
   which is why the body is 1.7 and Thai gets a little more again.
   ========================================================================== */

:root {
  /* Brand */
  --teal-900: #0b2f2c;
  --teal-800: #10403c;
  --teal-700: #15544e;
  --teal-600: #1a6b63;
  --teal-500: #22867c;
  --teal-100: #dcece9;
  --teal-050: #f0f7f5;

  --sand-500: #c9a882;
  --sand-300: #e6d5bf;
  --sand-100: #f7f1e8;

  /* One accent, used sparingly. */
  --brass-600: #9a7134;

  /* Neutrals */
  --ink-900: #17302f;
  --ink-700: #3b4c4b;
  --ink-500: #5c6b6a;
  --ink-300: #9aa8a7;
  --line: #dfe6e4;
  --paper: #ffffff;
  --paper-tint: #fafbfa;

  --danger-700: #8d2b20;
  --danger-050: #fdf1ef;
  --success-700: #1d6b45;
  --success-050: #eff8f3;
  --warn-700: #8a5a12;
  --warn-050: #fdf6e8;

  /* Type */
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans Thai",
    "Leelawadee UI", "Sarabun", Tahoma, "Helvetica Neue", Arial, sans-serif;

  /*
   * Simplified Chinese (1.41.0).
   *
   * A separate custom property applied only on `html[lang^="zh"]`, rather than CJK faces appended to
   * `--font-body`. A Chinese face has Latin glyphs too, and if it sat in the shared stack it would
   * capture the occasional Latin word on English and Thai pages — different metrics, different
   * weight, visibly wrong for the sake of a language that page is not in.
   *
   * System faces only. There are no web fonts in this project, `public/assets/fonts/` is empty, and
   * `TravelChargeTest` asserts the CSP permits no `googleapis` origin in `style-src` or `font-src` —
   * so Google Fonts is not available without weakening a security test.
   *
   * PingFang SC covers macOS and iOS, Microsoft YaHei covers Windows, Noto Sans SC and Source Han
   * Sans SC cover Linux and Android, and Hiragino Sans GB is the older macOS fallback.
   */
  --font-body-zh: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei",
    "Noto Sans SC", "Source Han Sans SC", "Hiragino Sans GB", "Segoe UI", sans-serif;
  --font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia,
    "Noto Serif Thai", serif;

  --step-0: 1rem;
  --step-1: 1.125rem;
  --step-2: 1.375rem;
  --step-3: 1.75rem;
  --step-4: 2.125rem;
  --step-5: 2.625rem;

  /* Space */
  --gap-1: 0.375rem;
  --gap-2: 0.75rem;
  --gap-3: 1.25rem;
  --gap-4: 2rem;
  --gap-5: 3rem;
  --gap-6: 4.5rem;

  --radius: 10px;
  --radius-lg: 16px;
  --shadow-1: 0 1px 2px rgba(11, 47, 44, 0.06), 0 2px 8px rgba(11, 47, 44, 0.05);
  --shadow-2: 0 4px 24px rgba(11, 47, 44, 0.1);

  --measure: 68ch;
  --container: 1140px;
}

/* --------------------------------------------------------------------------
   Reset, kept small
   -------------------------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.7;
  color: var(--ink-900);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

/* Thai runs a little taller. Applied via the html lang attribute, so it needs
   no extra class on the elements themselves. */
html[lang^="th"] body { line-height: 1.85; }

/*
 * Chinese, mirroring the Thai treatment (1.41.0).
 *
 * Same two problems and the same two answers: the body stack names no CJK face, and the serif
 * display face has no Han coverage at all — so headings fall back to the body stack here exactly
 * as they do in Thai. Han characters are dense and square, so they get a little more line-height
 * than Latin but less than Thai, which needs room for its stacked marks.
 */
html[lang^="zh"] body { font-family: var(--font-body-zh); line-height: 1.8; }
html[lang^="zh"] h1,
html[lang^="zh"] h2,
html[lang^="zh"] h3,
html[lang^="zh"] h4 {
  font-family: var(--font-body-zh);
  line-height: 1.4;
  font-weight: 700;
}
html[lang^="zh"] .brand__name { font-family: var(--font-body-zh); font-weight: 700; }
html[lang^="zh"] .price-tag { font-family: var(--font-body-zh); }
html[lang^="zh"] .card__title { font-family: var(--font-body-zh); }

img, picture, svg, video { max-width: 100%; height: auto; display: block; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  line-height: 1.25;
  color: var(--teal-900);
  margin: 0 0 var(--gap-2);
  font-weight: 600;
  text-wrap: balance;
}

html[lang^="th"] h1,
html[lang^="th"] h2,
html[lang^="th"] h3,
html[lang^="th"] h4 {
  /* The serif display face has no Thai coverage; falling back to the body stack
     keeps Thai headings from rendering in a mismatched substitute. */
  font-family: var(--font-body);
  line-height: 1.45;
  font-weight: 700;
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }

p, ul, ol, dl { margin: 0 0 var(--gap-3); }
p { max-width: var(--measure); }

a { color: var(--teal-700); text-decoration-thickness: 1px; text-underline-offset: 0.15em; }
a:hover { color: var(--teal-600); }

/* A single, obvious focus ring everywhere. §41: visible focus states. */
:focus-visible {
  outline: 3px solid var(--brass-600);
  outline-offset: 2px;
  border-radius: 4px;
}

/* --------------------------------------------------------------------------
   Utilities
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gap-3);
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--gap-3);
  top: -4rem;
  z-index: 100;
  background: var(--teal-900);
  color: #fff;
  padding: var(--gap-2) var(--gap-3);
  border-radius: 0 0 var(--radius) var(--radius);
  transition: top 0.15s ease;
}
.skip-link:focus { top: 0; }

.section { padding-block: var(--gap-5); }
.section--tint { background: var(--paper-tint); }
.section--sand { background: var(--sand-100); }
.section--deep { background: var(--teal-900); color: #eaf3f1; }
.section--deep h2, .section--deep h3 { color: #fff; }
.section--deep a { color: var(--sand-300); }

.section__head { margin-bottom: var(--gap-4); }
.section__head p { color: var(--ink-500); }
.section--deep .section__head p { color: #bcd4d0; }

.lede { font-size: var(--step-1); color: var(--ink-700); }

.grid { display: grid; gap: var(--gap-3); }
@media (min-width: 40rem) { .grid--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 56rem) {
  .grid--3 { grid-template-columns: repeat(3, 1fr); }
  .grid--4 { grid-template-columns: repeat(4, 1fr); }
}

.stack > * + * { margin-top: var(--gap-3); }

/* --------------------------------------------------------------------------
   Buttons — 44px minimum, per §41's tap-target requirement
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-1);
  min-height: 48px;
  padding: 0.7rem 1.4rem;
  border: 2px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-weight: 600;
  line-height: 1.3;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.btn--primary { background: var(--teal-700); color: #fff; }
.btn--primary:hover { background: var(--teal-800); color: #fff; }

.btn--secondary {
  background: transparent;
  color: var(--teal-800);
  border-color: var(--teal-700);
}
.btn--secondary:hover { background: var(--teal-050); color: var(--teal-900); }

.btn--line { background: #06c755; color: #05340f; border-color: #06c755; }
.btn--line:hover { background: #05b34c; color: #05340f; }

.btn--whatsapp { background: #25d366; color: #06341a; border-color: #25d366; }
.btn--whatsapp:hover { background: #1fbb59; color: #06341a; }

.btn--quiet { background: transparent; color: var(--teal-800); min-height: 44px; padding-inline: 0.6rem; }
.btn--quiet:hover { text-decoration: underline; }

.btn--danger { background: var(--danger-700); color: #fff; }

.btn--block { width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--gap-2); }

/* --------------------------------------------------------------------------
   Header
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 30;
  background: rgba(255, 255, 255, 0.97);
  border-bottom: 1px solid var(--line);
  backdrop-filter: saturate(1.4) blur(6px);
}

.site-header__bar {
  display: flex;
  align-items: center;
  gap: var(--gap-3);
  min-height: 72px;
}

.brand { display: inline-flex; align-items: center; gap: var(--gap-2); text-decoration: none; }
.brand__mark {
  width: 40px; height: 40px;
  border-radius: 50%;
  background: var(--teal-800);
  color: var(--sand-300);
  display: grid; place-items: center;
  font-family: var(--font-display);
  font-size: 1.15rem;
  flex: 0 0 auto;
}
/* The approved brand mark, in the same 40px box the lettermark occupied.
   A CSS background on an empty <span> since 1.32.1, not an <img>: the mark is
   decorative — .brand__name names the link — and an empty alt on an <img> reads to
   an SEO audit as a missing one. --brand-logo is set in a nonce'd <style> in
   partials/head.php, from the brand.logo_path setting.
   `height` is still explicit: the box has no content to give it one, so without this
   the span collapses to nothing and the mark disappears entirely. That is the same
   symptom the old `img { height: auto }` note described, arriving by a different route.
   `background-size: contain` does what `object-fit: contain` did — protect the aspect
   ratio whatever asset is configured, so a landscape logo letterboxes instead of
   stretching. `flex: 0 0 auto` stops it shrinking beside the nav, matching
   .brand__mark. */
.brand__logo {
  width: 40px; height: 40px;
  background-image: var(--brand-logo);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  flex: 0 0 auto;
}
.brand__name { font-family: var(--font-display); font-size: var(--step-1); color: var(--teal-900); font-weight: 600; line-height: 1.15; }
html[lang^="th"] .brand__name { font-family: var(--font-body); font-weight: 700; }
.brand__tag { display: block; font-size: 0.75rem; color: var(--ink-500); font-family: var(--font-body); font-weight: 400; }
/* 61.99rem is the breakpoint where the desktop nav becomes the drawer, so it is also where the
   brand block starts sharing its line with the toggle. The descriptor is dropped rather than the
   name: at 375px it is what forces the wrap. */
@media (max-width: 61.99rem) { .brand__tag { display: none; } }

.site-nav { margin-inline-start: auto; }
.site-nav__list { display: flex; align-items: center; gap: var(--gap-1); list-style: none; margin: 0; padding: 0; flex-wrap: wrap; }
.site-nav__link {
  display: inline-flex; align-items: center;
  min-height: 44px; padding-inline: 0.6rem;
  color: var(--ink-700); text-decoration: none; font-weight: 500;
  border-radius: var(--radius);
}
.site-nav__link:hover { background: var(--teal-050); color: var(--teal-900); }
.site-nav__link[aria-current="page"] { color: var(--teal-900); box-shadow: inset 0 -2px 0 var(--brass-600); }

/* --------------------------------------------------------------------------
   Desktop dropdowns

   <details> rather than a hover menu, matching the mobile drawer and the FAQ
   accordions: keyboard operable, announced by screen readers, and working with
   scripting off. The script only closes siblings, Escape and outside clicks.

   Seven flat items used to wrap the header onto two rows. Grouping six of them
   behind two of these leaves four, and .site-nav__list keeps flex-wrap as the
   fallback it always was.
   -------------------------------------------------------------------------- */
.nav-drop { position: relative; }
.nav-drop__summary { list-style: none; cursor: pointer; gap: 0.3rem; }
.nav-drop__summary::-webkit-details-marker { display: none; }

/* The only thing telling a visitor a panel exists, so it is drawn to be seen:
   full-strength ink rather than the muted grey it started as, and large enough to
   read as a control instead of punctuation. */
.nav-drop__chevron { font-size: 0.85em; color: var(--teal-700); transition: transform 140ms ease; }
.nav-drop__summary:hover .nav-drop__chevron { color: var(--teal-900); }
.nav-drop__details[open] > .nav-drop__summary { background: var(--teal-050); color: var(--teal-900); }
.nav-drop__details[open] .nav-drop__chevron { transform: rotate(180deg); }

/* The visitor is on a page inside this group — a treatment page lighting up
   "Services & Prices". Distinct from aria-current, which marks the exact page. */
.nav-drop__summary[data-current-section] { color: var(--teal-900); box-shadow: inset 0 -2px 0 var(--brass-600); }

/* Flush against the heading, with no gap.
   A gap looks tidier and is a dead zone: the pointer crossing it is over neither
   the heading nor the panel, so a hover menu closes itself halfway through being
   used. The rounded top corners are squared off for the same reason — the panel
   has to meet the heading edge to edge. */
.nav-drop__panel {
  position: absolute; top: 100%; inset-inline-start: 0;
  z-index: 30;
  display: flex; gap: var(--gap-4);
  min-width: max-content;
  padding: var(--gap-3);
  background: #fff;
  border: 1px solid var(--line);
  border-start-start-radius: 0; border-start-end-radius: 0;
  border-end-start-radius: var(--radius-lg); border-end-end-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}
.nav-drop__col ul { list-style: none; margin: 0; padding: 0; display: grid; }
.nav-drop__col a {
  display: block; min-height: 40px; padding: 0.5rem 0.6rem;
  color: var(--ink-700); text-decoration: none; border-radius: var(--radius);
  white-space: nowrap;
}
.nav-drop__col a:hover { background: var(--teal-050); color: var(--teal-900); }
.nav-drop__col a[aria-current="page"] { color: var(--teal-900); font-weight: 600; }
.nav-drop__heading {
  margin: 0 0 0.35rem; padding-inline: 0.6rem;
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-500);
}

/* Near the right-hand edge there may not be room to open leftwards. Only the
   first two items are dropdowns today, so this is insurance rather than need. */
@media (max-width: 75rem) {
  .site-nav__list > .nav-drop:last-child .nav-drop__panel { inset-inline-start: auto; inset-inline-end: 0; }
}

.header-actions { display: flex; align-items: center; gap: var(--gap-2); }

/*
 * The language switcher is a list (1.41.0).
 *
 * It was one <a> naming "the other language" while there were two. With three it is a list, and
 * the list markup is what keeps it announced correctly to a screen reader — the alternative,
 * bare anchors in a row, gives no indication of how many there are.
 *
 * `SeoTest::testTheHeaderMenuStaysNarrow` caps what the header may carry, so the items are kept
 * tight: the labels are two or three characters each in their own script.
 */
.lang-switch__list {
  display: flex; align-items: center; gap: 0.25rem;
  list-style: none; margin: 0; padding: 0;
}
.lang-switch__list > li { margin: 0; }

.lang-switch {
  display: inline-flex; align-items: center; gap: 0.35rem;
  min-height: 44px; padding-inline: 0.55rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--ink-700); text-decoration: none; font-size: 0.9rem;
  background: none; font-family: inherit; cursor: pointer;
}
.lang-switch:hover { border-color: var(--teal-500); color: var(--teal-800); }

/*
 * The language dropdown (1.43.0).
 *
 * Mirrors `.nav-drop` two elements to the left rather than inventing a second dropdown idiom:
 * same <details>, same chevron, same panel treatment, so the header has one way of opening things.
 * `.lang-switch` already declared `background: none; font-family: inherit; cursor: pointer`, which
 * is exactly what a <summary> needs, so the trigger reuses it unchanged.
 *
 * The panel is end-aligned. This control sits at the right of the bar, and a start-aligned panel
 * would open off the edge of a phone screen.
 */
.lang-drop { position: relative; }
.lang-drop__summary { list-style: none; gap: 0.4rem; }
.lang-drop__summary::-webkit-details-marker { display: none; }
.lang-drop[open] > .lang-drop__summary { border-color: var(--teal-500); color: var(--teal-800); }
.lang-drop[open] .nav-drop__chevron { transform: rotate(180deg); }

.lang-drop__panel {
  position: absolute; top: 100%; inset-inline-end: 0;
  z-index: 30;
  min-width: max-content;
  margin: 0; padding: var(--gap-1);
  list-style: none;
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}
.lang-drop__panel > li { margin: 0; }

.lang-drop__item {
  display: block;
  min-height: 44px; line-height: 44px;
  padding-inline: var(--gap-2);
  border-radius: var(--radius);
  color: var(--ink-700); text-decoration: none; white-space: nowrap;
}
a.lang-drop__item:hover { background: var(--teal-050); color: var(--teal-900); }

/* The page you are already on: present, legible, and plainly not a link. */
.lang-drop__item[aria-current] { color: var(--teal-900); font-weight: 600; }

/*
 * Below the desktop breakpoint the globe stands alone. This control shares the top bar with the
 * burger at every width — `.header-actions` carries no `site-nav--desktop` — and 简体中文 beside a
 * menu button is what pushed the bar onto two rows on a narrow phone.
 */
@media (max-width: 30rem) {
  .lang-drop__current { display: none; }
}

/* The mobile menu is a <details>, so it works with no JavaScript at all. */
.nav-toggle { margin-inline-start: auto; }
.nav-toggle > summary {
  list-style: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 0.4rem;
  min-height: 44px; padding-inline: 0.75rem;
  border: 1px solid var(--line); border-radius: var(--radius);
  font-weight: 600; color: var(--teal-800);
}
.nav-toggle > summary::-webkit-details-marker { display: none; }
/* The drawer holds 25 links and 3 group headings — around 1,390px of content. Until 1.34.0 it had no
   max-height and no overflow, so inside a `position: sticky` header more than half the navigation was
   simply unreachable on a phone: the panel could not scroll itself, and scrolling the page moved the
   header and its panel together. The pattern below is mindnmovement.sg's `.mobile-nav`, and every line
   of it is load-bearing:

     - max-height bounds the panel to the viewport. That is the fix; without it there is nothing to
       scroll.
     - vh FIRST, then dvh. `100vh` ignores the collapsing mobile address bar and overshoots, but it is
       what browsers without `dvh` keep — so the order is the fallback, not a duplicate. Deleting the
       `vh` line breaks old browsers; swapping the two breaks all of them. Neither shows on a desktop,
       which is why BrandingTest asserts the order.
     - overflow-y gives the panel its own scroll.
     - overscroll-behavior: contain stops the page behind scrolling once the panel hits its end.
     - -webkit-overflow-scrolling: touch restores momentum scrolling on iOS.

   --header-h is measured in site.js. The 7.5rem literal is the no-JS fallback and deliberately errs
   LARGE: subtracting more than the header height leaves a small gap under the panel and it still
   scrolls, while subtracting too little puts the last items off-screen — which is the bug being fixed.
   The header wraps to two lines on a narrow phone, so a literal can only be a generous guess. */
.nav-toggle__panel {
  position: absolute; inset-inline: 0; top: 100%;
  background: #fff; border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow-2);
  padding: var(--gap-3) 0;
  max-height: calc(100vh  - var(--header-h, 7.5rem));
  max-height: calc(100dvh - var(--header-h, 7.5rem));
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Clears the sticky booking bar, so the last link is not sitting under it. */
  padding-bottom: calc(var(--gap-3) + 5rem);
}

/* Lock the page behind the open drawer.

   `:has()` rather than a class toggled from JavaScript, because the menu is a <details> and works with
   JavaScript disabled — a property worth keeping. Where `:has()` is unsupported the lock simply does not
   apply and `overscroll-behavior: contain` above still prevents scroll chaining, so the drawer remains
   usable either way. Keyed on `.nav-toggle[open]`, which is the element the <details> actually toggles. */
body:has(.nav-toggle[open]) { overflow: hidden; }
.nav-toggle__panel .site-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
.nav-toggle__panel .site-nav__link { min-height: 52px; padding-inline: var(--gap-3); border-radius: 0; }

/* The drawer shows the same tree one level flat: a heading with its children
   indented, rather than a second <details> to open. The drawer is already open, so
   asking the customer to disclose twice to reach a treatment earns nothing. */
.nav-toggle__group {
  margin: var(--gap-3) 0 0.25rem; padding-inline: var(--gap-3);
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--ink-500);
}
.nav-toggle__children { list-style: none; margin: 0; padding: 0; }
.nav-toggle__children .site-nav__link { padding-inline-start: var(--gap-4); }

@media (min-width: 62rem) { .nav-toggle { display: none; } }
@media (max-width: 61.99rem) { .site-nav--desktop { display: none; } }

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero { background: linear-gradient(170deg, var(--teal-900), var(--teal-700) 62%, var(--teal-600)); color: #eaf3f1; }
.hero__inner { display: grid; gap: var(--gap-4); }

/*
 * The padding is on the TEXT column, not the grid (1.37.0).
 *
 * It used to be `padding-block: var(--gap-5) var(--gap-6)` on `.hero__inner`, which put 48px of green
 * above and 72px below EVERYTHING in the hero — including the photograph, which therefore stopped short
 * of both edges of the panel. Moving it onto the text is what lets the picture reach them.
 */
.hero__text { padding-block: var(--gap-5) var(--gap-6); }

/*
 * The desktop hero: the photograph fills the right half of the green (1.37.0).
 *
 * ## What it looked like before, and why
 *
 * `grid-template-columns: 1.15fr 0.85fr` with `align-items: center` gave the image a 454px track. With
 * `width: 100%; height: auto` and a 1600x900 file, that renders **255px tall** — inside a row the text
 * column sizes at roughly 460px. Centring the short image in the tall row left about 100px of bare green
 * above it and the same below, which is what the client was looking at. `max-height: 30rem` was dead
 * code: at 480px it was nearly twice a height the image could never reach in that track.
 *
 * ## Why the image is taken out of the flow
 *
 * Absolute positioning is what makes this free. The row is sized by the text column either way, so the
 * hero's height — and therefore the fold — is exactly what it was before: on a 1366x768 laptop the hero
 * occupies about 580px of the 695px above the fold, and this change does not add a pixel to that.
 * Stretching the image inside its grid track would have worked too, but only as far as the track, and
 * "most of the green" needs it past the container's edge.
 *
 * ## Why `width: 50%` cannot overlap the text
 *
 * The media is 50% of `.hero`, which is the full viewport; the text is the first of two equal columns
 * inside `.container`, which is capped at 1140px and centred. So the text always ends at or before the
 * container's midpoint, and the container's midpoint is at or left of the viewport's. Checked at the
 * widths that matter: 1920px leaves a 20px gap, 1366px leaves 16px, and 896px — the breakpoint itself,
 * the tightest case — leaves 16px.
 *
 * ## `overflow: hidden` is load-bearing
 *
 * It is not tidiness. A half-width absolutely positioned element is exactly how a horizontal scrollbar
 * gets introduced, and no horizontal scroll is the one layout rule this project asserts everywhere.
 * With this, the hero cannot produce one whatever the image does.
 *
 * Below 56rem NONE of this applies. There the hero stacks, the image already fills its width with no
 * green gap, and the stacked hero is already taller than the fold band on a phone — so making the
 * picture any taller is the one change that would break "still above the fold".
 */
/*
 * Two stages, because one breakpoint could not do both jobs without costing the fold.
 *
 * ## 56rem: fill the track, keep the column widths
 *
 * The green bands go, and nothing else moves. `align-items: stretch` sizes the media row to the text
 * row, and `height: 100%` on the image fills it — the hero's height is set by the text either way, so
 * this costs nothing above the fold. Measured: 596px before and after at 1366px and at 1440px.
 *
 * ## 64rem: the full bleed
 *
 * Only from 1024px up. Going edge-to-edge means the text takes half the container, and the lede is
 * capped at 54ch — about 460px. Below 1024px half a container is narrower than that, so the h1 and the
 * lede each gain a line and the hero grows: measured at 1024px, applying the bleed at 56rem instead
 * made the hero 596px -> 721px, which is 125px straight off the fold. At 1024px and above, half the
 * container is 476px and the text is unaffected.
 *
 * So 896-1024px keeps its column widths and gets the stretch; the bleed waits until there is room for
 * it. Both stages leave the fold exactly where it was.
 */
@media (min-width: 56rem) {
  .hero__inner { grid-template-columns: 1.15fr 0.85fr; align-items: stretch; }
}

@media (min-width: 64rem) {
  .hero { position: relative; overflow: hidden; }

  .hero__inner { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; }

  /*
   * The second grid column is now only a reservation: the media is positioned over it.
   *
   * `width: 50%` is of `.hero`, i.e. the viewport, while the text is the first of two equal columns
   * inside a container capped at 1140px and centred — so the text always ends at or before the
   * container's midpoint, which is at or left of the viewport's. A 16px gap at 1024px and at 1366px,
   * 20px at 1920px. It cannot overlap.
   */
  .hero__media {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    width: 50%;
  }
}

.hero h1 { color: #fff; font-size: var(--step-5); }
@media (max-width: 40rem) { .hero h1 { font-size: var(--step-4); } }
.hero__lede { font-size: var(--step-1); color: #d8e8e5; max-width: 54ch; }
.hero__promise {
  display: inline-flex; flex-wrap: wrap; gap: 0.5rem 1rem;
  margin: 0 0 var(--gap-3);
  padding: 0; list-style: none;
  font-size: 0.95rem; color: var(--sand-300); font-weight: 600;
}
.hero__promise li { display: flex; align-items: center; gap: 0.4rem; }
.hero__note { font-size: 0.9rem; color: #b8d1cd; max-width: 46ch; }

/*
 * The hero image (v1.18.0), replacing the dashed placeholder that stood here.
 *
 * `width: 100%` with `height: auto` and an intrinsic aspect ratio from the img attributes: the file is
 * 1600x900, and without this it renders at its natural width and pushes the page sideways on a phone —
 * which is the one failure mode the project asserts against everywhere else (no horizontal scroll).
 *
 * `object-fit: cover` with a capped height so the composition survives the column being short and wide
 * on a tablet. The subject sits right of centre, so `object-position` keeps her in frame when it crops.
 */
.hero__image {
  display: block;
  width: 100%;
  height: auto;
  max-height: 30rem;
  object-fit: cover;
  object-position: 60% center;
  border-radius: var(--radius-lg);
  /* A soft edge against the dark teal hero, so the light interior does not read as a cut-out. */
  box-shadow: 0 1.25rem 2.5rem rgb(0 0 0 / 0.28);
}

/*
 * Filling the panel, from 56rem up (1.37.0).
 *
 * `height: 100%` beats the `img { height: auto }` reset at the top of this file on specificity, and it
 * needs `max-height: none` with it — 30rem would otherwise cap the picture at 480px and leave green
 * below it on any window taller than that, which is the bug this rule exists to remove.
 *
 * The rounded corner and the shadow go with it. Both were there to soften a floating card against the
 * dark teal; an image that meets three edges of the panel is not a card, and a shadow on an edge that
 * is not there reads as a smudge. `object-fit: cover` with `object-position: 60% center` is already
 * above, and it is what keeps the subject in frame as the crop changes shape.
 */
@media (min-width: 56rem) {
  .hero__image {
    height: 100%;
    max-height: none;
  }
}

/* The card treatment goes only where the image actually meets the panel's edges. */
@media (min-width: 64rem) {
  .hero__image {
    border-radius: 0;
    box-shadow: none;
  }
}

/* --------------------------------------------------------------------------
   Cards
   -------------------------------------------------------------------------- */

.card {
  background: #fff;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--gap-3);
  box-shadow: var(--shadow-1);
  display: flex; flex-direction: column; gap: var(--gap-2);
}
.card--flat { box-shadow: none; }
.card h3 { margin: 0; font-size: var(--step-1); }
.card__meta { font-size: 0.9rem; color: var(--ink-500); margin: 0; }
.card__body { color: var(--ink-700); margin: 0; flex: 1 1 auto; }
.card__foot { margin-top: auto; }

.price-tag { font-family: var(--font-display); font-size: var(--step-2); color: var(--teal-800); font-weight: 600; }
html[lang^="th"] .price-tag { font-family: var(--font-body); }
.price-tag small { font-family: var(--font-body); font-size: 0.85rem; color: var(--ink-500); font-weight: 400; }

.price-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
.price-list li { display: flex; justify-content: space-between; gap: var(--gap-2); padding-block: 0.3rem; border-bottom: 1px dashed var(--line); }
.price-list li:last-child { border-bottom: 0; }
.price-list__minutes { color: var(--ink-700); }
.price-list__price { font-weight: 600; color: var(--teal-800); white-space: nowrap; }

/* Image placeholders. §40 requires them to be clearly labelled with the
   photograph that should replace them, so the brief is visible rather than
   hidden in a comment. */
.placeholder {
  border: 2px dashed var(--sand-500);
  background: repeating-linear-gradient(135deg, var(--sand-100), var(--sand-100) 12px, #fff 12px, #fff 24px);
  border-radius: var(--radius-lg);
  padding: var(--gap-3);
  display: grid; place-items: center; gap: var(--gap-1);
  min-height: 220px;
  text-align: center;
  color: var(--brass-600);
}
.placeholder__label { font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700; }
.placeholder__brief { font-size: 0.9rem; color: var(--ink-700); max-width: 34ch; margin: 0; }

/* --------------------------------------------------------------------------
   AEO components (§18)
   -------------------------------------------------------------------------- */

.answer {
  border-inline-start: 4px solid var(--teal-500);
  background: var(--teal-050);
  padding: var(--gap-3);
  border-radius: 0 var(--radius) var(--radius) 0;
  margin-block: 0 var(--gap-4);
}
.answer__label { font-size: 0.75rem; letter-spacing: 0.09em; text-transform: uppercase; font-weight: 700; color: var(--teal-700); margin: 0 0 0.25rem; }
.answer p { margin: 0; font-size: var(--step-1); color: var(--teal-900); max-width: 62ch; }

.takeaways { background: var(--sand-100); border-radius: var(--radius-lg); padding: var(--gap-3); margin-block: var(--gap-4); }
.takeaways h2, .takeaways h3 { font-size: var(--step-1); margin-top: 0; }
.takeaways ul { margin: 0; padding-inline-start: 1.2rem; }
.takeaways li { margin-bottom: 0.35rem; }

/* An article's own FAQ and author block (1.32.1). Both sit outside .prose, after the body, so they
   need their own top spacing — .prose's descendant margins do not reach them.
   `max-width: var(--measure)` for the same reason: outside .prose they inherit the container's full
   width, so the questions ran to roughly twice the width of the article text above them and the column
   visibly stepped outwards at the FAQ. This is the measure the body itself uses, so the article reads
   as one column all the way down. */
.article-faq, .article-author { max-width: var(--measure); }
.article-faq { margin-top: var(--gap-5); }
.article-faq > h2 { font-size: var(--step-2); margin-bottom: var(--gap-3); }
.article-author {
  margin-top: var(--gap-5);
  padding: var(--gap-3);
  background: var(--sand-100);
  border-radius: var(--radius-lg);
}
.article-author > h2 { font-size: var(--step-1); margin-top: 0; margin-bottom: var(--gap-2); }
.article-author p { margin: 0 0 0.4rem; }
.article-author p:last-child { margin-bottom: 0; }
/* The role reads as a subtitle to the name, not a second claim of equal weight. */
.article-author__role { color: var(--ink-700); font-size: 0.95rem; }
.article-author__role::before { content: "· "; }

.steps { counter-reset: step; list-style: none; margin: 0; padding: 0; display: grid; gap: var(--gap-3); }
@media (min-width: 48rem) { .steps--3 { grid-template-columns: repeat(3, 1fr); } }
.steps > li { counter-increment: step; background: #fff; border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--gap-3); }
.steps > li::before {
  content: counter(step);
  display: grid; place-items: center;
  width: 2.25rem; height: 2.25rem; margin-bottom: var(--gap-2);
  border-radius: 50%;
  background: var(--teal-800); color: #fff;
  font-family: var(--font-display); font-weight: 700;
}
.steps h3 { font-size: var(--step-1); margin-bottom: 0.35rem; }
.steps p { margin: 0; color: var(--ink-700); }

/* FAQs are <details>, so they work with JavaScript off and are keyboard
   operable for free. */
.faq { border: 1px solid var(--line); border-radius: var(--radius); background: #fff; }
.faq + .faq { margin-top: var(--gap-2); }
.faq > summary {
  cursor: pointer; list-style: none;
  padding: var(--gap-3);
  font-weight: 600; color: var(--teal-900);
  display: flex; align-items: flex-start; gap: var(--gap-2);
  min-height: 44px;
}
.faq > summary::-webkit-details-marker { display: none; }
.faq > summary::after { content: "+"; margin-inline-start: auto; font-size: 1.4rem; line-height: 1; color: var(--teal-600); }
.faq[open] > summary::after { content: "–"; }
.faq__answer { padding: 0 var(--gap-3) var(--gap-3); color: var(--ink-700); }
.faq__answer > :last-child { margin-bottom: 0; }

.comparison { width: 100%; border-collapse: collapse; }
.comparison th, .comparison td { text-align: start; padding: var(--gap-2); border-bottom: 1px solid var(--line); vertical-align: top; }
.comparison th { color: var(--teal-900); font-weight: 600; }
.table-scroll { overflow-x: auto; }

/* --------------------------------------------------------------------------
   Breadcrumbs, badges, notices
   -------------------------------------------------------------------------- */

.breadcrumb { padding-block: var(--gap-2); font-size: 0.9rem; }
.breadcrumb ol { display: flex; flex-wrap: wrap; gap: 0.4rem; list-style: none; margin: 0; padding: 0; }
.breadcrumb li::after { content: "/"; margin-inline-start: 0.4rem; color: var(--ink-300); }
.breadcrumb li:last-child::after { content: ""; }
.breadcrumb a { color: var(--ink-500); }
.breadcrumb [aria-current="page"] { color: var(--ink-900); }

.badge {
  display: inline-block;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  background: var(--teal-100);
  color: var(--teal-800);
  white-space: nowrap;
}
.badge--warn { background: var(--warn-050); color: var(--warn-700); }
.badge--danger { background: var(--danger-050); color: var(--danger-700); }
.badge--ok { background: var(--success-050); color: var(--success-700); }
.badge--muted { background: #eef1f1; color: var(--ink-500); }

/* A list of uploaded documents (the therapist upload page). Each row wraps rather than
   truncates: an original filename can be long, and an applicant checking that the right
   file arrived needs to read all of it. */
.doc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--gap-2); }
.doc-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.6rem;
  padding-bottom: var(--gap-2);
  border-bottom: 1px solid var(--line);
}
.doc-list li:last-child { border-bottom: 0; padding-bottom: 0; }
.doc-list .field__error { flex-basis: 100%; margin: 0; }

.notice {
  border-radius: var(--radius);
  padding: var(--gap-3);
  border-inline-start: 4px solid var(--teal-500);
  background: var(--teal-050);
  margin-bottom: var(--gap-3);
}
.notice--warn { border-color: var(--warn-700); background: var(--warn-050); }
.notice--error { border-color: var(--danger-700); background: var(--danger-050); }
.notice--success { border-color: var(--success-700); background: var(--success-050); }
.notice > :last-child { margin-bottom: 0; }
.notice strong { color: var(--ink-900); }

/* --------------------------------------------------------------------------
   Forms
   -------------------------------------------------------------------------- */

.form-grid { display: grid; gap: var(--gap-3); }
@media (min-width: 44rem) { .form-grid--2 { grid-template-columns: repeat(2, 1fr); } }

/*
 * `align-content: start` is load-bearing (1.36.0).
 *
 * A `.field` is a grid item of `.form-grid`, so it stretches to its row — and its own rows then
 * absorb the spare height, growing the LABEL row rather than the control. Two cells sharing a row
 * therefore put their controls at different heights whenever their content differs, which is what
 * misaligned "Preferred time" against "Preferred date" on booking step 3: the label measured
 * 31.0px in one cell and 27.2px in the other, from identical CSS.
 *
 * Pinned to the top, every field puts its label on one line and its control directly beneath,
 * whatever shares its row.
 */
.field { display: grid; gap: 0.35rem; align-content: start; }
/*
 * `margin: 0` is load-bearing, not tidiness (1.36.0).
 *
 * `.field__label` is a <p>, and the global reset above gives every <p> `margin-bottom: var(--gap-3)`.
 * `.field` is `display: grid`, so that margin is NOT collapsed away — it stacks on top of the grid gap.
 *
 * The date fields label with a real <label> (no margin); the time fields must use a <p>, because the
 * control is two <select>s and `for=` cannot point at both. So on booking step 3 the two time controls
 * sat ~20px lower than the date inputs sharing their grid row.
 *
 * `.field__hint` on the next line already resets this. The reset was applied to one sibling and missed
 * on the other, which is why the misalignment only ever showed where a field is labelled by a paragraph.
 */
.field > label, .field__label {
  font-weight: 600;
  color: var(--teal-900);
}
.field__label { margin: 0; max-width: none; }
.field__hint { font-size: 0.875rem; color: var(--ink-500); margin: 0; }
.field__optional { font-weight: 400; color: var(--ink-500); font-size: 0.875rem; }

input[type="text"], input[type="email"], input[type="tel"], input[type="date"],
input[type="time"], input[type="number"], input[type="url"], input[type="password"],
input[type="search"], select, textarea {
  width: 100%;
  min-height: 48px;
  padding: 0.6rem 0.75rem;
  font: inherit;
  color: var(--ink-900);
  background: #fff;
  border: 1px solid var(--ink-300);
  border-radius: var(--radius);
}
textarea { min-height: 7rem; resize: vertical; line-height: 1.6; }
select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--ink-500) 50%), linear-gradient(135deg, var(--ink-500) 50%, transparent 50%); background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; padding-inline-end: 2.5rem; }

input:focus, select:focus, textarea:focus { border-color: var(--teal-600); }

[aria-invalid="true"] { border-color: var(--danger-700) !important; background: var(--danger-050); }

.field__error { color: var(--danger-700); font-size: 0.9rem; font-weight: 600; margin: 0; }

.choice-list { display: grid; gap: var(--gap-2); }
@media (min-width: 40rem) { .choice-list--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 56rem) { .choice-list--3 { grid-template-columns: repeat(3, 1fr); } }

.choice {
  display: flex; align-items: flex-start; gap: var(--gap-2);
  padding: var(--gap-3);
  border: 1px solid var(--line); border-radius: var(--radius);
  background: #fff; cursor: pointer;
  min-height: 56px;
}
.choice:hover { border-color: var(--teal-500); background: var(--teal-050); }
.choice input { margin: 0.2rem 0 0; width: 1.15rem; height: 1.15rem; flex: 0 0 auto; accent-color: var(--teal-700); }
.choice__title { font-weight: 600; color: var(--teal-900); display: block; }
.choice__detail { font-size: 0.9rem; color: var(--ink-500); display: block; }
.choice:has(input:checked) { border-color: var(--teal-600); background: var(--teal-050); box-shadow: inset 0 0 0 1px var(--teal-600); }

/* Booking step 1: one block per person, because each of them chooses their own
   treatment and their own number of therapists. Bordered rather than merely spaced —
   with up to six on the page, the boundary between "person 2" and "person 3" has to
   be unmistakable on a phone. */
.guest-block {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--gap-3); margin: 0 0 var(--gap-3);
  background: var(--sand-050, #fdfcfa);
  display: grid; gap: var(--gap-3);
}
.guest-block__legend {
  padding: 0 var(--gap-1); margin-left: calc(var(--gap-1) * -1);
  font-weight: 600; color: var(--teal-900);
}

/* The party-size select and its Update button sit on one line. The button exists so
   the count can change with JavaScript disabled; the script hides it. */
.inline-set { display: flex; gap: var(--gap-2); align-items: center; flex-wrap: wrap; }
.inline-set select { width: auto; min-width: 5rem; }

/* An option the chosen treatment cannot take. Dimmed and un-clickable rather than
   removed: a control that vanishes reads as a bug, and the label still says why. */
.choice--disabled { opacity: 0.55; cursor: not-allowed; }
.choice--disabled:hover { border-color: var(--line); background: #fff; }

.summary-panel__lines { list-style: none; margin: 0 0 var(--gap-3); padding: 0; display: grid; gap: var(--gap-2); }
.summary-panel__lines li { display: flex; justify-content: space-between; gap: var(--gap-2); align-items: baseline; }
.summary-panel__detail { display: block; font-size: 0.85rem; color: var(--ink-500); }

.checkbox { display: flex; align-items: flex-start; gap: var(--gap-2); }
.checkbox input { margin: 0.3rem 0 0; width: 1.15rem; height: 1.15rem; flex: 0 0 auto; accent-color: var(--teal-700); }
.checkbox label { font-weight: 400; }

.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Step indicator */
.stepper { list-style: none; display: flex; flex-wrap: wrap; gap: var(--gap-1); margin: 0 0 var(--gap-4); padding: 0; font-size: 0.85rem; }
.stepper li { display: flex; align-items: center; gap: 0.4rem; color: var(--ink-500); }
.stepper li::after { content: ""; width: 18px; height: 1px; background: var(--line); }
.stepper li:last-child::after { display: none; }
.stepper__dot { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line); font-weight: 700; font-size: 0.78rem; background: #fff; }
.stepper li[aria-current="step"] { color: var(--teal-900); font-weight: 700; }
.stepper li[aria-current="step"] .stepper__dot { background: var(--teal-800); color: #fff; border-color: var(--teal-800); }
.stepper li[data-done="1"] .stepper__dot { background: var(--teal-100); border-color: var(--teal-500); color: var(--teal-800); }

.summary-panel { background: var(--sand-100); border-radius: var(--radius-lg); padding: var(--gap-3); }
.summary-panel dl { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem var(--gap-2); margin: 0; }
.summary-panel dt { color: var(--ink-500); }
.summary-panel dd { margin: 0; font-weight: 600; color: var(--teal-900); text-align: end; }
.summary-panel__total { border-top: 1px solid var(--sand-300); margin-top: var(--gap-2); padding-top: var(--gap-2); }

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.site-footer { background: var(--teal-900); color: #c9dcd8; padding-block: var(--gap-5) var(--gap-3); margin-top: var(--gap-5); }
.site-footer h2 { color: #fff; font-size: 1rem; font-family: var(--font-body); font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase; }
.site-footer a { color: #dfeae8; }
.site-footer ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.4rem; }
.site-footer__cols { display: grid; gap: var(--gap-4); }
/* The footer ground is --teal-900, the same dark green as the leaves inside the mark, and the
   artwork has a transparent background. Placed straight onto it the gold arch reads strongly and the
   green lotus flattens into the ground — a half-rendered logo, which reads as a fault. So the mark sits on a
   white plate. That is a surface behind the file, not a change to the file: the approved asset is
   untouched, which the brief requires. box-sizing is border-box globally, so the padding is inside
   the 56px.
   A CSS background on an empty <span> since 1.32.1 rather than an <img> with an empty alt — see
   .brand__logo. `background-origin: content-box` is what keeps the 6px working: it sizes and positions
   the artwork inside the padding box while `background-color` still fills to the edge, so the white
   plate and its inset are exactly as they were when the padding applied to an <img>. Without it the
   artwork would paint over its own margin and touch the plate's corners. */
.site-footer__mark {
  display: block;
  width: 56px; height: 56px;
  padding: 6px;
  background-color: #fff;
  background-image: var(--brand-logo);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  background-origin: content-box;
  border-radius: 12px;
  margin-bottom: var(--gap-2);
}
@media (min-width: 48rem) { .site-footer__cols { grid-template-columns: 1.4fr 1fr 1fr 1fr; } }
.site-footer__legal { border-top: 1px solid rgba(255, 255, 255, 0.14); margin-top: var(--gap-4); padding-top: var(--gap-3); font-size: 0.85rem; display: flex; flex-wrap: wrap; gap: var(--gap-2) var(--gap-3); justify-content: space-between; }

/* --------------------------------------------------------------------------
   Sticky mobile booking bar. A mobile-first site whose primary action is
   "book" should never require scrolling to find it.

   Restored in 1.30.1. It was deleted in 1.30.0, which put a Book button inside
   the floating menu instead — and the header's Book button is
   `.site-nav--desktop`, hidden below 62rem, so that left a phone's primary
   action two taps behind an icon.
   -------------------------------------------------------------------------- */
.book-bar {
  position: fixed; inset-inline: 0; bottom: 0; z-index: 25;
  display: flex; gap: var(--gap-2);
  padding: var(--gap-2) var(--gap-3);
  background: rgba(255, 255, 255, 0.98);
  border-top: 1px solid var(--line);
  box-shadow: 0 -2px 12px rgba(11, 47, 44, 0.08);
}
.book-bar .btn { flex: 1 1 auto; }
@media (min-width: 62rem) { .book-bar { display: none; } }
@media (max-width: 61.99rem) { body { padding-bottom: 5rem; } }

/* --------------------------------------------------------------------------
   The floating contact menu, bottom-left (1.30.0; reshaped to the pips.com.sg
   pattern in 1.30.1).

   Geometry ported from `.pips-cta-wrap`: left-anchored, 68px controls, the
   panel stacked above the launcher, tooltips to the right with an arrow.

   BOTTOM-LEFT because `.book-bar` above owns the bottom strip on phones. PIPS
   resolves the same collision the same way — its
   `@media (max-width:919.98px){ .pips-cta-wrap{bottom:90px} }` lifts the menu
   clear of its own sticky bar, and the rule below does it at this project's
   breakpoint. Two controls, two corners, no overlap.

   z-index 25 and not PIPS's 96: this project's scale is 100 (site header), 30
   (two overlay surfaces), 25 (the bar), 1. Matching the bar keeps both floating
   controls under the header, so an open nav dropdown covers them — which is
   correct, and a 96 here would put a contact shortcut over the menu.
   -------------------------------------------------------------------------- */

.contact-cta {
  position: fixed; z-index: 25;
  /*
     42px, lowered 25px from 67px on request (1.30.2), as a variable because the
     panel's height cap has to subtract the same number — and when it was written
     out twice, the mobile copy sat earlier in the file at equal specificity and
     was simply overridden. One source, no possible disagreement.

     The mobile rule below overrides it to 90px rather than lowering it too: the
     sticky bar occupies the bottom ~72px there, so 42px would put the launcher
     behind the bar.
  */
  --cta-bottom: 42px;
  left: 15px; bottom: var(--cta-bottom);
  display: flex; flex-direction: column; align-items: flex-start;
}

/* Lift clear of the sticky bar wherever the bar shows. Same purpose as PIPS's
   919.98px rule, at this project's own breakpoint. */
@media (max-width: 61.99rem) { .contact-cta { --cta-bottom: 90px; left: 12px; } }

/*
   De-chromed icon controls. The artwork is the whole appearance: each icon is a
   140×140 WebP of a flat colour circle with a white line-art glyph, so the
   button around it is transparent and carries no background, border or radius
   of its own. Six of the seven come from `pips.com.sg`; LINE is an inline SVG
   with its own circle for the reason the partial's docblock gives.

   `:focus-visible` keeps its ring, unlike PIPS — §41 requires a visible focus
   state and `site.css`'s global `:focus-visible` rule sets one for the site. A
   control that cannot be seen when focused is unusable by keyboard.
*/
.contact-cta__toggle,
.contact-cta__close,
.contact-cta__item {
  position: relative;
  width: 68px; height: 68px;
  padding: 0; margin: 0; border: 0;
  -webkit-appearance: none; appearance: none;
  background: transparent; box-shadow: none;
  cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center;
  transition: transform 0.15s ease;
}
.contact-cta__toggle:hover,
.contact-cta__close:hover,
.contact-cta__item:hover { transform: translateY(-2px); text-decoration: none; }

/*
   The image fills the control. `display: block` because the global reset gives
   `img` a baseline otherwise, which leaves a gap under each circle and makes the
   12px gaps read as uneven.

   The drop shadow is on the artwork rather than the button: the files have
   transparent corners outside their circles, so a shadow on the square button
   would draw a square.
*/
.contact-cta__toggle img,
.contact-cta__close img,
.contact-cta__item img,
.contact-cta__svg {
  display: block;
  width: 68px; height: 68px;
  border-radius: 50%;
  box-shadow: 0 6px 18px rgba(11, 47, 44, 0.28);
}

/*
   The panel, stacked above the launcher.

   Five controls at 68px with 12px gaps is 388px — PIPS's height, and short
   enough to sit under the header on a 1366×768 laptop. It was six controls and
   468px until 1.31.0, with a `max-height` cap and `overflow-y: auto` bolted on
   to survive that; both are gone. The cap had been added to contain a stack of
   SEVEN, which was itself the symptom of the menu's JavaScript never running —
   so it was insurance against a fault with an entirely different cause, and it
   put a scrollbar on the page for its trouble.
*/
.contact-cta__items {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 12px; margin-bottom: 12px;
}

/*
   The state machine, and why the collapsed state is the enhanced one.

   `is-collapsible` is added by `site.js`. Until it is, the panel is open and the
   launcher is beside it — so a visitor with the script blocked sees every
   channel and can use it. PIPS collapses by default and its channels are
   unreachable without JavaScript; that is the one part of the pattern not
   copied.

   🚨 `:focus-within` WAS IN THIS SELECTOR AND MUST NOT COME BACK (fixed 1.30.2).

   It was added so tabbing into the panel would open it even if the click handler
   never ran. It did two harmful things and nothing useful:

   1. IT BROKE THE CLOSE BUTTON. `setOpen(false)` returns focus to the launcher —
      which is INSIDE `.contact-cta` — so `:focus-within` matched the instant the
      panel closed, `:not(:focus-within)` failed, and the panel stayed open. The
      close button appeared to do nothing.
   2. IT PUSHED THE STACK INTO THE HEADER. With `.active` removed but the items
      still shown, the launcher reappeared too — seven controls instead of six,
      ~548px tall, tall enough to run under the sticky header on a laptop. Both
      symptoms reported, one cause.

   And it was never doing its job: this selector requires `.is-collapsible`, which
   only JavaScript adds. In the no-JS case the panel is open already, so
   `:focus-within` had nothing to rescue.
*/
.contact-cta.is-collapsible.active .contact-cta__toggle { display: none; }
.contact-cta.is-collapsible:not(.active) .contact-cta__items { display: none; }

/*
   Reveal on scroll, and fade at the footer — PIPS's behaviour, with the base
   hidden state gated behind a class JavaScript adds.

   `body.cta-revealable` is that gate. Without it neither rule below applies and
   the menu is simply always visible, which is the no-JS state. With it, the
   menu is hidden until roughly one viewport of scrolling and hidden again once
   the footer is on screen, so it never covers the hero or real footer content.
*/
body.cta-revealable .contact-cta {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.4s ease, visibility 0.4s ease;
}
body.cta-revealable.cta-shown .contact-cta {
  opacity: 1; visibility: visible; pointer-events: auto;
}
body.cta-revealable.footer-reached .contact-cta {
  opacity: 0; visibility: hidden; pointer-events: none;
}

/*
   The tooltip: a pill to the right of the icon with an arrow pointing back at
   it. A NICETY and never the only label — it does not appear on a touch device,
   which is why every control also carries `aria-label`.
*/
.contact-cta__tip {
  position: absolute; left: 82px; top: 50%;
  transform: translateY(-50%);
  background: #fff; color: var(--ink-900);
  font-size: 0.95rem; font-weight: 600; line-height: 1.2;
  padding: 10px 16px; border-radius: 16px;
  white-space: nowrap;
  box-shadow: 0 8px 22px rgba(11, 47, 44, 0.18);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
}
.contact-cta__tip::before {
  content: ""; position: absolute; left: -8px; top: 50%;
  transform: translateY(-50%);
  width: 0; height: 0;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-right: 8px solid #fff;
}
.contact-cta__has-tip:hover .contact-cta__tip,
.contact-cta__has-tip:focus-visible .contact-cta__tip {
  opacity: 1; visibility: visible;
  transform: translateY(-50%) translateX(6px);
}

@media (max-width: 47.99rem) {
  .contact-cta__toggle,
  .contact-cta__close,
  .contact-cta__item,
  .contact-cta__toggle img,
  .contact-cta__close img,
  .contact-cta__item img,
  .contact-cta__svg { width: 60px; height: 60px; }
  .contact-cta__tip { left: 74px; font-size: 0.875rem; padding: 8px 14px; border-radius: 14px; }
}

/* A phone in landscape has almost no vertical room, and six 60px controls would
   cover the page. Collapsed there unless the visitor opens them. */
@media (max-height: 30rem) {
  .contact-cta.is-collapsible:not(.active) .contact-cta__items { display: none; }
}

/* --------------------------------------------------------------------------
   Prose — the rendered body of a page, service or article
   -------------------------------------------------------------------------- */

.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--gap-3); }
.prose h2 { margin-top: var(--gap-4); }
.prose ul, .prose ol { padding-inline-start: 1.35rem; }
.prose li + li { margin-top: 0.3rem; }
.prose blockquote { margin: 0; padding-inline-start: var(--gap-3); border-inline-start: 3px solid var(--sand-500); color: var(--ink-700); font-style: italic; }
.prose table { width: 100%; border-collapse: collapse; }
.prose th, .prose td { padding: var(--gap-2); border-bottom: 1px solid var(--line); text-align: start; }

.toc { background: var(--paper-tint); border: 1px solid var(--line); border-radius: var(--radius); padding: var(--gap-3); }
.toc h2 { font-size: 1rem; margin-top: 0; }
.toc ol { margin: 0; padding-inline-start: 1.2rem; }

.byline { display: flex; flex-wrap: wrap; gap: var(--gap-1) var(--gap-3); font-size: 0.9rem; color: var(--ink-500); margin-bottom: var(--gap-3); }

.testimonial { background: #fff; border: 1px solid var(--line); border-radius: var(--radius-lg); padding: var(--gap-3); }
.testimonial blockquote { margin: 0 0 var(--gap-2); color: var(--ink-900); }
.testimonial figcaption { font-size: 0.9rem; color: var(--ink-500); }

.area-list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--gap-2); }
.area-list li { background: #fff; border: 1px solid var(--line); border-radius: 999px; padding: 0.45rem 0.95rem; font-size: 0.95rem; }
.area-list a { text-decoration: none; }
.area-list a::after { content: " →"; color: var(--teal-500); }

.pagination { display: flex; gap: var(--gap-2); align-items: center; justify-content: center; margin-top: var(--gap-4); }

@media print {
  .site-header, .site-footer, .book-bar, .contact-cta, .btn { display: none !important; }
  body { color: #000; }
  a::after { content: " (" attr(href) ")"; font-size: 0.85em; }
}

/* Step 4's therapist cards. A radio each, so the whole card is the control and selection needs no
   JavaScript; the portrait is a fixed square, so a tall photo cannot stretch the row. */
.therapist-choices { border: 0; padding: 0; margin: 0; }
/* A browsable list since 1.26.0, not a control: the cards cannot express a rank, so the three
   selects beneath them are the answer and these are the reference. `list-style: none` because it is
   a <ul> now — a <label> grid was never a list, and a list of people is. */
.therapist-choices__list {
  display: grid; gap: var(--gap-2); margin: var(--gap-2) 0;
  list-style: none; padding: 0;
}
@media (min-width: 48rem) { .therapist-choices__list { grid-template-columns: repeat(2, 1fr); } }
.therapist-choice {
  display: flex; align-items: flex-start; gap: var(--gap-2);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: var(--gap-2); background: #fff;
}

/* The availability hint. There is deliberately no opposite of this rule: a therapist who is not
   free renders no span at all, so nothing about anybody absence is derivable from the markup. */
.therapist-choice__hint { color: var(--teal-700); font-weight: 600; }

/* Set by site.js when a card has been clicked into one of the three ranks. Purely a mirror of
   the selects, which are the actual answer — without JavaScript no card ever carries it and the
   step still works, which is the whole arrangement. */
.therapist-choice.is-chosen {
  border-color: var(--teal-600);
  background: var(--teal-050);
  box-shadow: inset 0 0 0 1px var(--teal-600);
}
.therapist-choices__list .therapist-choice { cursor: pointer; }

/* First, second and third choice. One per row on a phone, three across from 44rem — the same
   breakpoint .form-grid--2 uses, so the step does not invent a second set of column rules. */
.therapist-ranks { display: grid; gap: var(--gap-3); margin-block: var(--gap-3); }
@media (min-width: 44rem) { .therapist-ranks { grid-template-columns: repeat(3, 1fr); } }

/* The ranked list in the step-6 summary panel. */
.summary__ranks { margin: 0 0 0.35rem; padding-left: 1.2rem; }
.therapist-choice__photo {
    width: 4rem; height: 4rem; border-radius: 50%; object-fit: cover;
    object-position: center 20%; flex: 0 0 auto;
}
.therapist-choice__body { display: block; min-width: 0; }
/* Neither --step--1 nor --ink-600 has ever been declared, so both properties were dropped and
   this line rendered at body size in body colour. Matching .card__meta, which is what it was
   plainly reaching for. Found while auditing var() resolution in 1.25.0. */
.choice__meta { display: block; font-size: 0.9rem; color: var(--ink-500); margin-top: 0.15rem; }

/* ==========================================================================
   1.24.0 — homepage conversion: therapists, messaging icons, action strip
   ========================================================================== */

/* --------------------------------------------------------------------------
   Classes the templates already used before they existed

   `our-therapists.php` and `booking.php` have referenced .muted, .card__title,
   .card--therapist and .card__portrait since they were written, and
   secondary-actions.php's `ghost` variant refers to .btn--ghost. None of them had a
   rule, so those pages have been rendering at browser defaults. Defining them fixes
   the existing pages as much as the new section.
   -------------------------------------------------------------------------- */

.muted { color: var(--ink-500); }
.section--deep .muted, .site-footer .muted { color: #bcd4d0; }

.btn--ghost { background: transparent; color: var(--teal-800); border-color: var(--line); }
.btn--ghost:hover { background: var(--teal-050); color: var(--teal-900); }

/* An <h2> for document-outline reasons — the page has one <h1> and each card is a
   sibling section — but it must not render at --step-3 inside a 160px card. `.card h3`
   already handles the <h3> case; this makes both levels look the same. */
.card__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-1);
  line-height: 1.25;
  color: var(--teal-900);
}
html[lang^="th"] .card__title { font-family: var(--font-body); }

/* Replaces `style="margin-top:var(--gap-3)"`, which our own CSP drops: style-src is
   'self' plus a nonce, and a nonce does not cover style *attributes*. */
.section__foot { margin-top: var(--gap-4); }

/* --------------------------------------------------------------------------
   Meet Our Therapists — the homepage section and the public directory
   -------------------------------------------------------------------------- */

/* `.grid--3` collapses to ONE column below 56rem, which would make six therapists six
   screens tall on a phone. This is 2-up from the smallest phone and 3-up from 44rem,
   so six cards read 2x3 on a phone and 3x2 on a tablet and desktop. */
.grid--therapists { grid-template-columns: repeat(2, 1fr); gap: var(--gap-2); }
@media (min-width: 34rem) { .grid--therapists { gap: var(--gap-3); } }
@media (min-width: 44rem) { .grid--therapists { grid-template-columns: repeat(3, 1fr); } }

/* Portrait-dominant: the picture runs to the card's own edges rather than sitting
   inside its padding, so the card reads as a portrait with a caption rather than a text
   box with a thumbnail. `overflow:hidden` is what clips it to the card radius. */
.card--therapist { padding: 0; gap: 0; overflow: hidden; }
.card--therapist .card__inner {
  display: flex; flex-direction: column; gap: var(--gap-1);
  padding: var(--gap-3);
  flex: 1 1 auto;
}
@media (max-width: 33.99rem) { .card--therapist .card__inner { padding: var(--gap-2); } }

/*
 * The portrait box, and why it is a box rather than an image.
 *
 * The eleven prepared portraits are all 1000px tall and 432-676px wide — a different
 * aspect ratio each. Left at their intrinsic size in a grid they render at eleven
 * different heights, so no two names line up, and each one *jumps* as it loads because
 * the ratio reserved from the width/height attributes is not the ratio that arrives.
 *
 * A fixed 4:5 box with `object-fit: cover` answers both: one shape for the row, reserved
 * before a byte arrives, and no distortion — the overflow is cropped, never squashed.
 * The img attributes must be a 4:5 pair too (300x375) or the pre-load placeholder
 * disagrees with the post-load box and the shift returns.
 *
 * `center 22%` follows the precedent .therapist-choice__photo set for booking step 4:
 * these are standing portraits, the head is near the top, and centring the crop would
 * behead half the roster.
 */
.card__portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center 22%;
  background: var(--teal-050);
}

/* A profile whose photograph was deleted still appears — publicDirectory() LEFT JOINs
   the document — so the box is held rather than leaving a ragged card in the row. */
.card__portrait--none { aspect-ratio: 4 / 5; background: var(--teal-050); }

/*
 * The client's marketing card as the tile (1.29.0).
 *
 * NOT .card__portrait, and not only because the ratio differs. HomepageConversionTest
 * asserts that every element carrying that class is served from /therapist-photo/{id} —
 * the gate that takes a withdrawn profile's picture down with it. This image comes
 * through /therapist-card/{id}, a different gate answering a different question, so it
 * carries a different class and its own assertion. A class that promises a gate must not
 * be borrowed by an image that does not come through it.
 *
 * 800x1250 is exact, not nominal: scripts/prepare-therapist-cards.php pads every card
 * onto that box rather than cropping or squashing it, and refuses past 35% padding. The
 * three source templates are 0.640, 0.667 and 0.474, so without that step this rule and
 * the img attributes would both be lying about a third of the roster.
 *
 * Declared here as well as in the attributes for the reason .card__portrait documents:
 * the reserved box and the delivered box must be the same box, or the tile jumps as the
 * picture arrives. At an exact ratio match `cover` crops nothing, and it is there so that
 * a future card at another shape is cropped rather than distorted.
 *
 * A card is 0.64 where a portrait is 0.8, so these tiles are appreciably taller than the
 * portrait tiles they replaced. That is the artwork's own shape and the grid keeps its
 * columns; the pages are simply longer.
 */
.card__card-art {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 800 / 1250;
  object-fit: cover;
  background: var(--teal-050);
}

/* --------------------------------------------------------------------------
   Messaging icons
   -------------------------------------------------------------------------- */

/* Sized here rather than with width/height attributes: the global reset sets
   `svg { height: auto }`, and `height:auto` on an inline SVG with no intrinsic size
   collapses it to zero in some engines. A class beats the bare `svg` selector. */
.icon {
  width: 1.15em; height: 1.15em;
  flex: 0 0 auto;
  vertical-align: -0.15em;
  display: inline-block;
}
.icon--lg { width: 1.5em; height: 1.5em; }

/* The buttons carrying one become flex rows so the glyph and the label share a
   baseline rather than the glyph sitting on its own line. */
.btn--line, .btn--whatsapp { display: inline-flex; align-items: center; gap: 0.5em; }

/* Icon-only, for the sticky bar where the label is a visually-hidden span. Square, so
   two of them plus a full-width primary still fit one row on the narrowest phone. */
.btn--icon { flex: 0 0 auto; width: 3rem; padding-inline: 0; justify-content: center; }
.btn--icon .icon { width: 1.5rem; height: 1.5rem; }

/* --------------------------------------------------------------------------
   "Ready to book?" — the mid-page conversion band
   -------------------------------------------------------------------------- */

.action-strip__inner { display: grid; gap: var(--gap-3); align-items: center; }
@media (min-width: 48rem) { .action-strip__inner { grid-template-columns: 1fr auto; } }
.action-strip h2 { margin-bottom: 0.25rem; }
.action-strip p { margin: 0; }

/* --------------------------------------------------------------------------
   Contact lists (footer and the homepage contact block)

   Replaces the inline `style="list-style:none;padding:0;…"` those blocks carried, which
   the CSP drops. `.site-footer ul` is (0,1,1) so it would win on gap without the second
   selector.
   -------------------------------------------------------------------------- */

.contact-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.site-footer .contact-list { gap: 0.5rem; }
.contact-list li { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.contact-list__label { color: var(--ink-500); }
.site-footer .contact-list__label { color: #bcd4d0; }

/* --------------------------------------------------------------------------
   1.25.0 — service photography
   -------------------------------------------------------------------------- */

/* Photograph-led, structured like `.card--therapist`: the picture runs to the card's own
   edges rather than sitting inside its padding, and `overflow:hidden` clips it to the radius. */
.card--service { padding: 0; gap: 0; overflow: hidden; }
.card--service .card__inner {
  display: flex; flex-direction: column; gap: var(--gap-2);
  padding: var(--gap-3);
  flex: 1 1 auto;
}
@media (max-width: 33.99rem) { .card--service .card__inner { padding: var(--gap-2); } }

.card__cover-link { display: block; }

/*
 * 3:2, because all eight photographs are 1536x1024 and that is 3:2 exactly.
 *
 * Declared in CSS as well as in the img's width/height pair, for the reason `.card__portrait`
 * documents: the reserved box and the delivered box must be the same box, or the card jumps as
 * the picture arrives. `object-fit: cover` means a future photograph at a different ratio is
 * cropped rather than squashed, and the 3:2 attribute pair stays correct because the rendered
 * box is 3:2 whatever arrives.
 *
 * NOT `.card__portrait`. HomepageConversionTest asserts every element carrying that class is
 * served from `/therapist-photo/{id}` — the gate that takes a withdrawn profile's picture down
 * with it. A static asset must not borrow a class that promises that.
 */
.card__cover {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: center;
  background: var(--teal-050);
}

/* A service still waiting for its photograph holds the same box, so a row does not go ragged and
   §40's labelled placeholder is what fills it. The placeholder's own min-height and radius are
   neutralised here: it is filling a box rather than being one. */
.card__cover--empty { aspect-ratio: 3 / 2; display: grid; }
.card__cover--empty .placeholder {
  min-height: 0; height: 100%;
  border-radius: 0; border-inline: 0; border-block-start: 0;
}

/* The service page's sidebar image. It had no dimensions at all until 1.25.0 — the sidebar
   reflowed on every visit — and an inline `style` our own CSP drops.

   `.venue__image` (1.42.0) is the same box in the same sidebar position on the three venue pages,
   so it shares the rule rather than duplicating it under a second name. The 3:2 ratio with
   object-fit is also what lets the venue images be whatever size they are generated at: the
   declared width and height come from the file, and the box crops the difference. */
.service__image,
.venue__image {
  width: 100%; height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background: var(--teal-050);
}

/*
 * Four across, for the eight photographed services.
 *
 * `.grid--4` already existed but only split at 56rem, straight from one column to four — which
 * puts four 3:2 photographs on a tablet at about 160px wide each. The 2-up step at 40rem and the
 * 4-up step at 62rem give a phone one card, a tablet two and a desktop four.
 */
.grid--4 { grid-template-columns: 1fr; }
@media (min-width: 40rem) { .grid--4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 62rem) { .grid--4 { grid-template-columns: repeat(4, 1fr); } }

/*
 * The LINE QR disclosure on /contact (1.25.0).
 *
 * Hidden below 48rem rather than merely collapsed: the phone that would scan this code is the
 * phone the code is displayed on, and the LINE button directly above it opens the same chat in
 * one tap. `display: none` and not `visibility`, so it is out of the accessibility tree too — a
 * screen-reader user on a phone should not be offered a control that leads nowhere useful.
 *
 * The code itself is capped at 15rem. It is a 536px square and rendering it at full size would
 * make the QR the largest thing on a contact page whose actual purpose is the buttons.
 */
.line-qr { display: none; }

@media (min-width: 48rem) {
  .line-qr {
    display: block;
    margin-block-start: var(--gap-2);
    border-block-start: 1px solid var(--line);
    padding-block-start: var(--gap-2);
  }

  .line-qr > summary {
    cursor: pointer;
    font-weight: 600;
    color: var(--teal-700);
  }

  .line-qr > summary:focus-visible {
    outline: 2px solid var(--teal-600);
    outline-offset: 2px;
    border-radius: var(--radius);
  }

  .line-qr__code {
    display: block;
    width: 100%;
    max-width: 15rem;
    height: auto;
    margin-block-start: var(--gap-1);
    border-radius: var(--radius);
  }
}

/* ==========================================================================
   The portrait overlay on booking step 4 (1.27.0)
   ========================================================================== */

/*
 * The thumbnail is now a link. It must not look like one — no underline, no colour change — because
 * it is a photograph, and the affordance is the picture plus the cursor. The focus ring is the part
 * that matters: this is a real control and a keyboard user has to be able to see where they are.
 */
.therapist-choice__zoom {
  display: block;
  flex: 0 0 auto;
  border-radius: 50%;
  line-height: 0;
  cursor: zoom-in;
}

.therapist-choice__zoom:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 2px;
}

/*
 * The dialog. `<dialog>` is display:none until opened, so nothing here needs a hidden state.
 *
 * `max-height: 90dvh` and not `vh`: on a phone the browser chrome eats the difference, and a portrait
 * that runs under the address bar is exactly the part of the picture somebody wanted to see.
 */
.portrait-dialog {
  border: 0;
  padding: 0;
  background: transparent;
  max-width: min(28rem, calc(100vw - 2rem));
  max-height: 90dvh;
  overflow: visible;
}

.portrait-dialog::backdrop {
  background: rgba(11, 47, 44, 0.65);
}

/* The marketing card's look, rebuilt from what the site already publishes: the portrait, the name,
   the languages and the years. No age, no off-duty photographs — see roster.json. */
.portrait-dialog__card {
  position: relative;
  display: flex;
  flex-direction: column;
  max-height: 90dvh;
  overflow: hidden;
  background: var(--sand-100);
  border: 1px solid var(--sand-300);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
}

/*
 * The close cross, over the photograph rather than under it.
 *
 * A translucent dark disc, because it sits on a portrait whose brightness we do not control — a bare
 * glyph would vanish against a pale background and a solid chip would look like a sticker. 2.75rem is
 * the 44px hit target; the icon inside is half that.
 */
.portrait-dialog__close {
  position: absolute;
  top: var(--gap-1);
  right: var(--gap-1);
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(11, 47, 44, 0.55);
  color: #fff;
  cursor: pointer;
}

.portrait-dialog__close:hover { background: rgba(11, 47, 44, 0.75); }

.portrait-dialog__close:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.portrait-dialog__close .icon { width: 1.375rem; height: 1.375rem; }

/*
 * `object-fit: contain`, unlike the card's `cover`. The card crops to a circle because it is a
 * thumbnail in a row; the whole point of opening it is to see the photograph the crop was hiding.
 */
.portrait-dialog__photo {
  display: block;
  width: 100%;
  height: auto;
  max-height: 60dvh;
  object-fit: contain;
  background: var(--sand-300);
}

.portrait-dialog__body {
  padding: var(--gap-3);
  overflow-y: auto;
}

.portrait-dialog__name {
  font-family: var(--font-display);
  font-size: var(--step-3);
  color: var(--brass-600);
  letter-spacing: 0.02em;
  margin: 0 0 var(--gap-1);
}

/* A hairline under the name, the one flourish the client's card template uses. */
.portrait-dialog__meta {
  border-top: 1px solid var(--sand-300);
  padding-top: var(--gap-1);
  color: var(--ink-700);
}

.portrait-dialog__meta .choice__title { display: none; }

.portrait-dialog__actions {
  display: grid;
  gap: var(--gap-1);
  margin-top: var(--gap-2);
}

@media (min-width: 26rem) {
  .portrait-dialog__actions { grid-template-columns: 1fr auto; }
}

/* --------------------------------------------------------------------------
   The two-part time control on booking step 3 (1.28.0)
   -------------------------------------------------------------------------- */

/*
 * Hour and minutes side by side with the colon between them, so the pair reads as one time rather
 * than two unrelated dropdowns. `align-items: end` keeps the colon on the selects' baseline when a
 * field below wraps.
 */
.time-parts {
  display: flex;
  align-items: end;
  gap: 0.5rem;
}

.time-parts .field { flex: 1 1 0; margin: 0; }

.time-parts__colon {
  font-weight: 600;
  color: var(--ink-500);
  padding-bottom: 0.55rem;
}

/* The date read-back. Empty until a date is chosen, and it must not reserve a line while empty —
   an always-present blank paragraph pushes every field below it down for no reason. */
[data-date-readback]:empty { display: none; }

/* --------------------------------------------------------------------------
   The therapist-card zoom overlay (1.51.0)

   `.grid--therapists` is 2-up from the smallest phone, so an 800x1250 card renders about 170px
   wide and everything printed on it is illegible. Tapping it opens the card here.

   🚨 The zoom and the pan are the BROWSER'S, not ours. The card sits in an `overflow: auto` box
   and `site.js` changes one property — the image's width. Scrollbars, momentum on iOS, keyboard
   arrows and pinch all come from that, and the alternative was hand-rolling two-finger tracking,
   inertia and bounds-clamping, whose failure mode is "feels wrong on a real device".

   Pinch works because nothing here takes it away: the viewport meta is
   `width=device-width, initial-scale=1` with no `user-scalable=no` and no `maximum-scale`, and
   `touch-action` below keeps pinch-zoom with the browser while still allowing pan.
   -------------------------------------------------------------------------- */

.card__art-zoom {
  display: block;
  cursor: zoom-in;
}

/* The card art is the whole tap target; the hidden span must not add a stray outline. */
.card__art-zoom:focus-visible {
  outline: 2px solid var(--teal-600);
  outline-offset: 2px;
}

/* Nearly the whole viewport: the point is to see the card, not to admire a frame around it. */
.card-zoom {
  border: 0;
  padding: 0;
  background: transparent;
  width: min(60rem, calc(100vw - 1.5rem));
  max-width: calc(100vw - 1.5rem);
  max-height: 94dvh;
  overflow: visible;
}

.card-zoom::backdrop {
  background: rgba(11, 47, 44, 0.82);
}

.card-zoom__frame {
  position: relative;
  display: flex;
  flex-direction: column;
  max-height: 94dvh;
  background: var(--sand-100);
  border: 1px solid var(--sand-300);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}

/* Matches `.portrait-dialog__close`: 2.75rem is the 44px hit target, and a translucent dark disc
   because it sits over artwork whose brightness we do not control. */
.card-zoom__close {
  position: absolute;
  top: var(--gap-1);
  right: var(--gap-1);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(11, 47, 44, 0.72);
  color: #fff;
  cursor: pointer;
}

.card-zoom__close:hover { background: rgba(11, 47, 44, 0.88); }
.card-zoom__close .icon { width: 1.375rem; height: 1.375rem; }

.card-zoom__name {
  margin: 0;
  padding: var(--gap-2) 3.5rem var(--gap-2) var(--gap-3);
  font-weight: 600;
  border-bottom: 1px solid var(--sand-300);
}

/* 🚨 The scroller. This is the whole zoom-and-pan mechanism.
   - `overflow: auto` gives panning, scrollbars and keyboard arrows
   - `overscroll-behavior: contain` stops a flick reaching the page behind (precedent: the mobile
     drawer at `.nav-panel`)
   - `touch-action: pan-x pan-y pinch-zoom` allows dragging AND leaves pinch to the browser.
     `touch-action: none` here would kill pinch, which is the one thing people try first. */
.card-zoom__viewport {
  flex: 1 1 auto;
  overflow: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-x pan-y pinch-zoom;
  background: var(--teal-050);
}

/* Width is what `site.js` writes, via the CSSOM. 100% is fit-to-frame; above that the box scrolls.
   `height: auto` keeps the 800:1250 ratio without a second property to set.

   🚨 `max-width: none` is what makes the zoom do anything at all. The base rule at the top
   of this file — `img, picture, svg, video { max-width: 100% }` — clamps the computed width back
   to the containing block, so `width: 300%` resolved to 100% and every control was visually
   inert: the overlay opened, the card appeared, the buttons changed a property with no effect.
   It cost nothing to write and nothing in the markup could have revealed it; it was found by
   measuring `scrollWidth` against `clientWidth` in a real browser at 390px. */
.card-zoom__art {
  display: block;
  width: 100%;
  max-width: none;
  height: auto;
  margin: 0 auto;
}

.card-zoom__controls {
  display: flex;
  gap: var(--gap-1);
  justify-content: center;
  padding: var(--gap-2);
  border-top: 1px solid var(--sand-300);
  background: var(--sand-100);
}

/* Three equal buttons that stay above the 44px tap target on the narrowest phone. */
.card-zoom__step {
  flex: 1 1 0;
  min-height: 2.75rem;
  max-width: 12rem;
}
