/* ==========================================================================
   THE SYNC · Design system
   Base palette locked by client: white / black / grey.
   Accent: Pulse Teal, signed off by the client 06/08/2026. One accent only.
   ========================================================================== */

:root {
  /* Paper + ink. Deliberately off-pure so contrast reads premium, not harsh. */
  --paper:      #FDFCFB;
  --paper-2:    #F5F3F0;
  --paper-3:    #EAE7E2;
  --ink:        #1A1918;
  --ink-2:      #3D3B38;
  --grey:       #6E6B66;
  --grey-2:     #9C978F;
  --line:       rgba(26, 25, 24, .11);
  --line-2:     rgba(26, 25, 24, .06);

  /* Night sections */
  /* Lifted from #121615/#1A1F1E on 25/08/2026: the client picked Option B
     from the greens artifact, mint buttons and night bands that read deep
     green rather than black. night-grey holds 6.2:1 and 5.3:1 on the new
     grounds, so the text tokens did not need to move. */
  --night:      #182421;
  --night-2:    #213029;
  --night-line: rgba(253, 252, 251, .12);
  --night-text: #EDEBE7;
  --night-grey: #9AA3A0;

  /* Accent: Pulse Teal. Chosen by the client 06/08/2026 from three options he
     compared live; the switcher that offered them is now deleted.
     --accent stays vivid for marks, lines and text. --btn was a deliberately
     deeper tone while buttons carried paper text; on 25/08/2026 the client
     picked the mint (Option B of the greens artifact), and mint with PAPER
     text fails AA at 1.7:1, so filled buttons now pair --btn with --btn-ink,
     near-black text on bright mint, 11.0:1. Anything using --btn as a TEXT
     colour on paper must use --accent-dk instead. */
  --accent:      #12B39D;
  --accent-hi:   #2ADFC5;
  /* Pulse Teal's own dark tone was #0C8271, which put the light-surface chips
     at 4.11:1 and failed AA. Same hue (171deg), deepened until they clear it.
     This token is the colour of every small accent-coloured label on paper,
     so it has to be the safe one, not the pretty one. */
  --accent-dk:   #096B5D;
  --accent-wash: #E4F7F4;
  --accent-tint: rgba(18, 179, 157, .12);
  /* Tint of --accent-hi, for chips on night surfaces. Kept as a token because
     it used to be a hardcoded rgba() of the old accent and silently stopped
     matching when the accent changed. */
  --accent-hi-tint: rgba(42, 223, 197, .14);
  --btn:         #2ADFC5;
  --btn-hover:   #12B39D;
  --btn-ink:     #0E1513;

  /* Selected-point red. Functional, not decorative: on the body map it marks
     "this is where it hurts", which is the one thing that must out-shout the
     teal. Muted brick rather than alarm red so it stays inside the palette.
     Deliberately NOT part of the accent switcher, it means the same thing
     whichever teal the client picks. */
  --flag:      #C0453B;
  --flag-dk:   #9E3229;

  /* There is deliberately NO second accent. A warm bronze was trialled here on
     01/08/2026 and the client rejected it on 05/08: "I wanted more white/black/
     grey with the accent teal/green you added. The gold I don't think matches."
     Neutrals carry the surface, one teal does all the signalling. If a moment
     needs to feel warmer, reach for weight and scale, not another hue. */

  --nav-h: 60px;
  --radius:   18px;
  --radius-lg: 26px;
  --maxw: 1200px;

  --ease: cubic-bezier(.22, 1, .36, 1);

  /* ── Bridge, 19/08/2026 ──────────────────────────────────────────
     The two clinics merged onto one group site, and this stylesheet
     is now the base. The Bali build brought pages the Sync never had
     (the journal, FAQ, contact, condition pillars) whose components
     name their tokens differently. Rather than rewrite those pages,
     their names are aliased onto the palette above, so one palette
     still decides every colour and there is no second source of
     truth. Nothing below introduces a new hue. */
  --sans: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --display: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --serif: "Instrument Serif", Georgia, "Times New Roman", serif;

  --card: #FFFFFF;
  /* --grey, not --grey-2. #9C978F is 2.83:1 on this paper and small text
     needs 4.5:1, so every label set in it failed: the body map hint, the
     address labels, form notes, captions. --grey-2 keeps its lighter tone
     for what it is actually good at, the decorative wave strokes and the
     scroll-cue gradient, where contrast rules do not apply. */
  --faint: var(--grey);
  --line-soft: var(--line-2);
  --accent-line: rgba(18, 179, 157, .32);
  --night-accent: var(--accent-hi);
  /* the body map's selected point is the same functional red as --flag */
  --bm-active: var(--flag);

  --wrap: var(--maxw);
  --wrap-narrow: 48.75rem;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --fab-inset: clamp(1rem, 3vw, 1.75rem);

  /* the ruler motif carried by the Bali condition and journal pages */
  --tick: rgba(26, 25, 24, .24);
  --ticks: repeating-linear-gradient(
    to right,
    var(--tick) 0, var(--tick) 1px,
    transparent 1px, transparent 2.5rem
  );
}



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

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: calc(var(--nav-h) + 16px);
}

body {
  font-family: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--paper);
  color: var(--ink);
  font-size: 17px;
  line-height: 1.6;
  letter-spacing: -.011em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  font-feature-settings: "kern" 1, "liga" 1, "cv11" 1;
}

img, svg { max-width: 100%; display: block; }
/* The carried Bali pages set width and height attributes on their images
   so the browser reserves the right box before load. Without this, those
   attributes win over the CSS aspect ratios and the images render at
   their intrinsic height. The Sync's own pages do not set the attributes,
   so this was never needed there. */
img { height: auto; }
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; color: inherit; }
/* The Sync's own chrome had no bare <button>, so its reset never needed
   to clear native chrome. The Resources dropdown carried over from the
   Bali build is one, and without this it renders as a boxed grey control
   in the middle of the header. Every button here is styled explicitly,
   so removing the default background and border takes nothing away. */
button { background: none; border: 0; cursor: pointer; }

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

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink); color: var(--paper);
  padding: .75rem 1.25rem; border-radius: 0 0 10px 0;
}
.skip-link:focus { left: 0; }

/* ── Layout primitives ───────────────────────────────────────────────── */

.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: clamp(1.25rem, 5vw, 2.5rem); }
.wrap-narrow { width: 100%; max-width: 820px; margin-inline: auto; padding-inline: clamp(1.25rem, 5vw, 2.5rem); }

.section { padding-block: clamp(4.5rem, 11vw, 9.5rem); }
.section-tight { padding-block: clamp(3rem, 7vw, 6rem); }

.night { background: var(--night); color: var(--night-text); }
.night .lead, .night .body-copy { color: var(--night-grey); }
.night h1, .night h2, .night h3 { color: var(--night-text); }
.tinted { background: var(--paper-2); }

/* ── Type scale ──────────────────────────────────────────────────────── */

h1, h2, h3, h4 { font-weight: 560; letter-spacing: -.035em; line-height: 1.04; text-wrap: balance; }

.display {
  font-size: clamp(2.9rem, 8.4vw, 6.6rem);
  letter-spacing: -.045em;
  line-height: .96;
  font-weight: 560;
}
.h1 { font-size: clamp(2.4rem, 6vw, 4.6rem); letter-spacing: -.042em; }
.h2 { font-size: clamp(2rem, 4.6vw, 3.5rem); letter-spacing: -.038em; }
.h3 { font-size: clamp(1.35rem, 2.3vw, 1.85rem); letter-spacing: -.028em; line-height: 1.18; }

.serif { font-family: "Instrument Serif", Georgia, "Times New Roman", serif; font-weight: 400; letter-spacing: -.02em; }

/* NO ITALICS ANYWHERE. The client asked for this in writing (05/08/2026):
   "please don't use italics. I don't like them. Please use normal or can bold
   text but I prefer no italics." Emphasis is carried by weight, colour and
   scale instead. There is no .serif-it utility for this reason. */

/* Brand mantra: one short line per page at most, always subordinate to the
   heading above it. Separated from body copy by weight and ink, not by hue. */
.mantra {
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  font-weight: 500;
  letter-spacing: -.022em;
  color: var(--ink);
}
.night .mantra { color: var(--night-text); }

.lead {
  font-size: clamp(1.09rem, 1.7vw, 1.4rem);
  line-height: 1.5;
  color: var(--grey);
  letter-spacing: -.017em;
  text-wrap: pretty;
  max-width: 62ch;
}
.body-copy { color: var(--ink-2); text-wrap: pretty; max-width: 68ch; }
.muted { color: var(--grey); }
.accent-text { color: var(--accent); }

/* Section label: sits UNDER the headline, quiet, no dash decoration. */
.caption {
  font-size: .875rem;
  color: var(--grey);
  letter-spacing: -.005em;
}

.tag {
  display: inline-block;
  font-size: .78rem;
  font-weight: 500;
  letter-spacing: .01em;
  padding: .3rem .7rem;
  border-radius: 100px;
  background: var(--accent-tint);
  color: var(--accent-dk);
}
.night .tag { background: var(--accent-hi-tint); color: var(--accent-hi); }

.center { text-align: center; }
.center .lead, .center .body-copy { margin-inline: auto; }

/* ── Buttons ─────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .8rem 1.6rem;
  border-radius: 100px;
  font-size: .97rem;
  font-weight: 500;
  letter-spacing: -.012em;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease), transform .25s var(--ease);
  white-space: nowrap;
}
.btn:active { transform: scale(.975); }

.btn-primary { background: var(--btn); color: var(--btn-ink); }
.btn-primary:hover { background: var(--btn-hover); }
/* .nav-links a (0,1,1) outranks .btn-primary (0,1,0) and was painting the nav
   CTA in --ink-2 on teal, a 2.9:1 mess. Restate it at matching specificity. */
.nav-links a.btn-primary { color: var(--btn-ink); }
.nav-links a.btn-primary:hover { color: var(--btn-ink); }
/* Same trap one level down: on book.html the CTA carries aria-current, and
   .nav-links a[aria-current] (0,2,1) is declared later than the rule above.
   (0,3,1) settles it so the label never falls back to --ink on teal. */
.nav-links a.btn-primary[aria-current="page"] { color: var(--btn-ink); }

.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--ink); }
.night .btn-ghost { color: var(--night-text); border-color: var(--night-line); }
.night .btn-ghost:hover { border-color: var(--night-text); }

.btn-lg { padding: 1rem 2rem; font-size: 1.03rem; }

.link-arrow {
  display: inline-flex; align-items: center; gap: .38rem;
  /* --accent-dk, not --accent: on paper the vivid accent is 2.57:1. The bright
     tone still gets used on night surfaces, in the override below. */
  color: var(--accent-dk);
  font-weight: 500;
  font-size: .97rem;
}
.link-arrow svg { transition: transform .28s var(--ease); }
.link-arrow:hover svg { transform: translateX(4px); }
.night .link-arrow { color: var(--accent-hi); }

.btn-row { display: flex; flex-wrap: wrap; gap: .75rem; }
.center .btn-row, .hero .btn-row { justify-content: center; }

/* ── Navigation ──────────────────────────────────────────────────────── */

.nav {
  position: sticky; top: 0; z-index: 90;
  height: var(--nav-h);
  display: flex; align-items: center;
  background: rgba(253, 252, 251, .72);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s var(--ease), background .3s var(--ease);
}
.nav.is-stuck { border-bottom-color: var(--line-2); }

.nav-inner { display: flex; align-items: center; gap: 1.5rem; width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: clamp(1.25rem, 5vw, 2.5rem); }

.brand { display: flex; align-items: center; margin-right: auto; }

/* The client's supplied wordmark. Source is 200x34 black-on-transparent, so
   it is displayed under 1x-per-CSS-pixel for crispness and inverted to white
   on dark surfaces rather than being recoloured. */
.brand-logo { display: block; width: auto; height: 18px; }
@media (min-width: 900px) { .brand-logo { height: 20px; } }
.brand-logo-light { filter: brightness(0) invert(1); }
.footer .brand-logo { height: 22px; }

.nav-links { display: flex; align-items: center; gap: 2rem; }
.nav-links a {
  font-size: .92rem; color: var(--ink-2); letter-spacing: -.01em;
  transition: color .2s var(--ease);
  position: relative;
}
.nav-links a:hover { color: var(--ink); }
.nav-links a[aria-current="page"] { color: var(--ink); font-weight: 500; }

/* The nav's single CTA. It used to be hidden below 900px because a separate
   "Visit us" text link carried the drawer; that link is gone, so the button is
   now the only nav route to book.html and must show at every width. */
.nav-cta { display: inline-flex; }

.nav-toggle {
  display: grid; place-items: center;
  width: 40px; height: 40px; margin-right: -8px;
  background: none; border: 0; cursor: pointer;
}
.nav-toggle span {
  display: block; width: 18px; height: 1.5px; background: var(--ink);
  border-radius: 2px; transition: transform .3s var(--ease), opacity .2s var(--ease);
}
.nav-toggle span + span { margin-top: 5px; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

@media (min-width: 900px) { .nav-toggle { display: none; } }
@media (max-width: 899px) {
  .nav-links {
    position: fixed; inset: var(--nav-h) 0 auto 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: .5rem clamp(1.25rem, 5vw, 2.5rem) 1.5rem;
    transform: translateY(-12px); opacity: 0; pointer-events: none;
    transition: opacity .28s var(--ease), transform .28s var(--ease);
    max-height: calc(100dvh - var(--nav-h)); overflow-y: auto;
  }
  .nav-links.is-open { transform: translateY(0); opacity: 1; pointer-events: auto; }
  .nav-links a { padding: .95rem 0; font-size: 1.15rem; border-bottom: 1px solid var(--line-2); }
  /* .nav-links a (0,1,1) would otherwise paint the drawer's list padding and
     hairline underline onto the CTA pill. Restated at (0,2,0) to win. */
  .nav-links .btn {
    margin-top: 1.1rem; width: 100%;
    padding: .95rem 1.6rem;
    font-size: 1.02rem;
    border-bottom-color: transparent;
  }
}

/* ── Hero ────────────────────────────────────────────────────────────── */

.hero {
  position: relative;
  min-height: min(92svh, 900px);
  display: grid; place-items: center;
  text-align: center;
  padding-block: clamp(4rem, 12vw, 7rem);
  overflow: hidden;
}
.hero-content { position: relative; z-index: 2; }
.hero .lead { margin-inline: auto; margin-top: 1.6rem; }
.hero .btn-row { margin-top: 2.4rem; }
.hero-tagline { margin-bottom: 1.5rem; }
/* Outside the motion query on purpose: reduced-motion users still need the
   two headline halves on separate lines. */
.hero-line { display: block; }

/* Signal band sits in the lower half so the headline never fights it. */
.hero-canvas {
  position: absolute; inset: auto 0 0 0; height: 58%; z-index: 1;
  pointer-events: none; overflow: hidden;
  mask-image: linear-gradient(to bottom, transparent 0%, #000 22%, #000 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 22%, #000 100%);
}
.hero-canvas svg { width: 100%; height: 100%; display: block; }

.hero-scroll {
  position: absolute; bottom: 2rem; left: 50%; transform: translateX(-50%);
  z-index: 2; color: var(--grey); font-size: .8rem;
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
}
.hero-scroll::after {
  content: ""; width: 1px; height: 34px;
  background: linear-gradient(to bottom, var(--grey-2), transparent);
}

/* ── Hero oscillation: travelling waves ──────────────────────────────
   Each path repeats on a 400-unit period and is animated by exactly one
   period, so translateX loops seamlessly and the wave reads as flowing
   rather than wobbling in place.                                        */

.osc-line  { fill: none; stroke: var(--accent); stroke-linecap: round; }
.wave-1    { stroke-width: 1.6; opacity: .58; }
.wave-2    { stroke-width: 1.1; opacity: .32; }
.wave-3    { stroke-width: .9;  opacity: .2; }
/* The middle wave runs neutral, so the band reads as two signals in
   conversation rather than one voice. Held at the same low opacity. It used
   to run bronze; with the second accent gone, grey does the same job. */
.wave-2 .osc-line { stroke: var(--grey-2); }

/* The quieter harmonics on the oscillations card. -soft was referenced by
   the card's SVG but never defined, so two of its three lines rendered with
   no stroke at all. -alt is the neutral second voice, echoing the hero. */
.osc-line-soft { fill: none; stroke: var(--accent); stroke-width: 1; opacity: .3; stroke-linecap: round; }
.osc-line-alt  { fill: none; stroke: var(--grey-2); stroke-width: 1.1; opacity: .5; stroke-linecap: round; }
.osc-base  { stroke: var(--accent); stroke-width: .7; opacity: .13; }
.osc-tick  { stroke: var(--accent); stroke-width: 1; opacity: .22; }

/* The travelling signal: one bright dash sweeping the primary wave. */
/* pathLength="1000" on the path normalises this regardless of geometry.
   45/1000 of a 4000-unit path is a ~180-unit streak: long enough to read as a
   signal travelling the line rather than a speck. */
.osc-pulse {
  fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 45 955;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .wave-1 { animation: waveRun 2.8s linear infinite; }
  .wave-2 { animation: waveRun 4.6s linear infinite reverse; }
  .wave-3 { animation: waveRun 1.9s linear infinite; }
  .osc-amp { animation: ampBreathe 3.8s ease-in-out infinite alternate; transform-origin: center; }
  .osc-pulse { animation: pulseRun 4.2s cubic-bezier(.45, 0, .55, 1) infinite; }

  @keyframes waveRun {
    from { transform: translateX(0); }
    to   { transform: translateX(-400px); }
  }
  @keyframes ampBreathe {
    from { transform: scaleY(.82); }
    to   { transform: scaleY(1.22); }
  }
  @keyframes pulseRun {
    from { stroke-dashoffset: 1000; opacity: 0; }
    10%  { opacity: 1; }
    90%  { opacity: 1; }
    to   { stroke-dashoffset: 0; opacity: 0; }
  }

  /* Headline entrance. Transform ONLY, never opacity: the h1 is the LCP
     element and an opacity fade would delay its paint to animation-end. */
  .js .hero-line { animation: heroRise .85s var(--ease) both; }
  .js .hero-line:nth-child(2) { animation-delay: .1s; }
  @keyframes heroRise {
    from { transform: translateY(26px); }
    to   { transform: none; }
  }
}

/* Static fallback: hold the pulse off-screen so nothing looks half-drawn. */
@media (prefers-reduced-motion: reduce) {
  .osc-pulse { display: none; }
}

/* ── Scroll reveal ───────────────────────────────────────────────────── */

/* Gated on .js so a JS failure can never leave a section invisible. */
@media (prefers-reduced-motion: no-preference) {
  .js .reveal { opacity: 0; transform: translateY(22px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
  .js .reveal.is-in { opacity: 1; transform: none; }
  .js .reveal-d1 { transition-delay: .08s; }
  .js .reveal-d2 { transition-delay: .16s; }
  .js .reveal-d3 { transition-delay: .24s; }
}

/* ── Cards + grids ───────────────────────────────────────────────────── */

.grid { display: grid; gap: clamp(1rem, 2.2vw, 1.5rem); }
/* Explicit two columns for the same reason grid-3 and grid-4 below went
   explicit: auto-fit landed THREE columns on a four-card set at desktop
   width and left an orphan on the second row. A -2 grid means two across,
   full stop. */
.grid-2 { grid-template-columns: 1fr; }
@media (min-width: 760px) { .grid-2 { grid-template-columns: repeat(2, 1fr); } }

/* Explicit column counts, not auto-fit. auto-fit was landing 3 columns on a
   4-card set at tablet width, leaving a lone orphan on the second row. These
   step 1 -> 2 -> n so a row is always full. */
.grid-3 { grid-template-columns: 1fr; }
.grid-4 { grid-template-columns: 1fr; }
@media (min-width: 600px) { .grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 880px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1040px) { .grid-4 { grid-template-columns: repeat(4, 1fr); } }

.card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 3vw, 2.25rem);
  transition: border-color .3s var(--ease), transform .3s var(--ease);
}
.card:hover { border-color: rgba(26, 25, 24, .22); }
.tinted .card { background: var(--paper); }
.night .card { background: var(--night-2); border-color: var(--night-line); }
.night .card:hover { border-color: rgba(253, 252, 251, .26); }

.card h3 { margin-bottom: .6rem; }
.card p { color: var(--grey); font-size: .98rem; }
.night .card p { color: var(--night-grey); }
.card .card-icon { margin-bottom: 1.4rem; }

/* Demoted descriptor. The card NAME must win the glance, so this sits well
   under it in size AND weight. Full prose lives in .visually-hidden beside it,
   still in the DOM for search. */
.card .card-tag {
  font-size: .9rem;
  line-height: 1.55;
  color: var(--grey);
  margin-bottom: 0;
  max-width: 34ch;
  text-wrap: pretty;
  /* Reserve two lines so a short descriptor cannot leave one card visually
     shorter than its neighbours. Cards read as a set, not as boxes that
     happened to get different amounts of text. */
  min-height: calc(1.55em * 2);
}

/* Never let a compound word split across lines: "Computer-guided" breaking to
   "Computer-" / "guided" is the tell that copy was poured into a fixed box. */
.card h3 { hyphens: none; -webkit-hyphens: none; text-wrap: balance; }
.night .card .card-tag { color: var(--night-grey); }

/* Whole-card link. */
.card-link { display: block; }
.card-link h3 { transition: color .22s var(--ease); }
.card-link:hover h3 { color: var(--accent); }
.night .card-link:hover h3 { color: var(--accent-hi); }
.card-link::after {
  content: "";
  display: block;
  width: 15px; height: 15px;
  margin-top: 1.25rem;
  background: currentColor;
  color: var(--accent);
  transition: transform .28s var(--ease);
  -webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14M13 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.night .card-link::after { color: var(--accent-hi); }
.card-link:hover::after { transform: translateX(4px); }

.stat-row { display: flex; flex-wrap: wrap; gap: clamp(1.5rem, 5vw, 4rem); }
.stat-num { font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -.04em; font-weight: 560; line-height: 1; }
.stat-lbl { font-size: .9rem; color: var(--grey); margin-top: .45rem; }

/* ── Section heading block ───────────────────────────────────────────── */

.head { max-width: 780px; margin-bottom: clamp(2.5rem, 5vw, 4rem); }
.head .lead { margin-top: 1.15rem; }
.head.center { margin-inline: auto; }

/* ── Platform showcase (3 systems) ───────────────────────────────────── */

.platform {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  padding-block: clamp(3rem, 7vw, 6rem);
  border-top: 1px solid var(--line);
}
.night .platform { border-top-color: var(--night-line); }
@media (min-width: 900px) {
  .platform { grid-template-columns: 1fr 1fr; }
  .platform:nth-child(even) .platform-visual { order: -1; }
}
.platform-visual {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
  background: var(--paper-2);
  display: grid; place-items: center;
  overflow: hidden;
  padding: clamp(1.5rem, 4vw, 3rem);
}
.night .platform-visual { background: var(--night-2); }
.platform-visual svg { width: 100%; height: 100%; }

/* SpinalSync carries a photograph instead of a diagram: it is the platform the
   client calls the star, and one real image breaks the three-identical-blocks
   rhythm exactly once. The padding that frames the diagrams has to go, or the
   photo sits in a box inside a box. */
.platform-photo { padding: 0; }
.platform-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.platform h3 { margin-block: .9rem .9rem; }
.platform ul { list-style: none; margin-top: 1.6rem; display: flex; flex-wrap: wrap; gap: .5rem; }
.platform ul li {
  font-size: .87rem; padding: .34rem .78rem;
  border: 1px solid var(--line); border-radius: 100px; color: var(--ink-2);
}
.night .platform ul li { border-color: var(--night-line); color: var(--night-grey); }

/* ── Body map ────────────────────────────────────────────────────────── */

.bodymap {
  display: grid;
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
}
@media (min-width: 940px) { .bodymap { grid-template-columns: minmax(280px, 400px) 1fr; } }

/* The figure is a transparent cutout sitting straight on the section, with no
   panel behind it. A baked-in backdrop could only ever match one section
   colour (Conditions is --paper, Home is --paper-2), so it always read as a
   framed photo pasted onto the page. */
.bodymap-figure {
  position: relative;
  width: 100%; max-width: 330px;
  margin-inline: auto;
  background: transparent;
}

.bodymap-img { display: block; width: 100%; height: auto; }

/* Stacked layout: cap the figure by HEIGHT so the figure, the readout and the
   chips all fit one screen. At full width the figure alone filled the viewport
   and a tap looked like it did nothing. Every hit area still clears 24px at
   this scale. */
@media (max-width: 939px) {
  .bodymap-figure { width: fit-content; max-width: 100%; }
  .bodymap-img { width: auto; height: min(56svh, 620px); max-width: 100%; }
}

/* Route to the detail on stacked layouts. Deliberately just a link: the chips
   below already show which area is selected, so a card repeating the title
   was the same information twice. */
.bodymap-readout { display: none; }
@media (max-width: 939px) {
  .bodymap-readout { display: flex; justify-content: center; margin-top: .9rem; }
  .readout-more {
    display: inline-flex; align-items: center; gap: .38rem;
    background: none; border: 0;
    padding: .5rem .9rem;
    color: var(--accent-dk); font-size: .95rem; font-weight: 500;
    cursor: pointer;
  }
  .readout-more svg { transition: transform .25s var(--ease); }
  .readout-more:hover svg, .readout-more:focus-visible svg { transform: translateY(2px); }
}

/* The overlay shares the image's coordinate system, so pins stay locked to
   anatomy at every display size. */
.bodymap-svg { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Hit areas render NOTHING, ever. Showing the zone shape drew a hard
   rectangle/ellipse over the anatomy, and the UA focus ring added a second
   black box on top. Selection is communicated by the pin and its ripple. */
.zone {
  cursor: pointer;
  fill: transparent;
  stroke: none;
  outline: none;
}
.zone:focus { outline: none; }
.zone:focus-visible { outline: none; }

/* Marker dots are the affordance. The paper ring lifts each pin off whatever
   sits behind it, which is what makes them readable on top of the spine. */
.dot {
  fill: var(--accent);
  stroke: var(--paper); stroke-width: 5;
  transition: r .25s var(--ease), fill .25s var(--ease);
}
.dot.is-hover { r: 13; fill: var(--accent-dk); }
/* Selected wins over hover: listed after, so a hovered+selected pin stays red. */
.dot.is-active { r: 13; fill: var(--flag); }

.dot-halo { fill: var(--accent); opacity: .2; transition: opacity .25s var(--ease), r .25s var(--ease), fill .25s var(--ease); }
.dot-halo.is-hover { opacity: .28; }
.dot-halo.is-active { opacity: .3; r: 26; fill: var(--flag); }

/* Keyboard focus reads as a ring on the pin, not a box on the hit area. */
.dot.is-focus { stroke: var(--accent-dk); stroke-width: 7; }
.dot.is-active.is-focus { stroke: var(--flag-dk); }

/* Selection ripple: rings resonating outward from the chosen point.
   transform-box:fill-box makes scale() pivot on each circle's own centre. */
.dot-ripple {
  fill: none;
  stroke: var(--flag);
  stroke-width: 3;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  pointer-events: none;
}
/* Opening wave: one slow, far-reaching pulse on selection, roughly twice the
   reach of the steady rings. The loop below is delayed to start as this
   fades, so a tap reads as one big signal settling into a heartbeat. */
.dot-burst {
  fill: none;
  stroke: var(--flag);
  stroke-width: 6;
  opacity: 0;
  transform-box: fill-box;
  transform-origin: center;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .dot-burst.is-active { animation: rippleBurst 1.5s cubic-bezier(.1, .75, .2, 1) 1 both; }
  @keyframes rippleBurst {
    0%   { transform: scale(.32); opacity: .85; stroke-width: 7; }
    55%  { opacity: .3; }
    100% { transform: scale(6.6); opacity: 0; stroke-width: .8; }
  }

  /* steady state, held back until the opening wave has spent itself */
  .dot-ripple.is-active { animation: ripple 2.1s cubic-bezier(.2, .6, .3, 1) infinite; animation-delay: .95s; }
  .dot-ripple.is-active.ripple-2 { animation-delay: 1.65s; }
  .dot-ripple.is-active.ripple-3 { animation-delay: 2.35s; }
  @keyframes ripple {
    0%   { transform: scale(.55); opacity: .62; stroke-width: 5; }
    70%  { opacity: .12; }
    100% { transform: scale(3.4); opacity: 0; stroke-width: 1.5; }
  }
}

@media (prefers-reduced-motion: no-preference) {
  .zone-pulse { animation: zonePulse 3.6s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
  @keyframes zonePulse { 0%, 100% { opacity: .16; } 50% { opacity: .42; } }
  .dot-halo.is-active, .dot-halo.is-hover { animation: none; }
}

.bodymap-hint { margin-top: 1rem; font-size: .86rem; color: var(--grey); text-align: center; }

/* Region chips (mobile-friendly alternative to hotspots) */
.zone-chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; justify-content: center; }
.zone-chip {
  font-size: .84rem; padding: .4rem .85rem; border-radius: 100px;
  border: 1px solid var(--line); background: var(--paper); color: var(--ink-2);
  cursor: pointer; transition: all .22s var(--ease);
}
.zone-chip:hover { border-color: var(--accent); color: var(--accent-dk); }
/* --btn, not --accent: this is a filled surface carrying --paper text, which is
   exactly the case --btn exists for. On --accent it measured 2.57:1. */
.zone-chip.is-active { background: var(--btn); border-color: var(--btn); color: var(--btn-ink); }

/* Detail panel */
.bodymap-panel {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: clamp(1.5rem, 3.2vw, 2.5rem);
  background: var(--paper);
  min-height: 380px;
}
.night .bodymap-panel { background: var(--night-2); border-color: var(--night-line); }

.panel-title { margin-block: .75rem .5rem; }
.panel-block { padding-block: 1.1rem; border-top: 1px solid var(--line-2); }
.night .panel-block { border-top-color: var(--night-line); }
.panel-block:first-of-type { border-top: 0; padding-top: .4rem; }
.panel-label {
  font-size: .78rem; font-weight: 500; color: var(--grey);
  margin-bottom: .35rem;
}
.panel-block p { color: var(--ink-2); font-size: .99rem; }
.night .panel-block p { color: var(--night-grey); }
.panel-foot { margin-top: 1.5rem; display: flex; flex-wrap: wrap; gap: .75rem; align-items: center; }

@media (prefers-reduced-motion: no-preference) {
  .panel-swap { animation: panelIn .45s var(--ease); }
  @keyframes panelIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
}

/* ── Accordion (FAQ / programs) ──────────────────────────────────────── */

.acc-item { border-top: 1px solid var(--line); }
.acc-item:last-child { border-bottom: 1px solid var(--line); }
.night .acc-item { border-top-color: var(--night-line); }
.night .acc-item:last-child { border-bottom-color: var(--night-line); }

.acc-btn {
  width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
  padding: 1.5rem 0; background: none; border: 0; cursor: pointer; text-align: left;
  font-size: clamp(1.05rem, 1.8vw, 1.3rem); font-weight: 500; letter-spacing: -.025em;
}
.acc-icon { flex: none; width: 20px; height: 20px; position: relative; }
.acc-icon::before, .acc-icon::after {
  content: ""; position: absolute; inset: 50% 0 auto 0; height: 1.5px;
  background: var(--accent); border-radius: 2px;
  transition: transform .3s var(--ease);
}
.acc-icon::after { transform: rotate(90deg); }
.acc-btn[aria-expanded="true"] .acc-icon::after { transform: rotate(0deg); }

/* Row-fr collapse, so nothing animates a layout property.
   The inner overflow:hidden is load-bearing: it zeroes the item's automatic
   minimum size, which is what lets the 0fr row actually collapse.
   Without JS the panels stay open rather than becoming unreachable. */
.acc-panel { display: grid; grid-template-rows: 1fr; }
.acc-panel > div { overflow: hidden; }
.js .acc-panel { grid-template-rows: 0fr; transition: grid-template-rows .34s var(--ease); }
.js .acc-panel[data-open="true"] { grid-template-rows: 1fr; }
@media (prefers-reduced-motion: reduce) { .js .acc-panel { transition: none; } }
.acc-panel p { padding-bottom: 1.6rem; color: var(--grey); max-width: 68ch; }
.night .acc-panel p { color: var(--night-grey); }

/* ── Steps ───────────────────────────────────────────────────────────── */

.steps { counter-reset: step; display: grid; gap: 0; }
.step {
  counter-increment: step;
  display: grid; gap: .4rem 1.75rem;
  grid-template-columns: auto 1fr;
  padding-block: clamp(1.5rem, 3vw, 2.25rem);
  border-top: 1px solid var(--line);
}
.night .step { border-top-color: var(--night-line); }
.step::before {
  content: counter(step, decimal-leading-zero);
  grid-row: 1 / span 2;
  font-size: .85rem; color: var(--accent-dk); font-weight: 500;
  padding-top: .35rem;
  font-variant-numeric: tabular-nums;
}
.night .step::before { color: var(--accent-hi); }
.step h3 { font-size: clamp(1.15rem, 2vw, 1.5rem); }
.step p { color: var(--grey); max-width: 60ch; }
.night .step p { color: var(--night-grey); }

/* ── Visit page photo ────────────────────────────────────────────────
   Source is only 409px wide, so it is capped near native size rather than
   stretched. Upscaling a real clinic photo to fill a column looks worse than
   showing it small and sharp. */

.visit-grid { display: grid; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
@media (min-width: 900px) { .visit-grid { grid-template-columns: minmax(260px, 360px) 1fr; } }

.visit-photo { margin: 0; }
.visit-photo img {
  width: 100%; max-width: 409px; height: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--night-line);
}
.visit-photo figcaption {
  margin-top: .85rem;
  font-size: .84rem;
  color: var(--night-grey);
  max-width: 34ch;
}

/* ── Motion system ───────────────────────────────────────────────────
   Four treatments, applied site-wide by js/motion.js. One shared curve
   (ease-out-expo) and one shared stagger step, so the whole site moves like
   one thing. Transform, opacity and clip-path only.

   Everything is gated on .js AND reduced motion: with either absent, nothing
   here applies and the page is a plain, fully visible document. */

@media (prefers-reduced-motion: no-preference) {
  .js [data-mo] {
    --mo-ease: cubic-bezier(.16, 1, .3, 1);
    --mo-delay: calc(var(--i, 0) * 65ms);
  }

  /* wipe: a measurement sweep across the heading.
     Opacity stays 1 so text paints immediately; only the clip animates. */
  .js [data-mo="wipe"] {
    clip-path: inset(0 100% 0 0);
    transition: clip-path .85s var(--mo-ease);
    transition-delay: var(--mo-delay);
  }
  .js [data-mo="wipe"].is-in { clip-path: inset(0 -.12em 0 0); }

  /* rise: copy lifting into place */
  .js [data-mo="rise"] {
    opacity: 0;
    transform: translate3d(0, 22px, 0);
    transition: opacity .8s var(--mo-ease), transform .95s var(--mo-ease);
    transition-delay: var(--mo-delay);
  }

  /* pop: tiles arriving with a touch of scale */
  .js [data-mo="pop"] {
    opacity: 0;
    transform: translate3d(0, 26px, 0) scale(.978);
    transition: opacity .7s var(--mo-ease), transform .9s var(--mo-ease);
    transition-delay: var(--mo-delay);
  }

  /* slide: the big platform blocks alternate sides */
  .js [data-mo="slide"] {
    opacity: 0;
    transform: translate3d(var(--mo-dx, -60px), 18px, 0);
    transition: opacity .85s var(--mo-ease), transform 1.15s var(--mo-ease);
  }
  .js [data-mo="slide"][data-dir="r"] { --mo-dx: 60px; }

  .js [data-mo="rise"].is-in,
  .js [data-mo="pop"].is-in,
  .js [data-mo="slide"].is-in {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1);
  }
}

/* ── Reading progress ────────────────────────────────────────────────── */

.scroll-progress {
  position: fixed; top: var(--nav-h); left: 0; right: 0;
  height: 2px; z-index: 89;
  pointer-events: none;
  background: transparent;
}
.scroll-progress span {
  display: block; height: 100%;
  background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  opacity: .85;
}

/* ── Scroll journey ──────────────────────────────────────────────────
   The section pins while four steps play through, so scrolling feels slowed
   and deliberate. The cards never move: the active one EXPANDS in place and
   the others stay collapsed to their title, like a horizontal accordion on
   desktop and a vertical one on phones.

   The page still scrolls NATIVELY: sticky positioning plus a scroll progress
   read, never preventDefault. Hijacking the wheel breaks keyboard paging,
   trackpad momentum and screen readers.

   Both the tall height and the pinning are gated on .js AND reduced motion.
   In either fallback every step is expanded and fully readable. */

.journey-stage { padding-block: 0; position: relative; }
.journey-stage > .wrap { position: relative; z-index: 1; }

/* Per-step line art, cross-fading behind the content. Held very quiet so it
   reads as texture, never as something competing with the cards. */
.journey-bg {
  position: absolute; inset: 0; z-index: 0;
  pointer-events: none; overflow: hidden;
  color: var(--accent);
}
.jbg {
  position: absolute;
  right: -3%; top: 50%;
  width: min(44%, 520px); height: auto;
  opacity: 0;
  transform: translateY(-50%) scale(.92) rotate(-3deg);
  transition: opacity .8s var(--ease), transform 1.1s var(--ease);
}
.jbg.is-on { opacity: .18; transform: translateY(-50%) scale(1) rotate(0deg); }

@media (max-width: 899px) {
  .jbg { right: -14%; width: 68%; }
  .jbg.is-on { opacity: .14; }
}
@media (prefers-reduced-motion: reduce) {
  .jbg { transition: none; }
}
/* position:relative is load-bearing. The rail is an absolutely-positioned
   ::before on this element; without a positioned ancestor here it resolved
   against .journey-stage and drew a stray line straight through the heading. */
.journey-track { list-style: none; display: grid; gap: 1rem; position: relative; }
.jstep { position: relative; min-width: 0; }

.jstep-card {
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.1rem, 2.2vw, 1.5rem) clamp(1.2rem, 2.4vw, 1.6rem);
  height: 100%;
  transition:
    background .55s var(--ease),
    border-color .55s var(--ease),
    box-shadow .55s var(--ease);
}
.jstep-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.jstep-title { font-size: clamp(1.02rem, 1.7vw, 1.22rem); letter-spacing: -.026em; }
.jstep-no {
  font-size: .78rem; font-weight: 500; color: var(--accent-dk);
  font-variant-numeric: tabular-nums; flex: none;
  transition: color .55s var(--ease);
}
.jstep-copy {
  padding-top: .55rem;
  font-size: .93rem; line-height: 1.55; color: var(--grey);
  transition: color .55s var(--ease);
}

/* Rail node */
.jstep-node {
  position: absolute;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--paper-3);
  z-index: 2;
  transition: background .45s var(--ease), border-color .45s var(--ease), transform .45s var(--ease);
}

/* The expanded state */
.jstep.is-current .jstep-card {
  background: var(--btn);
  border-color: var(--btn);
  /* Shadow stays the deep teal, not a tint of the mint: a bright surface
     needs a darker tone underneath it to read as lifted. */
  box-shadow: 0 24px 48px -28px rgba(9, 107, 93, .55);
}
/* Ink, not paper: the client picked the mint (PR #37) and paper on it is
   1.7:1. Missed by the post-change sweep because is-current only exists
   after the scroll animation runs, which a hidden pane never does; state
   classes need static auditing, not runtime sweeps. */
.jstep.is-current .jstep-title,
.jstep.is-current .jstep-no,
.jstep.is-current .jstep-copy { color: var(--btn-ink); }
.jstep.is-current .jstep-node {
  background: var(--btn); border-color: var(--btn); transform: scale(1.35);
}
.jstep.is-done .jstep-node { background: var(--accent); border-color: var(--accent); }

/* Progress rail */
.journey-rail {
  position: relative; height: 2px; border-radius: 2px;
  background: var(--paper-3);
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  overflow: hidden;
}
.journey-fill {
  position: absolute; inset: 0 auto 0 0; width: 100%;
  background: var(--accent); border-radius: 2px;
  transform: scaleX(var(--p, 0)); transform-origin: left;
  transition: transform .25s linear;
}

/* While journey.js probes each step's height, kill transitions so the probe
   reads settled values instead of whatever is mid-tween. */
.journey.is-measuring *,
.journey.is-measuring *::before,
.journey.is-measuring *::after { transition: none !important; }

/* ── Pinned behaviour ─────────────────────────────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .js .journey {
    height: calc(100svh + (var(--steps, 4) * 62svh));
    padding-block: 0;
  }
  .js .journey-stage {
    position: sticky; top: var(--nav-h);
    height: calc(100svh - var(--nav-h));
    display: flex; align-items: center;
  }

  /* Collapse the copy on every step, expand only the current one.
     The inner overflow:hidden is load-bearing: it zeroes the item's automatic
     minimum size, which is what lets the 0fr row actually collapse.

     The delays sequence width and copy so they never fight. Expanding, the
     copy waits for the card to widen; collapsing, it hides first and the card
     narrows after. Run simultaneously, the copy briefly wrapped inside a
     still-narrow card and spiked the row to 312px against a 202px resting
     height, which is what made the section jiggle on every step change. */
  .js .jstep-body {
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows .4s var(--ease);
    transition-delay: 0s;
  }
  .js .jstep-body > div { overflow: hidden; min-height: 0; }
  .js .jstep.is-current .jstep-body {
    grid-template-rows: 1fr;
    transition-delay: .32s;
  }
  /* nothing transient can push the row open */
  .js .jstep-card { overflow: hidden; }

  .js .jstep:not(.is-current) .jstep-card { opacity: .55; }
  .js .jstep-card { transition: background .55s var(--ease), border-color .55s var(--ease),
                                box-shadow .55s var(--ease), opacity .55s var(--ease); }

  /* Phones: vertical accordion, rail down the left. */
  @media (max-width: 899px) {
    /* No width transition here, so the copy has nothing to wait for. Keeping
       the desktop delay made the old card finish closing BEFORE the new one
       opened, which read as the list lurching down then back up. Run them
       together and the swap is one motion. */
    .js .jstep.is-current .jstep-body { transition-delay: 0s; }
    .js .jstep-body { transition-duration: .45s; }

    .js .journey-track { padding-left: 2.1rem; align-content: center; }
    .js .journey-track::before {
      content: ""; position: absolute;
      left: 6px; top: 8px; bottom: 8px; width: 2px;
      background: var(--paper-3);
    }
    .js .jstep-node { left: -2.1rem; top: 1.4rem; }
  }

  /* Desktop: horizontal accordion. Cards hold their place; the active one
     takes the extra width. Only 4 items change size and only on a discrete
     step change, so the layout cost is negligible. */
  @media (min-width: 900px) {
    .js .journey-track {
      display: flex; align-items: stretch; gap: 1.25rem;
      padding-top: 3.2rem;
    }
    .js .journey-track::before {
      content: ""; position: absolute;
      left: 0; right: 0; top: 1.55rem; height: 2px;
      background: var(--paper-3);
    }
    .js .jstep {
      flex: 1 1 0;
      transition: flex-grow .65s var(--ease);
    }
    .js .jstep.is-current { flex-grow: 2.5; }
    .js .jstep-node { top: -1.72rem; left: 50%; margin-left: -6.5px; }
  }
}

/* ── Form ────────────────────────────────────────────────────────────── */

.form-grid { display: grid; gap: 1.1rem; }
@media (min-width: 640px) { .form-2 { grid-template-columns: 1fr 1fr; } }

.field label {
  display: block; font-size: .87rem; font-weight: 500;
  margin-bottom: .45rem; color: var(--ink-2);
}
.field .req { color: var(--accent); }

.field input, .field select, .field textarea {
  width: 100%;
  padding: .8rem 1rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 12px;
  font-size: 16px; /* iOS zoom guard */
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
  -webkit-appearance: none; appearance: none;
}
.field textarea { min-height: 120px; resize: vertical; }
.field select {
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236E6B66' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 1rem center;
  padding-right: 2.5rem;
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-tint);
}
.field input:invalid:not(:placeholder-shown) { border-color: #B4453A; }
.field-error { color: #B4453A; font-size: .82rem; margin-top: .35rem; display: none; }
.field.has-error .field-error { display: block; }
.field.has-error input, .field.has-error select, .field.has-error textarea { border-color: #B4453A; }

.form-note { font-size: .85rem; color: var(--grey); margin-top: .25rem; }

.form-status { display: none; padding: 1.25rem 1.4rem; border-radius: var(--radius); margin-top: 1.25rem; }
.form-status.is-shown { display: block; }
.form-status.ok { background: var(--accent-wash); border: 1px solid var(--accent); }
.form-status.ok h3 { color: var(--accent-dk); font-size: 1.1rem; margin-bottom: .4rem; }
.form-status p { font-size: .95rem; color: var(--ink-2); }

/* ── Contact / location cards ────────────────────────────────────────── */

.info-row { display: flex; flex-wrap: wrap; gap: .35rem 1rem; align-items: baseline; padding-block: .85rem; border-top: 1px solid var(--line-2); }
.info-row:first-child { border-top: 0; }
.info-row dt { font-size: .84rem; color: var(--grey); min-width: 92px; }
.info-row dd { font-size: .99rem; }
.night .info-row { border-top-color: var(--night-line); }

/* ── Dealt-card entrance ─────────────────────────────────────────────
   Cards arrive from alternating sides, then their contents cascade in a beat
   later, so each card reads as composed rather than as one sliding slab.
   Transform and opacity only, so it stays on the compositor at 60fps.
   Gated on .js and on prefers-reduced-motion, and the cards are fully
   visible in both fallbacks. */

@media (prefers-reduced-motion: no-preference) {
  .js .deal .card.reveal {
    opacity: 0;
    /* fallbacks matter: an undefined var invalidates the whole transform */
    transform: translate3d(var(--dx, 0px), 34px, 0) scale(.955) rotate(var(--rot, 0deg));
    /* ease-out-expo: fast commit, long graceful settle */
    transition:
      opacity .75s cubic-bezier(.16, 1, .3, 1),
      transform 1.15s cubic-bezier(.16, 1, .3, 1);
    will-change: transform, opacity;
  }
  .js .deal .deal-l { --dx: -68px; --rot: -2.4deg; }
  .js .deal .deal-r { --dx:  68px; --rot:  2.4deg; }

  /* The fan starts each card 68px off its resting place. In the three-column
     grid it fans into space the grid already owns. In the one-column grid a
     phone gets, the card is nearly the full width of the screen and there is
     no such space: at 360px the right-hand card began at 406px and made the
     document 46px wider than the viewport. Every page could then be dragged
     sideways, and because the WhatsApp button is position:fixed it stayed put
     while the page slid under it, so it read as the button being off-screen.

     Not a transient, either. These sections sit below the fold, so the cards
     hold their entry offset until they are scrolled to and the page is
     side-scrollable from load.

     clip rather than hidden: hidden on an ancestor silently kills
     position:sticky inside it, and the journey section relies on that. The
     clip margin lets the card shadows still paint past the edge; where it is
     unsupported they crop at the boundary, which is a far smaller problem
     than a page that slides. */
  .deal { overflow-x: clip; overflow-clip-margin: 1.5rem; }

  /* Belt and braces, and better looking: on one column the fan is mostly
     off-screen anyway, so it comes in from a distance the screen can afford
     rather than being clipped away. */
  @media (max-width: 899px) {
    .js .deal .deal-l { --dx: -22px; --rot: -1.4deg; }
    .js .deal .deal-r { --dx:  22px; --rot:  1.4deg; }
  }

  .js .deal .card.reveal.is-in {
    opacity: 1;
    transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
  }
  /* second and third cards trail the first */
  .js .deal .card.reveal:nth-child(2) { transition-delay: .12s; }
  .js .deal .card.reveal:nth-child(3) { transition-delay: .24s; }

  /* Contents cascade after their own card has started settling. */
  .js .deal .card.reveal > * {
    opacity: 0;
    transform: translate3d(0, 16px, 0);
    transition:
      opacity .6s cubic-bezier(.16, 1, .3, 1),
      transform .75s cubic-bezier(.16, 1, .3, 1);
  }
  .js .deal .card.reveal.is-in > * { opacity: 1; transform: none; }

  .js .deal .card.reveal.is-in .card-icon { transition-delay: .30s; }
  .js .deal .card.reveal.is-in h3        { transition-delay: .38s; }
  .js .deal .card.reveal.is-in .card-tag { transition-delay: .46s; }

  .js .deal .card.reveal:nth-child(2).is-in .card-icon { transition-delay: .42s; }
  .js .deal .card.reveal:nth-child(2).is-in h3        { transition-delay: .50s; }
  .js .deal .card.reveal:nth-child(2).is-in .card-tag { transition-delay: .58s; }

  .js .deal .card.reveal:nth-child(3).is-in .card-icon { transition-delay: .54s; }
  .js .deal .card.reveal:nth-child(3).is-in h3        { transition-delay: .62s; }
  .js .deal .card.reveal:nth-child(3).is-in .card-tag { transition-delay: .70s; }

  /* The arrow is the last thing to land. */
  .js .deal .card-link::after {
    opacity: 0;
    transform: translate3d(-10px, 0, 0);
    transition: opacity .55s cubic-bezier(.16, 1, .3, 1), transform .6s cubic-bezier(.16, 1, .3, 1);
  }
  .js .deal .card-link.is-in::after { opacity: 1; transform: none; transition-delay: .60s; }
  .js .deal .card-link:nth-child(2).is-in::after { transition-delay: .72s; }
  .js .deal .card-link:nth-child(3).is-in::after { transition-delay: .84s; }
  /* hover must still win once the entrance has finished */
  .js .deal .card-link.is-in:hover::after { transform: translateX(4px); transition-delay: 0s; }
}

/* ── Clinician ───────────────────────────────────────────────────────
   Who you will see, deliberately NOT a founder story. The client asked for
   that in writing (05/08/2026): "please don't use me as founder. We can add
   my photo and say chiropractor (non-medical) but I don't want based on me."
   So there is no pull quote, no biography and no "Founder" anywhere: the
   photo and the role line are attribution for the practice, not a personal
   brand. The portrait was supplied B&W on near-black, so it gets the same
   framed treatment as the visit photo (radius + hairline) rather than
   pretending to be a cutout, and it sits on a night section so its own
   background reads as intentional. */

@media (min-width: 900px) {  }


/* Restored 06/08/2026 at the client's request. It originally set each "right"
   in <em>; italics are out under content rule 6, and Instrument Serif ships
   400 + italic only, so a serif <strong> would render as a faux-bold smear.
   Sans at a real 600 instead, the same fix .mantra took when its italics
   were removed. */
/* Standalone statement band since the clinician section around it was
   removed: the quote is the section now. The old 28ch cap stayed after the
   grid went, but nothing centred the block, so it sat against the wrap's
   left edge inside a full-height night band and read as misplaced. */
.clinician-quote { margin: 0; }
.clinician-quote p {
  font-size: clamp(1.6rem, 3.2vw, 2.5rem);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: -.028em;
  color: var(--night-text);
  text-wrap: balance;
  max-width: 30ch;
  margin-inline: auto;
}
.clinician-quote strong { font-weight: 600; }


/* Credentials, supplied by the client 06/08/2026. Deliberately one short block:
   he sent a full four-paragraph bio and said it "sounds cheesy and like all
   clinics", so only the registration facts and the 20-years line survive. */

.clinician-photo { margin: 0; width: 100%; max-width: 420px; justify-self: center; }
@media (min-width: 900px) { .clinician-photo { justify-self: end; } }
.clinician-photo img {
  width: 100%; height: auto;
  border-radius: var(--radius-lg);
  border: 1px solid var(--night-line);
}

/* ── Locations ───────────────────────────────────────────────────────── */

.loc-grid { display: grid; gap: clamp(1.25rem, 3vw, 2rem); }
@media (min-width: 900px) { .loc-grid { grid-template-columns: 1fr 1fr; } }

.loc-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper);
  overflow: hidden;
  display: flex; flex-direction: column;
}
.night .loc-card { background: var(--night-2); border-color: var(--night-line); }

/* Hover lift. Pointer devices only: on touch there is no hover, and a
   :hover rule there sticks after a tap. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  /* Scoped through .loc-grid so it outranks the motion system's
     `.js [data-mo="pop"]` transition, which otherwise wins the tie and
     leaves box-shadow and border-colour snapping instead of easing. */
  .js .loc-grid .loc-card,
  .loc-card {
    transition:
      transform .5s var(--ease),
      box-shadow .5s var(--ease),
      border-color .5s var(--ease),
      opacity .7s var(--ease);
  }
  .loc-card:hover, .loc-card:focus-within {
    transform: translate3d(0, -7px, 0) scale(1.012);
    border-color: var(--accent);
    box-shadow: 0 34px 60px -32px rgba(0, 0, 0, .55);
  }
  .night .loc-card:hover, .night .loc-card:focus-within {
    border-color: var(--accent-hi);
    box-shadow: 0 34px 62px -30px rgba(0, 0, 0, .8);
  }
  /* the map drifts a touch slower than the card, so it reads as depth */
  .loc-card .loc-map { transition: transform .65s var(--ease); }
  .loc-card:hover .loc-map { transform: scale(1.035); }
}

/* Fixed ratio reserves the space so the lazy iframe cannot shift layout. */
.loc-map { aspect-ratio: 16 / 10; background: var(--paper-3); }
.night .loc-map { background: var(--night); }
.loc-map iframe { width: 100%; height: 100%; border: 0; display: block; filter: saturate(.9); }

.loc-body { padding: clamp(1.35rem, 2.6vw, 2rem); display: flex; flex-direction: column; flex: 1; }
.loc-body h3 { margin-bottom: .3rem; }
.loc-note { color: var(--grey); font-size: .96rem; margin-bottom: 1rem; }
.night .loc-note { color: var(--night-grey); }
.loc-body dl { margin-top: auto; }
.loc-body .btn-row { margin-top: 1.4rem; }

/* ── SpinalSync system shot ──────────────────────────────────────────── */

/* The photograph's own background is pure #000, so the band is too. Anything
   else (--night, --night-2) leaves a visible rectangle where the two meet. */
.system-shot {
  margin: 0; background: #000;
  border-radius: var(--radius-lg);
  padding: clamp(1.25rem, 4vw, 2.75rem);
  text-align: center;
}
.system-shot img {
  display: block; width: 100%; height: auto;
  max-width: 760px; margin-inline: auto;
}
.system-shot figcaption {
  margin-top: clamp(1rem, 2.5vw, 1.5rem);
  font-size: .92rem; color: var(--night-grey);
  max-width: 46ch; margin-inline: auto; text-wrap: pretty;
}

/* ── Video facade ────────────────────────────────────────────────────── */

/* No YouTube request until click: no third-party cookie before consent, and
   no cost to the majority who never press play. */
.video-facade {
  position: relative; margin-top: clamp(1.75rem, 4vw, 2.5rem);
  border-radius: var(--radius); overflow: hidden;
  background: var(--night); aspect-ratio: 16 / 9;
  cursor: pointer;
}
.video-facade img, .video-facade iframe, .video-facade video {
  display: block; width: 100%; height: 100%; border: 0; object-fit: cover;
}
/* A self-hosted film is letterboxed, not cropped. object-fit: cover on the
   poster is fine because it is chosen to crop well; doing the same to the
   video would cut the top and bottom off anything that is not exactly 16:9. */
.video-facade video { object-fit: contain; background: #000; }
.video-play {
  position: absolute; inset: 0; margin: auto;
  width: 74px; height: 74px; border: 0; border-radius: 50%;
  background: var(--btn); color: var(--btn-ink);
  display: grid; place-items: center; cursor: pointer; padding: 0;
  box-shadow: 0 10px 34px rgba(0, 0, 0, .34);
  transition: background .25s var(--ease), transform .25s var(--ease);
}
.video-play svg { width: 30px; height: 30px; fill: currentColor; margin-left: 3px; }
.video-facade:hover .video-play { background: var(--btn-hover); transform: scale(1.06); }
.video-len {
  position: absolute; right: .8rem; bottom: .75rem;
  background: rgba(0, 0, 0, .66); color: var(--paper);
  font-size: .78rem; padding: .22rem .5rem; border-radius: 5px;
}
.video-facade.is-playing .video-play,
.video-facade.is-playing .video-len,
.video-facade.is-playing img { display: none; }
/* Once it is playing the wrapper must stop claiming the pointer, or the
   default cursor sits over the browser's own scrub bar and looks broken. */
.video-facade.is-playing { cursor: default; }
.video-play:focus-visible { outline: 3px solid var(--accent-hi); outline-offset: 3px; }

/* ── Clinic imagery ──────────────────────────────────────────────────── */

/* Full-bleed bands. Both sit between two sections of different tone, so they
   read as a breath rather than as content that needs reading. */
.clinic-loop, .clinic-band { position: relative; overflow: hidden; background: var(--night); }

.clinic-loop-media, .clinic-band img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 1440 / 606;
  object-fit: cover;
}
/* A 2.37:1 band is only ~150px tall on a phone, which reads as an accident.
   Squarer crop below 700px so it still has presence. */
@media (max-width: 700px) {
  .clinic-loop-media, .clinic-band img { aspect-ratio: 5 / 4; }
}

/* ── WhatsApp widget ─────────────────────────────────────────────────── */

/* Inset and size of the floating WhatsApp button. */
:root { --fab-inset: clamp(1rem, 3vw, 1.5rem); --fab-size: 56px; }

.wa {
  position: fixed; right: var(--fab-inset); bottom: var(--fab-inset); z-index: 85;
  display: flex; flex-direction: column; align-items: flex-end; gap: .75rem;
}

/* Without JS the button cannot toggle anything, so the card is simply left
   open and the dead control is hidden. Same .js gating as .reveal. */
.wa-fab { display: none; }
.js .wa-fab {
  display: grid; place-items: center;
  width: var(--fab-size); height: var(--fab-size);
  border: 0; border-radius: 50%;
  background: var(--btn); color: var(--btn-ink);
  cursor: pointer; padding: 0;
  box-shadow: 0 8px 28px rgba(26, 25, 24, .22);
  transition: background .25s var(--ease), transform .25s var(--ease);
}
.js .wa-fab:hover { background: var(--btn-hover); }
.js .wa-fab:active { transform: scale(.94); }
.wa-fab svg { width: 27px; height: 27px; fill: currentColor; display: block; grid-area: 1 / 1; transition: opacity .2s var(--ease), transform .25s var(--ease); }
.wa-fab .wa-x { fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; width: 22px; height: 22px; opacity: 0; transform: rotate(-45deg); }
.wa-fab[aria-expanded="true"] .wa-mark { opacity: 0; transform: rotate(45deg); }
.wa-fab[aria-expanded="true"] .wa-x { opacity: 1; transform: rotate(0deg); }

.wa-card {
  width: min(310px, calc(100vw - 2 * var(--fab-inset)));
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.15rem 1.15rem 1rem;
  box-shadow: 0 14px 44px rgba(26, 25, 24, .16);
  transform-origin: bottom right;
}
.js .wa-card {
  opacity: 0; visibility: hidden; transform: translateY(8px) scale(.97);
  transition: opacity .24s var(--ease), transform .28s var(--ease), visibility .28s;
}
.js .wa-card.is-open { opacity: 1; visibility: visible; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .js .wa-card { transition: opacity .01s, visibility .01s; transform: none; }
  .wa-fab svg { transition: opacity .01s; }
}

.wa-card-h { font-size: 1.02rem; font-weight: 500; letter-spacing: -.015em; margin-bottom: .35rem; }
.wa-card-p { font-size: .89rem; line-height: 1.5; color: var(--grey); margin-bottom: .9rem; }

.wa-opt {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  padding: .7rem .9rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  font-size: .92rem; font-weight: 500; color: var(--ink);
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
.wa-opt + .wa-opt { margin-top: .5rem; }
.wa-opt:hover { border-color: var(--accent); background: var(--accent-tint); }
.wa-opt svg { flex: none; width: 15px; height: 15px; fill: none; stroke: var(--accent); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; transition: transform .25s var(--ease); }
.wa-opt:hover svg { transform: translateX(3px); }

/* --grey-2 reads 2.83:1 here and fails AA at this size. --grey clears 5.1:1;
   the smaller type is what separates this line from the body copy. */
.wa-hours { font-size: .8rem; color: var(--grey); margin-top: .8rem; }

/* ── Footer ──────────────────────────────────────────────────────────── */

.footer { background: var(--night); color: var(--night-text); padding-block: clamp(3.5rem, 7vw, 5.5rem) 2rem; }
.footer-top { display: grid; gap: clamp(2rem, 5vw, 3.5rem); }
/* Four children, so three columns wrapped the last one onto its own row:
   the Singapore block sat under the brand blurb, away from Bali. Both footer
   variants on this site have four children, so this fixes them together.
   Two up first, then all four once there is room, which keeps the two
   locations side by side rather than stacked apart. */
@media (min-width: 780px)  { .footer-top { grid-template-columns: 1.4fr 1fr; } }
@media (min-width: 1040px) { .footer-top { grid-template-columns: 1.5fr .95fr 1.15fr .8fr; } }
.footer h4, .footer .footer-h { font-size: .82rem; font-weight: 500; color: var(--night-grey); margin-bottom: 1.1rem; letter-spacing: 0; }
/* In-content link list. Separate from .footer-links on purpose: that one is
   coloured for the dark footer (--night-text), and reusing it inside a light
   card rendered near-white on near-white at 1.16:1. */
.link-list { list-style: none; display: grid; gap: .5rem; }
.link-list a {
  font-size: .95rem;
  color: var(--ink-2);
  transition: color .2s var(--ease);
}

/* The full-list cards read as two tiers now: the system note is context and
   steps back, the symptom lines are what a visitor actually scans for and
   step forward. Before this the note and the list rendered at the same
   weight and nothing grouped. */
.card .card-note {
  font-size: .9rem;
  color: var(--grey);
  margin-bottom: 1.15rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--line-2);
}
.card .link-list a { font-size: 1.02rem; font-weight: 550; color: var(--ink); }
.card .link-list a:hover { color: var(--accent-dk); }
.link-list a:hover { color: var(--accent-dk); }
.night .link-list a { color: var(--night-text); }
.night .link-list a:hover { color: var(--accent-hi); }

.footer-links { list-style: none; display: grid; gap: .7rem; }
.footer-links a { font-size: .95rem; color: var(--night-text); transition: color .2s var(--ease); }
.footer-links a:hover { color: var(--accent-hi); }
.footer-blurb { color: var(--night-grey); font-size: .96rem; max-width: 42ch; margin-top: 1rem; }

.footer-bottom {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: 1.75rem;
  border-top: 1px solid var(--night-line);
  display: flex; flex-wrap: wrap; gap: .75rem 1.5rem; align-items: center;
  font-size: .85rem; color: var(--night-grey);
}
.aphyx-credit { flex-basis: 100%; text-align: center; margin-top: .75rem; font-size: .8rem; color: var(--night-grey); }
.aphyx-credit a { color: var(--accent-hi); }

.disclaimer {
  margin-top: 1.5rem; font-size: .78rem; color: var(--night-grey);
  opacity: .78; max-width: 84ch; line-height: 1.6;
}

/* ── Utilities ───────────────────────────────────────────────────────── */

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.mt-1 { margin-top: 1rem; } .mt-2 { margin-top: 2rem; } .mt-3 { margin-top: 3rem; }
.hr { height: 1px; background: var(--line); border: 0; }
.night .hr { background: var(--night-line); }


/* ===== CARRIED FROM THE BALI BUILD =====
   The two clinics merged onto one group site on 19/08/2026 and the Sync's
   stylesheet became the base. The Bali build brought 35 pages the Sync
   never had (condition pillars, the journal, FAQ, contact, about) whose
   components are not in the Sync's system. Their rules are ported here
   unchanged wherever they could be, so the Sync's design decisions above
   stay the single source of truth and nothing below introduces a new hue:
   every colour resolves through the token bridge at the end of :root.

   Where a rule HAD to change, the reason is noted inline. The three
   recurring reasons are: the Sync's scroll reveal uses .is-in where the
   Bali build used .in, the Sync's accordion is driven by aria-expanded
   and data-open where the Bali build used an .open class, and the Sync's
   mobile drawer is .nav-links.is-open at 899px where the Bali drawer was
   .nav.open at 920px.

   Deliberately NOT carried: every .ms-* rule, .methods-scene, and the
   D3-METHODS-NIGHT and D3-STAGE-HOP blocks (the pinned methods scene the
   client called "too much"; the Sync's own journey pattern replaces it),
   plus anything the Sync already defines, which keeps its version. */

:root {
  /* The Sync's .wrap holds its gutter as padding inside a capped box,
     where the Bali .wrap subtracted it from the width. Components that
     bleed out of the wrap need that number, so it is named once here
     rather than repeated. Layout only, no colour. */
  --wrap-pad: clamp(1.25rem, 5vw, 2.5rem);
}

/* ── Reset gap ─ every carried page ───────────────────────────────────
   The Bali reset carried `img { height: auto }` and the Sync's does not.
   Every image on the carried pages ships width and height attributes for
   layout stability, and those attributes are presentational hints: with a
   CSS width but no CSS height they win, the intrinsic ratio is ignored,
   and aspect-ratio never gets a say. That rendered the about page's photo
   pair 297px wide by 900px tall, and did the same to the method figures,
   the journal cards and the fold-out thumbnails. At (0,0,1) this loses to
   every explicit height in the Sync's system above, so it only neutralises
   the attribute. */
img { height: auto; }

/* ── Type and rule marginalia ─ every carried page ─────────────────────
   The Bali pages label their sections with .kicker where the Sync uses
   .caption or .tag, and close their heroes on .rule-strip, the ruler
   motif that carries proof readings. */
.kicker {
  font-size: .74rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--grey);
}
.kicker .no { color: var(--accent-dk); margin-right: .55rem; }
.night .kicker { color: var(--night-grey); }
.night .kicker .no { color: var(--night-accent); }
.head .kicker { display: block; margin-bottom: 1.1rem; }

.rule-strip {
  border-top: 1px solid var(--line);
  background-image: var(--ticks);
  background-size: 100% 12px;
  background-repeat: no-repeat;
  padding-top: 1.3rem;
  display: flex; flex-wrap: wrap; gap: .5rem 3rem;
}
.rule-strip span {
  font-size: .78rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--grey);
}
.rule-strip strong { color: var(--ink); font-weight: 700; }
.night .rule-strip { border-color: var(--night-line); }
.night .rule-strip span { color: var(--night-grey); }
.night .rule-strip strong { color: var(--paper); }

/* Hero lead wraps between sentences only, never mid-sentence. */
.lead-seg { display: inline-block; }

/* Narrow phones: a long CTA label wraps instead of pushing the page wide. */
@media (max-width: 340px) {
  .btn { white-space: normal; text-align: center; width: 100%; }
}

/* ── Brand marks ───────────────────────────────────────────────────────
   One mark across the group site, the Sync's single-line wordmark, in
   the nav and reversed by filter in the footer. There is no separate
   footer asset and no second logo file to keep in step.

   Removed here on 20/08/2026: a .nav .brand-logo height override of 34
   and 38px. It was correct while the pages carried the Bali THREE-line
   stagger, which needs the extra height or its middle line reduces to a
   smudge. Applied to the Sync's 7:1 single-line mark it stretched the
   wordmark to 272px wide against a declared 143px, which is what the
   client spotted. The Sync's own .brand-logo sizing now stands.

   The Bali mark is not deleted. It belongs on the Bali location page as
   the local clinic's identity, not in the group header. */

/* ── Page hero ─ conditions, our-methods, about, connect, blog, pillars,
   journal posts ─────────────────────────────────────────────────────── */
.page-hero { padding-block: clamp(3.5rem, 7vw, 6rem) clamp(2.5rem, 5vw, 4rem); }
.page-hero .kicker { display: block; margin-bottom: 1.2rem; }
.page-hero .lead { margin-top: 1.6rem; }
.page-hero .rule-strip { margin-top: clamp(2.2rem, 5vw, 3.5rem); }
/* Connect: the hero is pared to kicker + title, so pull its exit padding
   in and let the title hand over to the contact grid without a hole. */
.page-hero-slim { padding-bottom: clamp(1.2rem, 2.5vw, 2rem); }

/* ── Prose and callout ─ condition pillars, journal posts, about ─────── */
.prose { max-width: 62ch; }
.prose h2 {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.5rem, 2.9vw, 2rem); letter-spacing: -.022em; line-height: 1.1;
  margin: 2.6rem 0 .9rem;
}
.prose h3 { font-weight: 640; font-size: 1.12rem; margin: 2rem 0 .6rem; }
.prose p + p { margin-top: 1rem; }
.prose ul { margin: 1rem 0 1rem 1.1rem; list-style: none; }
.prose ul li { position: relative; padding-left: 1.1rem; margin-top: .45rem; }
.prose ul li::before {
  content: ""; position: absolute; left: 0; top: .62em;
  width: .45rem; height: 1px; background: var(--accent-dk);
}
.prose a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--accent-line); }
.prose a:hover { text-decoration-color: var(--accent); }
.callout {
  border-left: 1px solid var(--accent-line);
  background: var(--accent-tint);
  padding: 1.1rem 1.3rem;
  margin: 1.6rem 0;
  font-size: .98rem; color: var(--ink-2);
  max-width: 62ch;
}

/* ── Aside links ─ condition pillars and journal posts ───────────────── */
.aside-h {
  font-size: .78rem; font-weight: 620; letter-spacing: .12em;
  text-transform: uppercase; color: var(--grey);
  margin: 2.2rem 0 .8rem;
}
.aside-links li + li { margin-top: .55rem; }
.aside-links a {
  color: var(--ink); font-size: .97rem;
  border-bottom: 1px solid var(--line);
  transition: border-color .3s var(--ease), color .3s var(--ease);
}
.aside-links a:hover { color: var(--accent-dk); border-color: var(--accent); }

/* The ledger rows in those asides sit on paper, not on the night surface
   the Sync's .info-row was written for. Only the link treatment and the
   long-value wrapping are restated; the row itself keeps the Sync's. */
.info-row dd { overflow-wrap: anywhere; }
.on-paper .info-row dd a { border-bottom: 1px solid var(--line); }
.on-paper .info-row dd a:hover { border-color: var(--accent); }

/* ── Full-bleed image band ─ our-methods, about ──────────────────────── */
.band { position: relative; overflow: clip; }
.band img {
  width: 100%; height: clamp(340px, 55vw, 560px); object-fit: cover;
  filter: saturate(.9);
}
.band-caption {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: linear-gradient(to top, rgba(30, 28, 24, .78), transparent);
  padding: clamp(3rem, 7vw, 5rem) 0 clamp(1.4rem, 3vw, 2.2rem);
}
.band-caption .wrap { color: var(--paper); }
.band-caption p {
  font-family: var(--display); font-weight: 560;
  font-size: clamp(1.35rem, 3vw, 2.1rem); letter-spacing: -.02em;
  max-width: 26ch; line-height: 1.12;
}
/* About: window parallax band. js/parallax.js adds .parallax-on only when
   the effect will actually run (JS present, no reduced-motion preference,
   viewport at least 760px), and that class is what all of this keys on, so
   every fallback path renders the band exactly as the shared .band rules
   paint it. The section takes the band's normal height as its own; the
   photograph becomes a 135% window-filling layer hung 17.5% above the
   frame, and the script translates it +/-17.5% of the band height, so no
   edge can show at either extreme. Transform only; the shared .band
   overflow clip does the windowing. */
.band-parallax.parallax-on { height: clamp(340px, 55vw, 560px); }
.band-parallax.parallax-on img {
  position: absolute;
  left: 0;
  top: -17.5%;
  width: 100%;
  height: 135%;
  will-change: transform;
}

/* ── Photo pair ─ about, condition pillars ───────────────────────────── */
/* Two frames side by side, used to break a long prose column. */
.photo-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: .75rem;
  margin: 2.2rem 0 .5rem;
}
.photo-pair img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border: 1px solid var(--line);
  filter: saturate(.9);
}
@media (max-width: 620px) { .photo-pair { grid-template-columns: 1fr; } }

/* ── Reading grid ─ about, condition pillars, journal posts ──────────── */
.methods-grid {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}
.methods-sticky { position: sticky; top: 6rem; }
.methods-sticky .lead { margin-top: 1.2rem; }
.methods-sticky .link-arrow { margin-top: 2rem; }
.methods-note { margin-top: 2rem; color: var(--grey); font-size: .97rem; max-width: 58ch; }
.methods-note a { color: var(--ink); }
@media (max-width: 920px) {
  .methods-grid { grid-template-columns: 1fr; }
  .methods-sticky { position: static; }
}

/* ── Method rows ─ our-methods ────────────────────────────────────────
   Copy on one side, photograph on the other, hairline between each, and
   the sides swap every row via .method-row-flip. The photograph bleeds
   into the page margin on its own side, which is the edge treatment
   DESIGN.md asks for. */
.method-row {
  display: grid;
  grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  padding-block: clamp(3rem, 7vw, 5.5rem);
  border-top: 1px solid var(--line);
}
.method-row:first-child { border-top: 0; padding-top: 0; }
.method-copy h2 {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.6rem, 3.1vw, 2.25rem);
  letter-spacing: -.022em; line-height: 1.1;
  margin-bottom: .9rem;
}
.method-copy p { color: var(--ink-2); max-width: 44ch; }
.method-copy p + p { margin-top: 1rem; }
.method-copy a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--accent-line); }
.method-copy a:hover { text-decoration-color: var(--accent); }

.method-figure { margin: 0; overflow: hidden; }
.method-figure img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  filter: saturate(.9);
  max-height: 40rem;
}

/* The two closing notes, side by side so the page settles before the band. */
.method-coda {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 4.5rem);
  padding-top: clamp(2.6rem, 5vw, 4.2rem);
  border-top: 1px solid var(--line);
}
.method-coda h2 {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.25rem, 2.2vw, 1.55rem);
  letter-spacing: -.02em; line-height: 1.12;
  margin-bottom: .7rem;
}
.method-coda p { color: var(--ink-2); font-size: .985rem; }
.method-coda a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--accent-line); }

/* 100vw counts the scrollbar and the wrap does not, so the bleed lands a
   few pixels past the true viewport edge. Clipping the section absorbs
   that overshoot, and the horizontal axis alone is clipped so nothing
   inside loses vertical overflow or sticky positioning. */
.section-tight:has(> .wrap > .method-row) { overflow-x: clip; }

@media (min-width: 861px) {
  .method-row {
    /* Distance from the wrap's content edge to the viewport edge. The Bali
       original read (100vw - var(--wrap)) / 2, which was right for a wrap
       that subtracted its gutter from the width. The Sync's wrap pads
       inside a capped box, so the gutter has to be added back or the
       photograph stops short of the edge on wide screens and overshoots
       it around 1100px. */
    --edge: calc(var(--wrap-pad) + max(0px, (100vw - var(--maxw)) / 2));
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    gap: clamp(2.5rem, 6vw, 5.5rem);
  }
  .method-row .method-figure {
    grid-column: 1; grid-row: 1;
    max-width: none;
    margin-left: calc(var(--edge) * -1);
  }
  .method-row .method-copy { grid-column: 2; grid-row: 1; }
  .method-row-flip { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .method-row-flip .method-figure {
    grid-column: 2;
    margin-left: 0;
    margin-right: calc(var(--edge) * -1);
  }
  .method-row-flip .method-copy { grid-column: 1; }
}
@media (max-width: 860px) {
  .method-row { grid-template-columns: 1fr; gap: 1.6rem; }
  .method-coda { grid-template-columns: 1fr; gap: 2rem; }
  .method-copy p { max-width: none; }
  /* Edge-to-edge photograph above the copy: one negative wrap padding per
     side reaches the screen edge exactly, with no horizontal scroll. */
  .method-row .method-figure { margin-inline: calc(var(--wrap-pad) * -1); }
}

/* Entrance: the photograph slides in from its own edge of the page and
   settles to scale inside its crop; the copy rises on the base reveal a
   beat later (reveal-d2). From-states are gated on .js so no-JS renders
   everything; the reduced-motion resets sit at the end of this block.
   Repointed from .in to the Sync's .is-in, which is what its main.js
   sets on a revealed element. */
.js .reveal.reveal-slide-l:not(.is-in) { transform: translateX(-36px); }
.js .reveal.reveal-slide-r:not(.is-in) { transform: translateX(36px); }
.js .reveal.reveal-slide-l.is-in,
.js .reveal.reveal-slide-r.is-in { transition-duration: .95s; }

.method-row .method-figure img { transition: transform 1.05s var(--ease); }
.js .method-row .method-figure:where(.reveal) img { transform: scale(1.055); }
.js .method-row .method-figure:where(.reveal.is-in) img { transform: none; }
@media (hover: hover) {
  .method-row .method-figure:hover img { transform: scale(1.02); }
}
@media (prefers-reduced-motion: reduce) {
  .js .reveal.reveal-slide-l:not(.is-in),
  .js .reveal.reveal-slide-r:not(.is-in),
  .js .method-row .method-figure:where(.reveal) img,
  .method-row .method-figure:hover img { transform: none; }
  .method-row .method-figure img { transition: none; }
}

/* ── The team ─ about ─────────────────────────────────────────────────
   Intro copy beside a portrait photo pair from 1100px (the FAQ grid's
   5/12 + 7/12 proportions), stacked below. .team-pair rides the shared
   .photo-pair frame rules and swaps only the ratio: the team photographs
   are 3:4 portraits, and the shared 4:3 crop would take the wall sign
   and the faces with it. */
.team-intro .kicker { display: block; margin-bottom: 1.2rem; }
.team-intro p { max-width: 52ch; color: var(--ink-2); }
.team-intro p:first-of-type { margin-top: 1.4rem; }
.team-intro p + p { margin-top: 1rem; }
.team-pair { margin: clamp(2.2rem, 5vw, 3rem) 0 0; }
.team-pair img { aspect-ratio: 3 / 4; }
@media (min-width: 1100px) {
  .team-grid {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: clamp(3rem, 6vw, 6rem);
    align-items: center;
  }
  .team-pair { margin: 0; }
}

/* ── Condition index rows ─ conditions ───────────────────────────────── */
.cond-list { border-top: 1px solid var(--line); }
.cond-row {
  display: grid; grid-template-columns: auto minmax(0, 5fr) minmax(0, 6fr) auto;
  gap: 1rem clamp(1.5rem, 4vw, 3.5rem); align-items: baseline;
  padding-block: clamp(1.5rem, 3vw, 2.1rem);
  border-bottom: 1px solid var(--line);
  transition: transform .35s var(--ease), background .35s var(--ease);
}
.cond-row:hover { transform: translateX(.75rem); background: var(--accent-tint); }
.cond-no {
  font-size: .78rem; font-weight: 600; letter-spacing: .1em;
  color: var(--grey);
  font-variant-numeric: tabular-nums;
}
.cond-row:hover .cond-no { color: var(--accent-dk); }
.cond-row h2 {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.45rem, 3vw, 2rem); letter-spacing: -.022em; line-height: 1.08;
}
.cond-row p { color: var(--grey); font-size: .99rem; max-width: 44ch; align-self: center; }
.cond-row svg { color: var(--faint); align-self: center; transition: transform .3s var(--ease), color .3s var(--ease); }
.cond-row:hover svg { transform: translateX(5px); color: var(--accent-dk); }
@media (max-width: 760px) {
  .cond-row { grid-template-columns: auto 1fr auto; }
  .cond-row p { grid-column: 2 / 3; grid-row: 2; }
}

/* ── Beyond the big four ─ conditions ─────────────────────────────────
   The secondary conditions continue the page's index (005+) as hairline
   rows. js/cond-hover.js sets one of two modes on .csec: .csec-hover
   (hover-capable fine pointers: rows stay open and the framed figure
   beside the list crossfades to the hovered or focused row's photo) or
   .csec-tap (touch: rows collapse and tapping folds out a small photo +
   one line, same 0fr recipe as the FAQ accordion). Without JS neither
   class lands: rows render open with the photo inline, figure hidden. */
.csec { --csec-col: 2.6rem; --csec-gap: clamp(1.2rem, 3vw, 2.2rem); }
.csec-list { border-top: 1px solid var(--line); }
.csec-item {
  border-bottom: 1px solid var(--line);
  transition: background .35s var(--ease), transform .35s var(--ease);
}
.csec-h { margin: 0; }
.csec-btn {
  width: 100%; text-align: left;
  display: grid; grid-template-columns: var(--csec-col) minmax(0, 1fr) auto;
  column-gap: var(--csec-gap); align-items: baseline;
  padding-block: 1.15rem;
}
.csec-no {
  font-size: .78rem; font-weight: 600; letter-spacing: .1em;
  color: var(--grey); font-variant-numeric: tabular-nums;
  transition: color .3s var(--ease);
}
.csec-t {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.2rem, 2.4vw, 1.55rem); letter-spacing: -.018em; line-height: 1.12;
}
.csec-ind {
  font-family: var(--display); font-weight: 400; font-size: 1.4rem;
  color: var(--accent-dk); line-height: 1;
  transition: transform .35s var(--ease);
}
.csec-item.open .csec-ind { transform: rotate(45deg); }
html:not(.js) .csec-ind { display: none; }
.csec-panel { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .4s var(--ease); }
.csec-panel > div { overflow: hidden; }
.csec-fold {
  display: flex; gap: 1.1rem; align-items: flex-start;
  margin-left: calc(var(--csec-col) + var(--csec-gap));
  padding-bottom: 1.35rem;
}
.csec-fold img {
  flex: none; width: clamp(6.5rem, 24vw, 8.5rem);
  aspect-ratio: 4 / 3; object-fit: cover;
  border: 1px solid var(--line); filter: saturate(.9);
}
.csec-fold p { color: var(--grey); font-size: .99rem; max-width: 44ch; }

/* Tap mode (touch): collapsed until opened. */
.csec-tap .csec-panel { grid-template-rows: 0fr; }
.csec-tap .csec-item.open .csec-panel { grid-template-rows: 1fr; }

/* Hover mode (fine pointers): two columns, rows always open, the inline
   photo gives way to the preview figure. */
.csec-hover {
  display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(2rem, 5vw, 4.5rem); align-items: start;
}
.csec-hover .csec-btn { cursor: default; padding-block: 1.2rem .3rem; }
.csec-hover .csec-ind { display: none; }
.csec-hover .csec-fold { padding-bottom: 1.25rem; }
.csec-hover .csec-fold img { display: none; }
.csec-hover .csec-item:hover { background: var(--accent-tint); transform: translateX(.5rem); }
.csec-hover .csec-item.is-cur .csec-no { color: var(--accent-dk); }
.csec-fig { display: none; margin: 0; }
.csec-hover .csec-fig { display: block; position: sticky; top: calc(var(--nav-h) + 1.5rem); }
.csec-fig-frame {
  position: relative; aspect-ratio: 3 / 2; overflow: hidden;
  border: 1px solid var(--line); background: var(--paper-2);
}
.csec-fig-frame img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; filter: saturate(.9);
  opacity: 0; transform: scale(1.03);
  transition: opacity .45s var(--ease), transform .9s var(--ease);
}
.csec-fig-frame img.is-on { opacity: 1; transform: none; }
.csec-fig-cap {
  margin-top: .7rem; min-height: 1.3em;
  font-size: .78rem; font-weight: 600; letter-spacing: .1em;
  color: var(--grey); font-variant-numeric: tabular-nums;
}
/* Large screens: the preview figure takes an even split of the wider wrap
   instead of the 5/12 share, so the photograph reads as a proper
   editorial panel rather than a thumb floating in whitespace. */
@media (min-width: 1500px) {
  .csec-hover { grid-template-columns: minmax(0, 6fr) minmax(0, 6fr); }
}
@media (max-width: 560px) {
  .csec-fold { margin-left: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .csec-item, .csec-ind, .csec-no, .csec-panel, .csec-fig-frame img { transition: none; }
  .csec-hover .csec-item:hover { transform: none; }
  .csec-fig-frame img { transform: none; }
}


/* ── Journal hub ─ blog ───────────────────────────────────────────────
   One featured latest entry + topic sections of 3:2 thumbnail cards.
   Same language as the cond-row list it replaces: hairline frames,
   square corners, tabular date caps, accent tint on hover. Cards
   translate the cond-row hover into a lift, an accent date and an
   underline that grows across the title. */
.jdate {
  font-size: .76rem; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--grey);
  transition: color .3s var(--ease);
}

/* Featured entry: image and text share one hairline frame; the image
   bleeds to the frame on its side, additive-style. */
.jfeat {
  display: grid;
  border: 1px solid var(--line);
  background: var(--card);
  transition: background .35s var(--ease), border-color .35s var(--ease);
}
.jfeat:hover { background: var(--accent-tint); border-color: var(--accent-line); }
.jfeat-media { position: relative; overflow: hidden; }
.jfeat-media img {
  width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  filter: saturate(.9);
  transition: filter .5s var(--ease);
}
.jfeat:hover .jfeat-media img { filter: saturate(1); }
.jfeat-body {
  padding: clamp(1.6rem, 4vw, 3.2rem);
  display: flex; flex-direction: column; align-items: flex-start;
}
.jfeat-meta { display: flex; align-items: center; gap: 1rem; }
.jchip {
  font-size: .68rem; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--accent-dk);
  border: 1px solid var(--accent-line);
  border-radius: 2px;
  padding: .24rem .6rem;
}
.jfeat-body h2 {
  font-family: var(--display); font-weight: 620;
  font-size: clamp(1.6rem, 3vw, 2.3rem);
  line-height: 1.1; letter-spacing: -.022em;
  max-width: 22ch;
  margin-top: 1.2rem;
}
.jfeat-p { color: var(--grey); max-width: 52ch; margin-top: .9rem; }
.jfeat .link-arrow { margin-top: 1.6rem; }
.jfeat:hover .link-arrow { color: var(--accent-dk); }
.jfeat:hover .link-arrow svg { transform: translateX(4px); }

@media (min-width: 900px) {
  .jfeat { grid-template-columns: minmax(0, 58fr) minmax(0, 42fr); }
  .jfeat-media { min-height: 430px; border-right: 1px solid var(--line); }
  .jfeat-media img { position: absolute; inset: 0; height: 100%; aspect-ratio: auto; }
  .jfeat-body { justify-content: center; }
}

/* Topic sections: small hairline head, kicker-register category name,
   tabular article count as the measurement marginalia. */
.jsec { padding-block: clamp(2.4rem, 5vw, 3.8rem); }
.jsec-last { padding-bottom: clamp(4.5rem, 9vw, 7rem); }
.jsec-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  border-bottom: 1px solid var(--line);
  padding-bottom: .85rem;
  margin-bottom: clamp(1.7rem, 3.5vw, 2.6rem);
}
.jsec-head h2 {
  font-size: .74rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink);
}
.jsec-count {
  font-size: .74rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--grey);
  white-space: nowrap;
}

/* Cards: 3 columns, 2 from 900, 1 below; the whole card is the link. */
.jgrid { display: grid; gap: 2.6rem 2rem; }
@media (min-width: 900px) { .jgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1200px) { .jgrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.jcard {
  display: block;
  transition: transform .35s var(--ease);
}
.jcard:hover { transform: translateY(-.35rem); }
.jcard-media {
  border: 1px solid var(--line);
  overflow: hidden;
  margin-bottom: 1rem;
  transition: border-color .35s var(--ease);
}
.jcard-media img {
  width: 100%; aspect-ratio: 3 / 2; object-fit: cover;
  filter: saturate(.9);
  transition: filter .5s var(--ease);
}
.jcard:hover .jcard-media { border-color: var(--accent-line); }
.jcard:hover .jcard-media img { filter: saturate(1); }
.jcard .jdate { display: block; margin-bottom: .55rem; }
.jcard:hover .jdate { color: var(--accent-dk); }
.jcard h3 {
  font-family: var(--display); font-weight: 620;
  font-size: 1.13rem; line-height: 1.24; letter-spacing: -.012em;
  color: var(--ink);
}
.jcard h3 span {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0 1px;
  background-position: 0 100%;
  background-repeat: no-repeat;
  transition: background-size .35s var(--ease);
}
.jcard:hover h3 span { background-size: 100% 1px; }
.jcard p {
  margin-top: .5rem;
  color: var(--grey);
  font-size: .95rem; line-height: 1.55;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .jdate, .jfeat, .jfeat-media img, .jcard, .jcard-media, .jcard-media img, .jcard h3 span { transition: none; }
  .jcard:hover { transform: none; }
}

/* ── Journal post ─ the 24 restored posts ─────────────────────────────
   "The composed article". Client feedback: post content ran as a narrow
   line hugging the left of the page. Every post's <main> carries .jpost,
   and every post body shares one shape: .wrap > .methods-grid >
   (article.prose + aside clinic rail). From 1100px the grid runs three
   tracks: a flexible margin, the article on the prose measure sitting at
   the centre of the page, and a slim sticky rail in the right margin, so
   the spread is furnished edge to edge instead of leaning left. Below
   1100px article and rail stack on a centred measure. Scoped entirely
   under .jpost; nothing else on the site changes. */

/* Header: one display scale for every post title (the two post
   generations shipped .display and .h1 titles; this evens them out),
   and heroes without a labelled .rule-strip close on a drawn tick
   rule instead, so each article starts from the same ruler line. */
.jpost .page-hero h1 {
  font-family: var(--display);
  font-weight: 630;
  font-size: clamp(2.5rem, 5.2vw, 4.3rem);
  line-height: 1.04;
  letter-spacing: -.028em;
}
.jpost .page-hero .wrap:not(:has(.rule-strip))::after {
  content: "";
  display: block;
  height: 12px;
  margin-top: clamp(2.2rem, 5vw, 3.5rem);
  border-top: 1px solid var(--line);
  background-image: var(--ticks);
  background-size: 100% 12px;
  background-repeat: no-repeat;
}

/* Stacked default (below 1100px): the article and the rail sit on a
   centred measure instead of hugging the wrap's left edge. */
.jpost .methods-grid { display: block; }
.jpost .methods-grid > .prose { margin-inline: auto; }
.jpost .methods-grid > aside {
  max-width: 62ch;
  margin: clamp(2.6rem, 6vw, 4rem) auto 0;
}

/* Articles that open on a paragraph read it as a lede. */
.jpost .prose > p:first-child {
  font-size: clamp(1.06rem, 1.5vw, 1.2rem);
  line-height: 1.66;
  color: var(--ink-2);
}

@media (min-width: 1100px) {
  .jpost .methods-grid {
    display: grid;
    grid-template-columns: minmax(2rem, 1fr) minmax(0, 62ch) minmax(16rem, 1fr);
    column-gap: clamp(2.5rem, 4.5vw, 4.5rem);
    align-items: start;
  }
  .jpost .methods-grid > .prose { grid-column: 2; }
  /* Stretched so the sticky rail has travel room down the article,
     the same lesson the about page learned. */
  .jpost .methods-grid > aside {
    grid-column: 3;
    align-self: stretch;
    max-width: none;
    margin: 0;
  }
  .jpost .methods-sticky { top: calc(var(--nav-h) + 1.6rem); }
  /* Narrow rail: ledger rows stack label over value, and the booking
     CTA spans the rail's width. The Bali original set grid columns
     here; the Sync's .info-row is a flex row, so the same stacking is
     asked for in flex terms. */
  .jpost .methods-sticky .info-row { flex-direction: column; gap: .1rem; }
  .jpost .methods-sticky .btn { width: 100%; }
  /* In-article photographs break the measure by half a tick each
     side for rhythm; 3rem stays inside the column gap at every
     width, so the image never reaches the rail. */
  .jpost .prose > img {
    width: calc(100% + 6rem);
    max-width: none;
    margin-inline: -3rem;
  }
}

/* ── Reviews ─ the homepage ───────────────────────────────────────────
   Quoted reviews. Verbatim from the clinic's Google listing, the same
   excerpts the clinic already publishes itself. Reviewers with a real
   Google profile photo carry their avatar (self-hosted webp); reviewers
   without one carry a neutral initial mark instead, so the caption rows
   stay level across the three columns. See PRODUCT.md. */
.review-grid {
  display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 6vw, 5rem); align-items: center;
}
.review-num {
  font-family: var(--display);
  font-weight: 660;
  font-size: clamp(5rem, 12vw, 8.5rem);
  line-height: .95;
  letter-spacing: -.045em;
  margin-bottom: 1rem;
}
@media (max-width: 760px) { .review-grid { grid-template-columns: 1fr; } }

/* Review score meta: rule-strip typography without the tick pattern,
   per client feedback (too many ruler motifs in one section). */
.review-score-meta { display: flex; flex-direction: column; gap: .4rem; margin-top: 1.1rem; }
.review-score-meta span {
  font-size: .78rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--grey);
}
.review-score-meta strong { color: var(--ink); font-weight: 700; }

.svg-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

.review-quotes {
  margin-top: clamp(2.8rem, 6vw, 4.4rem);
  border-top: 1px solid var(--line);
  background-image: var(--ticks);
  background-size: 100% 12px;
  background-repeat: no-repeat;
  padding-top: clamp(1.8rem, 4vw, 2.6rem);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.rq {
  display: flex; flex-direction: column;
  padding: 0 clamp(1.3rem, 3vw, 2.4rem);
  margin: 0;
}
.rq:first-child { padding-left: 0; }
.rq:last-child { padding-right: 0; }
.rq + .rq { border-left: 1px solid var(--line-soft); }

.rq-stars { display: flex; gap: 3px; color: var(--accent-dk); margin-bottom: 1.05rem; }
.rq-star { width: 14px; height: 14px; fill: currentColor; flex: none; }

.rq-text {
  margin: 0;
  max-width: 44ch;
  font-size: .985rem;
  line-height: 1.63;
  color: var(--ink-2);
}
.rq-by { display: flex; align-items: center; gap: .8rem; margin-top: auto; padding-top: 1.6rem; }
/* The no-photo caption reserves the avatar row height so reviewer names
   sit level across the three columns. */
.rq-by-noimg { min-height: 2.9rem; }
.rq-avatar {
  flex: none;
  width: 2.9rem; height: 2.9rem;
  border-radius: 50%;
  object-fit: cover;
  filter: saturate(.9);
  background: var(--paper-2);
  border: 1px solid var(--line-soft);
}
/* Google-style initial mark for reviewers without a usable profile
   photo: same footprint as the photo avatars, accent-tinted, one
   letter, so the caption row reads even across the three columns. */
.rq-avatar-txt {
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-dk);
  font-weight: 640;
  font-size: 1.12rem;
  letter-spacing: .02em;
  user-select: none;
}
.rq-name { display: block; font-weight: 620; font-size: .93rem; letter-spacing: -.005em; }
.rq-meta {
  display: block; margin-top: .16rem;
  font-size: .7rem; letter-spacing: .11em; text-transform: uppercase;
  /* --grey, not --faint: at this size AA needs 4.5:1, and --faint on
     paper is 2.83. --faint stays for icons and hairlines only. */
  color: var(--grey);
}

/* Between 901 and 1099px the quotes drop a touch in size and gutter so
   three columns still breathe. */
@media (min-width: 901px) and (max-width: 1099px) {
  .rq { padding: 0 clamp(1rem, 2vw, 1.5rem); }
  .rq-text { font-size: .94rem; }
}
@media (max-width: 900px) {
  .review-quotes { grid-template-columns: 1fr; }
  .rq { padding: 1.9rem 0; }
  .rq:first-child { padding-top: 0; }
  .rq:last-child { padding-bottom: 0; }
  .rq + .rq { border-left: 0; border-top: 1px solid var(--line-soft); }
}

/* ── FAQ ─ faq ────────────────────────────────────────────────────────
   Intro and accordion share one section. Stacked below 1100px it
   reproduces the old two-section rhythm (same paddings, same narrow
   accordion measure); from 1100px it becomes a deliberate 5/12 + 7/12
   grid, the intro keeping the reader company via sticky while the
   accordion starts level with it at the top of the section.
   The accordion ITSELF is the Sync's: .acc-item, .acc-btn and
   .acc-panel all come from its system above, driven by aria-expanded
   and data-open. Only the two pieces the Sync has no equivalent for are
   carried: the h2 wrapper the Bali markup puts around each button, and
   the "+" indicator it uses where the Sync draws an .acc-icon cross. */
.acc-item .acc-h {
  margin: 0; font-family: var(--sans); font-size: 1rem;
  font-weight: 400; letter-spacing: 0; line-height: inherit;
}
.acc-btn .acc-ind {
  flex: none; font-family: var(--display); font-weight: 400; font-size: 1.4rem;
  color: var(--accent-dk); line-height: 1;
  transition: transform .35s var(--ease);
}
/* Keyed off aria-expanded, which is the state the Sync's main.js sets;
   the Bali build keyed off an .open class its own script added. */
.acc-btn[aria-expanded="true"] .acc-ind { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) { .acc-btn .acc-ind { transition: none; } }

.faq-layout { padding-block: clamp(3.5rem, 7vw, 6rem) clamp(3rem, 6vw, 5rem); }
.faq-intro .kicker { display: block; margin-bottom: 1.2rem; }
.faq-intro .lead { margin-top: 1.6rem; }
.faq-acc { width: min(var(--wrap-narrow), 100%); margin-inline: auto; margin-top: clamp(5.5rem, 11vw, 9rem); }

@media (min-width: 1100px) {
  .faq-grid {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: clamp(3rem, 6vw, 6rem);
    align-items: start;
  }
  .faq-intro { position: sticky; top: calc(var(--nav-h) + 1.5rem); }
  /* The full display size wraps badly in a 5/12 column; step it down so
     the title holds two to three calm lines beside the accordion. */
  .faq-intro .display { font-size: clamp(2.3rem, 3.4vw, 3.4rem); }
  /* Cap the accordion measure so ultrawide screens never stretch lines. */
  .faq-acc { width: auto; margin: 0; max-width: 44rem; }
}

/* ── Contact ─ connect ────────────────────────────────────────────────
   The details column and the form share a stretched two-column row, and
   the map leaves the left column for a full-wrap band below the row. The
   details column flexes to the form's height: its info rows split the
   spare height evenly, like ruled ledger lines, and the Book online /
   Get directions row closes the column flush with the form's last line,
   so neither column ends before the other. The shared .map-box base
   keeps its 16/10 ratio; only .map-box-connect changes size here. */
.map-box { border: 1px solid var(--line); aspect-ratio: 16 / 10; background: var(--paper-2); }
.map-box iframe { width: 100%; height: 100%; border: 0; filter: saturate(.85); }

.connect-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: stretch;
}
.connect-details { display: flex; flex-direction: column; }
.connect-details dl {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  margin-top: .9rem;
}
.connect-details .info-row { flex: 1 0 auto; align-content: center; }
.connect-details .btn-row { margin-top: 2rem; }
/* The empty live-region under the form otherwise adds a trailing margin
   that the stretched details column would mirror as a bottom offset. */
.connect-form-col .form-status:empty { margin-top: 0; }

.connect-map { margin-top: clamp(2.75rem, 6vw, 4.5rem); }
.map-box-connect { aspect-ratio: auto; height: clamp(380px, 42vh, 560px); }

@media (max-width: 899px) {
  .connect-grid { grid-template-columns: 1fr; }
  .connect-map { margin-top: 2.5rem; }
  .map-box-connect { height: auto; aspect-ratio: 16 / 10; }
}

/* ── Nav dropdown ─ every carried page ────────────────────────────────
   Primary-nav Resources dropdown (About / FAQ / Blog folded into one
   trigger so the journal is reachable from the header). Desktop: a
   small paper panel under the trigger. Opening on hover is pure CSS on
   pointer devices; js/navdrop.js owns the clicked state (.open on
   .nav-drop) plus Escape, outside-click and blur-out closing, and
   mirrors hover opens into aria-expanded. With scripting off,
   :focus-within takes over as the keyboard opener, so all three links
   stay reachable. Inside the mobile drawer the trigger hides and the
   group renders always expanded under a small label; the hamburger in
   main.js is untouched. .is-current on .nav-drop (set per page in the
   HTML when the active page lives inside the panel) gives the trigger
   the same active treatment as aria-current links, so orientation
   survives the folding.
   Two changes from the Bali original: the drawer breakpoint moves from
   920px to the Sync's 899px so the two never disagree about which nav
   is on screen, and the drawer-open selector becomes
   .nav-links.is-open, which is what the Sync's main.js sets. */

.nav-drop { position: relative; display: flex; align-items: center; }
.nav-drop-btn {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .92rem; color: var(--ink-2); letter-spacing: -.01em;
  padding-block: .2rem;
  transition: color .2s var(--ease);
}
.nav-drop.open .nav-drop-btn,
.nav-drop.is-current .nav-drop-btn,
html:not(.js) .nav-drop:focus-within .nav-drop-btn { color: var(--ink); font-weight: 500; }
.nav-drop-caret { flex: none; transition: transform .3s var(--ease); }
.nav-drop.open .nav-drop-caret,
html:not(.js) .nav-drop:focus-within .nav-drop-caret { transform: rotate(180deg); }

/* Invisible strip over the gap between trigger and panel so a hover
   open survives the pointer crossing down to the panel. */
.nav-drop::after { content: ""; position: absolute; left: 0; right: 0; top: 100%; height: .8rem; }

.nav-drop-panel {
  position: absolute; top: calc(100% + .75rem); left: 50%;
  min-width: 10.5rem;
  /* The Bali build cleared markers globally with ul[class]; the Sync's
     reset clears them per component and never had a list in its nav, so
     this one arrived carrying browser bullets and the indent that comes
     with them. Cleared here rather than by restoring a global rule,
     since this was the only classed list on the site still showing
     them, checked across all nine pages. */
  list-style: none;
  margin: 0;
  padding-inline: 0;
  background: var(--card);
  border: 1px solid var(--line);
  /* 2px, as the Bali build had it. Every carried surface (the journal
     frames, the body-map callout, the map, the fold-out figure) is
     square-cornered, so rounding this one to match the Sync's chrome
     would only make it the odd one out. Whether the carried set adopts
     the Sync's radii wholesale is a design call, not a port call. */
  border-radius: 2px;
  box-shadow: 0 14px 34px rgba(32, 30, 26, .1);
  padding-block: .45rem;
  transform: translateX(-50%) translateY(-6px);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: transform .3s var(--ease), opacity .3s var(--ease), visibility 0s .3s;
}
.nav-drop.open .nav-drop-panel,
html:not(.js) .nav-drop:focus-within .nav-drop-panel {
  transform: translateX(-50%);
  opacity: 1; visibility: visible; pointer-events: auto;
  transition: transform .3s var(--ease), opacity .3s var(--ease);
}
@media (hover: hover) {
  .nav-drop:hover .nav-drop-btn { color: var(--ink); }
  .nav-drop:hover .nav-drop-caret { transform: rotate(180deg); }
  .nav-drop:hover .nav-drop-panel {
    transform: translateX(-50%);
    opacity: 1; visibility: visible; pointer-events: auto;
    transition: transform .3s var(--ease), opacity .3s var(--ease);
  }
}
.nav-drop-panel a { display: block; padding: .52rem 1.05rem; }
.nav-drop-panel a:hover { background: var(--accent-tint); }
.nav-drop-panel a[aria-current="page"] {
  color: var(--ink);
  border-left: 2px solid var(--accent);
  padding-left: calc(1.05rem - 2px);
}

@media (max-width: 899px) {
  /* Inside the drawer: no disclosure, an always-open labelled group. */
  .nav-drop { width: 100%; display: block; }
  .nav-drop::after { content: none; }
  .nav-drop-btn { display: none; }
  .nav-drop-panel {
    position: static; min-width: 0; transform: none;
    opacity: 1; visibility: visible;
    background: none; border: 0; border-radius: 0; box-shadow: none;
    padding-block: 0; transition: none;
  }
  /* The drawer hides each trigger button and re-states its name as a group
     heading here. That heading was hardcoded to "Resources" and written
     before Locations existed, so once Locations was added BOTH groups
     announced themselves as Resources. It is per-dropdown now.
     A THIRD dropdown needs its own content rule or it inherits Resources. */
  .nav-drop-panel::before {
    content: "Resources";
    display: block; padding-top: .95rem; padding-bottom: .3rem;
    font-size: .72rem; font-weight: 600;
    letter-spacing: .14em; text-transform: uppercase;
    color: var(--grey);
  }
  .nav-drop-loc .nav-drop-panel::before { content: "Locations"; }
  .nav-drop-panel a { padding: .75rem 0 .75rem .85rem; font-size: 1.15rem; }
  .nav-drop-panel a[aria-current="page"] {
    border-left: 2px solid var(--accent);
    padding-left: calc(.85rem - 2px);
  }
  /* In the drawer the panel is hit-testable only while the drawer is
     open. The desktop open and hover rules must be overruled here, or
     About, FAQ and Blog stay tappable behind a shut, invisible drawer
     and steal taps from whatever sits under the sticky header. */
  .nav-drop:hover .nav-drop-panel,
  .nav-drop.open .nav-drop-panel { pointer-events: none; }
  .nav-links.is-open .nav-drop-panel { pointer-events: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .nav-drop-btn, .nav-drop-caret, .nav-drop-panel { transition: none; }
}

/* ── Cinematic hero ─ the homepage ────────────────────────────────────
   Split hero: copy on the night surface at left, photograph holding the
   right half and running to the edge of the viewport. The client asked
   for the Bali photograph as the hero, so this stays. */
.hero-cine {
  position: relative; overflow: clip;
  display: grid;
  grid-template-columns: minmax(0, 1.04fr) minmax(0, 1fr);
  align-items: stretch;
  /* One screen exactly: the hero ends where the fold does, so the CTAs and
     the proof strip are on screen without scrolling. */
  min-height: calc(100svh - var(--nav-h));
}
.hero-cine-media { position: relative; grid-column: 2; grid-row: 1; align-self: stretch; overflow: clip; }
.hero-cine-media img {
  /* Absolute so the photograph's own aspect cannot set the row height and
     push the hero past one screen. The column decides, the image fills. */
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center 42%;
  filter: saturate(.88);
  transform-origin: center 42%;
  animation: heroDrift 30s var(--ease) infinite alternate;
}
/* The clip sits over the still. The still stays as poster and as the
   reduced-motion and no-video fallback, so something correct is always
   painted even before the video is fetched. */
.hero-cine-media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center 42%;
  filter: saturate(.88);
  z-index: 1;
  pointer-events: none;
}

/* A slow push toward the neck. Long enough that it is felt rather than
   watched, and it never lets the hero sit completely still. */
@keyframes heroDrift {
  from { transform: scale(1); }
  to   { transform: scale(1.05); }
}

/* Two scrims: left panel for the copy, floor for the strip.
   The blend must sit ABOVE the video, or the clip paints over it and
   the column edge shows as a hard vertical seam. Starts at solid night so
   it meets the left panel exactly, then runs a long way into the frame so
   the join is a gradient rather than a line. */
.hero-cine-media::after {
  content: ""; position: absolute; inset: 0;
  z-index: 2;
  background:
    linear-gradient(to right,
      var(--night) 0%,
      rgba(30, 28, 24, .94) 9%,
      rgba(30, 28, 24, .74) 20%,
      rgba(30, 28, 24, .46) 34%,
      rgba(30, 28, 24, .22) 48%,
      rgba(30, 28, 24, .07) 62%,
      transparent 76%),
    linear-gradient(to top, rgba(30, 28, 24, .55) 0%, transparent 34%);
}
.hero-cine-inner {
  position: relative;
  grid-column: 1; grid-row: 1;
  z-index: 3;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding-inline: max(var(--wrap-pad), (100vw - var(--wrap)) / 2) clamp(1.8rem, 4vw, 3.5rem);
  padding-block: clamp(1.8rem, 5vh, 4.5rem) clamp(1.2rem, 3vh, 2.4rem);
}
.hero-kicker { margin-bottom: clamp(.7rem, 1.8vh, 1.5rem); display: block; }
.hero-cine .kicker { color: rgba(247, 244, 237, .92); }
.hero-title { position: relative; }
/* The wordmark stagger: each line steps right. */
.hero-title .l2 { display: block; padding-left: clamp(1.5rem, 5vw, 4.5rem); }
.hero-title .l3 { display: block; padding-left: clamp(3rem, 10vw, 9rem); }
.hero-title .accent-word { color: var(--night-accent); }
.hero-cine .lead { margin-top: clamp(.9rem, 2.2vh, 1.8rem); max-width: 56ch; }
.hero-cine .btn-row { margin-top: clamp(1.1rem, 2.6vh, 2.2rem); }
/* The measurement trace: per-segment readings drawing across the hero. */
.hero-trace { display: block; width: 100%; height: clamp(26px, 5vh, 56px); margin-top: clamp(1rem, 3vh, 3.2rem); opacity: .75; }
.hero-trace path {
  stroke: var(--night-accent); stroke-width: 1.5; fill: none;
  stroke-dasharray: 2600; stroke-dashoffset: 2600;
  animation: heroTrace 2.6s var(--ease) .5s forwards;
}
@keyframes heroTrace { to { stroke-dashoffset: 0; } }
.hero-rule { position: relative; z-index: 2; }

/* The trace as a live reading. After the one-time draw-on the base line
   settles dimmer, and a bright rounded segment sweeps the whole path on
   a loop, monitor style, carrying the light. */
.hero-trace .trace-base {
  animation: heroTrace 2.6s var(--ease) .5s forwards,
             traceSettle 1.2s var(--ease) 3.4s forwards;
}
@keyframes traceSettle { to { opacity: .5; } }
.hero-trace .trace-run {
  stroke: var(--night-accent);
  stroke-width: 2.25;
  stroke-linecap: round;
  stroke-dasharray: 130 2470;
  stroke-dashoffset: 2600;
  opacity: 0;
  filter: drop-shadow(0 0 5px rgba(158, 208, 184, .6));
  animation: traceRunFade .5s linear 3.4s forwards,
             traceRun 6s linear 3.4s infinite;
}
@keyframes traceRunFade { to { opacity: 1; } }
@keyframes traceRun {
  from { stroke-dashoffset: 2600; }
  to   { stroke-dashoffset: 0; }
}

/* Cursor glow: the warm red of the pain marker in the photograph, trailing
   the pointer across the hero. js/heroglow.js creates and drives the node;
   it moves on transform only, fades on opacity only, and never takes
   pointer events. Sits above the media and scrims (z2, painted after them)
   and below the copy (z3). */
.hero-glow {
  display: none;
  position: absolute;
  left: 0; top: 0;
  width: 420px; height: 420px;
  margin: -210px 0 0 -210px;
  background: radial-gradient(circle closest-side,
    rgba(255, 86, 56, .25) 0%,
    rgba(255, 112, 74, .10) 45%,
    rgba(255, 86, 56, 0) 100%);
  mix-blend-mode: screen;
  pointer-events: none;
  opacity: 0;
  transition: opacity .7s var(--ease);
  will-change: transform, opacity;
  z-index: 2;
}
.hero-glow.on { opacity: 1; }
/* Mouse-class devices only; reduced motion removes it outright, and the
   later block wins when both match. */
@media (hover: hover) and (pointer: fine) {
  .hero-glow { display: block; }
}

/* Tall screens: bottom-packed copy left the upper half empty and the block
   read as floating. Auto margins split the leftover space evenly around the
   core, which lands its centre a touch above the hero's midpoint while the
   footing keeps the floor. Below 850px of viewport the old bottom-anchored
   packing already fills the panel, so nothing changes there. */
@media (min-width: 921px) and (min-height: 850px) {
  .hero-core { margin-block: auto; }
}

@media (max-width: 920px) {
  /* Stacked: the photograph sits behind the copy again, since a split
     leaves neither side enough room on a phone. */
  .hero-cine { grid-template-columns: 1fr; min-height: calc(100svh - var(--nav-h)); }
  .hero-cine-media { position: absolute; inset: 0; grid-column: 1; }
  .hero-cine-inner {
    grid-column: 1; grid-row: 1;
    padding-inline: var(--wrap-pad);
    width: min(var(--wrap), 100%); margin-inline: auto;
  }
  .hero-title .l2 { padding-left: clamp(1rem, 6vw, 2.5rem); }
  .hero-title .l3 { padding-left: clamp(2rem, 12vw, 5rem); }
  .hero-trace { height: 40px; margin-top: 1.8rem; }
  .hero-rule .rule-strip { gap: .5rem 2rem; }
}

/* == D3-HERO-MOBILE ==
   Phone hero, v2, per client direction: copy panel on solid night up
   top with the title flush left and the CTAs mid-panel, the photograph
   anchoring the bottom of the hero, and the proof strip relocated to
   its own paper band under the section (.proof-band in the markup; the
   in-hero strip hides at this size). The trace rides just above the
   photograph on phones tall enough to hold it. Desktop keeps the split
   layout, the wordmark stagger and the in-panel strip untouched. */
@media (max-width: 920px) {
  /* v3: the photograph runs full-bleed behind the whole hero again
     (the base mobile rule absolutes it), under one long gradual veil:
     a hint of the image reads even behind the title, and it emerges
     clean toward the floor. Copy is set on a central axis, the CTAs
     land near the middle of the screen, and the trace rides low over
     the clearing photograph. */
  .hero-cine-media img,
  .hero-cine-media video { object-position: 58% 24%; }
  .hero-cine-media::after {
    background: linear-gradient(to bottom,
      rgba(30, 28, 24, .93) 0%,
      rgba(30, 28, 24, .86) 30%,
      rgba(30, 28, 24, .7) 48%,
      rgba(30, 28, 24, .48) 62%,
      rgba(30, 28, 24, .2) 76%,
      rgba(30, 28, 24, 0) 88%);
  }
  .hero-cine-inner { justify-content: flex-start; }
  /* The panel is measured in quarters: the statement sits around the
     first quarter of the hero, the CTAs around the third. The core
     takes the leftover height and the auto margin between lead and
     buttons opens the gap, so both land on their marks at any phone
     height without either block being pinned to an edge. */
  .hero-core {
    text-align: center;
    display: flex; flex-direction: column;
    flex: 1 1 auto;
    padding-top: clamp(.75rem, 5.6svh, 4rem);
  }
  .hero-title .l2,
  .hero-title .l3 { padding-left: 0; }
  /* The statement carries the panel, so the supporting line steps back
     from it rather than competing at nearly the same weight. */
  .hero-cine .lead {
    margin-inline: auto;
    font-size: clamp(.95rem, 3.75vw, 1.06rem);
  }
  .hero-cine .btn-row {
    justify-content: center;
    margin-top: auto;
    margin-bottom: clamp(2rem, 16svh, 9.5rem);
  }
  .hero-kicker { display: none; }
  .hero-cine .hero-rule { display: none; }
  .hero-trace { margin-top: auto; }
}
/* The trace yields where there is no room to read it. */
@media (max-width: 480px), (max-height: 700px) and (max-width: 920px) {
  .hero-trace { display: none; }
  .hero-rule { margin-top: clamp(1.2rem, 3.5vh, 1.8rem); }
}
@media (max-width: 480px) and (min-height: 701px) {
  .hero-trace { display: block; height: 30px; margin-top: auto; }
}
@media (max-width: 480px) {
  /* Sized so the longest line, "Chiropractic", still clears the
     gutters on a 360px phone. */
  .hero-title { font-size: clamp(2.9rem, 15vw, 3.9rem); }
  .hero-kicker { font-size: .7rem; letter-spacing: .12em; margin-bottom: .65rem; }
  .hero-cine .lead { margin-top: .8rem; }
  .hero-cine .btn-row { margin-top: 1.1rem; }
  .hero-cine-inner { padding-block: 1.4rem 1.1rem; }
  /* Two readings at most on the smallest screens: the language line goes. */
  .hero-rule .rule-strip span:nth-child(3) { display: none; }
}
/* Proof band: the hero strip as its own banner below the section. */
.proof-band { display: none; }
@media (max-width: 920px) {
  .proof-band { display: block; padding-block: 1.15rem 1.35rem; }
}

@media (prefers-reduced-motion: reduce) {
  .hero-trace path { animation: none; stroke-dashoffset: 0; }
  .hero-trace .trace-base { animation: none; opacity: 1; }
  .hero-trace .trace-run { animation: none; opacity: 0; }
  .hero-cine-media img { animation: none; transform: none; }
  .hero-cine-media video { display: none; }
  .hero-glow { display: none; }
}

/* ── Measurement band ─────────────────────────────────────────────────
   The Sync's oscillating trace, moved off the hero at the client's
   request. It is a measurement graphic, a baseline with tick marks and
   three waves at different amplitudes, so it belongs at the moment the
   page talks about measuring rather than behind a headline.
   ONLY the container is carried: .osc-*, .wave-* and the waveRun and
   ampBreathe keyframes are already defined in the Sync's hero
   oscillation block above, and that version stands. */
.measure-band {
  position: relative;
  height: clamp(120px, 17vw, 210px);
  overflow: hidden;
  pointer-events: none;
  background: var(--paper-2);
  border-block: 1px solid var(--line-soft);
  /* Fades in from the section above rather than starting on a hard edge. */
  mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 78%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 26%, #000 78%, transparent 100%);
}
.measure-band svg { width: 100%; height: 100%; display: block; }
/* On the night surface the trace lifts to the bright accent so it keeps
   its weight against the dark ground. */
.night .measure-band,
.measure-band.on-night {
  background: var(--night-2);
  border-color: var(--night-line);
}
.measure-band.on-night .osc-base,
.measure-band.on-night .osc-tick { stroke: var(--night-accent); }
.measure-band.on-night .wave-1 .osc-line { stroke: var(--night-accent); }
.measure-band.on-night .osc-line { stroke: var(--night-grey); }
/* ── Service availability, 19/08/2026 ──────────────────────────────
   The clinics do not run an identical service list: Singapore leads
   with spinal decompression and Bali does not offer it. On one shared
   site that is a booking problem, not a copy problem, because a Bali
   reader will otherwise arrive in Sanur expecting it.

   So a system is explained ONCE and carries a line saying where it can
   actually be had. Positive phrasing on purpose: "available at
   Singapore", never "not available in Bali", which reads as a clinic
   being diminished rather than a service being placed. */
.card-where {
  margin-top: .7rem;
  padding-top: .7rem;
  border-top: 1px solid var(--night-line);
  font-size: .82rem;
  line-height: 1.45;
  color: var(--night-grey);
}
.card-where::before {
  content: "";
  display: inline-block;
  width: .4rem; height: .4rem;
  margin-right: .5rem;
  border-radius: 50%;
  background: var(--accent-hi);
  vertical-align: .08em;
}
/* On paper surfaces the same mark reads in the darker tones. */
.section:not(.night) .card-where {
  border-top-color: var(--line);
  color: var(--grey);
}
.section:not(.night) .card-where::before { background: var(--accent); }
/* ===== /CARRIED ===== */

/* ===== D4-PAGE-BANNER ===== */
/* Light editorial banner for inner pages: type in ink on the paper
   surface, the photograph large in its own rounded block beside it,
   sitting on an offset accent-wash plate. This replaces the night
   split with the left-to-right veil, which the client reviewed on
   24/08 and rejected: the veil sat over the subject, and on a phone
   the picture "doesn't read very well". What he pointed back to was
   the old Bali hero, "that offset on the white and that color", so
   the photograph now sits clean on white, the mint wash steps out
   from behind it, and nothing is ever drawn over the image.

   THE IMAGE KEEPS ITS OWN COLUMN. Both sources are portrait. In a
   near-square column block, cover shows roughly 75 to 80 percent of
   the picture; stretched across the whole banner it showed 27 percent
   and read as an enormous zoom. On phones the block turns 4:5, which
   is close to the sources' own shape, so nearly the entire photograph
   is visible exactly where the client said it was hard to see.

   Art direction rides on two custom properties set inline on the
   media block, --banner-pos (desktop crop) and --banner-pos-m (phone
   crop), so each page states its own framing next to its own image.

   No size cap on the heading: .display governs, and min-height is a
   minimum, so a long title grows the banner rather than clipping.

   Names checked against every page and script before writing:
   banner-inner, --banner-pos, --banner-pos-m and --banner-ar-m exist
   nowhere else. Only /conditions/ and /bali/ carry this component. */
.page-banner {
  background: var(--paper);
  overflow: clip;
}
.banner-inner {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
  gap: clamp(2.5rem, 5vw, 4.5rem);
  align-items: center;
  min-height: min(66svh, 700px);
  padding-block: clamp(1.5rem, 4svh, 2.75rem) clamp(2rem, 5svh, 3.25rem);
  /* The copy starts at the site's content edge, so the title lines up
     with every other page. The media side is allowed to run PAST the
     wrap toward the viewport, which is what makes the block read as
     oversized against the grid. 100%, never 100vw: 100vw includes the
     scrollbar on Windows, and a right edge computed from it is a
     horizontal-overflow bug waiting to happen. */
  padding-left: max(var(--gutter), (100% - var(--wrap)) / 2);
  padding-right: max(var(--gutter), calc((100% - var(--wrap)) / 2 - clamp(0rem, 3vw, 3rem)));
}
.banner-copy .lead { max-width: 48ch; }

/* The media block. Stretch, with the row height held by the banner's
   min-height, so the photograph's own aspect can never push the
   banner past two thirds of a screen. */
.banner-media {
  position: relative;
  align-self: stretch;
  min-height: clamp(360px, 52svh, 560px);
}
/* The plate: the wash stepping down and out from behind the photo.
   Generated first, so the image paints over it without z-index. */
.banner-media::before {
  content: "";
  position: absolute; inset: 0;
  border-radius: var(--radius-lg);
  background: var(--accent-wash);
  transform: translate(clamp(.7rem, 1.3vw, 1.1rem), clamp(.7rem, 1.3vw, 1.1rem));
}
.banner-media img,
.banner-media video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--banner-pos, 50% 30%);
  border-radius: var(--radius-lg);
}
.banner-media video { z-index: 1; pointer-events: none; }

/* Tablets and landscape phones keep the split, on even columns so the
   photograph does not starve, because a stacked 4:5 block at 800px
   wide is a thousand pixels of image and the banner ran to 150 percent
   of a screen. Measured, not guessed. */
@media (max-width: 980px) and (min-width: 701px) {
  .banner-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(2rem, 4vw, 3rem);
  }
}

/* Phones: the block goes in flow under the copy at 4:5, full column
   width, no veil and no crop tricks. The heading stays first so the
   page still opens with its question, and the photograph follows as
   its own clean object. */
@media (max-width: 700px) {
  .banner-inner {
    grid-template-columns: 1fr;
    gap: 0;
    min-height: 0;
    padding-inline: var(--gutter);
    padding-block: clamp(2rem, 6vw, 3rem) clamp(2.25rem, 7vw, 3.5rem);
  }
  .banner-media {
    align-self: auto;
    min-height: 0;
    aspect-ratio: var(--banner-ar-m, 4 / 5);
    /* Inert on phones, where 4:5 of the column is well under it. It
       exists for the 500 to 700 band, foldables and small tablets,
       where 4:5 of a 630px column is 790px of image. */
    max-height: min(74svh, 600px);
    margin-top: clamp(1.6rem, 5vw, 2.4rem);
  }
  .banner-media::before { transform: translate(.55rem, .55rem); }
  .banner-media img,
  .banner-media video { object-position: var(--banner-pos-m, var(--banner-pos, 50% 30%)); }
  .banner-copy .lead { max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  .banner-media video { display: none; }
}
/* ===== /D4-PAGE-BANNER ===== */

/* ===== D5-VISIT ===== */
/* One unit, not two cards. The second card held "Getting here" and "Who
   comes here", which explained a drive the map already shows and described
   an audience nobody reading a clinic's address needs described to them.
   Both are gone, so the map gets the room instead, and the two actions sit
   on a single row rather than one per card.

   Named .visit rather than reusing .loc-card because that carries the grid,
   the hover lift and the map zoom, all card behaviours, and this is no
   longer a card. */
.visit {
  display: grid;
  gap: clamp(1.5rem, 3.4vw, 3rem);
  align-items: center;
}
@media (min-width: 900px) {
  .visit { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); }
}

/* Bigger and squarer than the old card thumbnail, which was 16/10 inside
   half a grid. Rounded and clipped so it reads as a considered object
   rather than a raw embed dropped on the page. */
.visit-map {
  aspect-ratio: 4 / 3;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line-2);
  background: var(--paper-3);
}
@media (min-width: 900px) { .visit-map { aspect-ratio: 16 / 11; } }
.visit-map iframe { width: 100%; height: 100%; border: 0; display: block; filter: saturate(.92); }
.night .visit-map { background: var(--night); border-color: var(--night-line); }

.visit-body .btn-row { margin-top: 1.75rem; }
/* ===== /D5-VISIT ===== */

/* ===== D6-SYS-CARD ===== */
/* The system cards on the two location pages. Each one now carries the same
   diagram that heads its section on /our-methods/, and the whole card is the
   link into it rather than the title alone.

   Explicit counts, not auto-fit. auto-fit fitted Singapore's four cards into
   four columns of about 200px, which broke "Precision Spinal Decompression"
   over three lines and squeezed every description into a narrow ribbon.
   Three suits Bali; four cards go two by two, the same shape the conditions
   page uses for its four programs. */
.sys-grid {
  display: grid;
  gap: clamp(1.25rem, 2.4vw, 1.75rem);
  grid-template-columns: 1fr;
}
@media (min-width: 700px)  { .sys-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .sys-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .sys-grid-4 { grid-template-columns: repeat(2, 1fr); } }

/* The diagram is sized by aspect ratio, so the two-up cards, being half again
   as wide as the three-up ones, grew it to 430px and 61 percent of the card.
   Capping the height instead holds it near the three-up size, around 280px
   and under half the card, so the drawing stays a header rather than the
   subject. Only above 1000px, where the four-card grid is two across; below
   that the cards are narrow again and the ratio behaves. */
@media (min-width: 1000px) {
  .sys-grid-4 .sys-visual { aspect-ratio: auto; height: clamp(240px, 19vw, 300px); }
}

/* The description takes the slack so every Good for block starts at the same
   height across a row, which keeps their rules on one line even when the
   descriptions run long. Pushing the block down with margin-top:auto aligned
   the card feet instead, and the rules still stepped when the symptom lists
   wrapped to different depths. */
.sys-card .loc-note { flex: 1; }

/* In the two-column band the longest system name, Precision Spinal
   Decompression, wraps to a second line while its neighbours keep one, and
   that difference pushed its Good for rule out of line with the rest of the
   row. Reserving two lines there costs a little space under the short names
   and keeps the row reading as a set. */
@media (min-width: 700px) and (max-width: 999px) {
  .sys-card .loc-body h3 { min-height: 2.3em; }
}

/* Night band behind the diagram. The drawings were authored against the
   night surface on /our-methods/ using --accent-hi, which is a bright teal
   that would wash out on paper, so rather than recolour them the card gives
   them the surface they were drawn for. */
.sys-visual {
  background: var(--night);
  aspect-ratio: 13 / 10;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 2.4vw, 1.6rem);
  color: var(--accent-hi);
}
.sys-visual svg { width: 100%; height: 100%; display: block; }

/* Whole card clickable, via a stretched pseudo-element on the real link
   rather than wrapping the card in an anchor. Keeps one link in the
   accessibility tree, keeps the href where a screen reader expects it, and
   still lets text inside the card be selected. */
.sys-card { position: relative; }
.sys-card .loc-body h3 a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.sys-card .loc-body h3 a { text-decoration: none; color: inherit; }
.sys-card:hover .loc-body h3 a { color: var(--accent-dk); }
/* The lift is the card's, so the focus ring has to be the card's too. */
.sys-card:focus-within { outline: 2px solid var(--accent); outline-offset: 3px; }
.sys-card .loc-body h3 a:focus-visible { outline: none; }

/* ===== G1-METHODS-MOTION ===== */
/* Ambient motion for the diagrams on /our-methods/, asked for by the
   client on 24/08 while he was over the three-platforms band: "can this
   move at all or be animated". His own brand thesis decides the how.
   The platforms are united by subtle, continuous micro movement, so the
   diagrams do not perform, they oscillate. Slow cycles, amplitudes of a
   few pixels, ease-in-out, nothing that competes with reading the copy
   beside them.

   Everything runs ambiently, like the hero waves, rather than waiting
   on a scroll trigger, and everything animates transform, opacity or
   the pulse's dash offset only, so none of it touches layout.

   The hooks exist only in our-methods/index.html on purpose. /bali/
   and /singapore/ carry copies of the same diagrams in their system
   cards, and a whole grid of breathing thumbnails is exactly the
   distraction this is trying not to be. Those copies stay still.

   Every animation assignment sits inside the no-preference block
   below, so under prefers-reduced-motion none of it is ever applied.
   The card's travelling pulse shares .osc-pulse with the hero, and the
   hero's existing reduce rule already hides that outright. */

/* The pulse at card scale. The hero draws it at 3 units across a
   viewBox thousands of units wide; at the card's 320 units that same
   3 renders as a rope. Two units keeps the streak a highlight. */
.osc-sway .osc-pulse { stroke-width: 2; }

@media (prefers-reduced-motion: no-preference) {

  /* The oscillations card. The copy beside the trace says every part
     of you runs at its own rate, so the three voices do exactly that:
     one motion, three durations, and delays that start each line
     mid-swing so they are never in step. The primary line also carries
     the hero's travelling pulse, slowed a touch for a reading pane. */
  /* The card's waves travel, exactly like the homepage hero: the paths are
     periodic with a 160-unit wavelength and one spare wavelength off each
     side, so translating by 160 and looping is seamless. The dash pulse the
     first pass reused was wrong here: on a short static path it read as a
     dot crawling the line, and the client called it out. Different speeds
     per voice, the middle one reversed, so the lines cross rather than
     march. */
  .osc-run { animation: oscTravel 6s linear infinite; }
  .osc-run-alt  { animation-duration: 9.5s; animation-direction: reverse; }
  .osc-run-soft { animation-duration: 13s; }
  @keyframes oscTravel {
    from { transform: translateX(0); }
    to   { transform: translateX(-160px); }
  }

  /* Decompression, one slow unload cycle. The halves ease apart, the
     disc regains a little height between them, and everything settles
     back. One shared duration and phase, so the figure breathes as a
     single controlled movement rather than as three parts. The travel
     is mirrored through --dy so both halves share a keyframe. */
  .dcp-up   { --dy: -2.5px; }
  .dcp-down { --dy: 2.5px; }
  .dcp-up, .dcp-down { animation: dcpEase 3.5s ease-in-out infinite alternate; }
  @keyframes dcpEase {
    from { transform: none; }
    to   { transform: translateY(var(--dy, 0px)); }
  }
  .dcp-disc {
    transform-box: fill-box;
    transform-origin: center;
    animation: dcpDisc 3.5s ease-in-out infinite alternate;
  }
  @keyframes dcpDisc {
    from { transform: none; }
    to   { transform: scale(1.015, 1.06); }
  }

  /* MyoSync. The lateral strands glide a few pixels sideways, each at
     its own rate, because gliding is the one thing the copy says
     fascia must do; the cross fibres hold still so the layers slide
     against something. The junction points brighten in turn: myoNode
     declares only its midpoint, so each dot swells from the resting
     opacity in its own markup and settles back to it. */
  .myo-lat path { animation: myoGlide 3.6s ease-in-out -1.8s infinite alternate; }
  .myo-lat path:nth-of-type(2) { animation-duration: 4.1s; animation-delay: -1.3s; }
  .myo-lat path:nth-of-type(3) { animation-duration: 4.6s; animation-delay: -2.6s; }
  .myo-lat path:nth-of-type(4) { animation-duration: 3.9s; animation-delay: -.7s; }
  .myo-lat path:nth-of-type(5) { animation-duration: 4.9s; animation-delay: -1.9s; }
  @keyframes myoGlide {
    from { transform: translateX(-3px); }
    to   { transform: translateX(3px); }
  }
  .myo-nodes circle { animation: myoNode 5.5s ease-in-out infinite; }
  .myo-nodes circle:nth-of-type(2) { animation-delay: -1.8s; }
  .myo-nodes circle:nth-of-type(3) { animation-delay: -3.7s; }
  @keyframes myoNode { 50% { opacity: .95; } }

  /* CellSync. The core breathes and the rings swell from the inside
     out, so once a cycle a pulse reads as radiating from the cell and
     the field then settles. The rings hold different resting opacities
     in the markup, so each names its own peak through --swell rather
     than sharing one value that would dim the brightest ring. */
  .cel-rings circle {
    transform-box: fill-box;
    transform-origin: center;
    animation: celSwell 6s ease-in-out infinite;
  }
  .cel-rings circle:nth-of-type(1) { --swell: .34; animation-delay: 1.5s; }
  .cel-rings circle:nth-of-type(2) { --swell: .46; animation-delay: 1s; }
  .cel-rings circle:nth-of-type(3) { --swell: .62; animation-delay: .5s; }
  .cel-rings circle:nth-of-type(4) { --swell: .85; animation-delay: 0s; }
  @keyframes celSwell {
    50% { opacity: var(--swell, .6); transform: scale(1.03); }
  }
  .cel-core {
    transform-box: fill-box;
    transform-origin: center;
    animation: celCore 6s ease-in-out -.4s infinite;
  }
  @keyframes celCore { 50% { transform: scale(1.12); } }
}
/* ===== /G1-METHODS-MOTION ===== */
/* ===== /D6-SYS-CARD ===== */

/* ===== D7-PILLAR-TYPE ===== */
/* On the condition pages the cards ARE the reading content, so the .98rem
   card small-print that suits a homepage teaser is too small there: the
   client read it against the 1.4rem lead and called it out. Scoped to
   .pillar rather than raising .card p globally, because the compact size is
   right everywhere cards are doors rather than destinations. */
.pillar .card p { font-size: 1.06rem; line-height: 1.6; }
.pillar .callout { font-size: 1.02rem; line-height: 1.6; }
/* ===== /D7-PILLAR-TYPE ===== */

/* ===== D8-STORY-BUBBLES ===== */
/* The Common-stories cards read as chat bubbles, because that is what they
   are: the things people message the clinic before they ever visit. Incoming
   style, so every bubble keeps the small corner on the same side, tinted in
   the accent wash so they read as speech rather than furniture, and sized to
   their own sentence the way real messages are. The text runs full ink a
   step larger than card small-print; the client called the grey version not
   obvious enough. */
.pillar .stories { justify-items: start; row-gap: 1.1rem; }
.pillar .stories .card {
  width: fit-content;
  max-width: min(100%, 34rem);
  background: var(--accent-wash);
  border: 0;
  border-radius: 20px 20px 20px 6px;
  padding: 1.05rem 1.35rem;
}
.pillar .stories .card p {
  color: var(--ink);
  font-size: 1.1rem;
  line-height: 1.55;
}
/* ===== /D8-STORY-BUBBLES ===== */

/* ===== D8-CARD-GOOD ===== */
/* The symptom line on a system card. The card says what the platform does;
   this says who it is for, in the words the conditions page already uses,
   so a visitor recognises their own complaint before deciding to book. */
.card-good {
  margin-top: .95rem;
  padding-top: .95rem;
  border-top: 1px solid var(--line-2);
  font-size: 1.06rem;
  line-height: 1.45;
  font-weight: 550;
  letter-spacing: -.012em;
  color: var(--ink);
}
.card-good span {
  display: block;
  font-size: .72rem; font-weight: 600;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent-dk);
  margin-bottom: .3rem;
}
.night .card-good { border-top-color: var(--night-line); color: var(--night-text); }
.night .card-good span { color: var(--accent-hi); }

/* The card's own description steps back so the three tiers read in the order
   that matters: the platform's name, then who it is for, then how it works.
   Before this the description and the symptom line were both 15px at weight
   400 and the eye had nowhere to land. */
.sys-card .loc-note {
  font-size: .93rem;
  line-height: 1.55;
  color: var(--grey);
}

/* Stars under the Google score, so 5.0 reads as a rating rather than a
   number. Reuses the #star symbol the review quotes already define. */
.review-stars { display: flex; gap: 4px; color: var(--accent-dk); margin-top: .7rem; }
.review-star { width: 18px; height: 18px; fill: currentColor; flex: none; }
/* ===== /D8-CARD-GOOD ===== */

/* ===== D9-REVIEWS-NIGHT ===== */
/* The location pages ran paper to tinted to paper all the way to the footer,
   with no dark band anywhere, so the eye had nothing to rest on. Bali's
   reviews take the night surface: it is the page's proof, it sits mid-page
   between two light sections, and a rating reads harder on dark than on
   another shade of white.

   These components had no night variants because they had never been on a
   night surface before. Every one of them is set here rather than inherited,
   which is the rule that stops a colour going invisible on the wrong ground. */
.night .review-num { color: var(--night-text); }
.night .review-score-meta span { color: var(--night-grey); }
.night .review-score-meta strong { color: var(--night-text); }
/* The bright mint, not accent-dk: the deep tone is for small text on paper
   and would nearly vanish here. */
.night .review-stars { color: var(--accent-hi); }
/* No card background. The quotes are separated by the ruler rule and the
   column gaps, not by panels, and giving each one a filled box turned the
   band into a row of clipped-looking slabs. */
.night .review-quotes {
  border-top-color: var(--night-line);
  /* --ticks is built from var(--tick), so redefining the tick here
     regenerates the whole ruler in a tone that shows on night. The paper
     value is near-black at 24 percent and simply vanished. */
  --tick: rgba(253, 252, 251, .22);
}
.night .rq-text { color: var(--night-text); }
.night .rq-name { color: var(--night-text); }
.night .rq-stars { color: var(--accent-hi); }
.night .rq-avatar { background: var(--night); border-color: var(--night-line); color: var(--night-text); }
/* ===== /D9-REVIEWS-NIGHT ===== */

/* ===== E1-SIGNOFF ===== */
/* The About story is signed, because the client wrote it in the first person
   and the names are how a reader knows whose belief they just read. Names sit
   at body weight, roles a step down in grey, so the block reads as a sign-off
   and not as a staff directory. Overrides .prose p + p, which would otherwise
   space the role away from its own name. */
.signoff {
  margin-top: 2.4rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  display: grid; gap: 1.2rem;
  max-width: 62ch;
}
@media (min-width: 620px) { .signoff { grid-template-columns: repeat(2, 1fr); gap: 1.8rem; } }
.signoff p { margin: 0; }
.signoff .signoff-name { font-weight: 560; letter-spacing: -.012em; color: var(--ink); }
.signoff .signoff-role { margin-top: .25rem; font-size: .88rem; line-height: 1.45; color: var(--grey); }
/* ===== /E1-SIGNOFF ===== */

/* ===== E3-ABOUT-STORY ===== */
/* about/: one reading column inside .wrap-narrow left the hero and the copy
   on different left edges, and most of a wide screen empty beside a 702px
   measure. This is /faq/'s answer applied per section: the same 5fr/7fr grid
   inside the full wrap, heading in the rail and its own paragraphs beside
   it, so the width carries the headings instead of carrying nothing. The
   reading measure gets NARROWER, not wider, which is the point of using the
   space rather than filling it.
   Below 1100px every row stacks and reads as it did before. That is
   /faq/'s breakpoint too, and for the same measured reason: split at 900px
   and the copy column lands at 52 characters on a tablet, which is worse
   than one column. */
.story { max-width: none; display: grid; gap: clamp(2.4rem, 4.5vw, 3.75rem); }
.story-row { display: grid; gap: .9rem; }
.story-row > h2 { margin: 0; }
.story-body > :first-child { margin-top: 0; }
/* Stacked, the copy would inherit the full 1200px wrap and run to 118
   characters, which is worse than the narrow column this replaced. The
   grid column is already narrower than this, so the cap only ever bites
   below the breakpoint. */
.story-body { max-width: 62ch; }

@media (min-width: 1100px) {
  .story-row {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: clamp(2rem, 5vw, 4rem);
    align-items: start;
  }
  .story-body { max-width: none; }
}

/* The photographs are the one block that gains from the whole width, so
   they sit outside the two-column rhythm rather than inside the copy. */
.story .story-wide { margin: .6rem 0; }

/* ONE portrait frame, where a landscape pair used to sit.
   The source is 3:4 and both ends of it carry the point: the live readings
   on the screen at the top, the instrument and the hands at the bottom.
   Cropping it into a landscape band loses one or the other, so it is never
   reshaped, only sized. */
.story-figure { margin: .6rem 0; }
.story-figure img {
  display: block;
  width: 100%;
  max-width: 30rem;
  margin-inline: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  filter: saturate(.9);
}

/* Landscape, so width is cheap and height is not: at 4:3 a 52rem frame is
   624px tall, where the old 3:4 portrait stood 853px at a narrower 40rem.
   It is a straight crop of the same photograph, never a stretch. Capped
   rather than full-bleed so it still reads as a figure inside the column. */
@media (min-width: 1100px) {
  /* Fills the column. 1120px is the ceiling here, not a choice: past that the
     figure would have to break out of the wrap, and the source is 1440px wide
     so anything larger is an upscale rather than more detail. */
  .story-figure img { max-width: none; }
}
/* ===== /E3-ABOUT-STORY ===== */

/* ===== E5-CLINIC-ALBUM =================================================
   A photo strip for the location page. Full bleed on purpose: the point is
   that it runs past the edge of the page, so a cropped photo at each end
   reads as "there is more" rather than as a mistake.
   Motion lives in js/album.js, because the brief is a speed CHANGE and a CSS
   animation cannot decelerate without restarting. This file owns the look. */
.album {
  position: relative;
  /* Full bleed comes free: this sits as a direct child of .section, outside
     the .wrap that caps the heading, and .section is already the full body
     width. It previously broke out with width:100vw and a negative margin,
     which overshot both edges by half a scrollbar (7.5px measured) because
     100vw counts the scrollbar and the body does not. body{overflow-x:hidden}
     hid the resulting scroll, but the mask below still faded on a box 15px
     too wide and offset left, so the end photos were cropped by the viewport
     instead of dissolving into it. 100%, never 100vw, as everywhere else in
     this file. Keep it a child of the section, not of a .wrap. */
  overflow: hidden;
  padding-block: clamp(1.25rem, 3vw, 2rem);
  /* Vertical page scrolling still belongs to the page. Horizontal does not:
     the strip moves itself, so a sideways drag has nothing to do, and ruling
     it out also kills double-tap-to-zoom, which otherwise fires when someone
     taps two photos quickly. pinch-zoom is added back explicitly, because
     pan-y alone would take a magnifying gesture away from anyone who needs
     one. Declared twice so a browser that rejects the two-value form keeps
     the plain pan-y rather than falling back to auto. */
  touch-action: pan-y;
  touch-action: pan-y pinch-zoom;
}
/* Softens both ends so photos dissolve instead of being guillotined by the
   viewport. Unsupported browsers simply get the hard edge. */
@supports (mask-image: linear-gradient(#000, #000)) {
  .album {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
    mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  }
}
.album-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.album-track {
  display: flex;
  gap: clamp(.7rem, 1.6vw, 1.15rem);
  width: max-content;
  will-change: transform;
}
.album-track figure {
  margin: 0;
  flex: 0 0 auto;
  width: clamp(15rem, 26vw, 23rem);
  position: relative;
  /* A long press otherwise raises the save-image callout, which interrupts
     a tap that was on its way to selecting the photo. user-select stops the
     blue selection flash on the second tap of a quick pair. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.album-track img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  filter: saturate(.92);
  transition: transform .45s var(--ease), box-shadow .45s var(--ease), filter .45s var(--ease);
}
/* The zoom is on the photo, not the frame, so neighbours do not shift. At
   1.16 a photo grows past the gap and overlaps the one beside it, and later
   siblings paint on top by default, so the raised photo needs the z-index or
   it sits UNDER its right-hand neighbour. That goes on the figure: the img is
   not positioned, so z-index there does nothing.

   Two blocks rather than one selector list, because :hover has to be gated
   behind a device that genuinely hovers. On a touchscreen :hover LATCHES: the
   photo tapped first stays zoomed until something else is tapped, so the next
   photo touched appears dead. Touch drives .is-active from js/album.js
   instead, which a tap puts on and a second tap takes off. */
@media (hover: hover) and (pointer: fine) {
  .album-track figure:hover { z-index: 1; }
  .album-track figure:hover img {
    transform: scale(1.16);
    filter: saturate(1);
    box-shadow: 0 1.6rem 3rem rgba(20, 24, 24, .28);
  }
}
.album-track figure.is-active,
.album-track figure:focus-within { z-index: 1; }
.album-track figure.is-active img,
.album-track figure:focus-within img {
  transform: scale(1.16);
  filter: saturate(1);
  box-shadow: 0 1.6rem 3rem rgba(20, 24, 24, .28);
}

/* No motion preference: a real gallery rather than a dead strip. It scrolls,
   snaps, and every photo stays reachable. */
.album[data-static] { -webkit-mask-image: none; mask-image: none; }
.album[data-static] .album-track {
  width: auto;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-inline: clamp(1.15rem, 5vw, 2rem);
  -webkit-overflow-scrolling: touch;
}
.album[data-static] .album-track figure { scroll-snap-align: center; }

@media (prefers-reduced-motion: reduce) {
  .album-track img { transition: none; }
}
/* ===== /E5-CLINIC-ALBUM ===== */

/* ===== E4-MOBILE ===== */
/* Findings from a 39-page sweep at 320 and 375px, 28/08/2026. */

/* 1. THE ACTUAL BUG. /our-methods/ scrolled sideways 42px at 320px and 2px
      at 360px, and nothing on the page was too wide: the H1's own word,
      "communication.", measures 327px at the 46px display size inside a
      265px box, and no rule let it break. The page slid, which reads as
      broken even though every box was correct.
      This is a safety net, not a style: overflow-wrap never touches a word
      that fits, so nothing else on any page moves. Applied to headings
      because display sizes are where a single word outgrows its column. */
h1, h2, h3, .display, .h1, .h2, .h3 { overflow-wrap: break-word; }

/* 2. The header wordmark is an 18px-tall link, so its tap target was 18px
      tall on every page. The padding grows the hit area to the full nav
      height without moving the logo, which the flex row still centres. */
.brand { padding-block: .9rem; margin-block: -.9rem; }

/* 3. Footer links were 19px tall with a 11px gap, which is 30px between the
      centres of two adjacent targets. Below the nav breakpoint they get
      their own row height instead, which is the one place on the site where
      a mis-tap sends someone to the wrong page entirely. */
@media (max-width: 899px) {
  .footer-links { gap: 0; }
  .footer-links a { display: inline-block; padding-block: .55rem; }
  .aphyx-credit a { display: inline-block; padding-block: .35rem; }
}

/* 4. The inline arrow links were 25px tall. Same padding-and-negative-margin
      trick as the wordmark, so the hit area grows and nothing moves. */
@media (max-width: 899px) {
  .link-arrow { padding-block: .45rem; margin-block: -.45rem; }
}

/* 5. The journal's "Latest" chip was the smallest type on the site at
      10.9px. It is uppercase, bold and widely tracked, so a step up costs
      nothing visually and stops it being the outlier. */
.jchip { font-size: .74rem; }
/* ===== /E4-MOBILE ===== */

/* ===== E6-ABOUT-STORY-V2 =============================================
   The About page reworked as a story rather than a reference document, plus
   the motion it never had. Page-level selectors are anchored to .pg-about on <main>, the same way
   .jpost scopes the journal: .page-hero is on 48 pages and .mantra on 4,
   so unanchored they would have shrunk 48 heroes and put a second ruler
   under every journal post. Everything else keys off .story / .signoff /
   .trace / .night-signal, which exist on this page only.

   Layout first, then the effects in the order they were reviewed: the hero
   ruler, the wipe and rise, the reading trace, the photo scan, the signature,
   and the signal in the night band. Behaviour is in js/about-story.js, which
   only that page loads.
   ================================================================== */

/* ---- VARIANT A ---- */

/* Scoped to /about/: .display is on 46 pages. 6.6rem rendered at 105.6px,
   the only thing on the page with any scale, so it shouted on its own.
   Smaller here lets three other moments carry weight. */
.pg-about .page-hero .display { font-size: clamp(2.7rem, 6.2vw, 5.2rem); }
.story { gap: clamp(2.6rem, 4vw, 3.6rem); }

/* 1. The turning point. The sentence the company came from was the fourth
      paragraph of a grey column. */
.story-say {
  font-size: clamp(1.45rem, 2.7vw, 2.2rem);
  line-height: 1.16; letter-spacing: -.03em; font-weight: 560;
  color: var(--ink); margin-top: 1.5rem; max-width: 26ch; text-wrap: balance;
}

/* 2. The people, on night. The one block a reader should feel they are
      standing inside rather than reading past. */
.story-row:nth-child(5) {
  background: var(--night);
  box-shadow: 0 0 0 100vmax var(--night);
  clip-path: inset(0 -100vmax);

  color: var(--night-text);
  padding-block: clamp(2.8rem, 5vw, 4.2rem);
  margin-block: clamp(1.4rem, 2.6vw, 2.2rem);
}
.story-row:nth-child(5) h2 { color: var(--night-text); }
.story-row:nth-child(5) p  { color: var(--night-grey); }
.story-row:nth-child(5) .signoff { border-top-color: var(--night-line); margin-top: 2.6rem; max-width: none; }
.story-row:nth-child(5) .signoff-name {
  color: var(--night-text); font-size: clamp(1.05rem, 1.7vw, 1.3rem); letter-spacing: -.02em;
}
.story-row:nth-child(5) .signoff-role { color: var(--night-grey); font-size: .92rem; }

/* 3. The mission line the client supplied himself, currently a paragraph. */
/* The client&#39;s draft sets several short lines as their own beat, e.g.
   "More measurable. More controlled. More consistent." They are copy, not
   headings, so they take a step up in weight and colour rather than a
   heading size, which would put a seventh voice into a page that already
   has six. */
.pg-about .story-beat {
  color: var(--ink); font-weight: 560; letter-spacing: -.014em;
  font-size: 1.06rem; line-height: 1.45;
}
.pg-about .story-row:nth-child(5) .story-beat { color: var(--night-text); }
.pg-about .mantra {
  font-size: clamp(1.55rem, 3.2vw, 2.5rem);
  line-height: 1.1; letter-spacing: -.035em; font-weight: 560;
  color: var(--ink); margin-bottom: 1.5rem; text-wrap: balance;
}



/* THE RULER CLOSES THE HERO.
   Journal posts already do this: a hero with no labelled .rule-strip closes on
   a drawn tick rule so every article starts from the same ruler line. /about/
   has neither, so its hero simply stops. This is that exact component, the
   same --ticks pattern and the same 12px height, wiped in left to right on
   load so the page opens by taking a measurement. */
.pg-about .page-hero .wrap::after {
  content: "";
  display: block;
  height: 12px;
  margin-top: clamp(2rem, 4.5vw, 3.2rem);
  border-top: 1px solid var(--line);
  background-image: var(--ticks);
  background-size: 100% 12px;
  background-repeat: no-repeat;
  /* Same sweep the motion system uses for headings, so the page opens in the
     site's own gesture rather than a new one. */
  clip-path: inset(0 100% 0 0);
  animation: ruleDraw 1.1s cubic-bezier(.16, 1, .3, 1) .45s forwards;
}
@keyframes ruleDraw { to { clip-path: inset(0 0 0 0); } }
@media (prefers-reduced-motion: reduce) {
  .pg-about .page-hero .wrap::after { animation: none; clip-path: inset(0 0 0 0); }
}



/* COPY RISES INTO PLACE.
   `rise` is the second treatment in js/motion.js ("copy lifting into place")
   and /about/ never receives it, because that script is on 3 of 56 pages.
   These are the system's own numbers: 22px, .8s opacity, .95s transform, and
   the same cubic-bezier(.16, 1, .3, 1) the wipe uses, so the gestures read as
   one language rather than two effects.
   Paragraphs stagger inside their own section (--i), so a block cascades
   rather than landing flat. The cap is deliberate: past the fourth paragraph
   the wait stops reading as rhythm and starts reading as lag. */
/* :not(.story-say) matters. That line is a <p> inside .story-body, so it
   matched this rule too, and at (0,2,1) this selector outspecifies the wipe's
   (0,2,0) and won the whole `transition` declaration. The clip-path it is
   supposed to sweep on was left with no transition and snapped open instead.
   The turning-point line takes the wipe and nothing else. */
.js .story-body > p:not(.story-say),
.js .story-body > .callout,
.js .signoff {
  opacity: 0;
  transform: translate3d(0, 22px, 0);
  /* Slower than the system's .8s/.95s, at the user's call. The trigger already
     fires below the fold, so the extra dwell is spent while the copy is
     arriving on screen rather than after it has settled, which is the only
     reason a longer duration reads as unhurried instead of as lag. */
  transition: opacity 1.1s cubic-bezier(.16, 1, .3, 1),
              transform 1.3s cubic-bezier(.16, 1, .3, 1);
  transition-delay: calc(var(--i, 0) * 110ms);
}
.js .story-body > p:not(.story-say).is-up,
.js .story-body > .callout.is-up,
.js .signoff.is-up { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  /* A crossfade with no travel, per the reduced-motion rule: the content still
     arrives, it just does not move. */
  .js .story-body > p:not(.story-say),
  .js .story-body > .callout,
  .js .signoff { transform: none; transition: opacity .35s linear; }
}



/* THE WHOLE SYSTEM, APPLIED.
   Headings take `wipe`, the measurement sweep, and the copy under them takes
   `rise`. That is exactly what js/motion.js would do to this page if it were
   included on it. Nothing here is invented: the values, the easing and the
   pairing are the system's own. */
.js .story-row > h2,
.js .story-say {
  clip-path: inset(0 100% 0 0);
  /* 1.2s against the system's .85s. These headings are two lines and up to 40
     characters, so the sweep has much further to travel than the single-line
     headings the value was tuned on, and the longer draw suits the distance. */
  transition: clip-path 1.2s cubic-bezier(.16, 1, .3, 1);
}
.js .story-row > h2.is-swept,
.js .story-say.is-swept { clip-path: inset(0 -.12em 0 0); }

@media (prefers-reduced-motion: reduce) {
  .js .story-row > h2, .js .story-say { clip-path: inset(0 -.12em 0 0); transition: none; }
}



/* THE READING TRACE.
   The home page's measurement trace turned vertical and bound to how far you
   have read: a hairline runs the height of the story with a tick at each
   section, and a bright segment travels it as you scroll, the same "monitor
   carrying the light" idea as .hero-trace. The page about measuring measures
   your way through itself.
   Decoration only. It sits in the rail's empty margin, takes no pointer
   events, is aria-hidden, and the page reads identically with it deleted. */
@media (min-width: 1100px) {
  .story { position: relative; }
  .trace {
    position: absolute; top: 0; bottom: 0; width: 1px;
    /* The offset is capped by the gutter, which is the wrap's 40px padding
       plus whatever the viewport has spare over the 1200px max-width. So it
       is a flat 40px from 1100 to 1280 and only grows past that: 80px at
       1280, 160px at 1440, 240px at 1600. -2.6rem put the line at -2px, off
       screen, on everything between 1100 and ~1250.
       Hence three steps rather than one value. The line sits as far off the
       column as each width can actually afford, and never nearer than 24px
       to the viewport edge. */
    left: -1.75rem;
    background: var(--line); pointer-events: none;
  }
  @media (min-width: 1280px) { .trace { left: -3.5rem; } }
  @media (min-width: 1440px) { .trace { left: -5rem; } }
  .trace-fill {
    position: absolute; left: 0; top: 0; width: 1px;
    background: var(--accent); opacity: .4;
    height: calc(var(--p, 0) * 100%);
  }
  /* The head: the hero's .trace-run treatment, rotated. */
  .trace-head {
    position: absolute; left: -1px; width: 3px; height: 56px; border-radius: 2px;
    background: linear-gradient(to bottom, transparent, var(--accent), transparent);
    filter: drop-shadow(0 0 7px var(--accent-tint));
    top: calc(var(--p, 0) * 100%); margin-top: -28px;
  }
  .trace-tick {
    position: absolute; left: -5px; width: 11px; height: 1px; background: var(--line);
    transition: background .45s var(--ease), width .45s var(--ease);
  }
  .trace-tick.is-read { background: var(--accent); width: 15px; }
}
@media (prefers-reduced-motion: reduce) {
  /* Still a readout, just nothing travels. */
  .trace-head { display: none; }
  .trace-fill { height: 100%; opacity: .22; }
  .trace-tick { transition: none; }
}



/* THE PHOTOGRAPH IS READ, NOT FADED IN.
   The image is a Sync specialist working while the segment readings update on
   the screen behind. It arrives under a mask that travels left to right, with
   a thin accent edge riding the boundary, so it reads as being scanned rather
   than dissolving. Same direction and the same easing as the heading wipe, so
   the two gestures are one language.
   Default is fully visible; the mask only arms once .js is present, so no-JS
   and headless renders show the finished photograph. */
.story-figure { position: relative; }
.js .story-figure img {
  -webkit-mask-image: linear-gradient(to right, #000 0 var(--scan, 100%), transparent var(--scan, 100%));
          mask-image: linear-gradient(to right, #000 0 var(--scan, 100%), transparent var(--scan, 100%));
}
.js .story-figure.is-armed img { --scan: 0%; }
.js .story-figure.is-read img {
  --scan: 100%;
  transition: --scan 1.5s cubic-bezier(.16, 1, .3, 1);
}
/* The reading edge: a hairline that travels with the mask boundary. */
.js .story-figure.is-armed::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 2px;
  background: var(--accent); opacity: 0;
  box-shadow: 0 0 12px var(--accent);
  left: var(--scan, 0%);
  transition: left 1.5s cubic-bezier(.16, 1, .3, 1), opacity .4s linear;
}
.js .story-figure.is-armed.is-read::after { opacity: .9; }
.js .story-figure.is-done::after { opacity: 0; }

@property --scan { syntax: '<percentage>'; inherits: true; initial-value: 100%; }

@media (prefers-reduced-motion: reduce) {
  .js .story-figure img { -webkit-mask-image: none; mask-image: none; }
  .js .story-figure::after { display: none; }
}



/* THE SIGNATURE.
   Two names carry the credibility of the whole page and they currently just
   sit there. The rule above them draws left to right when it is reached, then
   the names arrive after it, the way a signature follows the line it is signed
   on. On variant A this happens on the night ground, so the rule runs
   --night-accent rather than the paper line.
   Default is visible. The draw arms only under .js. */
.js .signoff { position: relative; }
.js .signoff::before {
  content: ""; position: absolute; top: -1px; left: 0; right: 0; height: 1px;
  background: var(--night-accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 1.1s cubic-bezier(.16, 1, .3, 1);
}
.js .signoff.is-signed::before { transform: scaleX(1); }
.js .signoff > div { opacity: 0; transform: translate3d(0, 10px, 0); transition: opacity .7s ease, transform .8s cubic-bezier(.16, 1, .3, 1); }
.js .signoff.is-signed > div { opacity: 1; transform: none; }
.js .signoff.is-signed > div:nth-child(2) { transition-delay: .16s; }

@media (prefers-reduced-motion: reduce) {
  .js .signoff::before { transform: scaleX(1); transition: none; }
  .js .signoff > div { opacity: 1; transform: none; transition: none; }
}



/* SIGNAL IN THE NIGHT BAND.
   Variant A puts the people on a dark ground; this gives that ground a pulse.
   Two travelling waves at low opacity behind the copy, built the way the home
   page hero builds its waves (each path repeats on a 400-unit period and is
   animated by exactly one period, so translateX loops seamlessly) and paired
   the same way: accent against neutral, two signals in conversation rather
   than one voice. Contained to a single section, so it decorates the page
   without running it. */
.story-row:nth-child(5) { position: relative; overflow: hidden; }
.night-signal {
  position: absolute; left: 0; right: 0; bottom: 0; height: 48%;
  pointer-events: none; opacity: .55;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 20%, #000 80%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 20%, #000 80%, transparent);
}
/* max-width: none is load-bearing. The base stylesheet sets
   img, svg { max-width: 100% }, which clamped this to the container and
   squeezed all five periods into it: the waves ran at twice the intended
   frequency, and translating by one period left a fifth of the band empty on
   the right at the end of every cycle. At 200% the strip is twice the band,
   so there is always more wave to the right of the edge to slide in. */
.night-signal svg { display: block; width: 200%; max-width: none; height: 100%; }
.night-signal .nw { fill: none; stroke-linecap: round; }
.night-signal .nw-1 { stroke: var(--night-accent); stroke-width: 1.5; opacity: .5;  animation: nightDrift 19s linear infinite; }
.night-signal .nw-2 { stroke: var(--night-grey);   stroke-width: 1.1; opacity: .34; animation: nightDrift 27s linear infinite; }
@keyframes nightDrift { from { transform: translateX(0); } to { transform: translateX(-400px); } }
@media (prefers-reduced-motion: reduce) {
  .night-signal .nw-1, .night-signal .nw-2 { animation: none; }
  .night-signal { opacity: .3; }
}

/* ===== /E6-ABOUT-STORY-V2 ===== */
