/* ===========================================================================
 * v4 — ClearPath-inspired light layout, Trance Ritual content
 *
 * Design prototype. Content is lifted from the live site so the layout can be
 * judged on real words and real photographs; none of it is new copy.
 *
 * PHOTOGRAPHY IS PLACEHOLDER. Every image is graded for the current near-black
 * site and is desaturated here to sit on a white page. One variable does it —
 * --photo-filter below. Delete that one line when the real photographs exist.
 *
 * FONTS - self-hosted, not Google's CDN. Same reasoning as the live site: a
 * Munich court ruled CDN font loading a GDPR breach in 2022 and German tourists
 * are a named audience. Both families are OFL so self-hosting is permitted.
 * latin-ext is not optional: Polish needs it.
 * =========================================================================== */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('../fonts/inter-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Crimson Text';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/crimson-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Crimson Text';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/crimson-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Crimson Text';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/crimson-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Crimson Text';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/crimson-italic-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Italiana — the wordmark only. One weight exists, which is why GRAN CANARIA
   in the lockup is enlarged and tracked rather than bolded: there is no bold
   to reach for. latin is enough here; the mark is pure ASCII and this site is
   English only (BUILD-BRIEF.md), so latin-ext would be dead weight. */
@font-face {
  font-family: 'Italiana';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/italiana-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2122, U+2212, U+FEFF, U+FFFD;
}

/* --- tokens ------------------------------------------------------------- */

:root {
  /* palette — GCGM v5, 2026-08-21.
     Replaces the sage taken from the Framer template. These are the three
     colours from the logo that is live on the server today, so the site keeps
     the brand it already has instead of inventing a new one. White ground is
     deliberate: Bodhi Center and Trance Ritual are both dark, and this site is
     meant to read as a different place.

     THREE ACCENTS, THREE JOBS. Keeping them separate is what stops the page
     looking busy — colour appears in every section without any section
     carrying more than one warm note.

       teal     structure.  Links, buttons, the main line stroke, form focus.
       saffron  warmth.     The ghost line, decorative numerals, rules, washes.
       red      emphasis.   One word per section, and active states. Rare.

     Contrast on white, measured not estimated:
       ink      16.78:1     body        9.00:1     muted   5.27:1
       teal      6.82:1     brown       8.05:1     red     6.58:1
       saffron   2.02:1  <- FAILS as text. Strokes, fills and rules only.
       saffron-ink 4.87:1 <- use this one when the warm accent must be READ. */
  --white: #FFFFFF;
  --surface: #FAF8F5;

  --teal: #00637D;
  --teal-rgb: 0, 99, 125;
  --teal-wash: rgba(0, 99, 125, 0.14);
  --teal-hair: rgba(0, 99, 125, 0.30);
  /* Teal lifted 55% toward white, and it is the exact mirror of --saffron-ink
     below: saffron is darkened until it can be READ on white, teal is lifted
     until it can be SEEN on a dark photograph. Raw --teal measures 1.56:1
     against the scrimmed band on coaching.php and 1.26:1 at the p90 -- a dark
     line on a dark picture. This one measures 4.98:1 / 4.05:1, which is
     saffron's own 5.25:1 / 4.26:1 on the same ground, and matching them is
     what makes the two read as a pair rather than as a line and its shadow.
     Use --teal on white, --teal-lift on a photograph. */
  --teal-lift: #8CB9C4;

  --saffron: #F2A809;
  --saffron-rgb: 242, 168, 9;
  --saffron-wash: rgba(242, 168, 9, 0.16);
  --saffron-hair: rgba(242, 168, 9, 0.42);
  /* Saffron darkened until it passes as text — 4.87:1. Use this whenever the
     warm accent has to be READ; use --saffron when it is a stroke or a fill. */
  --saffron-ink: #9C6604;

  /* The sarong red, sampled from Greg's own photograph — the same accent as
     palette C in PALETTES.html. 6.58:1 on white, so unlike saffron it is safe
     as text. It is the RARE accent: roughly one appearance per section, on the
     word that matters most. Spend it and it stops working. */
  --red: #A23A33;
  --red-rgb: 162, 58, 51;
  --red-wash: rgba(162, 58, 51, 0.12);
  --red-hair: rgba(162, 58, 51, 0.34);

  --brown: #7D4009;

  /* --teal taken down to roughly half its lightness. Tried on the hero band
     and rejected — Greg, 2026-08-21, the blue was wrong there and the band is
     the beige --surface instead. Kept because the measurement is worth having
     if a dark ground is ever wanted: white 13.7:1 on it, saffron 6.8:1.
     Nothing uses it today. */
  --teal-deep: #04323E;

  --ink: #2A1A0C;

  /* The dark ground behind the #ceiling proof row. Greg asked for black; this
     is #14100B, which is --ink taken most of the way down rather than pure
     #000. Every dark surface on this site is warm -- the band scrims, the
     photographs, the saffron -- and a true black beside them reads as a hole
     cut in the page rather than as part of it. It is 1.5% off black by
     luminance, so it is black to the eye and warm against the cream. */
  --proof-ground: #14100B;
  --body: #574636;
  --muted: #7A6957;
  --hairline: rgba(42, 26, 12, 0.10);

  --serif: 'Crimson Text', Georgia, 'Times New Roman', serif;
  --sans: 'Inter', system-ui, -apple-system, sans-serif;
  /* Logo only. Single weight, display sizes — never body copy. */
  --display: 'Italiana', 'Cormorant Garamond', Georgia, serif;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-soft: cubic-bezier(0.4, 0, 0.2, 1);

  --header-h: 76px;
  --gutter: clamp(1.25rem, 5vw, 5rem);
  --max: 1280px;
  /* ONE READING SIZE FOR THE PAGE'S PROSE. Greg, 2026-08-22: #what-this-is and
     About must be the same font, so they share a knob rather than two numbers
     that drift. 18.24px floor / 2.34vw / 25.6px cap — the cap binds from about
     1094px up, the floor below 779. Monotonic by construction: it only ever
     grows with the window. */
  --read-size: clamp(1.14rem, 2.34vw, 1.6rem);

  /* PLACEHOLDER PHOTOGRAPHY — delete this line once real images exist */
  /* THE GREYSCALE IS ITS OWN VARIABLE so the gallery can animate it. Greg,
     2026-08-25: the photographs are black and white and reveal their colour
     on hover. A second hardcoded filter on the hover rule would be two places
     to keep the contrast and brightness in step; one knob is one place.
     --photo-filter still reads exactly as it did, so every other picture on
     the site is unaffected. Both lines go when the real photographs land. */
  --photo-gray: 1;
  --photo-filter: grayscale(var(--photo-gray)) contrast(1.06) brightness(1.04);
}

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

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

body {
  margin: 0;
  background: var(--white);
  color: var(--body);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

.container {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { max-width: 860px; }

/* TIGHTER, PAGE-WIDE. Greg, 2026-08-22: every section carried too much air.
   This was clamp(5rem, 11vw, 10rem) — 158px top AND bottom at 1440, so 316px
   of nothing between one section's last line and the next one's first. Now
   94px a side, 188 between: a 41% cut that is felt everywhere at once rather
   than tuned section by section.

   Greg took the middle term to 3vw in DevTools on 2026-08-22 and kept it, and
   it is worth knowing what that does: 3vw is 43px at 1440, which is BELOW the
   3rem floor, so from 0 to about 1600px the FLOOR is the live branch and the
   padding is a flat 48px. The vw term only starts doing anything past 1600.
   That is a deliberate flat value on every screen Greg reviews on, not a fluid
   one — and if it should come down further, the number to change is the floor,
   not the middle.

   (The struck-through terms DevTools shows on a clamp are not an override.
   It strikes the two branches you are NOT getting and leaves the live one.) */
.section { padding-block: clamp(3rem, 3vw, 6.5rem); }
.section--surface { background: var(--surface); }

/* the header is fixed, so an in-page anchor must stop clear of it */
[id] { scroll-margin-top: calc(var(--header-h) + 1.5rem); }

/* --- type scale — measured from the template ---------------------------- */

.eyebrow {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--teal);
  margin: 0;
}
.eyebrow--muted { color: var(--muted); }
/* Section eyebrows rotate through the three accents rather than all sitting on
   teal. It is the cheapest way to get colour into every section: the eyebrow is
   already the first thing in each one, it is small, and rotating it means no
   single section reads as "the blue one".
   --saffron-ink, never --saffron: this is type. */
.eyebrow--red { color: var(--red); }
.eyebrow--warm { color: var(--saffron-ink); }

/* --- the slogan ----------------------------------------------------------
   "Professional massage. Man to man."

   It is carrying two jobs at once and the second one is invisible if you are
   not looking for it.

   Job one: say what the service is, plainly. Not "sensual", not "intimate" -
   the FAQ does the filtering on this site (BUILD-BRIEF.md), so the front of
   the house can just be professional.

   Job two: say nothing whatsoever about the therapist's sexuality, because on
   this site it is not information the client needs. "Man to man" describes the
   room - one man working on another - and describes nothing else. That is
   deliberate: plenty of clients do not care who the therapist goes home to,
   and the ones who do are not owed an answer by a headline.

   The idiom helps rather than hurts. "Man to man" already means direct, no
   games, no performance, which is exactly the register the rest of the copy
   wants.

   Set in the display face, so it reads as part of the mark rather than as body
   copy - and the second half is red, which is the emphasis accent spent here
   on the half that does the real work. */
.slogan {
  font-family: var(--display);
  font-size: clamp(1.05rem, 1.9vw, 1.5rem);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  line-height: 1.3;
  color: var(--ink);
  margin: 0.85rem 0 0;
}
/* nowrap so the phrase never splits across a line break - at 390px it was
   breaking as "MAN TO / MAN.", which reads as a mistake. Below 620px the two
   sentences stack instead, which is the only break the line should ever take. */
.slogan em { font-style: normal; color: var(--red); white-space: nowrap; }
@media (max-width: 620px) {
  .slogan em { display: block; }
}

.footer-slogan {
  font-family: var(--display);
  font-size: clamp(0.85rem, 1.4vw, 1.05rem);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--saffron);
  margin: -0.4rem 0 0;
}

.display-xl {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(3.25rem, 9.4vw, 8.5rem);
  line-height: 0.84;
  letter-spacing: -0.04em;
  color: var(--ink);
  margin: 0;
}
.display-lg {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2.5rem, 5vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0;
}
.display-sm {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.65rem, 2.4vw, 2.25rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0;
}
.statement {
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(1.6rem, 3.1vw, 2.75rem);
  line-height: 1.2;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0;
}
.sub {
  font-weight: 500;
  font-size: clamp(1.15rem, 1.8vw, 1.625rem);
  line-height: 1.4;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0;
}
/* Was 16px, then 18px, now the shared --read-size. Greg, 2026-08-22: About and
   #what-this-is must be the same font, so both read one knob and cannot drift.
   18.24px on a phone, 25.6px on the desktop.
   REM/vw rather than px on purpose — a reader who has raised their browser's
   default font size gets the increase, which a px value ignores. The rest of
   the page is still px-based; converting it is a separate job before upload.
   .body-text appears in TWO places on this page, both of them About. */
.body-text { font-size: var(--read-size); line-height: 1.32; margin: 0 0 1.25rem; }
.body-sm { font-size: 14px; line-height: 1.8; color: var(--body); margin: 0; }
.italic { font-style: italic; }

/* --- buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  /* THE LABEL SITS LOW ON PURPOSE. Greg, 2026-08-26, and it came out of a bug:
     while the gallery's underline rule was wrongly selecting this button it
     stripped the bottom padding to 2px, the words dropped into the lower half,
     and he liked it enough to want it kept. His words -- "I don't want it
     centred, I want it in the bottom half rather than in the centre", about
     twenty percent up from the bottom edge.

     THE HEIGHT DOES NOT CHANGE, which is the difference between this and the
     bug. The two paddings still sum to 2rem, so every button stays the 54px it
     is everywhere else and only the split moves. The accident made the button
     SHORTER as well as the label lower; only the second half was wanted.

     --btn-drop IS THE ONE NUMBER TO TURN. It is how far below centre the label
     sits, so 0 restores a centred button and nothing else has to be worked out.
     --btn-pad-y IS THE HEIGHT. The two paddings are it plus and minus the
     drop, so they always sum to twice it and the height is 2 x pad-y + the line
     box. 17px against a 12px line gives 46px, which is the 15% off the old 54
     that Greg asked for in the same breath as this.

     The floor is 4px rather than 0: the label is uppercase and has no
     descenders, but a 1px optical touch on the border would read as a mistake
     rather than as a placement. */
  --btn-pad-y: 16px;
  --btn-drop: 13px;
  padding: calc(var(--btn-pad-y) + var(--btn-drop)) 1.9rem calc(var(--btn-pad-y) - var(--btn-drop));

  /* line-height 1 IS WHAT MAKES THE 20% REACHABLE, and it is not a tidy-up.
     The label's own line box was 22px tall on 12px type -- inherited 1.6 from
     the body. Half of that is 11px, so the LOWEST the label's middle can sit is
     11px off the foot however the padding is split, and on a button shortened
     to 46px that floor is 24%. Greg asked for about twenty. Collapsing the line
     box to the glyphs takes the half-height to 6px and puts 20% in range.
     Safe here because the button is inline-flex with align-items: center: the
     dot and the arrow are flex items and centre on the line whatever it is. */
  line-height: 1;

  /* MINIMUM WIDTH, for Send. Greg: the other labels are long enough to carry
     themselves -- "Ask about this session" is 264px, "Back to the start" 232 --
     but Send is four letters and came out 125px, which read as a different,
     smaller button rather than as the same one with less to say.

     170 is chosen off the measurements rather than picked: the next narrowest
     button on the site is "Write to me" at 172, so this lifts Send into the
     family and leaves every other button untouched.

     min() RATHER THAN A BARE 170px. Greg flagged he could not tell how it would
     behave on a phone, and this is the answer: on any container narrower than
     the minimum the button takes the container instead and cannot push the page
     sideways. Nothing on the desktop is affected by it. */
  min-width: min(170px, 100%);
  /* 4px, NOT the pill it was. Greg, 2026-08-26, looking at "Write to me" on the
     gallery: rectangular with the corners only just taken off. Colour, fill and
     the drawn ring are untouched -- this is the radius and nothing else.

     4 AND NOT THE 2px THE MAP FRAME USES, because a radius reads relative to
     the thing it is rounding. This button is ~47px tall against that frame's
     several hundred, so the same number would look sharper here than there, not
     matching. 4 sits it in the same family as .contact-map's 2 and .map-shot's
     3 rather than starting a new one.

     .btn--solid::before TAKES CARE OF ITSELF -- it is border-radius: inherit,
     so the self-drawing ring follows this number and there is no second value
     to keep in step.

     THIS IS THE SITE'S BUTTON AND IT LANDS ON TWELVE OF THEM: eight on
     treatments, one each on the home page, the gallery, contact and the 404. */
  border-radius: 4px;
  transition: background-color 0.4s var(--ease-soft), color 0.4s var(--ease-soft),
              border-color 0.4s var(--ease-soft), transform 0.4s var(--ease-out);
}
.btn:hover { transform: translateY(-2px); }
/*
 * THE RING DRAWS ITSELF. Second attempt at #8, and the first one was wrong for
 * a reason worth writing down.
 *
 * Trance Ritual's travelling border light is a bright arc rotating behind the
 * button. It works there because that site is dark: a glow is ADDITIVE light,
 * so the arc is lighter than its surround and the eye reads emission. On a
 * white page nothing can be lighter than the ground, so the same arc stops
 * being a glow and becomes a yellow segment. Greg saw it immediately.
 *
 * So: no glow. The hairline DRAWS ITSELF round the pill instead, clockwise
 * from the top, and un-draws when the pointer leaves. That is the same idiom
 * as the rest of the page — this site's decoration is one traced line drawn on
 * scroll, and a button that draws its own outline is speaking it too.
 *
 * The mechanism is a conic gradient used as a REVEAL rather than as a light:
 * teal up to --btn-sweep, transparent after it, and the whole thing masked to
 * a 1px ring. Animating the angle therefore paints the border on. It is a
 * TRANSITION, not an animation, so leaving un-draws from wherever it reached
 * instead of snapping — the same lesson as the awards row.
 *
 * @property is what makes an angle interpolate at all. Where it is
 * unsupported the sweep jumps straight to full and the button simply has a
 * teal border on hover, which is a fine place to land.
 */
@property --btn-sweep {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* AT REST THE BUTTON IS BLUE AND STILL A BUTTON. Greg, 2026-08-22: the ported
   version had gone fully transparent, and a call to action with no ground
   stops looking pressable. So the teal fill comes back at a tenth of its
   strength — present, not solid — under a teal border at full readable weight.

   The RED is what the hover brings. The drawn ring sits exactly on the border,
   so the blue line does not disappear and get replaced: it TURNS RED as the
   stroke travels round it. That is the whole gesture, and it only works
   because the two lines occupy the same pixel. */
.btn--solid {
  position: relative;
  background: rgba(var(--teal-rgb), 0.10);
  /* THE ONE PLACE THE LABEL'S COLOUR IS SET. Greg, 2026-08-26: the gallery's
     button is the reference for the whole site, and its words are this warm
     brown while every other button's were teal.

     IT WAS NOT A CHOICE THERE, IT WAS A LEAK -- `.gal-close-in a` is (0,2,0)
     and beat this rule's (0,1,0), and `.tr-close-in a` was doing the same to
     one button on treatments. So two of twelve had it by accident. Greg wants
     it on all twelve, so it is declared HERE and both leaking selectors now say
     :not(.btn), which leaves exactly one declaration deciding the colour.

     The fill and the border stay teal. That is what the gallery button is, and
     the gallery button is the sample.

     .btn-dot IS background: currentColor, so the dot follows this line and is
     never set anywhere. One value, twelve buttons, mark included. */
  color: var(--saffron-ink);
  border: 1px solid rgba(var(--teal-rgb), 0.55);
}
.btn--solid::before {
  content: '';
  position: absolute;
  /* -1px, ON the border and not inside it. An absolutely positioned pseudo is
     laid out against the PADDING box, so `inset: 0` puts the red stroke one
     pixel in from the blue line and you see two lines instead of one changing
     colour. Measured: teal at y=20, the stroke at y=21. At -1px they are the
     same pixel and the border changes colour as the stroke passes. (Written
     when the stroke was red; it follows the label now.) */
  inset: -1px;
  padding: 1px;
  border-radius: inherit;
  pointer-events: none;
  /* From the top, clockwise. The second stop at 0 is not a mistake: a stop
     smaller than the one before it collapses to a hard edge, which is what
     gives the line a head rather than a fade. */
  /* currentColor, WAS var(--red). Greg, 2026-08-26: with the label now warm
     brown the red arc read as a third colour arriving on hover. currentColor
     makes the ring the same value as the words and the dot, so the button is
     one colour throughout and there is still only ONE declaration to change --
     .btn--solid's `color`. Nothing here needs editing again if it moves.

     A 1px arc is not text, so the 4.5:1 floor does not bind it -- the same
     reason the map's underline may be brighter than a label. */
  background: conic-gradient(from -90deg, currentColor var(--btn-sweep), transparent 0);
  /* `linear-gradient(#000 0 0)` is the shorthand usually quoted for this and it
     does not work: both stops land at 0px, the mask computes to a zero-length
     gradient and the whole pseudo-element disappears instead of a ring being
     cut from it. Two explicit stops paint solid. */
  -webkit-mask: linear-gradient(#000, #000) content-box, linear-gradient(#000, #000);
          mask: linear-gradient(#000, #000) content-box, linear-gradient(#000, #000);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  transition: --btn-sweep 0.75s var(--ease-out);
}
.btn--solid:hover,
.btn--solid:focus-visible {
  /* A shade deeper, and no more. The ring is the event; a solid ground would
     swallow the line it is drawing. */
  background: rgba(var(--teal-rgb), 0.14);
  color: var(--ink);
}
.btn--solid:hover::before,
.btn--solid:focus-visible::before { --btn-sweep: 360deg; }
/* The dot travels the last of the way with the line — it already sits at the
   end of the label, so it reads as the head of the stroke arriving. */
.btn--solid .btn-dot { transition: transform 0.75s var(--ease-out), background-color 0.4s; }
.btn--solid:hover .btn-dot { transform: translateX(4px); }
@media (prefers-reduced-motion: reduce) {
  .btn--solid::before { transition: none; }
}

.btn--ghost { border: 1px solid var(--teal-hair); color: var(--ink); }
.btn--ghost:hover { border-color: var(--saffron); background: var(--saffron-wash); }
.btn--light { background: var(--white); color: var(--ink); }
.btn--light:hover { background: var(--teal); color: var(--white); }

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--teal);
}
.link-arrow svg { transition: transform 0.4s var(--ease-out); }
.link-arrow:hover svg { transform: translateX(5px); }

/* --- header ------------------------------------------------------------- */

.header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: transform 0.5s var(--ease-out), border-color 0.5s var(--ease-soft);
  /* The bar is a moving element with a backdrop-filter on it, which is the one
     combination browsers re-rasterise every frame. Promoting it keeps the blur
     on its own layer while it slides. */
  will-change: transform;
}
.header.is-stuck { border-bottom-color: var(--hairline); }
.header.is-hidden { transform: translateY(-100%); }

.header-inner {
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
}

/* The wordmark — two stacked lines, Italiana, all caps.

   GRAN CANARIA is tracked out to roughly the width of GAY MASSAGE rather than
   being stretched to it. Optical, not forced: the four-word stack on the old
   logo was force-justified, and that is exactly what made GAY the biggest word
   on the mark, which fights the positioning. Regrouped to 12 characters over
   11, the two lines are near enough the same width naturally.

   The 0.34em tracking is doing the legibility work that a bold weight would
   normally do. Italiana ships in one weight, so there is no bold available. */
.wordmark {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.18em;
  font-family: var(--display);
  line-height: 1;
  color: var(--ink);
  white-space: nowrap;
}
/* Size and tracking are tuned by measurement, not by eye: at 0.73rem with
   0.60em tracking this line renders 169px against GAY MASSAGE's 168px, so the
   two edges line up without either being stretched. Re-measure if the big
   line's size changes — the match is not automatic. */
.wordmark-sm {
  font-size: 0.73rem;
  letter-spacing: 0.60em;
  /* tracking adds a trailing space after the last glyph; pull it back so the
     line stays optically centred over the one below */
  text-indent: 0.60em;
  color: var(--brown);
  text-transform: uppercase;
}
.wordmark-lg {
  font-size: 1.62rem;
  letter-spacing: 0.02em;
  color: var(--teal);
  text-transform: uppercase;
}
/* Saffron measures 2.02:1 on white, which fails WCAG for text. It is allowed
   here and nowhere else: WCAG 1.4.3 exempts logotypes, and this is the brand
   colour that is live on the server today. Do not reuse it for type. */
.wordmark-lg em { font-style: normal; color: var(--saffron); }

.nav { display: flex; align-items: center; gap: 2.1rem; }
.nav a {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--body);
  position: relative;
  padding-block: 0.35rem;
  transition: color 0.35s var(--ease-soft);
}
.nav a::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: var(--saffron);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.45s var(--ease-out);
}
.nav a:hover { color: var(--ink); }
.nav a:hover::after { transform: scaleX(1); }

/* --- the Services submenu ------------------------------------------------- */
/* Added 2026-08-27 when coaching.php earned a place in the bar. THE BAR STAYS
   SIX WORDS -- the submenu grows downward, where width is free, which is also
   why the third item is written out in full rather than abbreviated.

   NO JAVASCRIPT, AND NO BUTTON. :hover for the pointer, :focus-within for the
   keyboard -- tabbing into the parent link opens the panel and tabbing out of
   the last child closes it, which is the whole behaviour a menu of three static
   links needs. A button with aria-expanded would be the right answer if the
   panel held anything other than links, and it would need JS to be honest.

   NO GAP BETWEEN THE LINK AND THE PANEL, and it is load-bearing rather than
   cosmetic: `top: 100%` on a group whose height already includes .nav a's
   0.35rem padding-block means the panel starts on the link's own bottom edge.
   Open a gap there and the pointer crosses dead space on the way down, the
   :hover drops, and the menu closes under the cursor.

   IT ESCAPES THE HEADER BY DEFAULT. .header is a fixed bar with a set height
   and no overflow rule, and its backdrop-filter makes it a containing block
   only for FIXED descendants -- this panel is absolute against .nav-group, so
   it paints outside the bar without anything being loosened to let it.

   BELOW 900px THE WHOLE NAV IS HIDDEN and the hamburger takes over, so there is
   deliberately no small-screen behaviour here. The submenu becomes the phone
   design's problem on the day that menu is built, and the markup it will need
   -- a nested list inside the item -- is already what is in the HTML.

   ===== REWORKED TWICE ON 2026-08-27, and the second pass is the one to read.
   Greg on the first: "subtle, okay, but somehow missing something", the box
   wider than its contents, and a beige fill where the rest of the bar draws a
   line. On the second: the rule spanned the ROW instead of the WORD, which is
   not what .nav a does -- his catch, and he was right. Where it landed:

   1. IT IS THE BAR, CONTINUED, NOT A CARD. Same translucent white and the same
      14px backdrop blur as .header, and NO border and NO shadow. Those two were
      the whole of the card feeling; the surface was never the problem. The fill
      stays because 11px type over whatever the page happens to be showing is
      not readable, and the blur is what makes it legible without a hard edge.
   2. A VERTICAL RULE DOWN THE LEFT, which is Greg's idea and it is already the
      site's idiom -- .tr-note, .faq-notice and .tp-out all hang their contents
      off a rule on that side. It gives the three items a spine to sit against
      now that there is no box around them.
   3. THE SAFFRON LINE IS THE WIDTH OF THE WORD. .nav a::after is left:0 /
      width:100% on an anchor whose box IS the word, so the rule only stays
      honest here if the anchor is sized to its text: the LI carries the padding
      and the A is inline-block. The row is still comfortable to hit, the gaps
      between words are inside the panel so :hover on .nav-group never drops,
      and the ::after needs no override at all beyond its offset.
   4. THE ITEMS ARRIVE IN SEQUENCE, 60ms apart, the same delay chain the
      Services index uses on its four bands. The delays live only in the OPEN
      state, so closing is immediate -- a staggered close reads as lag. */
/* THE ANCHOR MUST BLOCKIFY. Every other item in the bar is a direct flex
   child of .nav and is therefore blockified; wrapping Services in a DIV
   left its <a> as an INLINE box, which ignores padding-block for layout --
   measured 25.19px tall against its neighbours' 31.58, sitting 2.47px
   higher. .nav a::after hangs off bottom:0 of that box, so the saffron rule
   under SERVICES was drawing 2.47px above every other one. Making the group
   a flex container makes the <a> a flex item again and the box matches.
   .nav-sub is absolutely positioned, so it is not a flex item and is
   unaffected. */
.nav-group { position: relative; display: flex; align-items: center; }
.nav-sub {
  position: absolute;
  top: 100%;
  /* the word lands under the parent label: the panel is pulled left by its own
     padding plus the rule's inset */
  left: -1.6rem;
  margin: 0;
  padding: 0.55rem 1.1rem 0.7rem 1.6rem;
  list-style: none;
  width: max-content;
  /* 0.96, NOT THE HEADER'S 0.82, AND IT IS A LEGIBILITY FIX RATHER THAN A
     STYLE CHOICE. The bar sits over whatever happens to be at the top of the
     scroll and the blur carries it; this panel sits over the page-head's own
     lede, and at 0.82 the sentence underneath read straight through the menu.
     Measured on screen, not guessed. The blur stays -- it is what keeps the
     edgeless panel from looking like a floating rectangle. */
  background: rgba(255, 255, 255, 0.96);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity 0.28s var(--ease-soft),
              transform 0.28s var(--ease-out),
              visibility 0.28s;
  z-index: 5;
}
/* the spine. Inset from the panel edge rather than sitting on it, so it reads
   as a mark beside the words and not as the border that was just removed. */
.nav-sub::before {
  content: '';
  position: absolute;
  left: 0.7rem;
  top: 0.7rem;
  bottom: 0.85rem;
  width: 1px;
  /* saffron, not the grey hairline: the spine and the animated rule under each
     word are the same mark in two directions, and at --saffron-hair's 0.42 it
     is a warm line rather than a border */
  background: var(--saffron-hair);
}
.nav-group:hover .nav-sub,
.nav-group:focus-within .nav-sub {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
/* the padding is on the LI so the A can be the width of its own word */
.nav-sub li {
  margin: 0;
  padding: 0.42rem 0;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 0.3s var(--ease-soft), transform 0.3s var(--ease-out);
}
.nav-group:hover .nav-sub li,
.nav-group:focus-within .nav-sub li { opacity: 1; transform: none; }
.nav-group:hover .nav-sub li:nth-child(2),
.nav-group:focus-within .nav-sub li:nth-child(2) { transition-delay: 0.06s; }
.nav-group:hover .nav-sub li:nth-child(3),
.nav-group:focus-within .nav-sub li:nth-child(3) { transition-delay: 0.12s; }
.nav-sub a {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--body);
}
.nav-sub a:hover { color: var(--ink); }
/* the browser default is a dotted underline, which would read as a second rule
   under a word that already gets the saffron one on hover */
.nav-sub abbr {
  text-decoration: none;
  border-bottom: 0;
  cursor: inherit;
}

.header-right { display: flex; align-items: center; gap: 1rem; }
.header-phone {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
}

/* Two lines at the right end of the bar: the number, and the flags under it.
   The stack is centred in the bar as a block, which is why the number sits
   higher than it used to — it is no longer the only thing on that side.

   The flags are centred on the number rather than hung off the bar's right
   edge: they are a set of three and they belong to the number above them, so
   the two lines read as one block. */
.header-contact {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.32rem;
  line-height: 1;
}
.header-langs { display: flex; align-items: center; gap: 0.4rem; }
/* Full colour, all three. The flags are 30px files shown at 20px — drawn down
   rather than up, so they stay crisp on a retina screen.

   NOT LINKS, and they never were meant to be — Greg, 2026-08-21. They used to
   be anchors pointing at "#", which is a link to nowhere on every page of the
   site. This site ships in English only and always will, so there is no
   translation to switch to: the flags are an indicator that a caller may speak
   any of these three fluently. The alt text carries that, each in its own
   language, which is also the only part a screen reader gets. Same treatment
   as the footer's row, which was built this way from the start. */
.header-langs img { display: block; width: 20px; height: 20px; }

/* M3 FIXED 2026-08-22. The BUTTON is 44x44, the ICON is still 28x20.
   It was 28x20 all through, against the 44x44 minimum, and it is the only
   control in the header on a phone — the one that has to be easy to hit.

   The bars are centred inside the larger box rather than the icon being grown:
   growing it would have changed the header's look to fix a hit area nobody can
   see. margin-right -8px pulls the extra width back out of the layout so the
   bar's spacing is exactly where it was.

   The three tops are 12/21/30 rather than 0/9/18 — the same 9px apart, shifted
   down by (44-20)/2 = 12 to sit in the middle of the taller box. The 9px
   translate in the open state is untouched and still lands on the centre bar. */
.menu-toggle {
  display: none;
  width: 44px;
  height: 44px;
  margin-right: -8px;
  position: relative;
}
.menu-toggle span {
  position: absolute;
  left: 8px;
  width: 28px;
  height: 1.5px;
  background: var(--ink);
  transition: transform 0.45s var(--ease-out), opacity 0.3s;
}
.menu-toggle span:nth-child(1) { top: 12px; }
.menu-toggle span:nth-child(2) { top: 21px; }
.menu-toggle span:nth-child(3) { top: 30px; }
.menu-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }

.mobile-menu {
  position: fixed;
  inset: var(--header-h) 0 0;
  z-index: 99;
  background: var(--white);
  padding: 2.5rem var(--gutter) 3rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  transform: translateY(-12px);
  opacity: 0;
  pointer-events: none;
  overflow-y: auto;
  transition: opacity 0.4s var(--ease-soft), transform 0.5s var(--ease-out);
}
.mobile-menu.is-open { opacity: 1; transform: none; pointer-events: auto; }
.mobile-menu a {
  font-family: var(--serif);
  font-size: clamp(1.9rem, 8vw, 2.75rem);
  line-height: 1.35;
  color: var(--ink);
  border-bottom: 1px solid var(--hairline);
  padding-block: 0.5rem;
}
.mobile-menu .mobile-menu-foot {
  margin-top: auto;
  padding-top: 2rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.mobile-menu .mobile-menu-foot a {
  font-family: var(--sans);
  font-size: 1rem;
  border: 0;
  padding: 0;
  color: var(--body);
}
/* The number and its icons on one row. The flags moved here from the header,
   where they sat beside the burger and qualified nothing — this site is
   English only, so they are not a language switcher; they say which languages
   a caller may use, and that only means something next to the number. Wraps
   rather than squeezes: at 320px the icon group drops under the number instead
   of the row overflowing. */
.mobile-menu .mm-reach { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 0.85rem; }
.mobile-menu .mm-icons { display: flex; align-items: center; gap: 0.45rem; }
.mobile-menu .mm-icons img { display: block; width: 20px; height: 20px; }
/* Same divider and the same extra 2px the footer gives it — a round mark reads
   smaller than a rectangle beside it at the same height. The rule above sets
   padding: 0 and border: 0 on every anchor in this block, so both are restated
   here rather than inherited. */
.mobile-menu .mobile-menu-foot .mm-wa {
  display: block;
  line-height: 0;
  margin-left: 0.2rem;
  padding: 0 0 0 0.7rem;
  border-left: 1px solid var(--hairline);
}
.mobile-menu .mobile-menu-foot .mm-wa img { display: block; width: 22px; height: 22px; }

/* --- scroll odometer (right edge) --------------------------------------- */

/* Sized to fit inside the page gutter — max-width is 1280px, so at 1440 there
   are 80px each side. A numeral wider than that lands on top of the content;
   it did, over the studio images, before this was clamped. */
.odometer {
  position: fixed;
  right: clamp(0.5rem, 1.5vw, 1.25rem);
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 0.1rem;
  pointer-events: none;
  mix-blend-mode: multiply;
}
.odometer-window {
  height: 1.05em;
  overflow: hidden;
  font-size: clamp(2rem, 3.4vw, 3.5rem);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.04em;
  color: var(--saffron-wash);
}
.odometer-reel {
  display: flex;
  flex-direction: column;
  transition: transform 0.75s var(--ease-out);
}
.odometer-reel span { height: 1.05em; display: block; }

/* --- hero --------------------------------------------------------------- */

/* --- hero plate: one photograph across a two-tone ground ------------------
   Everything is derived from one number. --shot-w sets the picture's width,
   its height follows from 16:9, and the band's depth is the header, plus the
   gap above the picture, plus two thirds of the picture. Change --shot-w and
   the whole arrangement stays in proportion; change --shot-share to move the
   join up or down the photograph. */
.hero-plate {
  --shot-w: min(1040px, calc(100vw - 2 * var(--gutter)));
  --shot-h: calc(var(--shot-w) * 9 / 16);
  --shot-top: clamp(2rem, 4vw, 3.5rem);
  --shot-share: 0.667;        /* how much of the picture sits on the dark */
  position: relative;
  padding: calc(var(--header-h) + var(--shot-top)) 0 clamp(4rem, 8vw, 7rem);
}
/* The band. Starts below the fixed header rather than at the top of the page. */
.hero-plate::before {
  content: '';
  position: absolute;
  left: 0; right: 0;
  top: var(--header-h);
  height: calc(var(--shot-top) + var(--shot-h) * var(--shot-share));
  background: var(--surface);
}
/* The white card of the SHADOW frame — section 02 in FRAMES.html. It is a
   plain white rectangle with the photograph inset 6px, so it is written as a
   background and a padding rather than cut out of the PNG: there is nothing in
   it to reproduce. The shadow is the part that cannot be faked, and that stays
   the PNG below.

   Above the join the card reads as a white frame against the beige. Below it,
   card and body are both white and the frame disappears, leaving the shadow to
   do the work — which is exactly what the old site did. */
.hero-shot {
  position: relative;
  z-index: 1;
  width: var(--shot-w);
  margin: 0 auto;
  background: var(--white);
  padding: 6px;
}
.hero-shot img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05);
}
/* Bottom edge only — see the note in index.php. Same border-image approach as
   .duo-frame::after, and the same reason: the picture is fluid and the PNG is
   not, so the shadow keeps its 25px and only its soft middle stretches. */
.hero-shot::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 0 25px 0;
  border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 25px 0 stretch;
  pointer-events: none;
}

.hero {
  position: relative;
  min-height: 100svh;
  display: flex;
  align-items: flex-end;
  padding-block: calc(var(--header-h) + 3rem) clamp(3rem, 7vw, 6rem);
  overflow: hidden;
}
.hero-slider { position: absolute; inset: 0; }
.hero-slide {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  filter: var(--photo-filter);
  opacity: 0;
  transform: scale(1.06);
  transition: opacity 1.6s var(--ease-soft), transform 7s linear;
}
.hero-slide.is-active { opacity: 1; transform: scale(1); }
/* Three layers, not one. The single bottom-up gradient left the headline
   sitting on bare photograph in landscape, where the text block rides higher —
   measured at 844x390 and it was unreadable over slider-3. The flat wash is the
   floor that makes it independent of which photograph is showing; the
   left-to-right pass buys back contrast under the text column specifically. */
.hero-scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to right, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.45) 45%, rgba(255,255,255,0.15) 100%),
    linear-gradient(to top, rgba(255,255,255,0.97) 0%, rgba(255,255,255,0.7) 40%, rgba(255,255,255,0.35) 72%, rgba(255,255,255,0.5) 100%),
    rgba(255, 255, 255, 0.3);
}

/* the two animated path lines — drawn as the page scrolls */
.hero-paths {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.hero-paths path {
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.25;
  opacity: 0.55;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
/* The lower of the two runs warm. Putting the saffron in the hero means the
   warm accent is on screen before the visitor scrolls at all — which was the
   whole problem with the first pass: orange existed only in the logo. */
.hero-paths path#path-b {
  stroke: var(--saffron);
  opacity: 0.7;
}

/* --- section lines — the hero technique, reused further down the page ----
   Two non-adjacent sections only. Non-adjacent means no two lines ever meet,
   so nothing has to align across a section boundary when the text reflows in
   ES / DE / PL. Each SVG is sized to its own section and never larger, so the
   repaint area stays one screen rather than the whole document.

   Scroll-linked since 2026-08-17, like the band and the statement lines: the
   draw is a function of the section's scroll position, so it runs backwards
   when you scroll back up. It used to draw once on entry via .is-in and then
   stay drawn forever, which is why it read as the one static set on the page.
   The .is-in observer still runs — the .reveal content animations need it. */
.section--lined { position: relative; overflow: hidden; }
.section--lined > .container { position: relative; z-index: 1; }

.section-lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 0;
}
/* --- THE SPINE: two copies of the drawing, joined -----------------------
   The home page's .band-lines brought onto a white page, and then doubled.

   THE WRAPPER IS WHAT MAKES THE JOIN FREE. Both svgs are ordinary blocks
   inside .spine at width 100%, so the second stacks directly under the first
   with no arithmetic -- no `top` to keep in step with a width that is a
   clamp. Sized by WIDTH, height derived from aspect-ratio, because the first
   version was sized by height like the home page's and a 3,303px section gave
   back a drawing 1,057px wide straight through the text.

   It starts at the top edge of the WHITE section, not the beige one, and runs
   behind the letters -- .section--lined puts .container at z-index 1, so text
   always wins. Kept at every width: it lives in the space the column leaves,
   which exists on a phone too.

   COLOUR AND WEIGHT are the two things a white ground changes. The band's line
   is white with a dark drop-shadow because it lies on a photograph; here it
   takes teal with the saffron ghost, and drops from 2px to 1.4px -- 2px on a
   photograph is a hairline, 2px on paper is a rope. */
/* WHERE IT STARTS IS NOT A CHOICE ANY MORE -- Greg, 2026-08-24. The chain is
   the masthead line continuing, so it begins at the x where the masthead line
   leaves the beige band, and it has to hold that at every viewport width.

   MEASURED, NOT EYEBALLED, and the measurement corrects the number in the
   brief. The mirrored practitioner path's DATA ends at x 760, but that point
   is at y 912 in a 900-tall viewBox -- 4px below the band, clipped by
   .section--lined's overflow. Where the stroke actually crosses the band's
   bottom edge is x 818.3, which is 56.83% of the 1440 viewBox. Both svgs span
   the full page width with preserveAspectRatio="none", so a percentage of the
   section is the same x on both.

   The drawing's own top point is at x 191.5 of its 880 viewBox -- 0.2176 of
   the box -- so the box is pushed left by that much of its own width and the
   line, rather than the box, lands on the origin. */
.spine {
  --spine-w: clamp(150px, 19vw, 260px);
  --spine-origin: 56.83%;
  position: absolute;
  top: 0;
  left: calc(var(--spine-origin) - var(--spine-w) * 0.2176);
  width: var(--spine-w);
  pointer-events: none;
  z-index: 0;
}
.spine-lines {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 880 / 2256;
  overflow: visible;
}
/* EVERY OTHER COPY IS UPSIDE DOWN. Its top point is the previous copy's end
   point, so consecutive copies touch at x 500 and x 191.5 alternately and the
   whole chain reads as one line. The copies are generated by main.js, which
   also puts this class on the odd-numbered ones. See the note in the markup. */
.spine-lines--flip { transform: scaleY(-1); }
.spine-lines path {
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.4;
  stroke-linecap: round;
  opacity: 0.5;
  /* NEVER vector-effect: non-scaling-stroke -- it breaks pathLength and the
     dash silently stops meaning "the whole path". */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.spine-lines path.is-ghost { stroke: var(--saffron); stroke-width: 1; opacity: 0.42; }

/* A BAND, NOT THE WHOLE SECTION. .section-lines is inset:0, which is right
   for a section around 1000px tall -- the height .section--lined was built
   against. The testimonials list is 3,303px, so the 900-unit viewBox stretched
   3.7x and the curve came out as a near-vertical smear drifting down three
   screens: not a line crossing the page, which is what it is for. Measured;
   at page scale it read as nothing at all.

   Pinned to the top at a fixed height instead, so the drawing keeps roughly
   the proportions it was drawn at however long the section below it runs. */
.section-lines--band {
  bottom: auto;
  height: clamp(240px, 30vw, 420px);
}
/* The third pair, dropped into the middle of the list. A line at the top of a
   3,300px section leaves three screens of letters with nothing in them, which
   is the monotony the whole exercise is about -- and unlike the top two this
   one is genuinely below the fold, so it is the pair that actually draws
   itself in front of the reader as they scroll. */
.section-lines--mid { top: 42%; }

.section-lines path {
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.25;
  /* vector-effect: non-scaling-stroke REMOVED 2026-08-17. It was here to stop
     the stroke distorting with the stretched viewBox, and it was silently
     truncating both lines instead: it makes the dash pattern measure in screen
     pixels, which breaks pathLength="1" normalisation, so the single
     full-length dash no longer matches the path's rendered length and the tail
     of every line fell into the gap. Measured at 1440x900 — the lines stopped
     around 60% of the way along and had never once drawn to their own end.
     This is OPEN-BUGS.md #1, which was fixed on .band-lines and missed here.

     What it cost to remove: the section box is 1425x982 for a 1440x900
     viewBox, so the axes scale 0.989 and 1.091 — the stroke renders between
     1.24px and 1.36px depending on which way the curve runs. A 10% variation
     on a 1.25px hairline is not visible. A line that stops two thirds of the
     way across is. */
  opacity: 0.5;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  /* No transition here on purpose, same as .band-lines: JS writes
     stroke-dashoffset every frame and a transition on top of that fights the
     lerp — the line would chase a value that is itself already easing. */
}
/* The second line was staggered with transition-delay: .35s. That delay went
   with the transition; the equivalent lag is now SECTION_LINE_LAG in main.js. */
.section-lines path:nth-child(2) {
  stroke: var(--saffron);
  opacity: 0.6;
}

/* --- THE SECOND LINE, WHERE IT IS ITS OWN SVG ---------------------------
   The rule above pairs two paths inside ONE drawing. The services index needs
   the pair split across two svgs instead, because they are driven differently:
   the blue one draws once on arrival and stays, the saffron one is
   scroll-linked in both directions. Same colours, same relationship, two
   independent sets in main.js.

   Greg, 2026-08-26: "a second line, yellow or orange, the same colour that we
   have on the home page in the hero section, animated up and down upon
   scroll."

   A HAIR LIGHTER THAN THE BLUE ONE, on purpose. Both lines cross the same
   words; at equal weight the pair reads as a rail rather than as one line
   following another. 1.1px against the blue 1.25px is the smallest difference
   that separates them without making the saffron look broken. */
.section-lines--follow {
  /* THE LINE LEAVES ITS OWN BOX AT THE TOP, on purpose. It runs to the LEFT of
     the blue curve, and the blue curve starts at x 0 -- the divider's own
     left corner -- so its companion has to start at a negative x or the two
     would touch at the one place the design most wants them separated. An svg
     clips to its viewBox by default and the first 16px would just be gone. */
  overflow: visible;
}
.section-lines--follow path {
  stroke: var(--saffron);
  stroke-width: 1.1;
  opacity: 0.6;
}

.hero-inner { position: relative; z-index: 2; width: 100%; }
.hero-title { margin: 1.75rem 0 0; }
.hero-title .line { display: block; overflow: hidden; }
.hero-title .line > span {
  display: block;
  transform: translateY(105%);
  transition: transform 1.15s var(--ease-out);
}
.hero.is-ready .hero-title .line > span { transform: none; }
/* The single red word on the first screen. Red is the emphasis accent and
   it is spent about once per section — here, on the word the whole hero is
   built around. */
.hero-title em { font-style: italic; color: var(--red); }

.hero-foot {
  margin-top: clamp(2rem, 4vw, 3.25rem);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 2rem;
  flex-wrap: wrap;
}
.hero-lead { max-width: 34ch; font-size: 1.0625rem; line-height: 1.7; margin: 0; }

.hero-anim { opacity: 0; transform: translateY(22px); transition: opacity 0.9s var(--ease-soft), transform 0.9s var(--ease-out); }
.hero.is-ready .hero-anim { opacity: 1; transform: none; }
.hero.is-ready .hero-anim--1 { transition-delay: 0.15s; }
.hero.is-ready .hero-anim--2 { transition-delay: 0.75s; }
.hero.is-ready .hero-anim--3 { transition-delay: 0.9s; }

.hero-dots { display: flex; gap: 0.55rem; }
.hero-dot {
  width: 34px;
  height: 2px;
  background: var(--teal-wash);
  border-radius: 2px;
  position: relative;
  overflow: hidden;
}
.hero-dot::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--teal);
  transform: scaleX(0);
  transform-origin: left;
}
.hero-dot.is-active::after { animation: dotFill 6s linear forwards; }
@keyframes dotFill { to { transform: scaleX(1); } }

/* --- reveal primitives -------------------------------------------------- */

.reveal { opacity: 0; transform: translateY(28px); transition: opacity 0.9s var(--ease-soft), transform 1s var(--ease-out); }
.reveal.is-in { opacity: 1; transform: none; }
.reveal--left  { transform: translateX(-34px); }
.reveal--right { transform: translateX(34px); }
.reveal--left.is-in, .reveal--right.is-in { transform: none; }
/* --- DISTINCT REVEALS, not one direction for the whole page -------------
   Greg, 2026-08-24: everything arrived from below, so the page had one gesture
   and repeated it. Three now, and each one is chosen for what the element is
   rather than for variety's sake.

   .reveal--wipe -- UNVEILED, NOT MOVED. clip-path opens from the bottom edge
   while the type stays exactly where it will end up, so nothing slides under
   the reader's eye. That is what a masthead wants: the h1 is the first thing
   read and movement in the glyphs themselves fights the reading. The 6px lift
   is there only so it does not look mechanically flat -- it is a fifth of what
   the default .reveal travels.

   Two things clip-path needs and neither is optional. The transition has to
   name clip-path explicitly or it snaps. And a clipped element cannot paint
   outside its box, so descenders and the italic's overhang need the inset to
   open past 100% -- hence -10% on the closed state's sides, which gives the
   glyphs room rather than shaving them.

   .reveal--rise -- THE STATEMENT'S OWN RULE DRAWS ITSELF. The blockquote
   already carries a 2px saffron border-left; here it scales from nothing to
   full height first, and the words follow it. The element's existing design is
   the animation, which is why this one belongs to the statement and to nothing
   else: it draws the eye down the rule to the paragraph the page exists to
   deliver. The border is transform-scaled on a pseudo-element rather than
   animating height -- height animates layout, transform does not.

   The left/right pair below is the old primitive and is unchanged; it is what
   the four service blocks now alternate on. */
/* THE SERVICE BLOCKS THROW FURTHER THAN THE PRIMITIVE. .reveal--left is 34px,
   which is right for a heading or a caption and invisible on a block 1080px
   wide -- the alternation Greg asked for only reads if the movement is a real
   fraction of the thing moving. 72px is about a fifteenth of the block and
   lands as a direction rather than a twitch.

   The overshoot is clipped, not scrolled: body carries overflow-x:hidden, so a
   block sitting 72px past the right edge mid-flight adds no scrollbar.
   Verified rather than assumed -- a horizontal scrollbar appearing for one
   second on arrival is exactly the kind of thing that is never noticed in
   review and always noticed by a visitor.

   Slower than the primitive too, 1.25s against 1s. A bigger object covering a
   longer distance at the same speed reads as flung; the extra quarter-second
   is what keeps four of them in a column calm. */
.svc.reveal--left  { transform: translateX(-72px); }
.svc.reveal--right { transform: translateX(72px); }
.svc.reveal--left, .svc.reveal--right {
  transition: opacity 1.1s var(--ease-soft), transform 1.25s var(--ease-out);
}
.svc.reveal--left.is-in, .svc.reveal--right.is-in { transform: none; }

.reveal--wipe {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.7s var(--ease-soft), transform 0.9s var(--ease-out);
}
.reveal--wipe.is-in { opacity: 1; transform: none; }
/* THE CLIP IS ON THE INNER SPAN AND THAT IS NOT TIDINESS, IT IS THE BUG.
   Put on the observed element itself, inset(... 100% ...) leaves it with zero
   visible area -- so intersectionRatio is 0, it never reaches the observer's
   0.15 threshold in main.js, and the element can never trigger the animation
   that would open it. The start state deadlocks its own trigger, silently:
   the markup and the CSS both read as correct and the heading simply never
   appears. Found by measuring is-in, not by reading this file.

   The outer element keeps .reveal and stays unclipped, so it is what the
   observer sees; the span does the wiping and is observed by nobody. */
.reveal--wipe > span {
  display: inline-block;
  clip-path: inset(0 -10% 100% -10%);
  transition: clip-path 1.1s var(--ease-out);
}
.reveal--wipe.is-in > span { clip-path: inset(0 -10% 0 -10%); }

.reveal--rise { opacity: 0; transform: none; transition: opacity 0.8s var(--ease-soft) 0.35s; }
.reveal--rise.is-in { opacity: 1; }
/* The rule is the border, moved onto a pseudo so it can be scaled. The
   blockquote's own border-left is dropped here rather than in .statement-turn,
   so that class stays correct anywhere else it is used. */
.reveal--rise.statement-turn { border-left: 0; position: relative; }
.reveal--rise.statement-turn::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: var(--saffron);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 0.9s var(--ease-out);
}
.reveal--rise.statement-turn.is-in::before { transform: scaleY(1); }

.reveal-d1 { transition-delay: 0.1s; }
.reveal-d2 { transition-delay: 0.2s; }
.reveal-d3 { transition-delay: 0.3s; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .reveal, .hero-anim, .hero-title .line > span { opacity: 1 !important; transform: none !important; }
  /* clip-path and the drawn rule are not transforms, so the blanket rule above
     does not reach them -- they have to be named or the statement keeps a rule
     that never draws and the masthead stays clipped shut. */
  .reveal--wipe > span { clip-path: none !important; }
  .reveal--rise.statement-turn::before { transform: none !important; }
}

/* --- statement ---------------------------------------------------------- */

.statement-block { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2rem; }
.statement-block .eyebrow { margin-bottom: 0.5rem; }
.statement-body { max-width: 22ch; }

/* --- statement lines — the band's drawing, on its side -------------------
   Same pair and same scroll-linked draw as .band-lines; see the note in
   index.php and the statementFrame block in main.js. Two things differ:

   Colour. The band is white because it sits on a dark photograph. This is on
   the light background, where white is invisible, so it takes --teal, the
   colour the hero and section lines already use on light.

   Stroke. .band-lines deliberately has no vector-effect, because that breaks
   pathLength="1" normalisation and the dash stops meaning "the whole path".
   Same applies here — the box is scaled uniformly (no preserveAspectRatio
   override), so the stroke needs no correcting anyway.

   The block is wider than the paragraph it sits under and is allowed to bleed
   past the container on both sides, because the drawing's interest is in its
   loops and cropping them to a text column wastes them. */
.statement-lines {
  display: block;
  width: 100%;
  height: auto;
  margin-top: clamp(1.5rem, 4vw, 3rem);
  pointer-events: none;
  overflow: visible;
}
/* Heavier than .band-lines, and deliberately. That box renders 421px wide for
   880 units, so its 2px stroke lands at ~1px on screen — enough, because white
   on a dark photograph is high contrast. This box is 1136px wide for 2256
   units, almost exactly the same 0.5 scale, but sage on white is a fraction of
   that contrast and 1px vanished. Measured at 1440x900: 3 units = 1.5px. */
.statement-lines path {
  fill: none;
  stroke: var(--teal);
  stroke-width: 3;
  stroke-linecap: round;
  opacity: 0.7;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.statement-lines path.is-ghost {
  /* Saffron, not a dimmer teal. The ghost is the one place the warm accent can
     sit at full chroma without competing with type, and it is what stops the
     page reading as blue-and-white. Slightly stronger opacity than the old
     mono ghost because saffron is lighter in value than teal — at 0.3 it
     disappeared on white. */
  stroke: var(--saffron);
  opacity: 0.62;
  stroke-width: 1.9;
}

/* --- the flattened second pair -------------------------------------------
   Same drawing, same viewBox, box 40% as tall. preserveAspectRatio="none" in
   the markup compresses the 880-unit height into it.

   The cost: that is a non-uniform scale, so the stroke is squashed with the
   geometry — full weight where the curve runs vertically, 40% where it runs
   horizontally. vector-effect: non-scaling-stroke is the textbook fix and is
   NOT usable here: it measures the dash pattern in screen pixels, which breaks
   pathLength="1" so stroke-dasharray:1 stops meaning "the whole path" and
   becomes a 1px dash. That bug already cost a debugging session once — see
   OPEN-BUGS.md and the note on .band-lines path.

   So the stroke is thickened instead. The rendered width at tangent angle t is
   w * 0.4 / sqrt(cos²t + 0.16sin²t), i.e. 40% of w where the curve runs
   horizontally and 100% where it runs vertically. This drawing, on its side,
   is mostly horizontal running — so the horizontal case is the one to match to
   the pair above, and the loop sides are allowed to come out heavier. At 6.2
   units the long runs land at 1.25px against that pair's uniform 1.51px, and
   the loop sides peak at 3.12px. Deliberately not matched exactly: pushing the
   runs to a full 1.51 would take the peaks past 3.7px and the weight variation
   starts reading as a mistake rather than as a flattened line. */
/* Mirrored top to bottom, so the flattened pair is not just a smaller repeat
   of the one above but its reflection — the two curve away from each other
   instead of running the same way twice.

   Done here rather than in the <g> transform in main.js on purpose: that <g>
   is shared by every .statement-lines box, and this is the only one that
   flips. A CSS transform on the box also leaves the draw untouched — the flip
   is on the horizontal axis, so left to right stays left to right, and
   stroke-dashoffset does not care about it either way. */
.statement-lines--low {
  aspect-ratio: 2256 / 352;   /* 352 = 880 * 0.4 */
  height: auto;
  margin-top: clamp(1rem, 2.5vw, 1.75rem);
  transform: scaleY(-1);
  transform-origin: center;
}
.statement-lines--low path { stroke-width: 6.2; }
.statement-lines--low path.is-ghost { stroke-width: 3.5; }

/* word-by-word reveal (philosophy + statement) */
/* The what-this-is paragraph. Sans and sentence case, bigger than body copy
   and smaller than a display line, JUSTIFIED so both edges are flush and only
   the last line runs short.

   62ch is the measure and it is doing real work: justified text sets its word
   spaces to fill the line, so a long measure at a large size opens rivers
   through the paragraph. `hyphens: auto` is the other half of that and needs
   the lang attribute on <html>, which this page has. */
.statement-set {
  font-family: var(--sans);
  /* WAS calc(clamp(1.9rem, 3.9vw, 3.45rem) * 0.6) — a copy of the H1's own
     clamp taken to 60%, which once made the paragraph exactly as wide as the
     headline. That relationship died when the measure went 31em -> 38em: the
     paragraph is now deliberately WIDER than the headline, so the tie to the
     H1's numbers was doing nothing but hiding the real size. Written as its
     own clamp now. Floor and slope are unchanged; only the CEILING moved.

     Greg, 2026-08-22: too big on the desktop. 33.1px -> 25.6px, which is the
     halfway point between where it was and .body-text's 18px.

       floor 1.14rem = 18.24px, binding below 779px
       slope 2.34vw
       cap   1.6rem  = 25.6px,  binding above ~1094px

     THE PHONE IS UNTOUCHED and that is the point: lowering a ceiling only
     affects wide screens, so nothing gets smaller as the window grows. A flat
     reduction would have broken the monotonic rule outright. The paragraph is
     also never smaller than .body-text — 18.24 against 18 at its narrowest.

     One knock-on, and it is an improvement: max-width is 38em, so the measure
     narrows with the type — 1259px at the old size, 973px now. Same character
     count, less window.

     Leading 1.32, down from 1.6. Justified text at a long measure wants tight
     leading: the eye returns on the vertical, and loose lines at 76 characters
     lose the row. */
  font-size: var(--read-size);
  line-height: 1.32;
  /* THE WIDTH IS NO LONGER TIED TO THE FONT. Greg, 2026-08-22, and he is right:
     shrinking the type must not shrink the column. `38em` did exactly that —
     the measure fell 1259px -> 973px the moment the size came down, because an
     em IS the font size.

     This clamp reproduces the width curve the paragraph had BEFORE the type
     changed, in viewport units, so it scales with the window instead:

       38 x 1.14rem = 43.32rem = 693px   (floor, below 779px)
       38 x 2.34vw  = 88.92vw            (slope)
       38 x 2.07rem = 78.66rem = 1258.6px (cap, above 1415px)

     Measured against the old effective widths, container included: 910px at
     1024, 1138 at 1280, 1136 at 1440, 1120 at 1920 — identical at all four.
     The .container's own 1280 less gutters is what binds from about 1300 up,
     which is why the last two do not reach the cap. */
  max-width: clamp(43.32rem, 88.92vw, 78.66rem);
  margin-inline: auto;
  text-align: justify;
  hyphens: auto;
  color: var(--ink);
  margin-block: 0;
}
/* Centred as a block, eyebrow included. The paragraph keeps its own justify —
   a parent's text-align does not reach into a child that sets its own. */
#what-this-is .statement-block { text-align: center; }

.words span {
  opacity: 0.16;
  transition: opacity 0.5s var(--ease-soft);
}
.words span.is-lit { opacity: 1; }

/* --- treatment cards ---------------------------------------------------- */

.cards {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1rem, 1.6vw, 1.5rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 20px;
  overflow: hidden;
  isolation: isolate;
  transition: border-color 0.6s var(--ease-soft), transform 0.7s var(--ease-out);
}
.card::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--teal-wash);
  transform: translateY(101%);
  transition: transform 0.75s var(--ease-out);
}
.card:hover { border-color: var(--saffron-hair); transform: translateY(-8px); }
.card:hover::before { transform: none; }

/* --- cards, layout B — full-bleed photo, copy over it --------------------
   The neighbour-ripple section (#treatments-b). Base height is --tch; the
   hovered card goes to 120% of it and its immediate neighbours to 110%.

   Two things make the growth behave:

   align-items:center — a taller item in a centred grid expands equally above
   and below its own centre line, which is what "10% up and 10% down" means.
   With the default stretch it would only ever grow downward.

   grid-auto-rows at the LARGEST a card can reach — the row is pre-sized to
   120%, so a card growing inside it changes nothing about the row. Without
   this the row tracks its tallest item, and every hover would shove the whole
   rest of the page down by up to 92px and back. The cost is dead space above
   and below the resting row, which is why the resting cards sit at 100% of a
   height chosen to look right, not at the row height. */
/* Eyebrow left, "All services" right, on one baseline. flex-end rather than
   center: the two have different cap heights and centring them left the link
   sitting visibly low against a 12px uppercase eyebrow. */
.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
}
.section-head-link {
  display: inline-flex; align-items: center; gap: .5rem;
  font-size: 12px; font-weight: 600; letter-spacing: .11em; text-transform: uppercase;
  color: var(--teal);
  white-space: nowrap;
  transition: color .35s var(--ease-soft), gap .35s var(--ease-out);
}
.section-head-link:hover { color: var(--red); gap: .85rem; }

.cards-tall {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  /* 1.1, was 1.2 — Greg, 2026-08-22: "when the images shrink, the gap is super
     huge". He is describing the reserve, and the reserve IS the growth.

     The row is held at the tallest a card can ever be so that nothing below it
     moves when the pointer arrives. The cost is that at rest, with every card
     at 1.0, the difference is dead space split above and below by
     align-items:center — at 1.2 that was 44px top and 44px bottom on a 440px
     card, 88px of nothing. At 1.1 it is 22px a side.

     There is no third option that keeps both: either the row reserves the
     growth and carries the gap at rest, or it sizes to the cards and the whole
     page below jumps every time the pointer crosses the row. Halving the growth
     halves the gap and keeps the ripple legible. */
  grid-auto-rows: calc(var(--tch) * 1.1);
  align-items: center;
  gap: clamp(1rem, 1.6vw, 1.5rem);
  /* 1.25rem, was clamp(2.5rem, 5vw, 4rem) — Greg, 2026-08-22: the same gap
     About puts between its eyebrow and the "I am Greg" paragraph, which is
     .duo-text > * + *. The two sections now open on identical spacing. */
  margin-top: 1.25rem;
  /* The reserve above already leaves .05 of --tch of clear space at the top of
     the row, so the measured gap to the eyebrow is 1.25rem PLUS that. Left as
     is deliberately: the two sections match on the number Greg asked them to
     match on, and the ripple's reserve is a separate decision. */
  --tch: clamp(300px, 32vw, 440px);
  /* One number for the whole effect. 2.5s was tried first and was too slow to
     complete during a normal pointer sweep — 300ms in, a card had moved only
     28% of the way. 1s, Greg's call. */
  --tcard-speed: 1s;
}

/* Square corners. No border-radius anywhere on this variant — the rounded
   corners belong to the card set above, and the two sections exist to be told
   apart. */
.tcard {
  position: relative;
  height: var(--tch);
  overflow: hidden;
  isolation: isolate;
  transition: height var(--tcard-speed) var(--ease-out);
}

/* THE IMAGE NEVER RESIZES. It is fixed at the tallest the card can ever be, so
   at rest the card is cropping 44px off the top and 44px off the bottom of it,
   and growing on hover opens that crop until the whole photograph is showing.
   The card is a window that widens; the picture behind it does not move and
   does not scale.

   This is why the height is tied to --tch * 1.2 and not to 100%: at 100% the
   image is the card's own height, so it would stretch and re-crop with every
   hover, which is the thing being avoided. Centring it with top:50% and a -50%
   translate keeps the reveal symmetrical, matching the 10%-up-10%-down growth.

   --photo-filter is tuned for photographs on the light page; under a dark wash
   it crushed these to near-black panels. Same grayscale to keep the set
   consistent, lifted and flattened rather than contrasted. */
.tcard-img {
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 100%;
  /* 1.1, tracking the reserve. This is the THIRD place the growth number lives
     and the one that fails silently: the image is fixed at the tallest the card
     can ever be, so if it stays at 1.2 while the card stops at 1.1, the crop
     never fully opens and the reveal quietly stops finishing. */
  height: calc(var(--tch) * 1.1);
  object-fit: cover;
  filter: grayscale(1) brightness(1.22) contrast(0.94);
  transition: filter 0.55s var(--ease-soft);
}
/* COLOUR UNDER THE POINTER, Greg 2026-08-22. The same behaviour the hero, the
   About frames and Sanctuary already have, so the page has one rule for
   photographs rather than four.

   ONLY the grayscale is dropped. brightness(1.22) and contrast(0.94) stay,
   because they are not a grade — they are what keeps these readable under the
   dark wash, and taking them off would darken the card at the same moment it
   gains colour, which reads as two things happening instead of one.

   Bound to .is-active, NOT to :hover alone. This row is driven by main.js from
   the pointer's X position, so a card can be growing while the pointer is above
   or below the row — and a card that grows but stays grey looks broken. :hover
   is kept beside it as the fallback for a pointer on the card with the script
   not running. 0.55s, the page's photograph timing. */
.cards-tall .tcard.is-active .tcard-img,
.tcard:hover .tcard-img {
  filter: grayscale(0) brightness(1.22) contrast(0.94);
}
/* Two separate washes, and the split matters.

   This one is atmosphere only — a light, whole-card tint that settles the
   photograph into the page. It is NOT what makes the type readable. */
.tcard::after {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(to top,
    rgba(46,50,49,.42) 0%,
    rgba(46,50,49,.20) 45%,
    rgba(46,50,49,.06) 100%);
}
/* This one is legibility, and it is anchored to the TEXT rather than to the
   card. A card-anchored gradient fails the moment the two change proportion:
   on a 300px phone card the copy block is two thirds of the height, so it rode
   up into the thin part of the wash and the Couples Massage title landed on a
   bright patch of grass. Anchored to the body, the backing travels with the
   text and holds at any card height — 300px on a phone, 528px hovered. The
   generous padding-top is the gradient's fade-in room, not spacing. */
.tcard-body {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1;
  padding: 3.25rem 1.4rem 1.5rem;
  color: #fff;
  background: linear-gradient(to top,
    rgba(46,50,49,.93) 0%,
    rgba(46,50,49,.88) 42%,
    rgba(46,50,49,.62) 74%,
    rgba(46,50,49,0) 100%);
}
/* 12px, not 11 — Greg, 2026-08-22, on phone readability. Raised at EVERY
   width rather than under a media query: a font may never be smaller on a
   wider screen, so a phone-only bump would have to be undone on the desktop. */
.tcard-eyebrow {
  margin: 0; font-size: 12px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  color: rgba(255,255,255,.72);
}
/* SIZED BY MEASUREMENT, Greg 2026-08-22 — the titles must each hold ONE line.
   Widths taken from fonts/crimson-latin.woff2 itself, letter-spacing -0.03em
   included, against the card's text column (card width minus 1.4rem a side):

     A Body That Works   7.51em   needs <= 29.6px at 1440, <= 23.1px at 1024
     Relaxation Massage  7.21em   needs <= 30.8px  ->  RENAMED "Relax Massage",
                                  5.34em, which clears every width easily
     Tantra Massage      5.82em   fine at any size the row uses
     Coaching            3.52em   fine

   .display-sm ran 2.4vw capped at 36px — 34.6px at 1440 — so two of the four
   wrapped, and the wrap is what threw the price rows out of line with each
   other. A Body That Works is the binding one and everything here is set by it.

   Why the cap is 28 and not 29.6: above 1280 the CONTAINER stops growing but
   the gutter does not, so the cards get slightly NARROWER as the window widens
   — 227px of text at 1280, 222 at 1440, 217 at 1600. 28px clears the tightest
   of those. And the floor is 20px so the type still only ever grows with the
   screen, never shrinks — Greg's standing rule.

   Only the size is overridden. Family, weight, line-height and letter-spacing
   still come from .display-sm.

   margin-top is 0 now: the duration eyebrow above it is gone, so the h3 is the
   first thing in the card body. */
.tcard-body h3 {
  margin: 0 0 .6rem;
  color: #fff;
  font-size: clamp(1.25rem, 2.15vw, 1.75rem);
}
/* 15px, same reasoning. This is the card's only body copy and it was the
   smallest running text on the page. */
.tcard-copy {
  margin: 0 0 1.1rem; font-size: 15px; line-height: 1.7;
  color: rgba(255,255,255,.86);
}
/* ONE ACCENT PER CARD, and it is carried by the rule and the arrow only —
   Greg, 2026-08-22. The cards themselves stay as they are: four coloured cards
   read as four brands, and the photographs are already doing the separating.

   The palette is the closed one — teal, saffron, red, brown. Blue and green
   were on the table and are not in it.

   2px, not 1px, and the accent at .55 alpha over the dark wash. At 1px and full
   opacity a hue this saturated reads as a border someone drew; at 2px and
   half-lit it reads as the card having a colour. The arrow takes the same hue
   solid, because a 14px stroke at .55 disappears.

   --tcard-accent defaults to white so a card with no modifier still works. */
.tcard-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding-top: .9rem;
  /* 1px, halved from 2px — Greg, 2026-08-22. Alpha raised .55 -> .75 with it:
     a half-lit hairline at 1px is most of the way to invisible on a grey
     photograph, and thinning is only free if the ink stays. 0.75 is the next
     step up if 1px turns out too quiet. */
  border-top: 1px solid rgba(var(--tcard-accent, 255, 255, 255), .75);
  transition: border-color .55s var(--ease-soft);
}
.tcard-price { font-size: 14px; font-weight: 600; }
/* "Read more" is gone — Greg, 2026-08-22: the arrow says it, and four cards
   repeating the same two words was the noisiest thing in the row. .tcard-arrow
   is the visual only; the hit area is .tcard-link below. */
.tcard-arrow {
  display: inline-flex; align-items: center;
  color: rgb(var(--tcard-accent, 255, 255, 255));
  transition: transform .35s var(--ease-out);
}
.tcard.is-active .tcard-arrow { transform: translateX(4px); }

/* THE WHOLE CARD IS THE TARGET, and this is the second attempt at it.

   The first put a stretched ::after on the arrow, which sat inside .tcard-body
   — and .tcard-body is position:absolute, so inset:0 measured against the
   CAPTION BLOCK instead of the card. Only the bottom strip took a click and
   the photograph did nothing. It reads as correct in the CSS, which is exactly
   why it survived: it was found by hit-testing four points on each card, not
   by looking at the rule.

   So the anchor is now an empty element and a DIRECT CHILD of .tcard, which is
   position:relative — the containing block it was supposed to have all along.
   No pseudo, no nesting, nothing to resolve against the wrong ancestor.

   z-index 3 clears .tcard-body's 1 and the wash on ::after. .tcard sets
   isolation:isolate, so this stacks inside the card and cannot reach a
   neighbour. */
.tcard-link {
  position: absolute;
  inset: 0;
  z-index: 3;
}
.tcard-link:focus-visible {
  outline: 2px solid rgb(var(--tcard-accent, 255, 255, 255));
  outline-offset: -4px;
}

.tcard--saffron { --tcard-accent: var(--saffron-rgb); }
.tcard--red     { --tcard-accent: 219, 124, 113; }  /* --red #A23A33 lifted, and
   it needed it most of the four: raw it measures 2.0:1 on the card's wash, so
   the arrow all but vanished. 4.4:1 lifted. Same hue, lightness only. */
.tcard--teal    { --tcard-accent: 77, 166, 190; }   /* --teal lifted: #00637D is
   too dark to read against the card's own wash. Same hue, raised in lightness
   until the rule is visible on a grey photograph. */
.tcard--brown   { --tcard-accent: 198, 124, 58; }   /* --brown #7D4009 lifted for
   the same reason, and it lands beside saffron rather than under it. */
.tcard-num {
  position: absolute; top: 1rem; right: 1.15rem; z-index: 1;
  font-family: var(--serif); font-size: 13px; letter-spacing: .1em;
  color: rgba(255,255,255,.75);
}

/* The ripple. Driven by CLASSES, not :hover — main.js decides which card is
   active from the pointer's X position alone, so the cards respond while the
   pointer is above or below the row and not only when it is on top of a card.
   An earlier version used :hover with :has() to reach the previous sibling;
   that could not see a pointer sitting outside the row, which is the whole
   point of this version.
   .is-near last would lose to nothing, so .is-active is declared after it —
   the two are mutually exclusive in JS anyway, this just makes it explicit. */
/* Halved with the reserve above, 2026-08-22: active 1.2 -> 1.1, near 1.1 ->
   1.05. These two and grid-auto-rows are one decision — the largest value here
   must equal the reserve, or the row either jumps or carries dead space. */
.cards-tall .tcard.is-near { height: calc(var(--tch) * 1.05); }
.cards-tall .tcard.is-active { height: calc(var(--tch) * 1.1); }

.card-media {
  position: relative;
  aspect-ratio: 4 / 3.4;
  overflow: hidden;
  margin: 0.75rem 0.75rem 0;
  border-radius: 14px;
}
.card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--photo-filter);
  transition: transform 1.1s var(--ease-out);
}
.card:hover .card-media img { transform: scale(1.07); }
.card-num {
  position: absolute;
  top: 0.75rem;
  left: 0.9rem;
  font-family: var(--serif);
  font-size: 1.5rem;
  color: var(--white);
  mix-blend-mode: difference;
}
.card-body { padding: 1.4rem 1.4rem 1.6rem; display: flex; flex-direction: column; flex: 1; }
.card-body h3 { margin: 0.6rem 0 0.75rem; }
.card-body p { font-size: 14px; line-height: 1.75; margin: 0 0 1.5rem; }
.card-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--saffron-hair);
}
.card-price {
  font-size: 14px;
  font-weight: 600;
  color: var(--red);
  letter-spacing: 0.01em;
}

/* --- split (philosophy / founder / story) ------------------------------- */

.split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
}
.split--reverse .split-media { order: 2; }

.split-media img {
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  border-radius: 20px;
  filter: var(--photo-filter);
}
.split-text > * + * { margin-top: 1.25rem; }

/* --- story feature ------------------------------------------------------ */

.story {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
}
.story-media { position: relative; }
.story-media img {
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  border-radius: 20px;
  filter: var(--photo-filter);
}
.story-media::after {
  content: '';
  position: absolute;
  inset: auto auto -22px -22px;
  width: 46%;
  height: 46%;
  border: 1px solid var(--teal-hair);
  border-radius: 20px;
  z-index: -1;
}
.story blockquote { margin: 0; }
.story cite { display: block; font-style: normal; font-size: 12px; font-weight: 600; letter-spacing: 0.11em; text-transform: uppercase; color: var(--muted); margin-top: 1.5rem; }

/* --- story feature B: the stacked pair (Screenshot_5.png) ---------------- */

.duo {
  display: grid;
  grid-template-columns: 0.92fr 1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: center;
}
.duo-text > * + * { margin-top: 1.25rem; }
.duo-text .btn { margin-top: 2.25rem; }

/* The dot after the label in the screenshot's pill. currentColor, so it
   follows the button through its hover swap without a second rule. */
.btn-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

/* Near-square on purpose: it is what makes both frames resolve to 2:3 from
   width percentages alone. Measured off the screenshot — 834 wide, 833 tall. */
.duo-media {
  position: relative;
  aspect-ratio: 834 / 833;
}

.duo-frame {
  position: absolute;
  margin: 0;
}
/* The clipping the figure used to do itself. It had to hand it over: a figure
   with overflow:hidden cannot let its own shadow hang outside it. */
.duo-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;          /* the inner parallax slides the img inside this */
}

/* The shadow, and it is the real one — frames/image-frame-portrait-shadow.png
   with its white card knocked transparent by tools/make-shadow-only.py. Square
   corners now, because the shadow was drawn for a square-cornered picture and a
   20px radius left the two disagreeing at every corner.

   border-image rather than a background: these frames are percentages of a
   fluid column, and border-image holds the shadow's true thickness at the edges
   (6px right, 25px bottom) while stretching only the soft middle. Stretching a
   soft shadow is invisible; stretching the frame's 1px hairline would not be,
   which is why only the shadow travels this way.

   It is on ::after with negative insets so it costs the layout nothing — a real
   border would eat into the photograph and break the 2:3 the column depends on.
   border-style must be set or border-image is never painted. */
.duo-frame::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: -6px; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 6px 25px 0;
  border-image: url(../frames/shadow-only-portrait.png) 0 6 25 0 / 0 6px 25px 0 stretch;
  pointer-events: none;
}
/* RESTORED from GCGM-v5/css/style.css, 2026-08-22. Greg: v5 is the source and
   the original, and the geometry had drifted here. The -49.8%/149.8% overhang
   that reached up into #what-this-is is gone.

   Back: high right, the larger of the two. 61% x 90.9% of a square column
   gives 2:3. */
.duo-frame--back  { top: 0; right: 0; width: 61%; height: 90.9%; }
/* Front: low left, 49% wide. It stays pinned to the column's bottom edge, so it
   hangs 9.1% below the back frame, and the 10% horizontal overlap is what
   61 + 49 = 110 buys.

   HEIGHT RAISED 20% TWICE, Greg 2026-08-22: 74.2% -> 89.04% -> 106.85%.
   bottom:0 is the anchor both times, so the whole increase is taken upward and
   the bottom edge has not moved. Width unchanged throughout.

   PAST 100% NOW, and that is the thing to know: the frame is taller than the
   column it sits in, so its top edge stands 6.85% — about 57px at 1440 — ABOVE
   .duo-media's box. Nothing clips it: .duo and .duo-media both leave overflow
   visible, so it simply hangs into the gap above the section. This is the same
   direction as the old -49.8%/149.8% overhang that was removed on 08-22, at a
   twelfth of the distance.

   It is also no longer 2:3 — 49 x 106.85 of a square column is 0.46:1 — while
   the back frame is untouched at 2:3, so the pair are deliberately different
   shapes. */
.duo-frame--front { bottom: 0; left: 0; width: 49%; height: 106.85%; }

/* RESTORED from v5. 126% leaves 26% of headroom for the inner parallax to
   travel through. The img is anchored to the top and JS moves it up from
   there, so the frame is never uncovered at either end of the scroll — the
   travel is bounded well inside the headroom. See duoFrame() in main.js for
   the arithmetic. */
.duo-frame img {
  position: absolute;
  top: -13%;                 /* centred: 13% of headroom above and below */
  left: 0;
  width: 100%;
  height: 126%;
  object-fit: cover;
  filter: var(--photo-filter);
  will-change: transform;
  transition: filter 0.55s var(--ease-soft);
}
/* Black and white, colour on hover — the same treatment as the sample frames
   in FRAMES.html, and the same 0.55s.

   Per photograph, not per section: the hover is on .duo-frame, so hovering one
   leaves the other in black and white. The two figures are siblings rather
   than nested, so the overlap resolves on its own — the front one is painted
   last and takes the pointer there, and the back one does not light up with it.

   The shadow does not change. It belongs to the PNG on ::after, which carries
   pointer-events:none, so it neither animates nor widens the area that counts
   as the photograph. */
.duo-frame:hover img { filter: none; }

/* --- "How" heading with the background line ----------------------------- */

/* The background line runs the full width behind the heading and draws itself
   left to right — the template's version of this is horizontal, not vertical.
   The heading sits on top with a background plate so the rule breaks around it. */
.how-head {
  position: relative;
  text-align: center;
  padding-block: clamp(2rem, 6vw, 5rem);
}
.how-head .how-line {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  background: linear-gradient(to right, transparent, var(--teal-hair) 12%, var(--saffron-hair) 50%, var(--teal-hair) 88%, transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.6s var(--ease-out);
}
.how-head.is-in .how-line { transform: scaleX(1); }
.how-head .eyebrow, .how-head .display-xl {
  position: relative;
  background: var(--white);
}
.how-head .display-xl { display: inline-block; padding-inline: clamp(1rem, 3vw, 2.5rem); }
.how-head em { font-style: italic; color: var(--red); }

/* --- numbered steps ----------------------------------------------------- */

.steps { margin-top: clamp(2rem, 5vw, 4rem); }
.step {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 5vw, 5rem);
  align-items: center;
  padding-block: clamp(3rem, 7vw, 6rem);
  border-top: 1px solid var(--hairline);
}
.step:last-child { border-bottom: 1px solid var(--hairline); }
.step--flip .step-media { order: -1; }

.step-num {
  display: block;
  font-size: clamp(5.5rem, 13vw, 12rem);
  font-weight: 500;
  line-height: 0.8;
  letter-spacing: -0.05em;
  color: var(--saffron-wash);
  margin-bottom: clamp(0.5rem, 1.5vw, 1.25rem);
  transition: color 0.9s var(--ease-soft);
}
.step.is-in .step-num { color: rgba(var(--saffron-rgb), 0.72); }
.step-body p { max-width: 42ch; margin-top: 1.25rem; }
.step-media img {
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  border-radius: 20px;
  filter: var(--photo-filter);
}

/* --- quote band --------------------------------------------------------- */

.band {
  position: relative;
  /* Brought in with everything else, though on the tall variant it changes
     nothing: that band's height comes from `min-height: 120vh` below, which is
     the room the scroll-drawn lines need to complete. THE BAND IS THE ONE
     SECTION THE PAGE-WIDE CUT DOES NOT REACH, and shortening it is a decision
     about the lines rather than about spacing. */
  padding-block: clamp(3rem, 7vw, 6.5rem);
  overflow: hidden;
}

/* Tall variant — the extra height is what gives the lines room to draw.
   1.2 viewports, measured off the ClearPath section. */
.band--tall {
  min-height: 120vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.band-media { position: absolute; inset: 0; }
.band-media img { width: 100%; height: 100%; object-fit: cover; filter: var(--photo-filter); }

/* The image is 135% of the section and sits 17.5% high, so it has 35% of
   travel to parallax through and still covers the section at both ends.
   JS moves it between +17.5% and -17.5%. The JPG is never stretched. */
.band--tall .band-media { overflow: hidden; }
.band--tall .band-media img {
  position: absolute;
  top: -17.5%;
  left: 0;
  height: 135%;
  will-change: transform;
}

/* --- the two scroll-linked lines ---------------------------------------
   No CSS transition here on purpose: JS writes stroke-dashoffset every
   frame, and a transition on top of that would fight the lerp. */
/* Sized to the section itself, not to a fixed 2000px box. The earlier version
   was 2000px tall and centred, so the section clipped it to the middle rows and
   the line's start and end were never on screen. The whole point is that the
   draw runs from the image's top edge to its bottom edge. */
.band-lines {
  position: absolute;
  top: 0;
  right: 3vw;
  /* The box takes its width from its height, so the drawing keeps line.jpg's
     own 880 x 2256 proportions at every window size. The fixed 421px width it
     replaced was correct at exactly one section height (1080px, i.e. a 900px
     viewport) and stretched everywhere else — measured 20% too tall at
     1920x1080, which is enough to visibly open out the loops. */
  height: 100%;
  width: auto;
  aspect-ratio: 880 / 2256;
  pointer-events: none;
  overflow: visible;
  z-index: 1;
  /* The reference photograph is uniformly dark blue, so a white line reads
     everywhere on it. This one has a near-white sheet across the lower half
     and the stroke vanished into it. A soft dark halo keeps the line legible
     over the light areas without darkening the whole photograph. */
  filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5));
}
.band-lines path {
  fill: none;
  stroke: #fff;
  stroke-width: 2;
  stroke-linecap: round;
  /* No vector-effect here on purpose. non-scaling-stroke measures the dash
     pattern in screen pixels, which breaks pathLength="1" normalisation —
     dasharray:1 stops meaning "the whole path" and becomes a 1px dash, so
     the line renders as an invisible dotted hairline. This SVG is drawn at
     its natural 680x2000, scale 1:1, so nothing needs correcting. */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
/* The ghost is dimmer AND thinner — both, not just dimmer. Dropping the
   opacity alone leaves a 2px stroke that still reads as a second line of equal
   weight; the reference's second stroke is visibly finer, which is what makes
   it read as an echo of the first rather than as a pair.
   Targeted by class, not by nth-child: the ghost is inserted before the source
   path (so the bright line wins every crossing), so position is the wrong
   thing to key off, and a positional rule silently dims the wrong line the
   moment the order changes — which is exactly what happened to the previous
   version of this rule. */
.band-lines path { opacity: 0.85; }
.band-lines path.is-ghost {
  opacity: 0.32;
  stroke-width: 1.1;
}
.band-scrim { position: absolute; inset: 0; background: rgba(46, 50, 49, 0.55); }
.band .container { position: relative; z-index: 2; text-align: center; }
.band blockquote {
  margin: 0 auto;
  /* THE FULL REVIEW IN FOUR LINES — Greg, 2026-08-22, and it is one instruction
     with two halves: keep every word, and make the block wide and short rather
     than narrow and tall.

     Both halves fall out of one sum. The review is 269 characters, which is
     105.8 em of Crimson at this weight. Four lines means each line is 26.4 em,
     and the widest the band can go is the container's 1136px at 1440 — so the
     type is 1136 / 26.4 = 43px. There is no second answer: the width is capped
     by the container and the line count is Greg's, so the size is determined.

     Measured, so the neighbours are on record:
       52px 4.8 lines · 48px 4.5 · 44px 4.1 · 43px 4.0 · 42px 3.9 · 38px 3.5

     max-width is gone rather than raised. A ch-based measure was fighting the
     font size — every change to one silently moved the other — so the block
     simply takes the container and the size does the work.

     line-height 1.02 -> 1.2 and letter-spacing -0.03em -> -0.015em: both are
     display settings, and at 43px over four lines this is running text being
     read, not a headline being looked at. */
  max-width: none;
  font-family: var(--serif);
  font-weight: 400;
  /* Cap 2.7rem -> 2rem, Greg 2026-08-22 on screen. 32px, and the 43px above was
     the arithmetic answer rather than the judged one — at 32px the block runs
     about 5.4 lines instead of 4 and reads as a person talking rather than as a
     statement. The 3vw middle is untouched, so the cap binds from 1067px up. */
  font-size: clamp(1.5rem, 3vw, 2rem);
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--white);
}
.band cite {
  display: block;
  font-style: normal;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.65);
  margin-top: 2rem;
}

/* TWO REVIEWS IN THE BAND, 2026-08-22. Stacked rather than side by side: 269
   characters beside 47 is not two columns, it is one column and a caption, and
   the short one loses every time.

   BOTH QUOTES ARE THE SAME SIZE — Greg, 2026-08-22, after hearing the case for
   making the second smaller and calling it weak. He is right. The argument was
   that hierarchy tells the eye where to start; the answer is that these are two
   people who each wrote something true, and the ratio proposed for shrinking one
   of them was picked rather than measured, which is the tell. The rule and the
   spacing do all the separating, and they are enough.

   The CITES keep their own treatment and are untouched — 12px uppercase, which
   is the page's caption setting everywhere. Only the reviews match.

   The margin-top on .band cite is 2rem, which is right under a three-line quote
   and too much under a one-line one, so the second figure takes it back. */
.band-quote { margin: 0; }
.band-quote--second {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(2.5rem, 5vw, 4rem);
  border-top: 1px solid rgba(255, 255, 255, 0.22);
}
.band-quote--second cite { margin-top: 1.1rem; }
/* the vertical animated line */
.band-line {
  width: 1px;
  height: clamp(3rem, 7vw, 6rem);
  margin: 0 auto clamp(2rem, 4vw, 3.5rem);
  background: rgba(255, 255, 255, 0.65);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 1.5s var(--ease-out);
}
.band.is-in .band-line { transform: scaleY(1); }

/* --- studio: irregular shaped reveals -------------------------------- */

.shapes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.25rem, 3vw, 2.75rem);
  margin-top: clamp(2.5rem, 5vw, 4rem);
}
.shape { display: block; }
.shape-media {
  position: relative;
  aspect-ratio: 1 / 1.12;
  overflow: hidden;
  /* not a square, not an oval — the irregular organic mask */
  border-radius: 62% 38% 54% 46% / 48% 58% 42% 52%;
  transition: border-radius 1.2s var(--ease-out);
}
.shape:nth-child(2) .shape-media { border-radius: 44% 56% 38% 62% / 56% 44% 60% 40%; }
.shape:nth-child(3) .shape-media { border-radius: 55% 45% 62% 38% / 40% 55% 45% 60%; }
.shape:hover .shape-media { border-radius: 30% 70% 46% 54% / 62% 36% 64% 38%; }
.shape-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--photo-filter);
  transform: scale(1.18);
  transition: transform 1.4s var(--ease-out);
}
.shape.is-in .shape-media img { transform: scale(1); }
.shape:hover .shape-media img { transform: scale(1.06); }
.shape-cap { margin-top: 1.25rem; }
.shape-cap h3 { margin: 0.4rem 0 0.5rem; }
/* "See more" is gone from all three — Greg, 2026-08-22, the same call he made
   on the service cards: the arrow says it, and three cards repeating two words
   was noise.

   The arrow sits ON THE TITLE LINE, pushed to the right — Greg, 2026-08-22.
   Title left, arrow right, one row. It is NOT on a row of its own under the
   caption, which is what a first pass did.

   No accent colour on these. #treatments earns four hues by having four
   different services in it; this is one room photographed three times, so it
   stays teal like every other link on the page. */
.shape-title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin: 0.4rem 0 0.5rem;
}
/* The h3's own margin moves to .shape-title so the two sides share one
   baseline; leaving it on the h3 pushed the arrow down with it. */
.shape-title h3 { margin: 0; }
.shape-cap .link-arrow { flex: none; }

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

.faq { max-width: 800px; margin-inline: auto; margin-top: clamp(2rem, 4vw, 3.5rem); }
.faq-item { border-top: 1px solid var(--hairline); }
.faq-item:last-child { border-bottom: 1px solid var(--hairline); }
.faq-q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-block: 1.6rem;
  text-align: left;
  font-family: var(--serif);
  font-size: clamp(1.2rem, 2vw, 1.65rem);
  line-height: 1.25;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.faq-icon { flex: none; width: 20px; height: 20px; position: relative; }
.faq-icon::before, .faq-icon::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: var(--teal);
  transition: transform 0.5s var(--ease-out);
}
.faq-icon::after { transform: rotate(90deg); }
.faq-item.is-open .faq-icon::after { transform: rotate(0deg); }
.faq-a { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.6s var(--ease-out); }
.faq-item.is-open .faq-a { grid-template-rows: 1fr; }
.faq-a > div { overflow: hidden; }
.faq-a p { margin: 0 0 1.6rem; max-width: 60ch; font-size: 15px; }

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

.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
.field { margin-bottom: 1.5rem; }
.field label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.6rem;
}
.field input, .field textarea, .field select {
  width: 100%;
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 12px;
  padding: 0.95rem 1.1rem;
  transition: border-color 0.35s var(--ease-soft), box-shadow 0.35s var(--ease-soft);
}
.field textarea { min-height: 140px; resize: vertical; }
.field input:focus, .field textarea:focus, .field select:focus {
  outline: none;
  border-color: var(--teal);
  box-shadow: 0 0 0 3px var(--teal-wash);
}
.contact-aside > * + * { margin-top: 2rem; }
.contact-row { display: flex; flex-direction: column; gap: 0.3rem; }
.contact-row a { font-size: 1.125rem; color: var(--ink); }
.contact-row a:hover { color: var(--teal); }
.contact-map {
  width: 100%;
  aspect-ratio: 16 / 11;
  border: 0;
  border-radius: 18px;
  filter: grayscale(1) contrast(1.05);
}

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

.footer {
  position: relative;
  color: var(--white);
  overflow: hidden;
  padding-block: clamp(5rem, 11vw, 9rem) 2rem;
}
.footer-media { position: absolute; inset: 0; }
.footer-media img { width: 100%; height: 100%; object-fit: cover; filter: var(--photo-filter); }
.footer-scrim { position: absolute; inset: 0; background: rgba(46, 50, 49, 0.78); }
.footer .container { position: relative; z-index: 2; }
.footer-word {
  font-family: var(--serif);
  font-size: clamp(3rem, 13vw, 12rem);
  line-height: 0.85;
  letter-spacing: -0.04em;
  color: var(--white);
  margin: 0 0 clamp(2.5rem, 6vw, 5rem);
}
.footer-word em { font-style: italic; color: var(--saffron); }
.footer-cols {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 2rem;
  padding-top: 2.5rem;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}
.footer-cols h4 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  margin: 0 0 1rem;
}
.footer-cols ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; }
.footer-cols a, .footer-cols li { color: rgba(255, 255, 255, 0.85); font-size: 15px; }
.footer-cols a:hover { color: var(--teal); }
.footer-legal {
  margin-top: clamp(3rem, 6vw, 5rem);
  padding-top: 1.75rem;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.75rem;
  align-items: center;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.65);
}
.footer-legal a { text-decoration: underline; text-underline-offset: 3px; }

/* --- responsive --------------------------------------------------------- */

@media (max-width: 1280px) {
  /* below this the gutter is too narrow to hold the numeral clear of content */
  .odometer { display: none; }
}

@media (max-width: 1080px) {
  .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .cards-tall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The ripple is a row effect and only reads as one while all four cards are
     in a single row. On two columns the neighbours are still the DOM siblings,
     so the card next to the active one can be the bottom-LEFT one — diagonally
     opposite, which looks like a bug rather than a wave. The active card still
     grows; only the neighbour step is cancelled. */
  .cards-tall .tcard.is-near { height: var(--tch); }
  .shapes { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
}

@media (max-width: 900px) {
  /* The CTA goes too. At 375px it wrapped to three lines and pushed the header
     to 100px tall — the hamburger already carries "Reserve" in the menu. */
  .nav, .header-phone, .header-right .btn { display: none; }
  /* The whole contact block, not just the number. Greg, 2026-08-22: the flags
     were the only thing left in it, sitting next to the burger and qualifying
     nothing. They move into the menu beside the number. */
  .header-contact { display: none; }
  .menu-toggle { display: block; }
  .odometer { display: none; }

  .split, .story, .duo, .contact-grid, .step { grid-template-columns: minmax(0, 1fr); }
  /* The pair itself is percentage-based, so it scales down intact rather than
     breaking — the parallax stands down via the 901px guard in main.js. This is
     survival, not a mobile design: that happens once the element is chosen. */
  .duo-text .btn { margin-top: 1.75rem; }
  .split--reverse .split-media { order: 0; }
  /* the alternating image side is a desktop rhythm; stacked, it just looks
     inconsistent, so every row goes back to text-then-image */
  .step--flip .step-media { order: 0; }
  .story-media::after { display: none; }
  .step { gap: 1.75rem; padding-block: 3rem; }
  .split-media img, .story-media img, .step-media img { aspect-ratio: 4 / 3; }
  .statement-body { max-width: none; }

  /* Section lines are routed through the empty space of the desktop grid.
     Stacked, that space is gone — the content fills the column — and the
     studio line ran straight through the heading and behind a photo.
     Hidden rather than distorted. The hero lines stay: the hero is a
     full-bleed image and has room for them at every width.
     Fixable with a second mobile-specific path per section if wanted.

     ONE EXEMPTION, ADDED 2026-08-26, AND IT IS DIRECTLY BELOW THIS BLOCK.
     The reason above is "stacked, that space is gone" -- and the services index
     is not stacked at 900 any more. It holds its two columns down to 760, so
     the empty column the line runs in still exists and the labels are still
     measured onto the curve. Hiding it there was throwing the drawing away in a
     layout that still had room for it, which is exactly what Greg reported:
     "the animated line already disappears at around 1024." */
  .section-lines { display: none; }

  /* Same call for the band lines, and Greg's own: there is no spare screen
     on mobile for them. Desktop is being judged first; if it is kept, mobile
     gets designed rather than squeezed. */
  .band-lines { display: none; }

  /* The ripple is a pointer effect and there is no pointer here. On a touch
     screen :hover latches on tap and sticks until you tap elsewhere, so the
     cards would sit at odd heights with no way to clear them. The row also
     stops needing its 120% reservation once nothing grows, and reclaiming it
     removes 20% of dead space from every card in a stacked column. */
  .cards-tall { grid-auto-rows: auto; }
  .tcard { transition: none; }
  .cards-tall .tcard.is-active,
  .cards-tall .tcard.is-near { height: var(--tch); }

  /* And the statement lines, for a reason of their own: that box is 2256
     units wide, so at a 390px screen the 3-unit stroke resolves to about half
     a pixel and the drawing renders as a grey smudge 152px tall. It is in
     normal flow rather than absolutely positioned, so unlike the two above it
     costs vertical space to show badly. Mobile wants a short excerpt of the
     drawing rather than the whole thing shrunk — designed, not squeezed. */
  .statement-lines { display: none; }

  .band--tall { min-height: 0; }
  .band--tall .band-media img { position: static; height: 100%; top: auto; transform: none; }
}

@media (max-width: 640px) {
  :root { --header-h: 64px; }
  .cards { grid-template-columns: minmax(0, 1fr); }
  .cards-tall { grid-template-columns: minmax(0, 1fr); }
  .shapes { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; }
  .footer-cols { grid-template-columns: minmax(0, 1fr); gap: 2.25rem; }
  .hero-foot { flex-direction: column; align-items: flex-start; }
  .hero-lead { max-width: none; }
  .band blockquote { max-width: none; }
}

/* Landscape phones: the hero must not lock to a viewport that is 380px tall */
@media (max-height: 520px) and (orientation: landscape) {
  .hero { min-height: auto; padding-block: calc(var(--header-h) + 2rem) 2.5rem; }
  .display-xl { font-size: clamp(2.25rem, 7vw, 3.5rem); }
  .mobile-menu a { font-size: 1.35rem; padding-block: 0.3rem; }
}

/* ===========================================================================
 * INNER PAGES — treatments · gallery · testimonials · faq · contact
 *
 * The six pages are the live site's own six, kept because every URL that ranks
 * today survives the rebuild. Everything below is the furniture those five
 * inner pages need and the home page did not.
 *
 * Emitted markup lives in tools/make-pages.py.
 * =========================================================================== */

/* The current page, marked in the bar. */
.nav a.is-current { color: var(--teal); }

/* --- the band every inner page opens with ------------------------------- */
/* Beige under the navigation, the same join the hero uses, so an inner page
   starts the way the home page does rather than with a bare heading on white. */
.page-head {
  background: var(--surface);
  padding-block: calc(var(--header-h) + clamp(2.5rem, 5vw, 4rem)) clamp(2.5rem, 5vw, 4rem);
}
.page-head h1 { margin: 0.75rem 0 0; max-width: 18ch; }
.page-intro {
  margin: 1.5rem 0 0;
  max-width: 62ch;
  color: var(--body);
  line-height: 1.62;
}

/* A size between display-lg and display-sm, for the treatment names. */
.display-md {
  font-family: var(--serif);
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  line-height: 1.1;
  color: var(--ink);
  margin: 0.4rem 0 0;
}

/* --- the shadow, shared ------------------------------------------------- */
/* The 2014 PNG cut loose from its frame by tools/make-shadow-only.py, applied
   with border-image so it keeps its true 25px at the edge and stretches only
   its soft middle. .shot-clip exists because a box that clips itself cuts its
   own shadow off — the figure stays visible, the clip does the clipping. */
.shot-clip { position: absolute; inset: 0; overflow: hidden; }

.treatment-shot,
.gal-shot {
  position: relative;
  margin: 0;
}
.treatment-shot::after,
.gal-shot::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 0 25px 0;
  border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 25px 0 stretch;
  pointer-events: none;
}
.treatment-shot img,
.gal-shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) contrast(1.05);
  transition: filter 0.55s var(--ease-soft);
}
.treatment-shot:hover img,
.gal-shot:hover img { filter: none; }

/* --- treatments --------------------------------------------------------- */
/* Picture and words side by side, alternating sides down the page. The copy is
   deliberately long here — that length is what separates this site from Trance
   Ritual — so the column is held to a readable measure rather than filling the
   half it is given. */
.treatment {
  display: grid;
  grid-template-columns: 0.85fr 1fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
  padding-block: clamp(3rem, 6vw, 5rem);
  border-top: 1px solid var(--hairline);
}
.treatment:first-child { border-top: 0; padding-top: 0; }
.treatment--flip .treatment-shot { order: 2; }
.treatment-shot { aspect-ratio: 4 / 5; position: sticky; top: calc(var(--header-h) + 2rem); }
.treatment-copy > p { max-width: 60ch; color: var(--body); line-height: 1.68; margin: 1rem 0 0; }
.treatment-n {
  font-family: var(--sans);
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  color: var(--saffron-ink);
  margin: 0;
}
.treatment-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.25rem;
  align-items: baseline;
  margin: 0.9rem 0 1.5rem !important;
  font-family: var(--sans);
  font-size: 0.85rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}
.treatment-price { color: var(--red); font-weight: 600; }
.treatment-lead { font-size: 1.12rem; color: var(--ink) !important; }

/* --- gallery ------------------------------------------------------------ */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(2rem, 4vw, 3.5rem) clamp(1.5rem, 3vw, 2.5rem);
}
.gal-shot { aspect-ratio: 4 / 3; }
.gal-shot figcaption {
  position: absolute;
  top: 100%;
  left: 0;
  padding-top: 1.1rem;
  font-family: var(--sans);
  font-size: 0.74rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- the turn-it-around statement --------------------------------------- */
/* The paragraph that catches people who arrived on an erotic search term. It
   has to stand out and it must not read as a telling-off — the reason it works
   is that it does not judge anyone for looking. So: larger type, a rule, and
   the warm accent. No red, which reads as a warning. */
.turnaround {
  border-left: 3px solid var(--saffron);
  padding: 0.25rem 0 0.25rem clamp(1.25rem, 3vw, 2rem);
  margin-bottom: clamp(3rem, 6vw, 5rem);
}
.turnaround p {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  line-height: 1.55;
  color: var(--ink);
  margin: 0 0 1rem;
  max-width: 56ch;
}
.turnaround p:last-child { margin-bottom: 0; }

/* --- faq ---------------------------------------------------------------- */
.faq-group {
  font-family: var(--sans);
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--saffron-ink);
  margin: clamp(3rem, 6vw, 4.5rem) 0 1.25rem;
}
.faq-group:first-of-type { margin-top: 0; }
.faq-a p + p { margin-top: 0.9rem; }

/* --- testimonials ------------------------------------------------------- */
.quote--lead {
  margin: 0 0 clamp(2.5rem, 5vw, 4rem);
  padding: 0;
}
.quote--lead p {
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(1.4rem, 2.8vw, 2rem);
  line-height: 1.3;
  color: var(--ink);
  margin: 0;
}
.quote--lead cite {
  display: block;
  margin-top: 1.25rem;
  font-family: var(--sans);
  font-style: normal;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.note-block {
  background: var(--surface);
  border-radius: 14px;
  padding: clamp(1.25rem, 3vw, 2rem);
  color: var(--body);
  line-height: 1.62;
}
.note-block p { margin: 0; }
.awards { margin-top: clamp(3rem, 6vw, 4.5rem); }
.awards-years {
  display: flex;
  gap: 1.5rem;
  margin: 0.75rem 0 0;
  font-family: var(--serif);
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  color: var(--teal);
}

/* --- contact ------------------------------------------------------------ */
.contact-details h2 { margin-top: 2.5rem; }
.contact-details h2:first-child { margin-top: 0; }
.contact-details p { color: var(--body); line-height: 1.68; margin: 0.75rem 0 0; max-width: 46ch; }
.contact-details a { color: var(--teal); }
.contact-form .field { margin-bottom: 1.25rem; }
.contact-form label {
  display: block;
  margin-bottom: 0.4rem;
  font-family: var(--sans);
  font-size: 0.74rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- inner pages, narrower screens -------------------------------------- */
/* Desktop is the design on this site and the phone is a separate job, but a
   two-column grid that never collapses is broken rather than unfinished, so
   these three fold at the tablet width and get revisited properly later. */
@media (max-width: 900px) {
  .treatment { grid-template-columns: 1fr; }
  .treatment--flip .treatment-shot { order: 0; }
  .treatment-shot { position: static; aspect-ratio: 16 / 10; }
  .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .gallery { grid-template-columns: 1fr; }
}

/* --- #9 back to top -------------------------------------------------------
   Greg, 2026-08-22. Appears on the way down, returns to the top.

   TEAL DISC, and that is not a free choice: the page already has one round
   teal control — the pulsing phone disc in the header — so a second round
   control in a different colour would read as a different KIND of thing. It
   does not pulse. The phone number is asking to be called; this is not asking
   for anything, it is just there when it is wanted.

   Bottom right, clear of the footer's legal strip at rest because it is fixed
   to the viewport rather than to the page.

   Hidden by visibility, not display: display:none cannot transition, and the
   fade is the whole reason it does not feel like something appearing suddenly
   in the corner. pointer-events goes with it so it cannot be clicked while
   invisible. */
.to-top {
  position: fixed;
  right: clamp(1rem, 2.5vw, 2rem);
  bottom: clamp(1rem, 2.5vw, 2rem);
  z-index: 90;               /* under the header's 100, over everything else */
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--teal);
  color: #FFFFFF;
  box-shadow: 0 6px 20px rgba(var(--teal-rgb), 0.28);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  pointer-events: none;
  transition: opacity .45s var(--ease-soft), transform .45s var(--ease-out),
              visibility 0s linear .45s, background .35s var(--ease-soft);
}
.to-top.is-on {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition-delay: 0s;      /* visibility flips in immediately on the way in */
}
.to-top:hover { background: var(--red); }
.to-top:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; }
/* One drawing, transformed. The arrow is the page's own right-pointing one,
   turned to face up rather than a second SVG being drawn. */
.to-top svg { transform: rotate(-90deg); }

/* --- the section divider --------------------------------------------------
   Greg, 2026-08-22, between #what-this-is and About. A hairline that fades at
   both ends rather than a hard rule: a solid line stopping dead at the text's
   edge draws attention to where it stops, and this is meant to separate two
   blocks, not to be looked at. Same gradient treatment as .hero-rule.

   --hairline is the page's own ink at 10%, so it sits in the palette rather
   than being a new grey. */
.rule-soft {
  border: 0;
  height: 1px;
  /* 2rem, was clamp(2.5rem, 5vw, 4rem) — Greg, 2026-08-22: the rule arrived
     with too much air around it. And nothing below it: #what-this-is gives up
     its bottom padding so this sits on the boundary with #story-duo, which is
     where the About line now terminates. The gap under the rule is that
     section's own padding and nothing is added to it. */
  margin: 2rem 0 0;
  background: linear-gradient(to right,
    transparent 0%, var(--hairline) 12%, var(--hairline) 88%, transparent 100%);
}

/* The statement's bottom padding goes to zero so .rule-soft lands exactly on
   the border between it and #story-duo — the y=0 the About line now ends at.
   Scoped to this section: .section's padding is right everywhere else. */
#what-this-is { padding-bottom: 0; }

/* ===========================================================================
   TAP TARGETS — added 2026-08-22 after a hit-test sweep across 320 to 1920.

   Everything here was under the 44x44 minimum: footer links measured 16px
   tall, the section-head links 20, the WhatsApp marks 22-25, the nav home
   icon 15x26. None of them broke anything — they are thumb-accuracy, and they
   were invisible until they were measured.

   THE PATTERN IS THE SAME EVERY TIME and it is why none of this changes the
   design: padding grows the hit area, a negative margin of the same size takes
   it back out of the layout, so every element stays exactly where it was
   drawn. Nothing moves. Only the area that answers a finger changes.

   These live at the end of the file on purpose — they are a layer over the
   design rather than part of it, and they must win against the rules above.

   THIS APPLIES TO THE OTHER FIVE PAGES TOO. The header, the footer and the
   section-head link are shared furniture; when treatments, gallery,
   testimonials, faq and contact come across, they inherit this file and are
   already fixed. Do not re-solve it per page.
   ======================================================================== */

/* Footer navigation and the address/email row. 13px text in a 16px box. */
.site-footer .f-inline a,
.site-footer .f-where a,
.site-footer .f-legal a {
  display: inline-block;
  padding-block: 0.85rem;
  margin-block: -0.85rem;
}

/* "All services" and "Gallery". The row uses align-items:flex-end, so the
   padding is split unevenly — more below, so the link's baseline stays put
   against the eyebrow beside it. */
.section-head-link {
  padding: 0.7rem 0.25rem 0.75rem;
  margin: -0.7rem -0.25rem -0.75rem;
}

/* The three WhatsApp marks — header, mobile menu, footer. The images stay
   24.6px and 22px; only the box around them grows. */
.header-wa,
.mobile-menu .mobile-menu-foot .mm-wa,
.site-footer .f-wa {
  padding: 0.6rem;
  margin: -0.6rem;
}
/* The menu one keeps its divider, so its left padding is the rule's spacing
   and must not be overwritten by the shorthand above. */
.mobile-menu .mobile-menu-foot .mm-wa {
  padding-left: 0.7rem;
  margin-left: -0.1rem;
}

/* The home icon in the nav. 15x15 of artwork in a 15x26 box — the smallest
   control on the desktop header.

   `.nav a` — NOT `.nav-home`. The first attempt used the class alone and did
   nothing: .nav a is (0,1,1) and .nav-home is (0,1,0), so the 0.35rem padding
   on the general rule won and the box never grew. Measured, not assumed.

   Horizontal padding too, because 15px of artwork is narrow whatever its
   height. .nav a::after draws on this box, so the underline widens with it —
   which is right: the underline should match what is clickable. */
.nav a.nav-home {
  padding-block: 0.85rem;
  padding-inline: 0.7rem;
  margin-block: -0.85rem;
  margin-inline: -0.7rem;
}

/* The header phone link: 141x20. The teal disc beside it is already large,
   but the number itself is the thing people aim at. */
.header-phone {
  padding-block: 0.7rem;
  margin-block: -0.7rem;
}

/* The footer phone number, 107x29. Its own rule rather than the group above,
   because .site-footer .f-phone is (0,2,0) and the group's selectors do not
   reach it — it is not inside .f-inline, .f-where or .f-legal. */
.site-footer .f-phone {
  display: inline-block;
  padding-block: 0.5rem;
  margin-block: -0.5rem;
}

/* The mobile menu's phone and email, 22px tall in the one place on the site
   that is ONLY ever touched — the menu does not exist on a desktop.

   `.mobile-menu .mobile-menu-foot a` is (0,2,1) and sets `padding: 0`
   explicitly, so this restates the same specificity to beat it. Same trick as
   everywhere else: padding out, margin back, nothing moves.

   .mm-wa is excluded — it has its own rule above with the divider's left
   padding, and the group here would flatten it. */
.mobile-menu .mobile-menu-foot a:not(.mm-wa) {
  padding-block: 0.65rem;
  margin-block: -0.65rem;
}

/* 44 wide, up from 37. The home icon is 15px of artwork and the last control
   still short of the minimum. */
.nav a.nav-home { padding-inline: 0.95rem; margin-inline: -0.95rem; }


/* ===========================================================================
   THE SHELL — moved out of index.php's <style> block, 2026-08-22.

   This is the move the build file always said would happen: "the hero, About
   and footer CSS is in the <style> block on purpose while it is still moving —
   it moves into the stylesheet when the page stops changing." The page has
   stopped changing, and the reason it has to move now is the other five pages:
   the header and the footer are shared furniture, and they cannot be shared
   from inside one page's <head>.

   APPENDED VERBATIM AND LAST, which is the whole safety of it. The <style>
   block sat after the <link> to this file, so its rules won every tie. Putting
   it at the end of this file reproduces that order exactly — same rules, same
   cascade, no behaviour change. Verified by re-running the 20-width sweep
   before and after.

   Not all of it is shared, and that is fine: the hero and About rules are
   scoped to .hero-08 and #story-duo, so they cost the inner pages nothing but
   the bytes.
   ======================================================================== */
    /* ===== HERO — option 08 ===============================================
       Ported wholesale from HERO.html, where the eight candidates were judged
       side by side. Kept in the page rather than in css/style.css while the
       hero is still the thing being worked on: nothing else on the site uses
       any of it, and it moves into the stylesheet once it stops changing.

       Two selectors from the study page are collapsed into .hero-08 here —
       .hero-opt carried what every specimen shared, .h-eight what option 08
       added, and on a real page that distinction has nothing left to mean. */
    .hero-08 {
      position: relative;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      /* The study page's header sat in the flow. This one is fixed, so the
         section has to make room for it — and once it does, the picture below
         measures from the same edge the column does. */
      padding-top: var(--header-h);
      --shot-gap: 2.75rem;
      /* THE knob. Every other number in the hero is derived from it, so
         enlarging the photograph is one edit and nothing goes stale behind it.
         It was three independent numbers before — a 46% left edge, a 49vw
         height formula and a 44% square — and the formula was written against
         a column width it did not read, so changing the column silently broke
         the ratio. */

      /* The two wordmark sizes live up here rather than on .brand-word, because
         the cap below has to do arithmetic on them. .brand-word still reads
         them by inheritance and its own optical-centring sum is unchanged. */
      --bw-sm: clamp(0.9rem, 1.5vw, 1.3rem);
      --bw-lg: clamp(2.6rem, 6vw, 5rem);

      /* 55vw is the number, and the min() is the floor under the square beside
         it. MASSAGE measures 4.36em of --bw-lg in Italiana at every size, and
         .brandbox clips what it cannot hold — so below about 1300px, where
         --bw-lg is still growing with the window and the column is not, a flat
         55vw eats the square and the word loses its tail. 5.4em is the word
         plus a little over an em of air. Above ~1300 --bw-lg is capped at 5rem,
         the cap stops binding and 55vw is what runs.

         NOT var(--hero-inset) here, and the difference is not cosmetic: that
         one carries a 5%, and --shot-h feeds a HEIGHT. A percentage inside a
         height resolves against the containing block's height, so the same
         expression came out 660px on the picture and 614px on the column
         beside it and the two stopped sharing a centreline. The vw twin is
         within a pixel of it and has no such second meaning. */
      --shot-w: min(55vw, calc(100vw - 2 * max(var(--gutter), 5vw) - var(--shot-gap) - 5.4 * var(--bw-lg)));
      --shot-h: calc(var(--shot-w) * 10 / 16);
      /* The inset the two columns SHARE, and the whole of the centring: the
         picture already stopped 5% off the right edge, so the lockup column is
         given the same 5% on the left and the pair is centred on the window at
         every width. max() with the site's gutter is the floor — on a narrow
         window 5% is a handful of pixels, and the picture must never arrive at
         the edge. */
      --hero-inset: max(var(--gutter), 5%);

      /* THE PICTURE'S OWN WIDTH, so the frame can be a share of it rather than
         a fixed number of pixels. Greg, 2026-08-22: halve the photograph and
         the frame should halve with it — at 1000px the 6px card was 1.1% of
         the picture against 0.57% at 1920, so the same frame read as twice as
         heavy on a laptop. Declared per layout because the picture stops
         reading --shot-w when it goes full width below 768. A LENGTH, never a
         percentage: --gutter is a clamp of rem and vw, so this resolves the
         same way in a padding and in a `bottom`. */
      --pic-w: var(--shot-w);
    }

    /* The bar's mark is a picture, so the lockup does not have to be shrunk to
       fit beside it — the name moves into the hero instead, at a size where
       the colour and the type do their job. */
    .header--mark .wordmark { display: flex; align-items: center; }
    .header--mark .wordmark img { display: block; height: 52px; width: auto; }
    /* Above 980 the figure is the mark and the name is in the hero. The text
       lockup is in the markup for the breakpoint below, not for here. */
    .header--mark .wordmark-sm,
    .header--mark .wordmark-lg { display: none; }

    /* The number carries an icon now that the hero's button is gone. Greg,
       2026-08-22: the button was the loudest thing on the first screen and he
       does not want it there, so calling has to be visible somewhere else —
       and the bar is where it already lives.

       Teal on the phone, green on WhatsApp: the green is WhatsApp's own and is
       the one colour on this page nobody has to be taught, so the phone takes
       the site's structural colour instead. Two green marks side by side would
       read as one channel offered twice. */
    .header-phone { display: inline-flex; align-items: center; gap: 0.4rem; }

    /* A DISC, not a glyph. Greg, 2026-08-22: it is the call to action now, so
       it has to read as something you press. Teal ground, white phone, the
       flags' own 20px so the two rows carry the same circle.

       The pulse is one ring, not the badge itself — scaling the badge would
       move the number beside it. `box-shadow` on a pseudo-element costs no
       layout and no repaint of anything else. */
    .call-badge {
      position: relative;
      flex: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      background: var(--teal);
    }
    .call-badge svg { width: 11px; height: 11px; display: block; fill: #FFFFFF; }
    .call-badge::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: 50%;
      box-shadow: 0 0 0 0 rgba(var(--teal-rgb), 0.55);
      animation: call-pulse 2.4s ease-out infinite;
    }
    @keyframes call-pulse {
      0%   { box-shadow: 0 0 0 0 rgba(var(--teal-rgb), 0.5); }
      70%  { box-shadow: 0 0 0 9px rgba(var(--teal-rgb), 0); }
      100% { box-shadow: 0 0 0 0 rgba(var(--teal-rgb), 0); }
    }
    @media (prefers-reduced-motion: reduce) { .call-badge::after { animation: none; } }
    .header-phone:hover .call-badge { background: var(--saffron-ink); }

    /* The green DISC matches the flags, which means the file does not.
       Measured off the PNG: it is 128px square, but the green circle inside it
       is only 104 — the rest is a white ring nobody sees on a white bar. So a
       20px file would show a 16.3px circle beside 20px flags, which is what
       Greg was looking at. 128/104 of 20px is 24.6. */
    .header-wa { display: block; line-height: 0; }
    .header-wa img { display: block; width: 24.6px; height: 24.6px; }

    /* Phone widths only: the badge on its own beside the burger, at a size
       that can be hit. 44px of target around a 34px disc. */
    .header-call { display: none; align-items: center; justify-content: center; width: 44px; height: 44px; margin-right: -0.35rem; }
    .header-call .call-badge { width: 34px; height: 34px; }
    .header-call .call-badge svg { width: 18px; height: 18px; }
    /* A hair more air before it than between the flags: it is a different kind
       of thing from them, and a link where they are not. */
    .header-langs .header-wa { margin-left: 0.2rem; }

    /* A mark in a row of words, so it is sized to the words' cap height rather
       than to its own box, and it inherits their colour and their hover. The
       underline .nav a::after draws under it like any other item. */
    .nav-home { display: inline-flex; align-items: center; }
    .nav-home svg { display: block; width: 15px; height: 15px; fill: currentColor; }

    /* The left column is given the picture's exact vertical band — the same gap
       under the bar, the same height — so centring inside it IS centring on the
       photograph, with no arithmetic to go stale. Sideways it now runs from
       --hero-inset to just short of the picture, so the square fills its own
       half of the block rather than the page container's. The square is taller than the
       band and overflows it evenly, which costs nothing: the box is invisible.
       justify-items stays at start, or the square would centre sideways too. */
    .hero-08 .inner {
      width: 100%;
      margin-top: var(--shot-gap);
      height: var(--shot-h);
      display: grid;
      align-items: center;
      justify-items: start;
    }
    .hero-08 > .container {
      /* Out of the site's 1280px band. The column used to hang off the page
         container's left edge — 400px in at 1920 — while the picture stopped
         96px off the right, and that gap between the two insets IS the lean to
         the right. Both are --hero-inset now, so the block is symmetrical
         about the centreline and stays so as the window changes. The rest of
         the page keeps its container; the hero is the one full-width section,
         which the picture already was. */
      max-width: none;
      padding-inline: var(--hero-inset);
      /* Only one gap below the picture rather than two — Greg's call, the
         sentence should sit closer to it than the bar does above it. */
      min-height: calc(var(--shot-h) + var(--shot-gap) + 1rem);
      padding-block: 0;
      display: flex;
      /* Not centred: the column sets its own band and starts at the top.
         Centring the flex line would re-centre it inside the leftover 1rem and
         put the letters 8px below the picture's centreline. */
      align-items: flex-start;
    }

    /* Same geometry the option was designed at: 46% of the window to 5% off
       the right edge, and a height derived from that width rather than from a
       grid column. A child of the section, not of the column, because the
       inset has to mean a share of the window. Both percentages resolve
       against the same box — the section — which is what makes the left inset
       above and this one exactly equal. */
    .hero-08 .shot {
      position: absolute;
      top: calc(var(--header-h) + var(--shot-gap));
      left: calc(100% - var(--shot-w) - var(--hero-inset));
      right: var(--hero-inset);
      height: var(--shot-h);
      z-index: 1;
    }
    .shot { position: relative; margin: 0; }
    .shot img { display: block; width: 100%; height: 100%; object-fit: cover; }
    /* The SHADOW family's white card. The card is a background and a padding —
       there is nothing in it to reproduce — and the clip is inset to match, or
       it would cover the frame it sits inside. */
    /* 0.5% of the picture, floored so it cannot vanish on a phone and capped
       so it cannot become a mount on a 4K screen. 5.3px at 1920, 4 at 1440,
       2.7 at 1000 — where it used to be 6 at all three. The 2px floor is the
       one place the proportion is deliberately broken: below about 400px the
       true share is under 2px and a card thinner than that stops separating
       the photograph from its shadow at all. */
    .shot--framed {
      --frame-w: clamp(2px, calc(var(--pic-w, 1056px) * 0.005), 9px);
      background: var(--white);
      padding: var(--frame-w);
    }
    .shot--framed .shot-clip { inset: var(--frame-w); }
    /* The real 2014 shadow, cut loose by tools/make-shadow-only.py.
       border-image keeps its true 25px at the edges and stretches only its soft
       middle. Never a CSS imitation — the original peaks at 25% and 75% of the
       width with a dip at the centre, which a rotated box-shadow cannot do. */
    .shot--land::after {
      /* Same idea, and this is the half that was really too heavy: 25px under
         a 532px picture at 1000 is 4.7% of it against 2.4% at 1920. 2.2% now,
         which is 23px at 1920 and 12 at 1000.

         The 34px ceiling is not taste, it is the raster: the strip is 25 real
         pixels tall and border-image stretches it to whatever border-width it
         is given. A soft shadow takes about 1.4x before it starts to band, so
         anything thicker than this wants re-cutting from the original rather
         than stretching. */
      --frame-sh: clamp(6px, calc(var(--pic-w, 1056px) * 0.022), 34px);
      content: ''; position: absolute; left: 0; top: 0; right: 0; bottom: calc(-1 * var(--frame-sh));
      border-style: solid; border-color: transparent; border-width: 0 0 var(--frame-sh) 0;
      border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 var(--frame-sh) 0 stretch;
      pointer-events: none;
    }
    /* Black and white, back to colour under the pointer — the treatment
       settled in FRAMES.html. Scoped to the figure, not to every img in the
       section. */
    .hero-08 figure img { filter: grayscale(1) contrast(1.05); transition: filter .55s var(--ease-soft); }
    .hero-08 figure:hover img { filter: none; }

    /* ===== THE HERO PICTURE MOVES AGAIN. Greg, 2026-08-22 — it had a slow
       drift at some point and it was lost. Restored, and the frame breathes
       with it, which was the second half of the ask.

       THE HOVER SCALE IS GONE — Greg, 2026-08-22: it was jerky on screen, and
       he is right that it may be why this was removed before. It was on the
       FIGURE, so every frame of it rescaled the shadow, and that shadow is a
       border-image: the browser re-samples the PNG and re-stretches its middle
       on each repaint rather than compositing a finished layer. There is no
       transform-only path for it, so it cannot go on the GPU. A 2.8s ease made
       that worse rather than better — it simply spread the expensive repaint
       over more frames.

       It could be bought back by moving the hover onto the img and leaving the
       frame still, but then the frame no longer breathes, which was the whole
       reason Greg asked for it. Removed instead. The colour still returns on
       hover and that is the picture's response to a pointer.

       WHAT IS LEFT is one animation on one element, and it is the cheap one:
       the img inside .shot-clip's overflow:hidden. Nothing else moves, so the
       shadow is painted once and never touched again.

       12s AND 1.06, which is the ordinary Ken Burns setting rather than a
       cautious one — Greg, 2026-08-22, after 48 and then 24 both came back
       "way too slow". Three passes, and the mistake each time was mine for
       tuning it below the threshold where it reads as movement at all.

       THE HOUSE NUMBER WAS ALREADY ON THIS PAGE and is where 1.06 comes from:
       .hero-slide in style.css scales to 1.06 over 7s as each slide arrives.
       This one runs the same distance at 12s because it never stops — a
       continuous loop at the slider's pace would be the busiest thing in the
       hero rather than the quietest.

       That is 47px of travel on a 792px picture, about 4px a second, which is
       what a hero photograph normally does. `alternate`, so it breathes in and
       out rather than jumping back to the start.

       ONE NUMBER TUNES IT. The duration below is the whole control; 1.06 is
       borrowed and should stay borrowed.

       transform-origin stays centred. Anything else makes the picture crawl
       toward a corner over the cycle. */
    @keyframes shotDrift {
      from { transform: scale(1); }
      to   { transform: scale(1.06); }
    }
    .hero-08 figure .shot-clip img {
      transform-origin: 50% 50%;
      animation: shotDrift 12s ease-in-out infinite alternate;
      will-change: transform;
    }

    @media (prefers-reduced-motion: reduce) {
      .hero-08 figure .shot-clip img { animation: none; }
    }

    /* The square. Logo on top, line underneath and quieter — two brand marks
       in one box compete unless one of them is clearly the support. */
    /* What is left of the column once the photograph has taken its share,
       less one --shot-gap of air. 100% here is the container's content width,
       which is the window less both insets — so the square can never collide
       with the picture however --shot-w moves. */
    .hero-08 .brandbox { width: calc(100% - var(--shot-w) - var(--shot-gap)); }
    .brandbox { position: relative; aspect-ratio: 1 / 1; display: grid; place-items: center; overflow: hidden; }

    /* The lockup, set rather than placed. Same faces and same colours as the
       bar — brown for the small line, teal for the big one, saffron on the
       last word — at four or five times the size, which is the whole point of
       moving it out of the navigation.

       Four words, four spans, and the layout is a flex wrap. That is what
       makes it reflowable: stacked in the square here, and one or two
       horizontal lines above the picture on a phone. */
    .brand-word {
      /* Centring the text BOX does not centre the letters. A line box carries
         the font's full ascent and descent, and these lines are all capitals:
         above GRAN there is only the gap between the ascender line and the cap
         line, while below MASSAGE there is a whole descender's worth of empty
         space — and it is empty at the BIG size, not the small one. Measured on
         Italiana at 80px: 9.2px above the caps, 11.2px below the baseline, so
         0.115em and 0.140em of slack at each end.

         Half the difference is how far the letters have to come down for the
         ink to sit where the box appears to. Written against the two font sizes
         rather than as a pixel, so it keeps holding while the clamps move.
         Cap of GRAN to the top edge of the photograph and baseline of MASSAGE
         to its bottom edge then agree within 0.6px from 1024 to 2560. */
      position: relative;
      top: calc((0.14 * var(--bw-lg) - 0.115 * var(--bw-sm)) / 2);

      z-index: 1;
      margin: 0;
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      align-content: center;
      gap: 0.06em 0.28em;
      font-family: var(--display);
      line-height: 0.98;
      text-transform: uppercase;
      text-align: center;
    }
    /* The two small words sit on one line between them, not one each — GRAN
       CANARIA is a place, and splitting it reads as two places. */
    .brand-word .bw-sm {
      flex: 0 0 auto;
      font-size: var(--bw-sm);
      letter-spacing: 0.42em;
      text-indent: 0.42em;
      color: var(--brown);
    }
    .brand-word .bw-lg {
      flex: 0 0 100%;
      font-size: var(--bw-lg);
      letter-spacing: 0.02em;
      color: var(--teal);
    }
    .brand-word .bw-em { color: var(--saffron); }
    /* A flex line break with nothing in it. Desktop does not need it — .bw-lg
       is flex 0 0 100% up there, so GAY and MASSAGE already take a line each
       and the two small words are alone on the first. Below 980 the big words
       go back to auto width and share a row, and without this the row filled
       up as GRAN CANARIA GAY / MASSAGE. It is a break rather than a wrapper
       round the small pair on purpose: the four spans stay four independent
       spans, which is the whole reason the lockup is set rather than an image. */
    .brand-word .bw-break { display: none; }

    .brand-lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
    .brand-lines path {
      fill: none;
      stroke: var(--teal);
      stroke-width: 2.2;
      stroke-dasharray: 0 2;
      stroke-linecap: round;
    }
    .brand-lines .is-ghost path { stroke: var(--saffron); stroke-width: 1.5; }
    /* The line is TRACED, not moved: it draws from the top of the window down
       to the bottom, erases from the top, and the next stretch of the same
       drawing begins where the last one ended. Only the resting state lives
       here — the rest is the script at the foot of the page. */
    .brand-lines .is-ghost { opacity: 0.36; }

    /* The centred line under the hero. Close under the picture: the top
       padding here plus the 1rem inside the column above is the whole gap. */
    .hero-line {
      text-align: center;
      /* Both ends brought in — Greg, 2026-08-22. The headline was sitting 24px
         under the photograph and 80px above nothing, and the 80 was the single
         biggest hole above the fold. */
      padding: 0.85rem var(--gutter) clamp(1.5rem, 3vw, 2.75rem);
    }
    /* SANS, CAPS, EVERY WIDTH. Greg, 2026-08-22, and it replaces the serif
       here only — the rest of the page keeps Crimson.

       Measured rather than argued, per unit of CAP HEIGHT, which is what the
       eye reads as size: Crimson caps run 30.99 widths per cap height at
       0.055em, Inter runs 29.05 at 0.03em — 6% narrower for the same apparent
       size, which is the room he was after. The bigger gain is not width: a
       text serif at 14-20px on a phone loses its hairlines and its serifs to
       the pixel grid, and Inter has neither to lose.

       Sized against the same cap height rather than the same em: Inter's caps
       are 73% of the em against Crimson's 65%, so the desktop clamp comes down
       by that ratio (4.4vw -> 3.9vw) and the headline keeps the size it had.

       THE COST, and it is real: this site self-hosts Inter roman only, so
       "Man to man." LOSES ITS ITALIC. It is weight and colour now, not slant —
       a browser-slanted Inter is a mechanical shear and worse than no italic.
       Two ways back if the slant is wanted: self-host Inter Italic (OFL, one
       more woff2), or revert this block. */
    .hero-line h1 {
      font-family: var(--sans);
      font-size: clamp(1.9rem, 3.9vw, 3.45rem);
      font-weight: 400;
      text-transform: uppercase;
      letter-spacing: 0.03em;
      line-height: 1.14;
      color: var(--ink);
      margin: 0;
      max-width: none;
      white-space: nowrap;
    }
    /* Bordeaux — the sarong red sampled from Greg's own photograph, and the
       accent this palette already spends about once a section. Teal was the
       structural colour and made the sentence read as a link. The weight is
       what carries the emphasis now that the slant is gone. */
    .hero-line h1 em { font-style: normal; font-weight: 600; color: var(--red); }
    /* One line above 768, so the break is off there. */
    .hero-line h1 .h1-br { display: none; }
    /* THE AWARDS. Full colour, on Greg's instruction — they are somebody
       else's mark and the whole value is that they are not ours to restyle.
       They sit under the place line, which puts them under the H1 block as a
       whole rather than inside it.

       Sized by height so all three sit on one baseline whatever their files
       do, and scaled with the window like everything else in this hero. The
       row wraps rather than overflows, though it has never had to: three at
       76px plus gaps is 300px at the widest. */
    .hero-awards {
      --aw-gap: clamp(0.6rem, 1.6vw, 1.4rem);
      display: flex;
      /* nowrap on purpose: given the choice the row must SHRINK rather than
         wrap two-and-one, which is what it did at 375. */
      flex-wrap: nowrap;
      align-items: center;
      justify-content: center;
      gap: var(--aw-gap);
      list-style: none;
      margin: clamp(1.25rem, 2.4vw, 2.25rem) 0 0;
      padding: 0;
    }
    .hero-awards a { display: block; line-height: 0; }
    /* NATIVE SIZE, 120x111 — Greg, 2026-08-22, and the reason is legibility:
       the years are set small inside the badge and they stop reading when the
       file is scaled down. The min() is not a second opinion about the size,
       the flex basis is not a second opinion about the size, it is arithmetic:
       three at 120 plus two gaps need 380px, and a 375px phone has 335. Below
       about 425 they shrink to share the row rather than wrap two-and-one, and
       above it they are the file's own size. Sizing lives on the LI: a
       percentage on the img resolves against the item, not the row, and
       collapsed all three to 23px. */
    .hero-awards li { flex: 0 1 120px; min-width: 0; }
    .hero-awards img { display: block; width: 100%; height: auto; }

    /* ONE SLOW SWELL, and it moves on the way out as well as in. Greg,
       2026-08-22: the keyframe version read as chaotic, and he is right about
       why — a bounce with a hard start and stop is the wrong gesture on a
       massage page.

       So it is a TRANSITION, not an animation: 0.9s each way on the same
       ease, staggered 0.12s left to right, so the ripple runs forward when the
       pointer arrives and unwinds in the same order when it leaves. Nothing
       snaps back, and an interrupted hover eases from wherever it had got to
       instead of restarting. 4% and 2px — small enough to notice rather than
       watch. */
    .hero-awards a {
      transition: transform 0.9s cubic-bezier(0.33, 0, 0.2, 1);
    }
    .hero-awards li:nth-child(2) a { transition-delay: 0.12s; }
    .hero-awards li:nth-child(3) a { transition-delay: 0.24s; }
    .hero-awards:hover a { transform: translateY(2px) scale(1.04); }
    @media (prefers-reduced-motion: reduce) { .hero-awards:hover a { transform: none; } }

    /* Named, not shouted. Greg, 2026-08-22: no caps here — Title Case and the
       serif italic, which is the one italic this page has and the register the
       live site used for the same line. */
    .awards-cap {
      margin: 0.75rem 0 0;
      font-family: var(--serif);
      font-style: italic;
      font-size: 16px;
      letter-spacing: 0.01em;
      color: var(--body);
      opacity: 0.8;
    }

    /* The ampersand is a joint, not a word, and it is set smaller than the
       words at every width to say so — Greg, 2026-08-22. It keeps the ink
       colour: the red belongs to TANTRA alone. */
    .hero-line h1 .h1-amp { font-size: 0.7em; }
    .hero-place {
      font-family: var(--sans);
      /* 0.8rem, was 0.78 — it computed to 11.8px at 320 and 12px is the floor
         for running text. It only binds below about 360; every wider screen is
         governed by --eb-size in the rule below, so this raises the smallest
         case and nothing else. Monotonic either way. */
      font-size: 0.8rem;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      color: var(--saffron-ink);
    }
    /* One knob for the place line at EVERY width, and it is the same 1.3rem
       the phone block caps at — so 980 to 981 is a step of zero. Greg,
       2026-08-22: a font may never get smaller as the screen gets wider, and
       this one was falling 20.8px -> 12.5px at that boundary. */
    .hero-line { --eb-size: 1.3rem; }
    .hero-line .hero-place { font-size: var(--eb-size); margin-top: 0; }
    /* The hairline runs on the desktop too, same construction: 23.5x the place
       line's own size, which is that line's width plus 4%. */
    .hero-line .hero-rule {
      display: block;
      width: calc(var(--eb-size) * 23.5);
      max-width: 100%;
      height: 1px;
      margin: 0.5rem auto;
      background: linear-gradient(90deg, transparent, var(--saffron-ink) 18%, var(--saffron-ink) 82%, transparent);
      opacity: 0.5;
    }
    @media (max-width: 700px) { .hero-line h1 { white-space: normal; } }

    /* ===== THE BAR HOLDS ITS FULL LAYOUT DOWN TO 768 ====================
       Greg, 2026-08-22. The bar used to fold at 980 and the hero restacked at
       the same width. BOTH now turn at 768, together — so 769 to 980 is the
       desktop design at a narrower window, two columns and all, rather than a
       third layout of its own.

       The nav's gap is what buys it: 2.1rem fixed became clamp(0.9rem, 2.2vw,
       2.1rem), which is unchanged at 1280 and 16.9px at 768. Measured, the row
       needs the figure mark (52) + the six items (375 of glyphs) + the contact
       block (114); at 768 the bar's inner width is 691 and the whole row comes
       to 626, leaving 32px each side of the nav. It fits, and it is tight —
       below 768 there is no arrangement of gaps that saves it.

       WHY THE MARK CANNOT COME WITH IT: the text lockup is 169px against the
       figure's 52, which needs ~880px of window, so between 768 and 980 the
       bar shows the figure and the NAME GOES BACK INTO THE HERO. That is M8's
       rule kept, not broken — the name appears exactly once above the fold,
       either in the bar or over the picture, never both and never neither. */
    .nav { gap: clamp(0.9rem, 2.2vw, 2.1rem); }
    @media (min-width: 769px) and (max-width: 900px) {
      /* style.css folds the header at 900 for every page on the site. This
         page overrides it in its own band rather than editing the shared rule,
         because the inner pages carry the 169px text lockup in the bar and do
         not have the room this one does. */
      .nav { display: flex; }
      .header-contact { display: flex; }
      /* inline-flex, not block: the number carries an icon beside it now, and
         `display: block` here quietly overrode that and squeezed the pair into
         the flags' width. */
      .header-phone { display: inline-flex; }
      .menu-toggle { display: none; }
    }

    @media (max-width: 768px) {
      /* The bar's mark becomes the name. .wordmark is a column by default;
         .header--mark had flattened it to a row for the figure. */
      .header--mark .wordmark { flex-direction: column; align-items: flex-start; gap: 0.18em; }
      .header--mark .wordmark img { display: none; }
      .header--mark .wordmark-sm,
      .header--mark .wordmark-lg { display: block; }

      /* FLUSH LEFT AND FLUSH RIGHT with the line under it. Greg, 2026-08-22:
         GRAN CANARIA started 7px right of GAY's G. The 7px was not an accident
         — `text-indent: 0.60em` in style.css cancels the trailing letter-space
         so the line sits optically CENTRED over the one below, which is right
         for the hero and wrong for a bar where both lines are flush left.

         So: no indent here, and the tracking is re-derived to fill the width
         instead. The glyphs alone measure 85.1px and there are 11 gaps, so
         167.9 - 85.1 over 11 is 7.53px — 0.6444em at this 11.68px size. Both
         sizes are fixed rem, so one figure holds at every width. */
      .header--mark .wordmark-sm { text-indent: 0; letter-spacing: 0.6444em; }

      /* The name is in the bar now, so the block above the picture goes — the
         page must not say its own name twice within forty pixels. Above this
         width the bar carries the figure instead and the hero keeps the
         lockup. The four spans and their break stay in the markup either way. */
      .hero-08 .inner { display: none; }

      .nav, .header-phone, .header-right .btn, .header-contact { display: none; }
      .menu-toggle { display: block; }
      .header-call { display: inline-flex; }
      /* The square goes horizontal above the picture, which is the saving Greg
         was after: the words keep their size and stop owning a column. GAY
         MASSAGE comes off its own line and joins GRAN CANARIA, so the lockup
         is two short rows instead of a tall block.

         Both centring corrections are switched off here: the lockup is above
         the picture now, not beside it, so there is no shared centreline left
         to line the letters up against. */
      .hero-08 .brandbox { width: 100%; aspect-ratio: auto; padding-block: 1rem 2rem; }

      /* The line comes off. Greg, 2026-08-22, and it is the right call rather
         than a smaller version of the same idea: the drawing was composed to
         fill a square standing beside the photograph, and stacked there is no
         square left — .brandbox is a short wide strip, so `inset: 0` on a fixed
         viewBox squashed the whole path onto the type and drew it through GRAN
         CANARIA. With the picture taking the full column and the lockup sitting
         above it, the line has no job here.

         display:none rather than deleted from the markup: the hero path is
         drawn on load by the script at the foot of the page, and it should
         still find its element. The desktop hero is untouched. */
      .brand-lines { display: none; }

      /* Baseline, not the flex default. Stacked, GRAN CANARIA shares a row with
         GAY instead of sitting on its own line above — and stretched items put
         the small caps at the TOP of the big letters, which is what Greg was
         looking at. On the baseline they sit down on GAY's feet, which is the
         way the two sizes are meant to read as one lockup. */
      .brand-word { --bw-lg: clamp(2.2rem, 9vw, 3.4rem); top: 0; align-items: baseline; }
      .brand-word .bw-lg { flex: 0 0 auto; }
      /* GRAN CANARIA on the first row, GAY MASSAGE on the second. Greg,
         2026-08-22, looking at 425 where the row was breaking after GAY. */
      .brand-word .bw-break { display: block; flex: 0 0 100%; height: 0; }
      /* The picture stops being a side panel and takes the full column, with
         the lockup sitting above it instead of beside it. */
      /* Stacked, there is no pair left to centre, so the hero rejoins the
         gutter every other section uses and the picture's own margin below
         matches it. */
      .hero-08 > .container { min-height: 0; padding-inline: var(--gutter); }
      /* Caps, and smaller. Greg, 2026-08-22: the sentence was outweighing the
         photograph on a phone. The desktop clamp bottoms out at its 2.4rem
         FLOOR on every phone width — 4.4vw is only 18.7px at 425 — so the line
         was running at 38.4px against a 239px-tall picture, and nothing about
         it was responsive.

         Caps do half the work on their own: no descenders and every letter at
         cap height, so the block reads tighter and more deliberate at a size
         where mixed case would look shrunken. Tracking is not optional with
         them — a text serif set in caps at 21px closes up without it. The
         italic accent survives, which is why the face stays Crimson: Italiana
         has no italic to give it. */
      /* One knob, read by the headline AND the place line under it, so the
         second can never grow past the first. Greg, 2026-08-22. */
      /* ONE layout for every width below 980, and that is the point. Greg,
         2026-08-22: the place line was jumping in size at the width where it
         moves above the headline, and the fix is not to soften the step but to
         delete the step — there is no longer a second phone layout to switch
         to. Place line, hairline, two-line headline, at 280 and at 979.

         The two knobs are locked to each other. The place line sets to 22.5x
         its own size at its tracking and PROFESSIONAL MASSAGE. sets to 13.63x
         its own, so 3.7vw and 6.1vw put the two rows at the SAME WIDTH — 1px
         apart, measured — and they hold that at every width because they share
         the slope. Both cap at 485px so the block stops growing on a tablet
         rather than becoming a poster; capping BOTH at the same width is what
         keeps the widths matched above it. */
      .hero-line {
        --eb-size: min(3.69vw, 1.047rem);
        --h1-size: min(6.7vw, 1.9rem);
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      .hero-line h1 {
        font-size: var(--h1-size);
        line-height: 1.3;
        white-space: normal;
      }
      /* The row break is the <br>, not the em: the accent is one word inside
         the sentence now, so making IT the block would orphan TANTRA on a line
         of its own. Rows are PROFESSIONAL MASSAGE / AND TANTRA. */
      .hero-line h1 em { display: inline; white-space: nowrap; }
      .hero-line h1 .h1-br { display: inline; }

      /* The place line is now a FRACTION of the headline, not a fixed 0.78rem.
         Greg, 2026-08-22: at phone widths the headline had shrunk to about the
         eyebrow's size and the qualifier was reading louder than the claim.
         0.66 keeps it clearly subordinate; the min() means it never grows past
         the 0.78rem it has everywhere else, so tablet and up are unchanged. */
      .hero-line .hero-place { order: -2; font-size: var(--eb-size); margin: 0; }

      /* Above the headline here, and tighter: the three rows are one block on a
         phone, so the rule sits close to both rather than dividing them. */
      .hero-line .hero-rule { order: -1; margin-block: 0.3rem; }

      /* 1.25rem, not 0 and not the desktop 2.75. With the lockup out of the
         column the picture landed hard against the bar, and the bar is a
         translucent white with nothing under it yet (#7) — the photograph was
         running straight into it. Reduced is what Greg asked for; removed is
         not the same thing. */
      /* Full width now, so the frame's share is measured against the column
         rather than against --shot-w, which nothing here reads any more. */
      .hero-08 { --pic-w: calc(100vw - 2 * var(--gutter)); }
      .hero-08 .shot { position: relative; top: auto; left: auto; right: auto; height: auto; aspect-ratio: 16 / 10; margin: 1.25rem var(--gutter) 0; }
    }


    /* THE TEXTURE. Greg's own file, TEXTURES-GREG/texture.png, copied into
       images/. Measured before placing it: 1920x1080, and the grain is not
       spread over it — it is a BAND. Rows 0-25% and 60-100% are fully
       transparent, and everything visible sits between about 30% and 55%, at a
       mean alpha of 4 across the whole file. So it is sparse grey speckle with
       its own fade top and bottom, which is why it needs no gradient mask of
       ours.

       It hangs from the top of this section and runs 55vh PAST it, so the fade
       finishes inside About rather than at a section edge — Greg's condition.
       `z-index: -1` puts it behind everything in the page's flow while staying
       above the white ground, so About's photographs and text sit on top of it
       and the grain shows in the gaps.

       `cover` rather than a stretch: the band's grain is a photograph of a
       surface, and scaling it non-uniformly would smear it. */
    #what-this-is { position: relative; }
    #what-this-is::after {
      content: '';
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
      height: calc(100% + 55vh);
      background: url(../images/texture.png) center top / cover no-repeat;
      /* The file fades out at its own bottom edge but starts hard at the top,
         and a hard horizontal line across the page reads as a mistake. This
         only softens the entry — the exit is the file's own. */
      -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%);
              mask-image: linear-gradient(to bottom, transparent 0, #000 16%);
      pointer-events: none;
      z-index: -1;
    }

    /* ===== ABOUT — the crop point on the back frame ======================
       STALE, kept for the reasoning only: the file below is greg-massage-therapist.jpg
       (560x1484) and has been for a while. greg.jpg itself is gone — moved to
       ZZ-WORK FILES on 2026-08-22 with five other unused Greg photographs. The
       numbers describe the file that WAS here and the argument still holds,
       because the replacement is a 1:2.65 sliver against the same 2:3 frame.

       greg.jpg is 320x992, a 1:3.1 sliver, and the frame is 2:3. Cover scales
       it to the frame's width and then shows 48% of its height — centred, that
       is chest to knees, and it cut his head off in a section called About.

       12% rather than 0%: hard to the top left a strip of ceiling above him.
       The percentage holds at every width because the frame's ratio is fixed
       at 2:3, so this does not need a breakpoint. It is a patch on a file that
       is the wrong shape and the wrong size — a portrait re-export nearer 2:3
       retires it. */
    .duo-frame--back img { object-position: 50% 12%; }


    /* ===== ABOUT — the front frame shows the whole cutout =================
       Greg, 2026-08-22: greg-personal-trainer.png must fit inside the
       frame completely, nothing cropped. cover was scaling it up until it
       filled the box and throwing away whatever hung outside; contain scales
       it down until the whole PNG is inside instead.

       The frame is NOT resized by this — it stays 49% x 106.85%. Only the
       photograph changes.

       AND IT IS SIZED FOR THE PARALLAX, Greg 2026-08-22. contain alone was
       not enough: the picture still moves inside the frame, so it has to be
       small enough to survive the travel, not just small enough to fit at
       rest. The arithmetic, written down so it is not re-derived later.

         The frame is 49% wide by 106.85% tall of a square column, so its own
         ratio is 0.459:1.

         greg-personal-trainer.png is 704x1726 — 0.408:1. Measured from
         the file, NOT the 1680x2976 of personal-trainer-greg-2.jpg described
         further down the page; they are different files and that note does
         not describe this one.

         Left at the inherited 126%/top:-13%, contain was WIDTH-constrained:
         it rendered at the full frame width and 112.4% of the frame height,
         so it hung 6.2% past the frame at top and bottom before the scroll
         did anything at all.

         duoFrame() in main.js shifts this img by up to 3% of the frame height
         either way — DUO_INNER_FRONT was HALVED from 0.12 to 0.06 on Greg's
         call, 2026-08-22, precisely to buy this size back. So the picture may
         be at most 100 - 2*3 = 94% of the frame height. 92% is used, which
         keeps 1% clear at both ends of the scroll.

       So: height 92%, top 4% — centred, and now HEIGHT-constrained, which is
       what puts the whole 92% on screen. Rendered width falls to about 82% of
       the frame and contain centres it horizontally on its own.

       THESE TWO NUMBERS ARE ONE DECISION. The travel in main.js and the height
       here trade against each other and must be changed together: at the old
       0.12 the ceiling was 88%, at 0.06 it is 94%, and at zero travel it would
       be 100%. Change one alone and the cutout gets cropped by the scroll.

       The back frame is untouched: it still fills its frame with cover at
       126%/top:-13%, and main.js is unchanged for both.

       WHAT THIS DOES NOT SOLVE, and it is for Greg's eye rather than a fix:
       the frame's drop shadow lives on .duo-frame::after and is drawn at the
       FRAME's right and bottom edges. The photograph no longer reaches them,
       so the shadow is now cast by transparent space. Judge it on screen. */
    .duo-frame--front img {
      object-fit: contain;
      top: 4%;
      height: 92%;
    }


    /* ===== SANCTUARY — black and white, colour under the pointer ==========
       Greg, 2026-08-21. The three photographs are shot in three different
       colour schemes and putting them in a row makes that the loudest thing
       about the section. Grey removes the argument until the grade is agreed;
       hovering gives the picture back its own colour, which is the same
       treatment the hero photograph and the About frames already use, so the
       page has one behaviour for photographs rather than three.

       filter:none rather than a saturate() number on purpose — none is the
       photograph exactly as shot, which is what "natural saturation" means and
       what has to be judged when the unified grade is chosen.

       This overrides --photo-filter for these three only. That variable is the
       page-wide placeholder grade and it stays where it is. */
    #studio .shape-media img {
      filter: grayscale(1) contrast(1.05);
      transition: transform 1.4s var(--ease-out), filter 0.55s var(--ease-soft);
    }
    #studio .shape:hover .shape-media img { filter: none; }


    /* ===== BAND — the photograph keeps its own colour =====================
       Greg, 2026-08-21. Every other photograph on this page is grey, either
       from --photo-filter or from the deliberate grey-to-colour treatment on
       the hero, About and Sanctuary. This one is left as shot so the palette
       can be judged against a real colour photograph rather than against a
       page that has quietly removed all the colour from itself.

       Only the grade is dropped. The dark scrim over it stays, and so does the
       parallax — the picture is still 135% of the section travelling through
       35% of itself, so it covers at both ends and is never stretched. */
    #band .band-media img { filter: none; }

    /* ===== FOOTER — option C from FOOTER.html ==============================
       Chosen 2026-08-21 out of three. It replaces a footer that was 1006px
       tall on a desktop and 1050 on a phone — taller than the screen it sat
       in — and a full-bleed photograph behind it. This one is 234px and its
       ground is a colour the palette already owned.

       --teal-deep was measured when it was tried on the hero band and
       rejected there: white 13.72:1 on it, saffron 6.78:1. So it is the one
       dark ground on this site where the warm accent can be READ rather than
       only drawn, and nothing else was using it.

       THE ONE DRAWING. The lines are the site's decorative element and they
       are busy further up the page, so the footer gets exactly one: the same
       language flattened into a rule along the top edge, and left STILL. By
       the time a reader is down here they have stopped looking for movement.

       THREE ROWS, and the split is Greg's:
         1  the mark, the pages, the phone number, the flags and WhatsApp
         2  the address and the email
         3  copyright, the disclaimer, and the pages the law wants
       The address used to share a column with the copyright, which made that
       column 406px wide and put it on course to collide at middling widths.
       Split out it is 202px, and the row breathes at 1024. */
    /* THE FOOTER IS RAISED, AND NOTHING IS DARKENED TO DO IT ---------------
       Greg, 2026-08-24: the cast shadow "makes it look dirty". He is right and
       the reason is not the numbers. Black at low alpha over a warm white is a
       NEUTRAL GREY, and grey beside warm white reads as dirt rather than as
       depth. Every version of that idea fails the same way, including putting
       an inset shadow on the section above -- same grey, painted from the
       other side. So the cast shadow is gone entirely and the lift is built
       from two cues that never touch the white:

       1  OCCLUSION -- the radius, and the negative margin under it. The eye
          reads "in front of" faster than it reads shading; it is the strongest
          depth cue there is. A full-bleed slab cannot occlude anything, though,
          because its edge is a straight line either way -- so the top corners
          are rounded and the page shows through the notches. That is what
          turns a band into a card lying ON the page. The -14px pulls it over
          the section above; the last content there is a hairline 46px up, so
          nothing is covered.

       2  THE EMBOSS -- inset 0 1px 0. A one-pixel lighter line along the top
          edge is the slab's top face catching the light, which is what an
          emboss actually is: light the edge, do not darken the neighbour. It
          follows the radius round both corners for free, and it costs the
          white page nothing at all.

       White at 0.34 rather than saffron: this is light on a surface, not an
       accent, and the saffron rule already running across the footer top would
       have to compete with it. 0.20 was tried first and was invisible at page
       scale against #04323E -- the emboss only does its job if the lit edge
       can actually be seen.

       THE CORNER IS AN ELLIPSE, NOT A CIRCLE, and that is the whole point of
       the `/` in the value: 140px of horizontal radius against 6px of
       vertical. A round corner ties the two together, so the only way to get a
       gentle bend was a big radius, and a big radius drops the corner a long
       way down -- at 56 round, the white cut 56px into the footer at each end.
       Greg's note was exactly this: keep the softness, lose the white.

       Split apart, the two are independent. 140 across gives a long relaxed
       sweep; 6 down means the white barely enters the footer at all. The
       progression, all measured side by side: 16 and 22 round were too sharp,
       56 round was gentle but ate 56px of white, 56/5 was back to an abrupt
       little nick, 140/6 is the sweep without the white, 280/8 is the same
       idea stretched further and is the next step if this still wants
       flattening.

       AND NO CONTACT SEAM. A 6px 0.16 one was measured on top of this and it
       is the dirt in miniature -- a grey halo sitting on the white above the
       curve. Small, but the same failure, so it is not there. If this ever
       wants more weight, it comes from the radius or the highlight, never from
       darkening the page. */
    .site-footer {
      position: relative;
      margin-top: -14px;
      border-radius: 140px 140px 0 0 / 6px 6px 0 0;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.34);
      background: var(--teal-deep);
      color: #FFFFFF;
      overflow: hidden;
      padding-block: clamp(2.25rem, 4vw, 3rem) 0;
    }
    /* ===== #16 — THE FOOTER'S EFFECT. Greg, 2026-08-22: a halo, and his own
       suggestion was two of them rather than one in the middle. Two it is, and
       the pair is the better call: one central glow behind a centred wordmark
       would read as a lighting mistake, while two off-centre ones read as a
       room with light in it.

       At 20% and 80% — his "one fifth from each side". Anchored to the BOTTOM
       edge so the light rises, which is what makes it a reflection rather than
       a spotlight.

       Saffron, because this is the one ground on the site where the warm accent
       can actually be read: --teal-deep was measured against it at 6.78:1 when
       it was tried on the hero band and rejected. .13 at the core — a halo that
       can be pointed at is too strong, and the whole justification for putting
       an effect this far down the page is that it rewards a second look rather
       than demanding a first one.

       Ellipses, not bars. 26% wide against 150% tall is a column of light with
       no edge to it; a real bar would need a hard stop and would compete with
       the saffron rule along the top.

       ONE PAINT, not two elements: both live in a single ::before as two
       backgrounds. z-index 0 puts them over the ground and under every row,
       all of which already carry z-index 1. */
    .site-footer::before {
      content: '';
      position: absolute;
      inset: 0;
      z-index: 0;
      pointer-events: none;
      background:
        radial-gradient(ellipse 26% 150% at 20% 100%,
          rgba(242, 168, 9, 0.13) 0%, rgba(242, 168, 9, 0.05) 45%, transparent 72%),
        radial-gradient(ellipse 26% 150% at 80% 100%,
          rgba(242, 168, 9, 0.13) 0%, rgba(242, 168, 9, 0.05) 45%, transparent 72%);
    }
    .site-footer .f-line {
      position: absolute;
      left: 0; right: 0; top: 0;
      height: clamp(60px, 9vw, 110px);
      width: 100%;
      pointer-events: none;
    }
    .site-footer .f-line path {
      fill: none;
      stroke: var(--saffron);
      stroke-width: 2.4;
      stroke-linecap: round;
      opacity: 0.5;
      /* non-scaling-stroke is deliberately NOT used: it breaks pathLength and
         has cost this project a day once already. The stroke is thin enough
         that the preserveAspectRatio="none" stretch does not show. */
    }

    /* --- row one --------------------------------------------------------- */
    .site-footer .f-row {
      position: relative;
      z-index: 1;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1.5rem clamp(2rem, 4vw, 3.5rem);
      flex-wrap: wrap;
      padding-bottom: clamp(1.1rem, 2vw, 1.6rem);
    }
    /* The name at a size that identifies the site and then stops. The hero is
       where it is enormous; doing that twice spends the gesture. */
    .site-footer .f-mark { margin: 0; font-family: var(--display); line-height: 1; text-transform: uppercase; }
    .site-footer .fm-sm {
      display: block;
      font-size: 0.68rem;
      letter-spacing: 0.42em;
      text-indent: 0.42em;
      color: rgba(255, 255, 255, 0.6);
      margin-bottom: 0.5rem;
    }
    .site-footer .fm-lg { display: block; font-size: clamp(1.5rem, 2.6vw, 2.1rem); letter-spacing: 0.04em; }
    .site-footer .fm-lg em { font-style: normal; color: var(--saffron); }

    /* The navigation as one line of text. Six links is a sentence, not a
       column — the column only exists because footers have always had one. */
    .site-footer .f-inline {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.4rem 1.4rem;
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .site-footer .f-inline a {
      font-size: 13px;
      letter-spacing: 0.06em;
      color: rgba(255, 255, 255, 0.88);
    }
    .site-footer .f-inline a:hover { color: var(--saffron); }

    /* THE TWO ROWS SHARE A CENTRE LINE. Greg, 2026-08-26: the flags, the
       divider and the WhatsApp mark should sit centred under the phone number.
       They were flex-start, so both rows hung off the left edge and the icons
       finished 8.6px short of the number at 1440 and 4.8px at 1200 -- close
       enough to look like a mistake rather than a decision.

       align-items ON THE COLUMN, not align-self on the icons, and the reason is
       the case where the number is the NARROWER of the two: the phone is
       clamp(1.05rem, 1.7vw, 1.35rem), so below about 1130px it shrinks past the
       icons' fixed 119.8px and the icon row becomes the widest thing in the
       column. Centring only the icons would do nothing there. Centring the
       column keeps the two concentric whichever one is wider, which is what
       "centred in relation to the phone number" has to mean at every width.

       This is the shared footer, so it lands on all eight pages. */
    .site-footer .f-reach { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; margin: 0; }
    .site-footer .f-phone { font-family: var(--serif); font-size: clamp(1.05rem, 1.7vw, 1.35rem); color: #FFFFFF; }
    .site-footer .f-phone:hover { color: var(--saffron); }

    /* The flags under the phone number, at the bar's own treatment: 30px files
       drawn down to 20px so they stay crisp on a retina screen. Here they mean
       the languages a call can be made in, not a site-language switcher — this
       site is English only — so they are NOT links.

       WhatsApp joins them as a fourth icon rather than as another text link,
       so the group reads as one answer to "how do I reach him". Its PNG is
       128px drawn down to 22, and it gets the extra 2px because a round mark
       reads smaller than a rectangle beside it at the same height. */
    .site-footer .f-icons { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.7rem; }
    .site-footer .f-icons img { display: block; width: 20px; height: 20px; }
    .site-footer .f-wa {
      display: block;
      line-height: 0;
      margin-left: 0.35rem;
      padding-left: 0.6rem;
      border-left: 1px solid rgba(255, 255, 255, 0.22);
      opacity: 0.9;
      transition: opacity 0.3s var(--ease-soft);
    }
    .site-footer .f-wa img { width: 22px; height: 22px; }
    .site-footer .f-wa:hover { opacity: 1; }

    /* --- row two: where he is, and the other way to write ----------------- */
    /* Email sits under the phone and the icons, so everything a visitor can
       reach him by ends up in the same column of the page. */
    .site-footer .f-where {
      position: relative;
      z-index: 1;
      display: flex;
      flex-wrap: wrap;
      justify-content: space-between;
      align-items: baseline;
      gap: 0.35rem clamp(1.5rem, 4vw, 3rem);
      padding-bottom: clamp(1.1rem, 2vw, 1.6rem);
      font-size: 13px;
      line-height: 1.5;
      color: rgba(255, 255, 255, 0.72);
    }
    .site-footer .f-where a { color: rgba(255, 255, 255, 0.72); }
    .site-footer .f-where a:hover { color: var(--saffron); }

    /* --- row three ------------------------------------------------------- */
    /* ===== #17 — THE LEGAL STRIP GETS ITS OWN GROUND. Greg, 2026-08-22:
       "different tone, something darker, to make a distinction."

       #01212B against the footer's #04323E — the same hue, dropped in lightness
       only, so it separates without introducing a colour the palette does not
       own. It is a 1.22:1 step: enough to read as a distinct band, not enough to
       read as a second section.

       Contrast goes UP rather than down, which is the point — the disclaimer is
       the one line on this page that a hostile reader quotes back. White at 55%
       measures 5.8:1 on it (was 5.2), links at 72% measure 9.1:1, and saffron
       on hover 8.3:1.

       FULL-BLEED, and that is why it is a ::before rather than a background.
       .f-floor sits inside .container, so a plain background would stop at the
       gutter and at 1280px — a legal strip that is inset looks like a mistake.
       The 100vw pseudo-element spans the window instead, and .site-footer's
       overflow:hidden is what keeps it from producing a scrollbar. It also has
       to sit ABOVE the halo, which is why the halo is at z-index 0. */
    .site-footer .f-floor {
      position: relative;
      z-index: 1;
      padding-block: 0.9rem 1.1rem;
    }
    /* The rule moved OFF .f-floor and onto this, 2026-08-22, and both halves of
       that were wrong before: it was rgba(255,255,255,.14), which is invisible
       against a ground this dark, and it stopped at the container's gutter while
       the strip it was capping ran the full window — so the line ended 72px
       short of the colour change at 1440.

       0.55 white, and it is a border on the full-bleed layer, so the rule and
       the ground now start and stop together. */
    .site-footer .f-floor::before {
      content: '';
      position: absolute;
      top: 0; bottom: 0;
      left: 50%;
      transform: translateX(-50%);
      width: 100vw;
      z-index: -1;
      background: #01212B;
      border-top: 1px solid rgba(255, 255, 255, 0.55);
      pointer-events: none;
    }
    /* Three columns rather than one line of small grey text holding three
       unrelated things. The third is sized to take a second link: aviso legal
       joins Privacy Policy there when it is written — deferred, Greg's call,
       2026-08-21, on the grounds that the live site has never had one. */
    .site-footer .f-legal {
      display: grid;
      grid-template-columns: auto minmax(0, 1fr) auto;
      gap: 0.4rem clamp(1.5rem, 4vw, 3rem);
      align-items: center;
      font-size: 12px;
      line-height: 1.5;
      color: rgba(255, 255, 255, 0.55);
    }
    .site-footer .fl-mid { text-align: center; }
    .site-footer .fl-end { text-align: right; display: flex; gap: 1.25rem; justify-content: flex-end; }
    .site-footer .f-legal a { color: rgba(255, 255, 255, 0.72); }
    .site-footer .f-legal a:hover { color: var(--saffron); }

    @media (max-width: 760px) {
      .site-footer .f-row { gap: 1.25rem; }
      /* Three columns need three columns' worth of width. Stacked and
         left-aligned below this, which is also the order they are read in. */
      .site-footer .f-legal { grid-template-columns: minmax(0, 1fr); }
      .site-footer .fl-mid { text-align: left; }
      .site-footer .fl-end { text-align: left; justify-content: flex-start; }
      /* space-between on one wrapped line leaves a ragged gap between the two;
         stacked, they are simply two lines. */
      .site-footer .f-where { justify-content: flex-start; gap: 0.2rem; }
      .site-footer .f-where > * { flex: 0 0 100%; }
    }

/* ===========================================================================
   INNER PAGES — the shared opener, and the testimonials list.
   Built 2026-08-22 with testimonials.php, the first page after the home page.
   .page-head and .page-lede are for every inner page, not just this one.
   ======================================================================== */

/* The opener. Same shape as a section head on the home page — the eyebrow
   carries the heading and there is no display-lg above it — so an inner page
   opens the way the home page's sections do rather than announcing itself in
   a different voice. Here the eyebrow IS the h1. */
.page-head { padding-bottom: clamp(1.5rem, 3vw, 2.5rem); }
.page-head .eyebrow { margin-bottom: 1.25rem; }
/* THE 18ch CAP AT LINE ~2615 WAS WRITTEN FOR A DISPLAY-SIZED h1 and this shell
   puts the h1 in the eyebrow instead, at 12px uppercase -- where 18ch is 142px
   and wraps any title of more than two words. It went unnoticed for five pages
   because Coaching, Gallery, FAQ, Contact and Testimonials are all one word.
   "A Body That Works" is four, and it broke after "THAT". A measure belongs to
   a size, so it is lifted for the eyebrow variant rather than widened by a
   number that would be wrong at the next title. */
.page-head h1.eyebrow { max-width: none; }
.page-lede {
  margin: 0;
  max-width: 46ch;
  font-family: var(--sans);
  font-size: var(--read-size);   /* the page's one reading size, shared with
                                    #what-this-is and About */
  line-height: 1.55;
  color: var(--body);
}

/* --- the testimonials list ---------------------------------------------
   ONE COLUMN, not a grid of cards. These are letters of very unequal length —
   273 characters against 6,328 — and a card grid would either crop them or
   leave holes. A column lets each one be its own size, which is what they are.

   60ch, wider than the FAQ's 800px because this is somebody talking at length
   rather than an answer to a question. */
/* BREAKING THE COLUMN, QUIETLY -- Greg, 2026-08-24, second pass. The first
   attempt was a four-step cycle: right, 11%, 0, 22%, repeating. He called it
   "too strong, too much" and he is right -- a 510px jump to the right margin
   and back is a staircase, and a staircase is a pattern the eye solves in two
   items and then reads as mechanical. This is a premium service page; it
   wants composure, not choreography.

   SO THE OFFSETS SHRANK AND STOPPED REPEATING. Nothing is right-aligned any
   more; the largest indent is 9% where it was 22%, and the sequence runs
   0, 7, 0, 4, 9, 0, 5, 0, 8 -- nine hand-set values with no cycle in them, so
   there is nothing to predict. It reads as a column that breathes rather than
   one that steps.

   THE MEASURE IS STILL THE POINT. .t-list opens to the full container and the
   62ch moves onto .t-item, so every letter stays at a reading width and only
   its position varies. */
 /* CENTRED ON STEFAN -- Greg, 2026-08-24. The offsets, the asymmetry and the
   indentation are all unchanged; the whole list is moved sideways once so that
   the FIRST letter sits centred in the viewport and every other letter keeps
   its own offset relative to him.

   ONE NUMBER, DERIVED, NOT NINE. Stefan is flush left at offset 0 and 62ch
   wide inside a 1136-wide list, so his centre sits at 313 where the viewport's
   is at 568 -- the list is already centred, so the shift is simply half the
   slack: (1136 - 625.8) / 2 = 255.1px at 1440. Written as a percentage of the
   list's own width, because translateX resolves % against the element itself,
   so it stays correct at every width without a media query.

   max() floors it at zero: below about 800px the list is narrower than the
   measure, the slack goes negative, and an unclamped calc would drag the
   column off the left edge. */
/* MINUS STEFAN'S OWN INDENT -- 2026-08-25. He is no longer flush left: Greg
   moved Martin to the edge and pushed Stefan 3% off it, so the derivation
   above, which assumed offset 0 on the first letter, would have carried his
   centre 34px right of the viewport's at 1440. Subtracting the same 3% puts
   him back on it to the pixel and moves the rest of the column left around
   him instead. The relative geometry between letters is untouched either way;
   this only decides where the whole column sits in the window, and that was
   settled yesterday. max() still floors the whole expression at zero. */
.t-list {
  max-width: none;
  transform: translateX(max(0px, (100% - 62ch) / 2 - 3%));
}
.t-item {
  position: relative;
  max-width: 62ch;
  /* ASYMMETRIC NOW -- Greg asked for the rule to come closer to the text it
     follows. The gap above the line drops from 52px to 28px at 1440; the gap
     below it, which separates one letter from the next, is unchanged. */
  padding-top: clamp(2.25rem, 4vw, 3.25rem);
  padding-bottom: clamp(1.25rem, 2.2vw, 1.75rem);
}
/* THE RULE IS THE LETTER'S OWN BOTTOM EDGE, not the next letter's top -- Greg,
   2026-08-24, and it is what makes the button placeable. A border-top belongs
   to the block below it, so the hairline under Tom carried the NEXT letter's
   left-aligned geometry and its "end" was on the right; he asked for Tom's
   mark on the left. Owned by the letter above, the rule takes that letter's
   offset, width and alignment, and its end is always the side the mark goes.

   A pseudo-element rather than a border, because the rule has to stop short of
   the mark: 40px of ring plus a 6px gap. A border cannot be shortened, and a
   transparent ring sitting on top of an unbroken hairline would have the line
   running straight through the plus. */
.t-item::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: calc(100% - 46px);
  height: 1px;
  background: var(--hairline);
}
/* Nothing is cut off above the first letter, so it keeps a full-width rule of
   its own -- the only one on the page that is not somebody's bottom edge. */
.t-item:first-child { border-top: 1px solid var(--hairline); }
/* THE OFFSET BELONGS TO THE LETTER, NOT TO ITS POSITION -- rewritten
   2026-08-25, when Deon & Oliver moved from second to last. Under nth-child
   every letter after the one that moved silently inherited a stranger's
   indent, and the saffron rule landed on whoever happened to be third. The
   value is now carried as a class on the article, so a letter can be
   reordered, added or dropped and the irregular column survives it.

   Still irregular and still without a cycle: 3, 0, 4, 9, 0, 5, 0, 8, 2 --
   Stefan, Martin, John, John, Tom, Rob, Bart, Noel, Deon. Tom's 0 is not an
   offset at all; he is the right-aligned letter and his left edge is wherever
   his measure ends up.

   THREE OF THESE ARE NEW AND THE REST ARE EACH LETTER'S OWN. Greg,
   2026-08-25: Martin goes to the left edge and Stefan comes off it, which
   inverts the pair rather than nudging one of them, and Martin's 0 is what
   makes "the edge" mean anything on the page. Deon moved 7 -> 2 for a reason
   that only appeared once he was last: at 7 he sat 1% off Noel's 8 directly
   above him, and two offsets that close read as a failed repeat rather than
   as asymmetry. Every adjacent pair now steps by at least 2%. */
.t-off-2 { margin-left: 2%; }
.t-off-3 { margin-left: 3%; }
.t-off-4 { margin-left: 4%; }
.t-off-5 { margin-left: 5%; }
.t-off-8 { margin-left: 8%; }
.t-off-9 { margin-left: 9%; }
/* NOEL has no READ MORE, so his rule has no mark to stop for. */
.t-item--whole::after { width: 100%; }

/* THE RULE, ON TWO OF THE NINE. Same saffron 2px the FAQ statement uses, so
   the pages share one accent rather than inventing a second. Two, not four:
   it is an accent on the page's two longest letters, not a decoration handed
   out on a schedule. Both sit flush left, so the rule lands on the column's
   own edge. */
/* A CLASS NOW, for the same reason the offsets are. The old pair of selectors
   was :nth-child(3) and :nth-child(6):not(.t-item--right) -- and the second
   half matched NOTHING, because the sixth letter WAS Tom and Tom is the
   right-aligned one, so the :not() written to stop him carrying two saffron
   edges removed the rule from the page entirely. One letter carried it, not
   the two the note claimed. Martin keeps it; a second is Greg's to name. */
.t-item--accent {
  padding-left: clamp(1.25rem, 3vw, 2.25rem);
  border-left: 2px solid var(--saffron);
}

/* THE FOURTH LINE IS CUT IN HALF ACROSS, NOT DOWN -- Greg, 2026-08-24.
   My first attempt capped the box at 3.5 line-heights, which slices the fourth
   line horizontally: you get the top half of a row of glyphs. What he actually
   described is three full lines and then a fourth that RUNS OUT halfway across
   -- text, then an ellipsis, then the ring sitting in the space that leaves.
   Same idea of "half a line", opposite axis.

   A FLOAT DOES IT, and it has to be a plain block to work. The float reserves
   the right half of the fourth line only -- margin-top of three lines, height
   of one -- so lines 1 to 3 set full width and line 4 wraps short. Tested
   inside a -webkit-box with line-clamp first and the float is ignored there:
   box children are laid out as box items, not floated. So the clamp is a
   plain max-height of four lines instead, and .t-cut carries the ellipsis
   rather than the browser generating one.

   The ellipsis therefore sits at a fixed 50% rather than tight against the
   last word. The gap is at most one word wide and it is the same gap on every
   letter, which is what makes the ring's position constant down the page. */
.t-item {
  /* ONE DECLARATION, INHERITED. The reading size and line-height used to be
     written twice -- once on .t-quote p and once as a --t-size the cap was
     computed from -- and the two resolved differently, so the fold landed at
     4.08 lines instead of the number asked for. Declared on .t-quote,
     inherited by the paragraphs, and the cap is a multiple of the same em. */
  --t-line: 1.65em;
  /* the FAQ ring's palette, so the control is the same object on both pages */
  --sv-ink: var(--teal);
  --sv-line: var(--teal);
  --sv-rgb: var(--teal-rgb);
}
.t-quote {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 1.7vw, 1.2rem);
  line-height: 1.65;
  color: var(--body);
  max-height: calc(4 * var(--t-line));
  overflow: hidden;
}
/* THE EXCLUSION IS SHAPED, NOT MARGINED, and this is the bug that made the
   first version look like the whole column had collapsed. margin-top pushed
   the float's BORDER box down three lines -- measured, it sat exactly where it
   should -- but line boxes avoid a float's MARGIN box, and that still began at
   zero. So every line wrapped at half width, not just the fourth.

   shape-outside states the exclusion directly. The float's box covers all four
   lines; the shape says only the bottom one excludes text. padding-top pushes
   the ellipsis down to that same line, and box-sizing:border-box keeps the box
   four lines tall rather than seven. */
.t-cut {
  float: right;
  width: 50%;
  height: calc(4 * var(--t-line));
  padding-top: calc(3 * var(--t-line));
  shape-outside: inset(calc(3 * var(--t-line)) 0 0 0);
  color: var(--muted);
}
/* NOEL is 273 characters and has no READ MORE. Nothing to open, so nothing is
   cut -- it carries no .t-cut in the markup either. */
.t-item--whole .t-quote { max-height: none; }
.t-item.is-open .t-quote { max-height: none; }
.t-item.is-open .t-cut { display: none; }

/* THE NAME AND THE "first session" NOTE, restored -- an edit above removed
   them along with the old four-line clamp, and the letters were setting their
   headings in the sans body face as a result. Recovered verbatim from the last
   commit rather than retyped, so nothing drifted.

   The name is the h2 and it is set in the serif at heading size: the person is
   the heading, the treatment label above it is the eyebrow. A stranger's name
   carries more than "TANTRA MASSAGE" does nine times down a page. */
.t-name {
  margin: 0.4rem 0 1.25rem;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.4rem, 2.2vw, 1.9rem);
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
}
/* John wrote twice, months apart, and the pair is the most persuasive thing
   on the page -- it only works if the reader can see it is the same man
   returning, so the distinction sits quietly beside his name. */
.t-note {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted);
  vertical-align: 0.35em;
  margin-left: 0.5rem;
}


/* THE PARAGRAPHS INHERIT AND ONLY SET THEIR SPACING. Restored here after an
   edit above took the whole block out with the old four-line clamp -- without
   them the letters fell back to the sans body face at the page's default
   line-height, which is also why the first measurement of the fold made no
   sense against the numbers in this file. */
.t-quote p { margin: 0 0 1.1rem; }
/* The gap comes out while it is clamped: line-clamping counts lines but the
   box it cuts still contains the margins, so the fold sat lower than asked. */
.t-quote p { margin-bottom: 0; }
.t-item.is-open .t-quote p { margin-bottom: 1.1rem; }
.t-item.is-open .t-quote p:last-child { margin-bottom: 0; }

.t-rest p {
  margin: 1.1rem 0 0;
  font-family: var(--serif);
  font-size: clamp(1.05rem, 1.7vw, 1.2rem);
  line-height: 1.65;
  color: var(--body);
}
.t-rest p:last-child { margin-bottom: 0; }

/* THE CONTROL IS THE FAQ'S RING, and the word is gone. Greg: the plus is self
   explanatory, and "Read more" was doing nothing the circle does not. It keeps
   an aria-label, because a button whose only content is decorative markup is
   silent to a screen reader.

   IT SITS ON THE RULE NOW, NOT ON THE TEXT -- Greg, 2026-08-24. The whole
   negative-margin mechanism that pulled the ring onto line four is gone. The
   mark caps the end of the letter's own divider, which means it is in the same
   place on every letter regardless of where the fourth line runs out, and it
   does not move when the letter opens: the rule stays, the plus becomes a
   minus, and both travel down with the bottom edge as the panel grows.

   THE OFFSETS ARE THE TAP TARGET, not the ring. The site's floor is 44x44 and
   the ring is 40, so the button is padded out to 44 and pulled back by the
   2px on each side -- the ring lands exactly where it would have without the
   padding. bottom:-22px is half of 44: it puts the button's centre, and so the
   ring's, on the hairline itself. */
.t-more {
  position: absolute;
  right: -2px;
  bottom: -22px;
  display: block;
  width: 44px;
  height: 44px;
  padding: 2px;
  margin: 0;
  cursor: pointer;
  z-index: 2;
}
/* .svc-mark IS A <span> AND HERE IT HAS NO GRID TO SIT IN. On the FAQ it is a
   grid item, so it gets a box and its 40x40 applies. Dropped into a plain
   button it stays inline, width and height are ignored, and it collapses to
   nothing -- which is why the circle never painted and why the button measured
   zero tall, putting the ring most of a line above where the arithmetic said
   it was. Both symptoms, one cause. */
.t-more .svc-mark { display: block; }
.t-item.is-open .svc-mark { --sv-sweep: 360deg; }
.t-more:hover .svc-mark { --sv-sweep: 360deg; }
.t-item.is-open .svc-bar::after { transform: rotate(0deg); opacity: 0; }

/* --- ONE LETTER READS FROM THE RIGHT -----------------------------------
   Greg's idea and it is the best thing in this pass: the rule moves to the
   right edge, the text sets ragged-left against it, and the ring goes to the
   other side. Nothing else on the page does this, which is exactly why it
   works -- it is a single interruption rather than another rule.

   It only holds because the measure is unchanged. The column is the same 62ch
   it always was; it has been pushed to the far side of the container and had
   its alignment reversed, so it reads as the same page turning round rather
   than as a second layout. */
.t-item--right {
  /* NOT FLUSH TO THE CONTAINER -- Greg, 2026-08-24. margin-left:auto put the
     rule hard against the right edge of the page, 400px past where any other
     letter ends, so it read as a stray line rather than as the column turned
     round. The other blocks are 62ch wide with offsets up to 9%, which puts
     the furthest right text edge at x 728 (John's second session, the "(8
     months)" line -- Greg's own example).

     7.5% lands the rule just inside that. Deliberately just inside rather than
     exactly on it: matching it precisely would introduce the symmetry the
     whole page is trying to avoid, and Greg offered the indent himself. */
  margin-left: 7.5%;
  margin-right: 0;
  text-align: right;
  padding-left: 0;
  padding-right: clamp(1.25rem, 3vw, 2.25rem);
  border-left: 0;
  border-right: 2px solid var(--saffron);
}
/* Ragged-left text runs out on the LEFT, so the cut and the ring swap sides
   with it and the ellipsis sets against its own right edge. The rule keeps its
   right edge and stops short on the LEFT -- the end of a right-aligned line is
   the left end, which is the principle Greg gave for the mark. */
.t-item--right .t-cut { float: left; text-align: right; }
.t-item--right::after { left: auto; right: 0; }
.t-item--right .t-more { left: -2px; right: auto; }
.t-item--right .t-note { margin-left: 0; margin-right: 0.5rem; }

/* The panel itself is unchanged: grid-template-rows 0fr to 1fr, which
   animates to the content's real height without anyone measuring it. The
   control above it is now the FAQ's ring; the old inline-flex label button and
   its .faq-icon are gone with the words they carried. */
.t-rest {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.6s var(--ease-out);
}
.t-item.is-open .t-rest { grid-template-rows: 1fr; }
.t-rest > div { overflow: hidden; }

/* ===========================================================================
   THE INNER-PAGE HEADER — .header--text, added 2026-08-22.

   The home page's bar carries the FIGURE and puts the name enormous in the
   hero. Inner pages have no hero to put it in, so they carry the text lockup
   instead — Greg's rule, and the point of it is that the wordmark is never
   missing from the site.

   The figure's <img> is removed from the markup on these pages rather than
   hidden here. Without .header--mark it has no size rule at all and would
   render at its natural 400px; hiding it in CSS would leave that one deletion
   away from a very large accident on any page that forgot the class.

   Everything below is the treatment .header--mark already uses under 768px,
   lifted to every width. The reasoning for each number is in that block —
   including the 0.6444em, which is derived: the glyphs measure 85.1px, there
   are 11 gaps, and 167.9 - 85.1 over 11 is 7.53px at the fixed 11.68px size.
   Both sizes are rem, so the one figure holds at every width.

   IT FITS, measured rather than assumed. The text lockup is 168px against the
   figure's 52. At 981 — the narrowest desktop width, where the bar is
   tightest — the home page had 305px of slack, so this leaves about 189px.
   ======================================================================== */
.header--text .wordmark {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.18em;
}
.header--text .wordmark-sm,
.header--text .wordmark-lg { display: block; }
.header--text .wordmark-sm { text-indent: 0; letter-spacing: 0.6444em; }

/* --- FAQ page: the group numeral and the turn-it-around statement ---------
   Built 2026-08-22 with faq.php. .faq, .faq-item, .faq-q and .faq-a already
   exist above — this is only what the page adds.

   The numeral is set apart rather than run into the title: "1 · Before you
   write to me" reads as one string, and the six groups are a sequence a reader
   should be able to see themselves moving through. Serif, muted, and it does
   not compete with the title beside it. */
.faq-group-num {
  font-family: var(--serif);
  font-size: 0.85em;
  color: var(--muted);
  margin-right: 0.7rem;
}
/* The FAQ list is centred at 800px; the group heading above it has to start on
   the same left edge or the page reads as two columns that nearly line up. */
.faq-group { max-width: 800px; margin-inline: auto; }

/* THE TURN-IT-AROUND STATEMENT. It has to stand out — Greg's guideline is that
   somebody scrolling past must see it — without reading as a telling-off.

   So: larger than body copy, serif, and a rule down the left in saffron. The
   rule is what makes it a block rather than a paragraph; the warm accent is
   what stops it being a warning. Red was on the table and is argued against in
   COPY-TREATMENTS.md — it would contradict a tone that took several drafts to
   get away from lecturing.

   No quotation marks and no italics. It is Greg speaking in his own voice on
   his own page, not a quotation from somewhere else. */
.statement-turn {
  margin: 0;
  padding-left: clamp(1.25rem, 3vw, 2.25rem);
  border-left: 2px solid var(--saffron);
}
.statement-turn p {
  margin: 0 0 1.1rem;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 2vw, 1.45rem);
  line-height: 1.55;
  color: var(--ink);
}
.statement-turn p:last-child { margin-bottom: 0; }
/* The last line is the instruction the whole block exists to deliver. */
.statement-turn p:last-child { color: var(--saffron-ink); font-weight: 600; }

/* --- CONTACT page --------------------------------------------------------
   Built 2026-08-22. .contact-grid already exists in the responsive block
   (it stacks below 900 with .split and .duo); this is what the page adds.

   Details LEFT, form right, and the ratio is deliberate: the details are short
   and the form is tall, so an even split would leave the left column half
   empty. 0.85fr against 1fr puts the weight where the height is. */
/* THE TWO COLUMNS ARE BEHIND A MIN-WIDTH, and they have to be. style.css
   already stacks .contact-grid at 900 along with .split, .duo and .step — but
   that rule is a max-width media query several hundred lines ABOVE this one,
   and this block is appended at the end of the file. An unconditional
   grid-template-columns here silently beat it: the form stayed two columns on
   a phone and ran 106px off the screen at 320. Caught by the width sweep.

   901 is the same boundary the existing rule uses, so the two agree rather
   than nearly agree. */
.contact-grid {
  display: grid;
  gap: clamp(2.5rem, 5vw, 5rem);
  align-items: start;
}
@media (min-width: 901px) {
  .contact-grid { grid-template-columns: 0.85fr 1fr; }
}
.contact-h {
  margin: 2.25rem 0 0.6rem;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--teal);
}
.contact-h:first-child { margin-top: 0; }
.contact-line {
  margin: 0 0 0.9rem;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.65;
  color: var(--body);
}
.contact-reach { list-style: none; margin: 0 0 0.5rem; padding: 0; }
.contact-reach li { margin-bottom: 0.35rem; }
.contact-reach a { color: var(--body); }
.contact-reach a:hover { color: var(--red); }
/* The number and WhatsApp are the two things most people came for, so they are
   the only text on the page set above body size. */
.contact-strong {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.9vw, 1.4rem);
  color: var(--ink) !important;
  padding-block: 0.4rem;      /* 44px tap target, as everywhere else */
}
.contact-strong:hover { color: var(--red) !important; }
.contact-faq { margin-top: 1.5rem; }
.contact-faq a { color: var(--teal); text-decoration: underline; text-underline-offset: 3px; }

/* --- the form ---------------------------------------------------------- */
.contact-form { display: grid; gap: 1.4rem; }
.field { display: grid; gap: 0.5rem; }
.field label {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted);
}
.field input,
.field select,
.field textarea {
  font-family: var(--sans);
  font-size: 16px;            /* 16px is the floor that stops iOS zooming the
                                 whole page when a field takes focus */
  color: var(--ink);
  background: var(--white);
  border: 1px solid var(--hairline);
  border-radius: 0;           /* square, like every other edge on this site */
  padding: 0.85rem 1rem;
  width: 100%;
  transition: border-color 0.35s var(--ease-soft);
}
.field textarea { resize: vertical; min-height: 9rem; }
.field input:focus,
.field select:focus,
.field textarea:focus { outline: none; border-color: var(--teal); }
.contact-form .btn { justify-self: start; }
.form-note {
  margin: 0;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--muted);
}

/* THE HONEYPOT. Off-screen, NOT display:none — some bots skip anything that
   is display:none, which is the one thing this field must not be skipped by.
   aria-hidden and tabindex="-1" in the markup keep it away from anyone using
   a screen reader or a keyboard. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* The message contact.php injects above the form after a submission. */
.form-msg {
  margin: 0 0 1.5rem;
  padding: 0.9rem 1.1rem;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.5;
  border-left: 2px solid;
}
.form-msg--ok  { border-color: var(--teal); background: var(--teal-wash); color: var(--ink); }
.form-msg--bad { border-color: var(--red);  background: var(--red-wash);  color: var(--ink); }

/* =========================================================================
   FAQ PAGE: FOUR SERVICES
   Built 2026-08-24, second attempt. The first was seven peer topics and Greg
   rejected it on two grounds, both right:

   1. FIVE OF THE SEVEN WERE TANTRA WEARING DIFFERENT HATS. "Start here" was
      tantra. "Health and the harder questions" was tantra. The page claimed to
      be organised by subject when the subject was one service.
   2. IT LOOKED CHEAP. A list of text rows with a decorative line dropped
      behind it is not what a site charging 40% over the local average looks
      like. The home page sets the register and this page has to meet it.

   So: FOUR BLOCKS, ONE PER SERVICE, EACH CARRYING ITS OWN PHOTOGRAPH -- the
   same photograph and the same accent its card already has on the home page.
   That is the whole move. The two pages now name the same four things the same
   way, and the FAQ inherits the register instead of inventing a weaker one.

   Inside a block: subsections as headings, questions as the accordion. Two
   clicks to any answer, never three.

   THE ACCENT PAIRS ARE THE HOME PAGE'S:
     Coaching brown . A Body That Works teal . Massage saffron . Tantra red

   Saffron needs two values because it is 2.02:1 on white and cannot be read:
   --sv-ink for anything with a glyph in it, --sv-line for strokes and fills.
   ========================================================================= */

.svc-list {
  max-width: 1080px;
  margin-inline: auto;
  margin-top: clamp(2.5rem, 5vw, 4.5rem);
}

.svc {
  --sv-ink: var(--teal);
  --sv-line: var(--teal);
  --sv-rgb: var(--teal-rgb);
  position: relative;
  border-top: 1px solid var(--hairline);
}
.svc:last-child { border-bottom: 1px solid var(--hairline); }

.svc--brown   { --sv-ink: var(--brown);       --sv-line: var(--brown);   --sv-rgb: 125, 64, 9; }
.svc--teal    { --sv-ink: var(--teal);        --sv-line: var(--teal);    --sv-rgb: var(--teal-rgb); }
.svc--saffron { --sv-ink: var(--saffron-ink); --sv-line: var(--saffron); --sv-rgb: var(--saffron-rgb); }
.svc--red     { --sv-ink: var(--red);         --sv-line: var(--red);     --sv-rgb: var(--red-rgb); }

/* --- the closed block ----------------------------------------------------
   A button inside an <h2>: keyboard reachable, announces its own state, and
   the four services stay a heading outline for a screen reader and for Google.

   EVERY CELL IS PLACED EXPLICITLY. Auto-placement put the mark in column 1 on
   the phone layout of the first attempt -- taking one child out of the auto
   flow moves the cursor for the ones after it, and a child with a definite
   grid-row is placed against that cursor rather than at the end of the row.
   Found by measuring, not by reading. */
.svc-h { margin: 0; font-size: inherit; font-weight: inherit; }

.svc-head {
  width: 100%;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: clamp(1.25rem, 2.6vw, 2.25rem);
  padding: clamp(1.5rem, 2.6vw, 2.1rem) 0;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  color: var(--ink);
  position: relative;
}
.svc-fig   { grid-column: 1; grid-row: 1; }
.svc-text  { grid-column: 2; grid-row: 1; }
.svc-count { grid-column: 3; grid-row: 1; }
.svc-mark  { grid-column: 4; grid-row: 1; }

/* The wash bleeds half a gutter past the container so an open block reads as a
   band across the page rather than an inset box. Darkness, never light: on a
   white ground an additive highlight is a coloured smear, which is the whole
   finding behind OPEN-BUGS #8. */
.svc-head::before {
  content: '';
  position: absolute;
  inset: 0 calc(var(--gutter) * -0.5);
  background: rgba(var(--sv-rgb), 0.05);
  opacity: 0;
  transition: opacity 0.55s var(--ease-soft);
  pointer-events: none;
}
.svc.is-open .svc-head::before { opacity: 1; }
.svc-head:hover::before { opacity: 0.5; }

/* --- the photograph ------------------------------------------------------
   THE SAME FOUR IMAGES THE HOME PAGE USES FOR THE SAME FOUR SERVICES, and the
   same treatment: grayscale at rest, colour under the pointer, 0.55s. The
   filter values are copied from .tcard-img rather than re-invented --
   brightness and contrast stay on in both states, so gaining colour is one
   event rather than two.

   A white card and a soft shadow instead of the home page's PNG frames: those
   are fixed-pixel sprites cut for large slots, and at 260px they would be
   re-cut rather than scaled. A shadow is darkness and is allowed. */
.svc-fig {
  position: relative;
  display: block;
  width: clamp(150px, 18vw, 260px);
  aspect-ratio: 3 / 2;
  padding: 6px;
  background: var(--white);
  transition: transform 0.7s var(--ease-out);
}
/* THE SHADOW IS THE SITE'S OWN PNG NOW, not a CSS box-shadow. Greg, 2026-08-27:
   the same shadow the rest of the site uses. THE WHITE CARD STAYS -- his
   instruction in the same breath: add the shadow underneath the frame, do not
   take the frame away. So the 6px white padding above is untouched and this
   only replaces the soft CSS shadow that was standing in for the real one.

   LANDSCAPE CUT, because these four are 3:2. shadow-only-portrait.png -- the
   one .duo-frame and .tr-fig use -- also runs 6px down the right edge, which is
   drawn for a tall picture; the landscape file carries its depth along the
   bottom only.

   Same construction as .duo-frame::after and .tr-fig::after: ::after with a
   negative bottom inset so it costs the layout nothing, border-image so the
   shadow holds its true thickness while only the soft middle stretches,
   border-style set or border-image is never painted.

   .svc-fig's overflow:hidden had to go with it, and that is the trap here: a
   pseudo-element is clipped by its OWN element's overflow, so the shadow would
   have been invisible with no error anywhere. Nothing needed it -- the img is
   width/height 100% inside the padding box and .svc-num sits inside the frame.

   18px, not the 25px the file is cut at. The slice scales to the border width,
   so it is the same shadow drawn tighter, and it has to be: the figure is 100px
   tall at the narrow end where the row's own padding is 24px, and a 25px shadow
   would land on .svc-rule, the hairline under the row. */
.svc-fig::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: -18px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 0 18px 0;
  border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 18px 0 stretch;
  pointer-events: none;
}
.svc-fig img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1) brightness(1.06) contrast(0.97);
  transition: filter 0.55s var(--ease-soft), transform 1.1s var(--ease-out);
}
.svc-head:hover .svc-fig img,
.svc.is-open .svc-fig img { filter: grayscale(0) brightness(1.06) contrast(0.97); }
.svc-head:hover .svc-fig { transform: translateY(-3px); }
/* The open block used to deepen the CSS shadow. Gone with it: a second shadow
   under a PNG one is two shadows, not a deeper one. The open state is already
   carried by the wash, the rule, the mark and the photograph gaining colour. */
/* The numeral sits ON the photograph, as it does on the home page's cards,
   which is what removes a whole column from this row. Italiana, the logotype
   face, white over the corner of the image. */
/* Inside .svc-fig, which is the positioned ancestor. It was a SIBLING of the
   figure in the first build and therefore absolute against .svc-head, so four
   white numerals painted on the white page and vanished. */
.svc-num {
  position: absolute;
  left: 14px;
  bottom: 10px;
  z-index: 1;
  font-family: var(--display);
  font-size: clamp(1.5rem, 2.6vw, 2.1rem);
  line-height: 1;
  color: #fff;
  text-shadow: 0 1px 10px rgba(20, 12, 4, 0.55);
  pointer-events: none;
}

.svc-text { min-width: 0; }
.svc-title {
  display: block;
  font-family: var(--serif);
  font-size: clamp(1.45rem, 2.9vw, 2.15rem);
  line-height: 1.12;
  letter-spacing: -0.015em;
  color: var(--ink);
  transition: color 0.45s var(--ease-soft);
}
.svc.is-open .svc-title { color: var(--sv-ink); }
/* The deck is the reason a closed block is useful: it tells a first-time
   reader what is inside without opening it. 74ch clears the longest of the
   four on one line at desktop, which is what keeps the four rows level. */
.svc-deck {
  display: block;
  margin-top: 0.45rem;
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
  max-width: 74ch;
}

.svc-count {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.45s var(--ease-soft);
}
.svc-count-n { font-weight: 600; color: var(--sv-ink); margin-right: 0.35em; }
.svc.is-open .svc-count { color: var(--sv-ink); }

/* --- the mark ------------------------------------------------------------
   The ring DRAWS on hover: a conic gradient masked to a 1px ring, the same
   mechanism as the CTA in #8. @property is what lets an angle interpolate at
   all; where it is unsupported the sweep jumps to full and the mark simply has
   a ring, which is a fine fallback. */
@property --sv-sweep {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.svc-mark {
  --sv-sweep: 0deg;
  position: relative;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  transition: --sv-sweep 0.75s var(--ease-out);
}
.svc-mark::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from -90deg, var(--sv-line) var(--sv-sweep), transparent 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
          mask: radial-gradient(farthest-side, transparent calc(100% - 1px), #000 calc(100% - 1px));
  opacity: 0.8;
}
.svc-head:hover .svc-mark,
.svc.is-open .svc-mark { --sv-sweep: 360deg; }
/* A resting ring so the control is visible before anyone hovers it -- a mark
   that only appears on hover is invisible on a touch screen. */
.svc-mark::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid rgba(var(--sv-rgb), 0.22);
}
/* The bars. The horizontal one is a child of the mark rather than a third
   pseudo-element, because the ring took ::before and the resting ring took
   ::after. */
.svc-bar,
.svc-bar::after {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 1.5px;
  margin: -0.75px 0 0 -7px;
  background: var(--sv-ink);
  transition: transform 0.55s var(--ease-out), opacity 0.45s var(--ease-soft);
}
.svc-bar::after { content: ''; top: 0; left: 0; margin: 0; transform: rotate(90deg); }
.svc.is-open .svc-bar::after { transform: rotate(0deg); opacity: 0; }

/* --- the rule ------------------------------------------------------------
   Draws from the left rather than fading in. A transition, not an animation,
   so an interrupted hover eases back from wherever it reached instead of
   snapping -- the same lesson as the awards row and the CTA ring. */
.svc-rule {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--sv-line);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.9s var(--ease-out), opacity 0.4s var(--ease-soft);
  opacity: 0.7;
}
.svc-head:hover .svc-rule { transform: scaleX(1); opacity: 0.4; }
.svc.is-open .svc-rule { transform: scaleX(1); opacity: 1; height: 2px; }

.svc-head:focus-visible { outline: 2px solid var(--sv-ink); outline-offset: 4px; }

/* --- the panel -----------------------------------------------------------
   grid-template-rows 0fr -> 1fr, the same mechanism as .faq-a, which reaches
   the content's real height without measuring anything. */
.svc-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.8s var(--ease-out);
}
.svc.is-open .svc-panel { grid-template-rows: 1fr; }
/* THE CLIPPING BOX CARRIES NO PADDING, and that is not tidiness -- it is the
   bug. With grid-template-rows: 0fr the track collapses to zero and
   overflow:hidden clips the CONTENT, but an element's own padding is part of
   its box and is not clipped. A closed panel was still showing its first
   question. So: clipper outside with nothing on it, padding on a wrapper
   inside. Found in a screenshot, not in the stylesheet. */
.svc-panel > .svc-panel-in { overflow: hidden; min-height: 0; }
.svc-panel-pad {
  position: relative;
  padding: 0.5rem 0 clamp(2rem, 4vw, 3.5rem);
}

/* --- THE TRACED LINE, IN THE FRAME ----------------------------------------
   Greg, 2026-08-24, and it is a rule rather than a placement: THE LINES ARE
   PART OF THE DESIGN, SO THEY BELONG IN THE PAGE, NOT INSIDE A PANEL THAT IS
   SHUT MOST OF THE TIME. Two attempts got this wrong -- once flat behind the
   questions, once as the subsection rules -- and both hid the site's one
   signature element behind a click.

   So there are two pairs, both in the frame and both always there: one under
   the masthead, one closing the list. Same class family, same driver and the
   same ghost as the home page's .statement-lines -- cloned from #ritual-line,
   scroll-linked in both directions, with the lerp that keeps them travelling
   after the scroll has stopped.

   NOT hidden on a phone. The home page hides .statement-lines below 900,
   because a 2256-unit box at 390px renders the stroke at half a pixel and the
   whole drawing becomes a grey smudge. Its own comment says the answer is a
   short EXCERPT rather than the whole thing shrunk -- so that is what main.js
   gives these: a narrower viewBox window below 900, which is more of the
   drawing per pixel rather than less. */
.faq-lines-wrap {
  position: relative;
  margin-top: clamp(2.5rem, 5vw, 4.5rem);
  pointer-events: none;
}
.faq-head-lines,
.faq-lines {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
  pointer-events: none;
}
/* The masthead pair sits in the beige band under the lede. It is above the
   fold, so its draw window resolves immediately and it draws itself in on
   arrival -- the hero's behaviour, not the band's. */
.faq-head-lines {
  margin-top: clamp(1.5rem, 3.5vw, 2.75rem);
  height: clamp(52px, 8vw, 104px);
}
.faq-lines { height: clamp(60px, 9vw, 132px); }
/* The second of the pair is flattened and mirrored, which is exactly how the
   home page stacks its two: same drawing, same clone, same driver, and the
   squash is done here rather than in the script. */
.faq-lines--low {
  height: clamp(24px, 3.6vw, 53px);
  transform: scaleX(-1);
  margin-top: -0.35rem;
  opacity: 0.65;
}
.faq-head-lines path,
.faq-lines path {
  fill: none;
  stroke: var(--teal);
  stroke-width: 3;
  stroke-linecap: round;
  opacity: 0.75;
  /* pathLength="1" on the clone, so 1 is the whole curve. NEVER add
     vector-effect: non-scaling-stroke -- it breaks pathLength and silently
     truncates the draw. That bug cost a session on the Trance Ritual build and
     read as a design opinion for weeks. */
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.faq-head-lines path.is-ghost,
.faq-lines path.is-ghost { stroke-width: 1.6; opacity: 0.42; }
/* Saffron on the closing pair, teal on the masthead -- the page's two
   structural colours, one at each end, and neither is an accent belonging to
   one of the four services. */
.faq-lines path { stroke: var(--saffron); }
.faq-lines path.is-ghost { stroke: var(--saffron); }

/* --- the subsection accordion --------------------------------------------
   Greg, 2026-08-24: the subsections FOLD. The first is open, opening another
   closes it, and the chip index that used to sit above them is gone -- once
   the headings are folded you can see the whole shape of a service at a
   glance, so a second navigation for the same job was noise.

   This is the third level and the only one that is ever pre-opened, which is
   what keeps the page at two clicks in practice: block, then question. */
.svc-sub { border-top: 1px solid var(--hairline); }
.svc-sub:first-child { border-top: 0; }
.svc-sub-h { margin: 0; font-size: inherit; font-weight: inherit; }
.svc-sub-head {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 1rem;
  /* 44px minimum comes out of the padding, not a height, so nothing moves. */
  padding: 1.1rem 0;
  background: none;
  border: 0;
  text-align: left;
  cursor: pointer;
  color: var(--sv-ink);
}
.svc-sub-title {
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--sv-ink);
}
/* The count is the difference between "this might be long" and knowing it is
   four questions. Tabular so the column does not jitter down the panel. */
.svc-sub-count {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The rule between the label and the mark fills whatever is left, which is
   what makes a short label read as a divider rather than as a lost word. */
.svc-sub-head::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(to right, rgba(var(--sv-rgb), 0.4), rgba(var(--sv-rgb), 0.06));
  transition: opacity 0.45s var(--ease-soft);
  opacity: 0.55;
  order: 1;
}
.svc-sub-count { order: 2; }
.svc-sub-mark { order: 3; }
.svc-sub-head:hover::after,
.svc-sub.is-open .svc-sub-head::after { opacity: 1; }
.svc-sub-head:focus-visible { outline: 2px solid var(--sv-ink); outline-offset: 3px; }

/* A smaller sibling of the block's mark, on purpose: the two levels have to be
   told apart at a glance. Ring, plus, no sweep -- the sweep is the block's. */
.svc-sub-mark {
  position: relative;
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid rgba(var(--sv-rgb), 0.3);
  transition: background 0.45s var(--ease-soft), border-color 0.45s var(--ease-soft);
}
.svc-sub-head:hover .svc-sub-mark { border-color: rgba(var(--sv-rgb), 0.6); }
.svc-sub.is-open .svc-sub-mark { background: rgba(var(--sv-rgb), 0.1); border-color: rgba(var(--sv-rgb), 0.5); }
.svc-sub-mark::after,
.svc-sub-bar {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 10px;
  height: 1.4px;
  margin: -0.7px 0 0 -5px;
  background: var(--sv-ink);
  transition: transform 0.5s var(--ease-out), opacity 0.4s var(--ease-soft);
}
.svc-sub-bar { transform: rotate(90deg); }
.svc-sub.is-open .svc-sub-bar { transform: rotate(0deg); opacity: 0; }

/* THE CLIPPING BOX CARRIES NO PADDING. With grid-template-rows: 0fr the track
   collapses and overflow:hidden clips the CONTENT, but an element's own
   padding is part of its box and is not clipped -- a closed panel then shows a
   sliver of what it is meant to be hiding. Learned on .svc-panel the same day.
   Padding goes on .faq, which is inside the clipper. */
.svc-sub-panel {
  /* head padding-bottom + half the 26px mark = how far the sub-heading's rule
     sits above this panel's top edge. Both gaps around .faq-notice come off
     it, which is what centres the notice between the two rules. */
  --sub-rule-drop: calc(1.1rem + 13px);
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.65s var(--ease-out);
}
.svc-sub.is-open .svc-sub-panel { grid-template-rows: 1fr; }
.svc-sub-panel > div { overflow: hidden; min-height: 0; }
.svc-sub-panel .faq { padding-bottom: 0.75rem; }

/* --- the notice ----------------------------------------------------------
   Greg, 2026-08-26: an entry that is NOT a question, at the top of "Start
   here" in the tantra group, stressed and in its own colour, "so that once I
   refuse a session I can say clearly it was all on my website."

   IT IS NOT AN ACCORDION AND MUST NOT BECOME ONE. Everything else in this
   panel is closed until it is clicked; this one is the thing a man has to have
   read before he books, so it carries no button and no is-open state.

   Padding is on the notice itself, which is safe HERE and would not be on the
   panel -- .faq-notice sits INSIDE .svc-sub-panel > div, the element that
   clips. See the warning above .svc-sub-panel. */
/* THE PANEL IS CENTRED BETWEEN THE TWO RULES, and the arithmetic is the whole
   point. Greg, 2026-08-26: "those two horizontal dividers are the markers
   between which the pink square must be centred."

   THE TOP RULE IS NOT AT THE BOTTOM OF THE HEADING. It is .svc-sub-head::after,
   a flex child centred in a box that is 26px of mark plus 1.1rem of padding
   each side -- so the rule already sits --sub-rule-drop above the panel's top
   edge before this element adds anything. That is why a matching pair of
   margins looked lopsided: above was 30.6 + 21.6, below was 21.6.

   So the top margin is ZERO and the gap below is set to the drop. Both visual
   gaps are then --sub-rule-drop and nothing else, and the square is centred by
   construction rather than by a number that happened to look right. The head's
   padding cannot be trimmed instead: it is what holds the 44px tap target, and
   it is shared by every sub-group on the page. */
.faq-notice {
  max-width: 800px;
  margin: 0 auto;
  padding: clamp(1.3rem, 2.6vw, 2rem);
  border-left: 3px solid var(--red);
  background: var(--red-wash);
  /* TEXT LEFT, THE MONK IN THE SPACE ON THE RIGHT. Greg, 2026-08-26: the
     notice is correct but strong, and a figure with the hands in namaste
     "signals willingness to serve" and softens it without softening a word.
     auto on the second track means the figure states its own width and the
     copy takes the rest -- so the column cannot squeeze the type. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: clamp(1.2rem, 3vw, 2.4rem);
  align-items: end;
}
.faq-notice-body { min-width: 0; }
/* NOT greyed. --photo-filter is for the placeholder PHOTOGRAPHS; this is a
   drawing and its copper is already the page's own warm family. */
/* WIDER THAN THE COPPER ONE WAS, and the reason is the drawing rather than
   the layout: namaste2 is a near-square pencil sketch (526x474) where the
   copper was portrait (415x600), and a fine-line sketch shown at a third of
   its size loses the lines themselves. Greg: "once the image got smaller it
   is somehow losing the sharpness." */
.faq-notice-fig {
  margin: 0;
  width: clamp(150px, 19vw, 232px);
  align-self: end;
}
.faq-notice-fig img { display: block; width: 100%; height: auto; }
.faq-notice-h {
  margin: 0 0 1.1rem;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--red);
}
.faq-notice-h .faq-notice-star { margin-right: 0.55em; font-size: 13px; }
.faq-notice p { margin: 0 0 1rem; max-width: 60ch; font-size: 15px; }
.faq-notice p:last-child { margin-bottom: 0; }
.faq-notice strong { color: var(--ink); }
/* the starred line: italic, hung off its own rule, deliberately a different
   voice from the paragraphs above it -- it is Greg speaking about Greg. */
.faq-notice-sig {
  margin-top: 1.4rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--red-hair);
  font-style: italic;
}
.faq-notice-sig .faq-notice-star { font-style: normal; color: var(--red); margin-right: 0.35em; }
@media (max-width: 640px) {
  /* one column, and the monk goes UNDER the text rather than beside it --
     at 320 a 150px figure leaves the copy 112px. .svc-sub-mark is 24px down
     here, so the drop loses the pixel with it. */
  .faq-notice { grid-template-columns: 1fr; justify-items: center; }
  .svc-sub-panel { --sub-rule-drop: calc(1.1rem + 12px); }
  .faq-notice-fig { width: 132px; margin-top: 1.4rem; }
}

/* --- the question list ---------------------------------------------------
   .faq, .faq-item, .faq-q and .faq-a are the page's existing accordion. This
   only re-fits them inside a panel that already has a heading above it. */
.svc-panel .faq {
  max-width: none;
  margin: 0;
}
/* THE GAP UNDER THE NOTICE, AND IT HAS TO OUT-SPECIFY THE RULE ABOVE.
   `.svc-panel .faq` is 0,2,0 and sets margin:0. A plain `.faq-notice + .faq`
   is also 0,2,0 and loses on order, so the square sat directly on the first
   divider -- which is what Greg saw. Three classes, and it holds. */
.svc-panel .faq-notice + .faq { margin-top: var(--sub-rule-drop); }
.svc-panel .faq-item:first-child { border-top: 1px solid var(--hairline); }
.svc-panel .faq-item:last-child { border-bottom: 0; }
.svc-panel .faq-q { padding-block: 1.05rem; }
/* The inner plus/minus takes the block's accent, which is the cheapest way to
   keep a reader oriented once the block's own row has scrolled off the top. */
.svc-panel .faq-icon::before,
.svc-panel .faq-icon::after { background: var(--sv-ink); }
.svc-panel .faq-a p { max-width: 70ch; }
.svc-panel .faq-a a {
  color: var(--sv-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* --- tablet and phone ----------------------------------------------------
   The photograph stays. It is the thing that makes this page belong to the
   same site as the home page, and it is the first thing that would be dropped
   by reflex -- so it is said plainly: it stays, and it shrinks.

   The row becomes three columns and two rows: picture, text, mark on top; the
   count under the deck where there is room for the whole word. */
@media (max-width: 900px) {
  .svc-head {
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 1rem clamp(0.9rem, 3vw, 1.4rem);
    padding-block: 1.35rem;
  }
  .svc-fig   { grid-column: 1; grid-row: 1 / span 2; align-self: start; width: clamp(96px, 26vw, 150px); }
  .svc-text  { grid-column: 2; grid-row: 1; }
  .svc-count { grid-column: 2; grid-row: 2; justify-self: start; font-size: 11px; }
  .svc-mark  { grid-column: 3; grid-row: 1 / span 2; width: 34px; height: 34px; }
  .svc-num   { left: 10px; bottom: 7px; font-size: 1.25rem; }
  .svc-deck  { font-size: 13px; margin-top: 0.35rem; }

  .svc-sub-head { gap: 0.75rem; padding: 1rem 0; }
  .svc-sub-title { font-size: 11.5px; letter-spacing: 0.13em; }
  .svc-sub-mark { width: 24px; height: 24px; }
}

@media (max-width: 480px) {
  /* Below 480 the picture and the title cannot share a row and both stay
     legible, so the picture goes full width above the text -- it is not
     dropped. 16:9 rather than 3:2 so a full-width photograph does not eat a
     third of the screen. */
  .svc-head {
    grid-template-columns: minmax(0, 1fr) auto;
    row-gap: 0.9rem;
  }
  .svc-fig   { grid-column: 1 / span 2; grid-row: 1; width: 100%; aspect-ratio: 16 / 9; }
  .svc-text  { grid-column: 1; grid-row: 2; }
  .svc-count { grid-column: 1; grid-row: 3; }
  .svc-mark  { grid-column: 2; grid-row: 2 / span 2; align-self: center; }
  .svc-num   { font-size: 1.6rem; left: 12px; bottom: 9px; }
}

@media (prefers-reduced-motion: reduce) {
  .svc-panel,
  .svc-rule,
  .svc-sub-panel,
  .svc-mark,
  .svc-fig,
  .svc-fig img { transition: none; }
}

/* The hidden traced-path source on faq.php. It has to be IN the document for
   main.js to clone from, and out of the layout entirely. Injected by
   tools/make-faq.py, copied from index.php so the `d` lives in one place. */
.line-source {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
}

/* FAQ page: the statement shares the service list's frame. It was in the 860px
   narrow container while the list is 1080, so their left edges disagreed by
   110px and read as a misalignment rather than as two deliberate widths. The
   blockquote keeps a reading measure of its own inside the wider frame, so the
   saffron rule now starts exactly where the photographs do. */
/* 1080 PLUS THE GUTTERS, not 1080. .container carries padding-inline INSIDE
   its max-width, so a flat 1080 put the statement's content 72px to the right
   of the list's -- measured, 244 against 172. The list is 1080 of content
   centred in the wide container's content box, so the narrow one has to be
   1080 of content too. */
#statement .container--narrow { max-width: calc(1080px + 2 * var(--gutter)); }
#statement .statement-turn { max-width: 68ch; }


/* ==========================================================================
   SERVICES (treatments.php) -- built 2026-08-25
   ==========================================================================
   Seven long descriptions on one page. The whole design problem is that the
   copy is DELIBERATELY long -- COPY-TREATMENTS.md, "deliberately longer and
   plainer than Trance Ritual", and that length is what separates the two
   sites. So the layout's job is to make a long read navigable rather than to
   make it look short.

   Three moves do that and nothing else is decorated:
     1. an index at the top, every row a link, price on the row
     2. three groups with their own heading, so the page has three stops
        instead of seven
     3. the photograph STICKS while its description scrolls past it, so a man
        reading paragraph five still has the picture of what he is reading
        about. That is what buys the length.

   The `tr-` prefix is this page's; `tri-` is the index row; `trr-` the price
   rows. Nothing here is shared with .svc (the FAQ's accordion) on purpose --
   that component opens and closes, and none of this does. */

/* --- how time is counted, under the lede ---------------------------------
   Same saffron left rule as the FAQ's statement, deliberately: it is the same
   KIND of thing -- Greg speaking in his own voice about how he works, ahead
   of the list rather than inside it. One accent, used twice on the site,
   rather than a second invented for this page. */
.tr-note {
  margin-top: clamp(1.5rem, 3vw, 2.5rem);
  max-width: 60ch;
  padding-left: clamp(1.25rem, 3vw, 2.25rem);
  border-left: 2px solid var(--saffron);
}
.tr-note p {
  margin: 0 0 0.9rem;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
.tr-note p:last-child { margin-bottom: 0; }
.tr-note strong { color: var(--ink); font-weight: 600; }

/* --- the index -----------------------------------------------------------
   A contents list, not a menu of cards. Rows are a numeral, a name, a rule
   that stretches, and the meta -- the shape of a printed table of contents,
   which is the most legible way anyone has found to put seven named things
   with seven prices on one screen.

   The rule is the moving part: it is the page's hairline at rest and saffron
   under the pointer, so the row answers without anything jumping. */
/* --- the index -----------------------------------------------------------
   REBUILT 2026-08-25. The flat seven-row list was Greg's own "not elegant
   enough". What he asked to keep is the concept: everything available, on one
   screen, with its price. What changed is that the seven rows are now four
   families, each carrying the colour it already has on the home page and the
   FAQ, in an asymmetric two-column grid.

   ONE VARIABLE DOES THE COLOUR WORK. Each family sets --fam once and the
   numeral, the separator, the family name and every hover state read from it.
   Adding a fifth family is one modifier, not five rules.

   THE INKS, NOT THE LIFTED RGB. .tcard--red and friends lift their colours
   because they sit on a photograph; here the ground is warm white and these
   are type, so they use the base inks that were chosen to pass contrast.
   --saffron-ink and never --saffron: saffron fails as type on this ground. */
.tr-index {
  /* ONE ROW-PADDING NUMBER, USED TWICE. The family label's top padding has to
     put it on the first row's baseline, so it is that row's own top padding
     plus a nudge. They were two separate clamps saying the same thing and they
     had to be kept in step by hand; now the row owns it and the label reads it.
     Tightened 2026-08-26 with everything else on this list. */
  --tri-pad: clamp(0.45rem, 0.85vw, 0.7rem);
  /* The line is a child of THIS box, not of .container. .container is wider by
     its own horizontal padding, so a line drawn against it started 72px to the
     left of the divider -- visible, and the opposite of what Greg asked for. */
  position: relative;
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}
.tri-fam {
  --fam: var(--saffron-ink);
  display: grid;
  /* 17rem, not 15: the two longest notes -- "food, movement, breathing, sleep"
     and "emotions, other people, and love" -- wrapped to a second line at 15
     and made the bottom two families read heavier than the top two. Widening
     the column was the fix that kept every word, and "breathing" is one Greg
     would not want dropped. */
  /* 18rem, DOWN FROM 20, AND THE GUTTER DOWN FROM 3.5rem TO 2.25 -- Greg,
     2026-08-26: "if you bring slightly the names of the treatments closer to
     the main blocks, it will give us better control on the responsive narrower
     screens."

     WHAT SETS THE FLOOR, and it is not the family word. It is the longest NOTE
     against its own wave indent: "food, movement, breathing, sleep" measures
     217px and the curve pushes that label in by 47px at the widest the index
     ever gets, so the column cannot go under 264px. 18rem is 288px -- 24px of
     room. The indent SHRINKS on narrower screens, because the curve's x scales
     with the container while the text does not, so the widest layout is the
     worst case and every narrower one has more room, not less.

     DO NOT go to 17rem to buy another 16px. That is 8px from wrapping
     "breathing" onto a second line, which is the one word Greg would not want
     dropped. */
  /* AND IT IS A CLAMP SINCE 2026-08-26, NOT A FLAT 18rem. The label column's
     content is the family word and its note, and NEITHER SHRINKS -- both are
     set at a fixed 12px and 10px, so a column sized for them at 1440 carries
     the same 288px down to a phone while the rows beside it lose width the
     whole way. Measured at 600: the widest label content was 209px in a 288px
     column, and the 79px of waste was the last thing pushing the prices out of
     their row.

     THE FLOOR IS THE LONGEST NOTE PLUS THE CURVE'S OWN INDENT AT ITS HEIGHT.
     Worst case is `emotions & sexual energy` at 167px against an 88px indent,
     which is 255px. Everything narrower indents less, because the curve's x
     scales with the container while the text does not -- measured, the slack
     grows from 20px at 1280 to 47px at 600. 17rem is 272px.

     DO NOT DROP THE FLOOR TO 16rem TO BUY THE ROWS ANOTHER 16px. It was there
     for an hour and measured 4px of slack at 1280, which is one font-rendering
     difference away from wrapping `emotions & sexual energy` onto a second
     line -- and a wrapped note is measured onto the curve in the empty gap
     between its own lines, so it would sit at the wrong distance as well as
     look wrong. The rows below 640 are bought with `white-space` instead. */
  grid-template-columns: minmax(0, clamp(17rem, 20vw, 18rem)) minmax(0, 1fr);
  gap: clamp(0.9rem, 2.2vw, 2.25rem);
  padding-block: clamp(0.9rem, 1.8vw, 1.5rem);
}
/* THE DIVIDERS HANG OFF THE LINE INSTEAD OF CROSSING IT -- Greg, 2026-08-26:
   "the horizontal spacers between the different sections are crossing the line
   and starting on the left side. So you either match their endpoint directly
   on the line, or make them start with a gap from the line."

   IT IS A PSEUDO-ELEMENT, NOT A BORDER, and it has to be: a border runs the
   full width of the box and there is no way to start one part-way along. This
   is absolutely positioned on the family's top edge, which is exactly where
   the border was, and takes its left edge from --divider-x.

   --divider-x IS MEASURED, NOT DESIGNED. main.js writes it per family, using
   the same function that puts every label 10px from the curve -- read at the
   divider's own y rather than a label's. So the three rules sit off the line
   by the same 10px the words do, and the drawing runs the height of the index
   without a single rule cutting through it.

   THE FALLBACK IS THE FAMILY'S OWN --wave, which is the four 1440-calibrated
   numbers in the block below. Without JavaScript the rule still clears the
   line at a plausible distance rather than starting at the margin; it simply
   does not track the curve. It also collapses to 0 in one column, where there
   is no curve to clear -- see the flat block near the foot of this file. */
.tri-fam + .tri-fam { position: relative; }
.tri-fam + .tri-fam::before {
  content: "";
  position: absolute;
  top: 0;
  left: var(--divider-x, var(--wave, 0px));
  right: 0;
  height: 1px;
  background: var(--hairline);
}
.tri-fam--red     { --fam: var(--red);         --wave: 50px; --wave-note: 62px; }
.tri-fam--saffron { --fam: var(--saffron-ink); --wave: 88px; --wave-note: 81px; }
.tri-fam--teal    { --fam: var(--teal);        --wave: 44px; --wave-note: 47px; }
.tri-fam--brown   { --fam: var(--brown);       --wave: 78px; --wave-note: 84px; }

/* --wave IS MEASURED, NOT DESIGNED. Greg, 2026-08-25: the four family labels
   sit to the RIGHT of the line, "which will give the wave element to those
   treatments".

   TWO NUMBERS PER FAMILY, NOT ONE, and that is the whole correction of
   2026-08-25 evening. Greg: "sometimes they are further apart, sometimes
   closer... the tantra three levels gap is way too big." One offset per family
   was read at the label block's CENTRE -- the empty space between the family
   word and its note, where no text sits -- and the real per-line gaps ran
   10.6px to 48.3px. The word and the note are now measured separately, each at
   its own height, so --wave belongs to the first line and --wave-note to the
   second.

   THESE EIGHT NUMBERS ARE THE NO-JS FALLBACK AND NOTHING ELSE. main.js re-reads
   the path and rewrites both inline on every label, at load, at fonts.ready and
   on resize -- perpendicular to the curve, in screen pixels, 10px. It has to:
   the svg stretches on both axes, so the path's x moves with the container
   width while a pixel offset here does not. These are correct at 1440 and out
   everywhere else, which is exactly what a fallback should be. Do not chase
   them with more numbers.

   The one thing that DOES belong here is the shape, and it lives in
   treatments.php as the path's `d`. Change that and the labels follow on their
   own. */

/* The family name sits on the first row's baseline rather than the block's
   top edge, so the label reads as belonging to the row beside it. The 0.5rem
   is .tri a's own top padding at the small end of its clamp. */
.tri-fam-name {
  margin: 0;
  padding-left: var(--wave, 0px);
  padding-top: calc(var(--tri-pad) + 0.35rem);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--fam);
}
/* A SECOND NOTE LINE IS A SECOND ELEMENT, NEVER A <br> -- see the comment on
   the loop in main.js. Each line is measured onto the curve at its own height,
   so the two step apart the way the family word and its note already do; a
   wrapped line inside one box would take one offset measured in the empty gap
   between them. `For the Body` is the only family carrying two today. */
.tri-fam-note + .tri-fam-note { margin-top: 0.2rem; }
.tri-fam-note {
  display: block;
  margin-top: 0.4rem;
  /* THE NOTE HAS ITS OWN EDGE, and it is a margin because the difference can be
     negative. The family word above it takes --wave as the block's padding;
     this shifts the note off that edge to wherever the curve is at ITS OWN
     height. Greg's call, 2026-08-25: the two lines may step apart, because a
     single shared edge is exactly what made the gap read as 10px on one line
     and 48px on another. Without JS both fall back to --wave and nothing
     moves. */
  margin-left: calc(var(--wave-note, var(--wave, 0px)) - var(--wave, 0px));
  /* 10px, not 11: the wave pushes the longest note in by up to 98px and
     "food, movement, breathing, sleep" no longer fitted. Dropping a pixel kept
     every word, and "breathing" is one Greg would not want dropped. That note
     is two lines since 2026-08-26 and no longer sets the floor, but the size
     is unchanged -- it is now the note that gives the label column its slack,
     not the one that eats it. */
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.tr-index-list { list-style: none; margin: 0; padding: 0; }
.tri a {
  display: flex;
  align-items: baseline;
  /* 0.6rem, down from 0.85. Three gaps per row, so it is 12px of the width
     back on every line. */
  gap: 0.6rem;
  padding: var(--tri-pad) 0;
  text-decoration: none;
  color: inherit;
}
/* THE LEVEL MARK IS A BAR, NOT A LETTER. Greg, 2026-08-25: he wants one bar,
   two bars, three bars -- "I do not like those artistic endings of the
   letters", which is serifs. The glyphs are still I / II / III so they stay
   readable and copyable, but set in --sans, where the capital I has no serifs
   and reads as a plain vertical stroke. The extra letter-spacing is what keeps
   II and III from closing into a single mark.

   The column exists on every row, empty on the four that are not levels, so
   the four names still line up with the three above them. */
.tri-rung {
  /* 1.5rem, down from 2.1 -- Greg, 2026-08-26, on the tantra ladder: the three
     level marks "take some space and bring the problem later with
     responsiveness, so bring them closer to the word." The bars still have
     their own column, so the four unnumbered names keep the common left edge
     with the three numbered ones; the column is simply no wider than III needs.
     Measured: III sets 21.8px at this size, so 24px is the floor. */
  /* RIGHT-ALIGNED AND CLOSED UP TO THE NAME -- Greg, 2026-08-26: the marks
     "should be adjusted to right and closing to the first letter, which will
     save us space already there."

     THE COLUMN STILL EXISTS ON EVERY ROW, empty on the five that are not
     levels, so the unnumbered names keep the common left edge with the three
     numbered ones. What changed is which end of it the mark sits at: left-set,
     a lone `I` sat 18px away from its name and read as a stray tick. Right-set,
     all three marks finish on the same pixel and that pixel is the name.

     THE NEGATIVE MARGIN IS THE LETTER-SPACING COMING BACK OFF. 0.16em is added
     after the LAST glyph as well as between them, so right-aligning leaves a
     2.3px ghost column that belongs to nothing. */
  flex: 0 0 1.4rem;
  text-align: right;
  margin-right: -0.16em;
  font-family: var(--sans);
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: var(--fam);
}
.tri-name {
  flex: 0 0 auto;
  /* 14rem, down from 17.5. This is the common left edge every price aligns to,
     so it is pure reserved width -- and the longest name, "Multiorgasmic
     Tantra", measures 206px against the 224px this reserves. 18px of room, and
     56px handed back to the layout on every row. Greg, 2026-08-26: "if you
     bring the prices closer to the names, we will have better control for
     responsiveness."

     AND IT IS A CLAMP, NOT A FIXED WIDTH, which is what lets the two-column
     layout hold down to a tablet. At 900px this reserved a flat 224px against a
     name that had itself shrunk to 158px -- 66px of reserved emptiness on every
     row, and it was the single reason the prices ran off the right edge and the
     whole index had to collapse to one column. The name's own font-size is
     already a clamp; this now tracks it.

     IF A LONGER SERVICE NAME IS EVER ADDED, the top of this clamp is the number
     that has to move -- the names do not wrap, they push the prices out of
     alignment.

     THIS IS THE NARROW HALF OF THE RULE NOW. Above 1150px it is overridden by a
     flat 25rem, which is what pushes the prices to the right on a wide screen;
     below 870 the price stops sitting beside the name at all and the reserve
     goes to zero. Both are further down this file. What is left here is the
     band in between. */
  min-width: clamp(11rem, 16vw, 14rem);
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.9vw, 1.5rem);
  line-height: 1.15;
  color: var(--ink);
  transition: color 0.35s var(--ease-soft);
}
/* SHORT, AND THE SAME LENGTH ON EVERY ROW. Greg, 2026-08-25: with a border
   above and below, a leader running the full width makes the row all lines and
   "takes the effect away". It used to be flex: 1 1 auto and stretched to the
   right margin. Now the name column sets a common left edge, the rule is a
   fixed short dash, and the time and price sit immediately after it -- so the
   prices align down the page and the framing lines survive. */
.tri-rule {
  /* Shortened 2026-08-26 with the rest of the row. It is a mark that says "and
     here is the price", not a leader that has to span anything, so it costs
     width for nothing once the two columns either side of it are tight.

     0 1, NOT 0 0: it is the one part of the row that may be squeezed without
     anything being lost, so it is the first thing to give when the row runs out
     of space. */
  flex: 0 1 clamp(1.1rem, 1.8vw, 1.75rem);
  height: 1px;
  transform: translateY(-0.3em);
  background: var(--hairline);
  transition: background-color 0.35s var(--ease-soft);
}
.tri-meta {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.35s var(--ease-soft);
}
.tri-sep { padding: 0 0.5rem; color: var(--fam); }
.tri a:hover .tri-name,
.tri a:focus-visible .tri-name { color: var(--fam); }
.tri a:hover .tri-rule,
.tri a:focus-visible .tri-rule { background: var(--fam); }
.tri a:hover .tri-meta,
.tri a:focus-visible .tri-meta { color: var(--ink); }

/* --- a group of services -------------------------------------------------
   Three groups: the tantra ladder, the two massages, and the two that happen
   with clothes on. The heading is what turns seven blocks into three stops. */
.tr-group { padding-block: clamp(2.5rem, 4vw, 5rem); }
.tr-group-h {
  max-width: 56ch;
  padding-bottom: clamp(1.5rem, 2.5vw, 2.25rem);
  border-bottom: 1px solid var(--hairline);
}
.tr-group-h .eyebrow { margin: 0 0 0.75rem; }
.tr-group-lede {
  margin: 0.9rem 0 0;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--muted);
  max-width: 52ch;
}

/* --- one service ---------------------------------------------------------
   The picture is the narrower column. It carries no information the text
   does not, and on a page whose argument is entirely verbal the type should
   win the width. */
.tr { padding-block: clamp(2.5rem, 5vw, 5.5rem); }
.tr + .tr { border-top: 1px solid var(--hairline); }
.tr-grid {
  display: grid;
  grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
  gap: clamp(2rem, 4.5vw, 5rem);
  align-items: start;
}
/* Every second block turns over. The reveal directions in the markup are
   turned over with it, so a block always enters from its own side. */
.tr--flip .tr-fig { order: 2; }

/* THE PHOTOGRAPH STICKS. This is the one move that makes the length
   readable: the description runs to six paragraphs and the picture stays
   beside all of them instead of scrolling away after the first.
   `align-items: start` on the grid is what allows it -- a stretched grid
   item is full height and has nothing to stick within. */
.tr-fig {
  position: sticky;
  top: calc(var(--header-h) + clamp(1rem, 3vw, 2.5rem));
  margin: 0;
}
/* THE SHADOW, AND IT IS THE SAME ONE THE REST OF THE SITE USES. Greg,
   2026-08-26, asked for a shadow on all seven and left the choice open --
   frame, shadow only, or the modern pair in frames/.

   SHADOW ONLY, PORTRAIT, NO FRAME, and the reason is one page rather than one
   picture. The index at the top of THIS page won its comparison on exactly
   this PNG -- "the first section has a shadow and displacement on hover, and I
   want to delete the one without the shadow" -- so repeating it makes the page
   one system instead of two. image-frame-single.png would put a white card and
   a hairline round every photograph, which is furniture, on a page that just
   lost two rules per price row for being too busy. And the modern shadow only
   exists as a landscape cut; a portrait would have to be re-made to use it.

   Same construction as .duo-frame::after: ::after with negative insets so it
   costs the layout nothing, border-image so the 6px right and 25px bottom hold
   their true thickness while only the soft middle stretches, border-style set
   or border-image never paints. .tr-fig is already positioned (sticky), so it
   is the containing block. */
.tr-fig::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: -6px; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 6px 25px 0;
  border-image: url(../frames/shadow-only-portrait.png) 0 6 25 0 / 0 6px 25px 0 stretch;
  pointer-events: none;
}
.tr-fig img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  /* PLACEHOLDER PHOTOGRAPHY -- the same greyscale every other page puts on
     the 2022 images, so nothing on this page is judged on a picture yet. */
  filter: var(--photo-filter);
  transition: filter 0.55s var(--ease-soft);
}
/* --- .tr-fig--cutout: the same slot, holding a cut-out --------------------
   a-body-that-works.php #programme, 2026-09-20, when Greg replaced a second
   copy of personal-trainer-gran-canaria.jpg with greg-personal-trainer.png.

   THREE THINGS IN THE BASE RULE ARE FATAL TO A CUT-OUT, and each one is
   turned off here rather than softened:

     - `object-fit: cover`. The file is 704x1726, a ratio of 0.408:1, against
       a 4/5 box at 0.8:1. Cover crops to the box, which means taking 846 of
       the 1726 pixels of height -- his head or his legs, pick one. `contain`
       scales the whole figure down inside the box instead, which is the same
       decision index.php made on this same file in 2026-08-22.
     - `aspect-ratio: 4 / 5`. Contained inside 4/5 the figure renders at about
       half the column's width and reads as a small picture in a large empty
       box. 3/5 is taller, so the height that governs a contained image is
       larger and he comes out around 68% of the column -- present, and still
       shorter than the nine-item list beside him.
     - THE SHADOW. `.tr-fig::after` draws the site's photograph shadow at the
       BOX's edges. The figure no longer reaches them, so the shadow would be
       cast by transparent space -- a rectangle floating behind a man with
       nothing casting it. index.php left exactly this open for Greg's eye on
       .duo-frame; here there is no frame to justify it, so it goes.

   THE FLIP IS IN THE HTML'S REASONING AND IN THIS DECLARATION. It is on
   `transform`, and the hover rule below changes `filter`, so the two never
   contend. */
.tr-fig--cutout::after { display: none; }
.tr-fig--cutout img {
  aspect-ratio: 3 / 5;
  object-fit: contain;
  /* ORDER IS THE WHOLE RULE HERE. Transform functions compose left to right,
     each one setting the coordinate system for the next, so translate and
     rotate are evaluated BEFORE the mirror and therefore in unmirrored space:
     --fig-x positive is right on screen, which is what anyone reading
     figureFrame() would expect. Put scaleX first and both signs invert
     silently. The rotation is mirrored, which is why its sign in main.js was
     chosen against the screen rather than against the arithmetic. */
  transform:
    translate3d(
      calc(var(--fig-x, 0px) + var(--mag-x, 0px)),
      var(--mag-y, 0px),
      0)
    rotate(var(--fig-r, 0deg))
    scaleX(-1);
  will-change: transform;
}

/* --- THE LEAN ------------------------------------------------------------
   Greg, 2026-09-20: the sticky figure "looks somehow boring because we have so
   much text, and it takes a long time before this image moves... some slight
   animation, horizontal, or the angle, or something."

   HE IS DESCRIBING A SIDE EFFECT OF STICKY, NOT A MISSING ANIMATION. The
   figure parks at the top of the column and the nine-item list scrolls past
   it -- measured, 3510px of copy against a 732px picture. For most of that
   scroll nothing on the left half of the screen changes at all.

   AND IT IS WHY THE SEAM'S DRIFT CANNOT BE REUSED. seamFrame reads the
   picture's position in the VIEWPORT, which is exactly what sticky holds
   still: p would sit near constant for thousands of pixels and the figure
   would not move. This one reads progress through the ARTICLE instead -- 0
   when its top reaches the sticky line, 1 when its bottom leaves -- so the
   driver is the thing that is actually moving.

   TWO PROPERTIES, ONE GESTURE. 18px across and 1.8 degrees of rotation, tied
   to the same number, read as a man slowly shifting his weight. They are not
   two animations: this site already learned on .trx-img that two things moving
   at two speeds is one thing too many, and a third axis was tried and cut for
   that reason. The numbers are deliberately at the edge of noticeable -- over
   3510px of scroll the movement is never seen happening, only seen to have
   happened, which is the difference between atmosphere and a distraction.

   --- AND THE MAGNET, added the same day on Greg's ask -------------------
   --mag-x / --mag-y are the cursor's pull, written by the same frame loop and
   summed into the SAME translate rather than given a transform of their own.
   That is deliberate: two translate3d functions on one element would each
   need their own coordinate step and the rotation would then apply to only
   one of them.

   IT DOES NOT BREAK THE ONE-GESTURE RULE ABOVE, and it is worth saying why,
   because on the face of it this is the third moving thing. The lean answers
   to the scroll and the magnet answers to the cursor -- two different inputs,
   never both changing at once in normal use, so they are never seen as two
   animations competing. The site's rule is about two things moving at two
   speeds under ONE trigger; that is still not happening here. */
@media (prefers-reduced-motion: reduce) {
  .tr-fig--cutout img { transform: scaleX(-1); will-change: auto; }
}

/* COLOUR ONLY. These seven had no hover at all -- the page's only moving
   photographs were the four in the index, which made the long half of the page
   the still half. Greg asked for the services page to match the FAQ; this is
   the half of it he did not have to ask for.

   THE IMAGE ITSELF DOES NOT MOVE, and that is a removal rather than an
   omission. It scaled 1.04 inside the frame for about an hour on 2026-08-26,
   underneath the frame's own displacement, and Greg cut it: "you remove this
   effect with the resize on hover. Just the displacement is enough, it's
   subtle." Two things moving at two speeds was one thing too many. The figure
   lost its overflow:hidden at the same time -- it was there only to give the
   scaling picture a window, and an unexplained clip on a sticky element is
   exactly the kind of thing that gets inherited and never questioned.

   HARD-CODED grayscale(0) RATHER THAN unsetting --photo-filter, so it stays
   correct on the day that variable is deleted and the real photographs land:
   the hover state simply becomes the same as the resting state. */
.tr:hover .tr-fig img { filter: grayscale(0) contrast(1.06) brightness(1.04); }
/* THE FIGURE MOVES TOO, and the trigger is the whole block for the same reason
   the bands' trigger is the whole row -- see the long note on .trx-frame. Here
   there is no smaller stable element to hang it on: the figure IS the frame,
   so anything that moves it and carries its own :hover oscillates at the edge.

   HOVERING THE BLOCK RATHER THAN THE PICTURE IS ALSO THE HONEST READING. These
   photographs are not links and never have been. The picture answering while a
   man reads the description beside it is the FAQ's relationship exactly --
   there the head is hovered and the figure responds.

   IT MOVES TOWARDS ITS OWN OUTER MARGIN, so the pair of columns opens rather
   than closes. Every second block turns over, and the direction turns with it.

   `translate` again, because .tr-fig carries .reveal--left / --right, whose
   entire mechanism is a `transform` relaxing to none. Two properties, two
   owners, no conflict. It is also position:sticky -- verified that translate
   does not disturb the stick. */
.tr-fig { transition: translate 0.7s var(--ease-out); }
.tr:hover .tr-fig      { translate: -6px -5px; }
.tr--flip:hover .tr-fig { translate: 6px -5px; }
/* The rung numeral, on the picture rather than beside the name. It reads as
   a plate on a frame, which is the only reason a roman numeral belongs on a
   photograph at all. */

.tr-body .eyebrow { margin: 0 0 0.85rem; }
.tr-name {
  margin: 0 0 1.4rem;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2rem, 3.6vw, 3.15rem);
  line-height: 1.02;
  letter-spacing: -0.03em;
  color: var(--ink);
}
/* The second word carries the warm accent, the same way the wordmark does.
   --saffron-ink and never --saffron: this is type and saffron fails at
   reading weight (2.02:1). */
.tr-name em {
  font-style: italic;
  color: var(--saffron-ink);
}

/* The decoder for the nav's ABTW. Sans, small and muted against a 3rem serif
   heading, because it is an aside inside the title rather than part of it --
   at the heading's own size and colour it would read as a second name. The
   whitespace is non-breaking in the markup so "(ABTW)" never wraps alone onto
   a line of its own under "That Works". */
.tr-abbr {
  font-family: var(--sans);
  font-size: 0.3em;
  font-weight: 500;
  letter-spacing: 0.12em;
  color: var(--muted);
  vertical-align: 0.55em;
  white-space: nowrap;
}

/* --- the price rows ------------------------------------------------------
   Same leader-rule shape as the index, one size down. Prices are a LIST and
   not a table because most of them have one rung; Relaxation has five and is
   the exception the shape has to survive. */
/* THREE HORIZONTAL LINES PER ROW WAS TWO TOO MANY. Greg, 2026-08-26: the list
   had a rule above it, a rule under every row, and a filler rule stretching
   the full width between the time and the price -- "the three lines definitely
   doesn't look good."

   So the borders are gone and the filler stopped stretching. The rule is a
   short fixed dash now, which is what pulls the price back beside its own
   duration instead of parking it against the right edge of the column.

   THE DURATION COLUMN HAS A MIN-WIDTH AND THAT IS WHAT ALIGNS THE ROWS. With
   an auto width, "2 HOURS" and "2.5 HOURS" start their dashes at different
   places and a three-rung block reads as ragged. 7.2rem clears every short
   label; the two sentence-length ones (coaching, the programme) simply run
   past it, and they are single-row blocks where nothing has to line up. */
.tr-rates {
  list-style: none;
  margin: 0 0 clamp(1.5rem, 2.5vw, 2.25rem);
  padding: 0;
}
.tr-rates li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: flex-start;
  gap: 0.8rem;
  padding: 0.24rem 0;
}
.trr-dur {
  min-width: 7.2rem;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted);
}
.trr-rule {
  flex: 0 0 auto;
  width: clamp(20px, 3vw, 34px);
  height: 1px;
  transform: translateY(-0.25em);
  background: var(--hairline);
}
.trr-price {
  font-family: var(--serif);
  font-size: 1.4rem;
  line-height: 1;
  color: var(--ink);
}

.tr-copy p {
  margin: 0 0 1.15rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--body);
}
.tr-copy strong { color: var(--ink); font-weight: 600; }
/* The first line of each description, one size up. It is the sentence Greg
   wrote to do the whole job if nothing else is read. */
.tr-copy .tr-lead {
  font-size: var(--read-size);
  line-height: 1.45;
  color: var(--ink);
}

/* --- the pointer to the next rung ---------------------------------------
   Only the tantra blocks carry one. It is the ladder made operable: every
   rung names the rung a man should be on instead, which is the honest half
   of the upsell as well as the useful half. */
.tr-next {
  margin: 0 0 1.75rem;
  padding: 0.85rem 1.1rem;
  max-width: 60ch;
  background: var(--surface);
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--muted);
}
.tr-next a {
  color: var(--saffron-ink);
  text-decoration: none;
  border-bottom: 1px solid var(--saffron-hair);
  transition: border-color 0.35s var(--ease-soft);
}
.tr-next a:hover { border-bottom-color: var(--saffron); }

.tr-ask {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2vw, 1.75rem);
  flex-wrap: wrap;
  margin: 0;
}
.tr-tel {
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.35s var(--ease-soft), border-color 0.35s var(--ease-soft);
}
.tr-tel:hover { color: var(--ink); border-bottom-color: var(--hairline); }

/* --- the seam ------------------------------------------------------------
   The horizontal traced pair, which had been parked above the home page's
   footer with no home. It marks the one real boundary on this page -- tantra
   above it, ordinary massage below -- and it is the only line on the page,
   which is the whole reason it reads as a boundary rather than as
   decoration. .statement-lines carries its own styling and its own driver;
   this rule only gives the pair room. */
.tr-seam {
  /* Tight, because the drawing brings its own space. At container width the
     upper pair renders 443px tall and the flattened one 177px, and the svg
     boxes are mostly air -- the stroke only occupies the middle of each. Left
     on their default margins the seam read as a hole in the page rather than
     as a boundary, so the padding is taken off and the drawing IS the gap. */
  padding-block: clamp(0.5rem, 1.2vw, 1.25rem);
}
.tr-seam .statement-lines { margin-top: 0; }
.tr-seam .statement-lines--low { margin-top: clamp(0.5rem, 1.2vw, 1rem); }

/* --- the close ----------------------------------------------------------- */
.tr-close { padding-top: clamp(2.5rem, 4vw, 4.5rem); }
.tr-close-in {
  border-top: 1px solid var(--hairline);
  padding-top: clamp(2rem, 3.5vw, 3rem);
}
.tr-close-in .eyebrow { margin: 0 0 0.75rem; }
.tr-close-in .display-sm { margin-bottom: 1rem; }
.tr-close-in p {
  margin: 0 0 1.5rem;
  max-width: 54ch;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
.tr-close-in a:not(.btn) { color: var(--saffron-ink); }

/* --- narrow ---------------------------------------------------------------
   The phone layout of this site is a SEPARATE design and is not being made
   yet (BUILD-BRIEF.md, desktop first from 2026-08-21). What is here is only
   enough that the page does not break while the desktop layout is judged:
   one column, and the picture stops sticking because there is nothing beside
   it to stick against. */
@media (max-width: 900px) {
  .tr-grid { grid-template-columns: 1fr; }
  .tr--flip .tr-fig { order: 0; }
  /* relative, NOT static -- 2026-09-19. Static stopped the sticking but also
     cut the figure's frame (::after, inset 0 -6px -25px 0) loose from it: the
     frame anchored to a full-width ancestor instead and ran 6px past the
     screen, so every treatment page scrolled sideways on a phone. Relative
     stops sticking just the same and keeps the frame on its picture.
     top:auto with it: the sticky offset is a `top`, and on a relative box
     that same `top` pushed the picture 80px down over the treatment's name. */
  .tr-fig { position: relative; top: auto; }
  .tr-fig img { aspect-ratio: 16 / 10; }
  /* Everything else here is a cropped landscape by this width. A contained
     cut-out inside a 16/10 box would be height-constrained to a sliver, so it
     keeps its own upright box and simply gets narrower with the column. */
  .tr-fig--cutout img { aspect-ratio: 3 / 5; }
}

/* --- the index holds its two columns further down than the rest ----------
   MOVED OFF 900px, 2026-08-26, and then off 870 the same day. Greg: "the
   animated line already disappears at around 1024 -- by optimising the gaps
   between them, we will have a chance to see the animated line on the narrower
   screen."

   THE LINE DOES NOT DISAPPEAR, THE WAVE DOES, and that is the same thing to
   look at. Collapsing to one column zeroes the label's --wave indent, so the
   labels stop riding the curve and the curve is left running behind the text
   with nothing referring to it. The drawing is still painted; it has simply
   stopped meaning anything.

   SO THE FIX WAS NEVER THIS BREAKPOINT, it was the width the two-column layout
   needs, and it came down in two goes. First the row was tightened by about
   150px -- the name column, the label column, the gutter, the dash, the numeral
   and the three flex gaps. Then the row stopped needing to hold a price beside
   a name at all: below 870 the price goes under it, which is Greg's own move
   and the one that did most of the work. 900 -> 870 -> 560.

   IT IS STILL A DESKTOP FIGURE AND THE PHONE DESIGN IS STILL SEPARATE.
   BUILD-BRIEF.md, desktop first. What this buys is the tablet and the small
   laptop, where the wave was being thrown away for no reason. */
/* THE INDEX KEEPS ITS LINES WHERE IT KEEPS ITS COLUMNS. The blanket
   `.section-lines { display: none }` at 900px is right for every other lined
   section on the site -- they stack, and a line routed through a column that no
   longer exists runs through the words. The index does not stack until 560, so
   everything above that still has the empty column the curve lives in.

   Two classes to the general rule's one, so it wins on specificity without an
   !important. Above 900 it changes nothing, because nothing is hiding them
   there. The band it actually buys is 561 to 900. */
@media (min-width: 561px) {
  .tr-index .section-lines { display: block; }
}

/* --- THE PRICES MOVE RIGHT ONCE THERE IS ROOM FOR THEM --------------------
   Greg, 2026-08-26: "on the wider screens we could push the prices to the
   right, but then on the narrower screen they are going to be squeezed closer
   towards the names."

   THAT IS ONE NUMBER, because the name's reserved width IS the price column's
   left edge. Everything to the right of it -- the dash, the duration, the
   figure -- is carried along by it.

   WHY A FLAT NUMBER AND NOT A CLAMP. The container stops growing at 1136px,
   which it reaches at about a 1240px viewport. Above that the row is a fixed
   ~816px at every width, so a vw-tracking value would keep growing against a
   column that does not, and would push the longest price row off the right
   edge on a large monitor. Below the breakpoint the clamp on the base rule
   takes over and the whole column shrinks with the viewport, which is the
   second half of what Greg asked for.

   1150 IS MEASURED, NOT ROUNDED. It is the narrowest viewport where the row
   still carries a 400px reserve without the widest price crossing its right
   edge -- at 1100 the row is 662px against a 674px requirement and it
   overflows. Below it the layout is the base rule's, which is left-heavy on a
   wide window and is the one band this step does not fix.

   400px IS NOT THE MAXIMUM AND THAT IS DELIBERATE. Measured, the row can carry
   a 541px reserve before the widest price -- `6 - 12 MONTHS · FROM 300€`, at
   200px -- touches the right edge. Going the whole way leaves `Coaching`, a
   90px name, sitting 450px from its own price. The number is what closes most
   of the empty right-hand quarter while keeping the shortest name and its
   price recognisably one row. */
@media (min-width: 1150px) {
  .tri-name { min-width: 25rem; }
}

/* ==========================================================================
   THE INDEX, IMAGE VARIANT (.tr-index.trx) -- treatments.php, 2026-08-26
   ==========================================================================
   Greg's reason is the PAGE, not the index: "when I open this treatments page
   we have three sections, and the first one is text, the second one is text,
   and the third one is text. Putting images in the third section already
   breaks the monotony."

   TWO COLUMNS. Column one is the family label, unchanged and still measured
   onto the curve by main.js -- that is the JavaScript half, and it has to be,
   because a label glued to a line has to know where the line got to. Column
   two is a photographic band carrying that family's services, and it is pure
   CSS: it clips in from the right on a fixed delay and knows nothing about the
   drawing.

   AND THE SECOND COLUMN IS WHERE THE SIMPLIFICATION IS. The text version put a
   name, a dash and a price on one line beside the label, so all three had to
   fit -- which needed a reserved name width, a wide-screen override to push
   the prices right, a stacking step, and a collapse: four rules holding two
   columns in step at four different widths. A band has no fixed partner. It
   takes whatever is left and crops the photograph to it, so there is one
   breakpoint instead of four.

   THE THREE-COLUMN VERSION WAS MEASURED AND REJECTED before this was built --
   label, image, and a separate text column. At a 1440 viewport the image would
   have had 356px, at 1024 210px, and at 900 ninety-five. Putting the type on
   the band is what makes two columns possible at all.

   THE PHOTOGRAPHS ARE PLACEHOLDERS. Greg: "don't worry about the images yet,
   because the images later can be substituted with something else -- even AI
   generated images or some other backgrounds." All four are existing 2022
   files. Nothing below depends on which picture is in which band. */

/* The label is a heading for a band now, not a line on a list, so it sits at
   the top of its row rather than on a first row's baseline. */
.trx .tri-fam { align-items: start; }

.trx-panel {
  position: relative;
  overflow: hidden;
  /* WIDTH COMES FROM THE COLUMN, FULL STOP -- and it has to be said out loud
     because `aspect-ratio` plus `min-height` will otherwise derive it. With
     both set and width left auto, the band's preferred width becomes
     min-height x 16/5 = 608px, and it kept that at every viewport from 900
     down: a 608px band inside a 503px column, overflowing by 105px and hidden
     by the section's own overflow rule rather than reported. Found by
     measuring the band against its grid column, not by looking at the page. */
  width: 100%;
  min-width: 0;
  /* A FIXED RATIO, SO THE FOUR BANDS MATCH. Sizing each band to its own
     content would make Tantra's three services twice the height of A Body
     That Works' one, and the short one would read as a stub -- the same fault
     Greg fixed in the text version by breaking a note over two lines. Equal
     bands remove it outright.

     16/5 rather than 21/9: at the widest the column ever gets, 812px, this is
     254px tall, comfortably more than the tallest list needs -- three rows and
     the wash's fade-in room come to about 190. A taller ratio makes the index
     300px longer for no more information. */
  aspect-ratio: 16 / 5;
  /* AND A FLOOR UNDER THE RATIO, because the two shrink at different rates and
     the ratio loses. The band's height tracks its width; the list on it does
     not -- the type shrinks a little and the padding not at all. Measured, at a
     900px viewport the band was 157px tall carrying a 168px list, and since the
     list is anchored to the BOTTOM and the band is overflow:hidden, the top row
     of Tantra was being cut off with nothing to show it had happened.

     205px is the tallest list, measured at the narrowest width the list is
     ever asked to hold -- 199px at a 360px viewport, where the type has
     stopped shrinking but the band has not. Above a 656px column the ratio is
     the taller of the two and governs; below it this does, and the photograph
     simply crops harder. Nothing else has to change. */
  min-height: 205px;
  /* Something to reveal before the photograph decodes. CHARCOAL UNTIL
     2026-09-20, when the four photographs became cut-outs on white: a dark
     rectangle clipping in ahead of a white picture was a flash of the old
     design every time the page loaded. It matches the photograph now. */
  background: var(--white);
}
.trx-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* The site-wide placeholder grey. Deleting --photo-filter un-greys the whole
     site at once when the real photographs land; this band is not an exception
     to that. */
  filter: var(--photo-filter);
  transition: filter 0.55s var(--ease-soft);
}

/* THE WASH IS ANCHORED TO THE TEXT, NOT TO THE BAND, and that is a fix this
   site has already had to make once -- see .tcard-body. A band-anchored
   gradient fails the moment the two change proportion: on a narrow screen the
   list is most of the band's height and rides up into the thin part of the
   wash, and a service name lands on a bright patch of photograph. Anchored to
   the list, the backing travels with the type and holds at any band height.
   The large padding-top IS the gradient's fade-in room, not spacing. */
.trx-list {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 1;
  list-style: none;
  margin: 0;
  /* PUSHED DOWN AND THINNED OUT, 2026-08-26. Greg: "the landscape layout makes
     it take over basically the whole height of the image. What about if we push
     the text a little further down towards the edge, reduce the line spacing,
     and reduce the intensity of this overlay, so we make the whole image a
     little brighter."

     He is describing a wash that was tuned for a DIFFERENT SHAPE. .tcard-body
     carries these numbers on a 300-528px tall card where the copy is a third of
     the height. On a 16/5 band 256px tall, the same block measured 190px --
     74% of the picture under a wash meant to sit at the foot of one.

     3rem of fade-in room becomes 2.25 and the bottom padding drops by 5px, so
     the block is shorter AND sits lower. */
  padding: 2.25rem clamp(1rem, 1.6vw, 1.5rem) clamp(0.5rem, 0.75vw, 0.7rem);
  color: #fff;
  /* AND THE WASH ITSELF COMES DOWN, WHICH ONLY THE MEASUREMENT MADE SAFE.
     Taken at the brightest pixel behind every line of type -- the worst case
     for white text -- the old set measured 6.05:1 at its weakest, against the
     4.5:1 AA needs. That headroom is what paid for this. The numbers are not a
     guess at how far it could go; they are what was left after checking. */
  background: linear-gradient(to top,
    rgb(46 50 49 / var(--trx-wash, 0.74)) 0%,
    rgb(46 50 49 / calc(var(--trx-wash, 0.74) * 0.89)) 42%,
    rgb(46 50 49 / calc(var(--trx-wash, 0.74) * 0.68)) 74%,
    rgb(46 50 49 / 0) 100%);
}
.trx-row + .trx-row { border-top: 1px solid rgba(255,255,255,.18); }
.trx-row a {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  /* Tightened with the wash above -- Greg's "reduce the line spacing". Two
     paddings per row and three rows on the tantra family, so this is where
     most of the block's height was. It comes off the picture, not off the
     type: the names keep their size and their 1.15 leading. */
  padding: clamp(0.15rem, 0.3vw, 0.28rem) 0;
  text-decoration: none;
  color: inherit;
}
/* Right-aligned and closed up to the name, exactly as in the text version and
   for the same reason -- Greg, 2026-08-26. The -0.16em takes back the
   letter-spacing added after the last glyph as well as between them. The
   column is present and empty on the five rows that are not levels, so those
   names keep the common left edge with the three that are. */
.trx-rung {
  flex: 0 0 1.4rem;
  text-align: right;
  margin-right: -0.16em;
  font-family: var(--sans);
  font-size: 0.9rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  color: rgb(var(--trx-accent));
}
.trx-name {
  font-family: var(--serif);
  font-size: clamp(1.05rem, 1.5vw, 1.3rem);
  line-height: 1.15;
  transition: color 0.35s var(--ease-soft);
}
/* margin-left:auto IS THE WHOLE ALIGNMENT SYSTEM HERE, and it is what the text
   version needed four rules to achieve. The price goes to the right edge of
   whatever the band happens to be, at every width, with nothing reserved and
   nothing to keep in step. */
.trx-meta {
  margin-left: auto;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255,255,255,.8);
  white-space: nowrap;
}
.trx-sep { padding: 0 0.5rem; color: rgb(var(--trx-accent)); }

.trx-row a:hover .trx-name,
.trx-row a:focus-visible .trx-name { color: rgb(var(--trx-accent)); }

/* --- HOVER, PART ONE: THE COLOUR --------------------------------------
   Greg, 2026-08-26: "on the FAQ page the images have an effect of vertical
   displacement on hover that gives an extra small animation. I would like
   some kind of effect on hovering the images on the services page too, on top
   of the saturation reveal. I want some movement."

   THE MOVEMENT IS THE NEXT RULE, ON THE FRAME. This one is only the colour.

   FIRED FROM THE ROW, NOT THE BAND, so the colour and the displacement have
   the same trigger and arrive as one gesture. Hovering the family label brings
   its own band to life, which is the FAQ's relationship exactly.

   THE IMAGE ITSELF DOES NOT SCALE, and that is a removal rather than an
   omission. It grew 1.05 inside the panel for about an hour on 2026-08-26,
   underneath the frame's own displacement, and Greg cut it: "you remove this
   effect with the resize on hover. Just the displacement is enough, it's
   subtle." Two things moving at two speeds was one thing too many.

   0.55s on --ease-soft is the FAQ's own filter timing, kept -- it is a little
   slower than the 0.7s displacement, so the colour is still arriving as the
   movement settles rather than both stopping together. */
.tri-fam:hover .trx-img { filter: grayscale(0) brightness(1.1) contrast(0.98); }

/* THE WHOLE FRAME DISPLACES, WITH THE PICTURE INSIDE IT -- Greg, 2026-08-26,
   correcting the first attempt: "the same effect like we have on the FAQ page.
   I refer to the displacement of the whole frame with the image inside of it."

   AND THE TRIGGER IS THE ROW, NOT THE FRAME, WHICH IS THE PART THAT MAKES IT
   WORK. It is also exactly how the FAQ does it, and I had missed that the
   first time: `.svc-head:hover .svc-fig` -- the button is hovered, the FIGURE
   moves. An element that both carries :hover and moves under it oscillates,
   because a pointer near its edge is left outside the element it just moved,
   which un-hovers, which moves it back. Hovering the family row instead means
   the hit area never moves. It also means the label lifts its own band, which
   is the FAQ's behaviour rather than an addition.

   `translate`, NOT `transform`, AND THAT IS LOAD-BEARING. In the slide
   prototype this same element is animated on `transform`, and an animation's
   filled value cannot be overridden by an ordinary declaration -- a hover
   transform there would simply never apply. `translate` is an independent
   property that composes with `transform`, so the reveal owns one and the
   hover owns the other and neither has to know about the other.

   6px ACROSS AND 5px UP. The FAQ lifts 3px and does not move sideways; Greg
   asked for the x as well. Rightward, into the empty half of the row, because
   leftward is where the curve and the labels are and this must not read as the
   band touching the line. 0.7s is the FAQ figure's own timing. */
.trx-frame { transition: translate 0.7s var(--ease-out); }
.tri-fam:hover .trx-frame { translate: 6px -5px; }

/* THE BASE INKS, AND THAT IS A REVERSAL -- 2026-09-20. These were the LIFTED
   triples, raised in lightness so they could be read through a charcoal wash,
   and the note here said so. The wash is gone and the ground is white, so the
   lift now works against them: 219,124,113 measures 2.4:1 on white and the
   family word disappears. Back to the four inks the rest of the site uses on
   white, each of which is measured above 4.5:1 -- saffron is --saffron-ink
   rather than --saffron for exactly that reason. */
.trx .tri-fam--saffron { --trx-accent: 156, 102, 4; }  /* --saffron-ink */
.trx .tri-fam--red     { --trx-accent: 162, 58, 51; }  /* --red */
.trx .tri-fam--teal    { --trx-accent: 0, 99, 125; }   /* --teal */
.trx .tri-fam--brown   { --trx-accent: 125, 64, 9; }   /* --brown */

/* NO PLATES EITHER -- Greg, 2026-09-20, having replaced all four photographs
   with cut-outs on white: "you do not have to use those backgrounds behind the
   text. Simply adjust it to black colours while keeping some colours for the
   titles."

   THIS IS THE THIRD AND LAST STATE OF THIS BLOCK and the history is the
   argument for it. It was a gradient wash the width of the card, which read as
   dirt once two photographs came in on white. It became a plate the size of
   the text, charcoal on the dark pictures and white on the light ones, which
   was correct while the two kinds sat beside each other. Now every picture is
   white, so there is nothing left for a backing to protect the type from --
   and a white plate on white is a rectangle you can see the edge of and
   nothing else.

   SO THE TYPE GOES STRAIGHT ONTO THE PICTURE. Black ink for the service names,
   the prices and the family note; the four family colours kept for the family
   word, the level numerals and the separator, which is Greg's "some colours
   for the titles". The accents above had to come down out of their lift for
   this -- see the note there.

   IT IS SAFE BECAUSE OF WHERE THE SUBJECTS ARE, not because white is white.
   In all four photographs the figure sits right of centre and the left third
   is empty, and the type is anchored top-left and bottom-left. The widest
   block is Tantra's three rows; at the widest the band ever gets, 812px, it
   reaches about 44% across. If a photograph is ever swapped for one with a
   subject on the left, this is the rule that breaks and a plate is what fixes
   it -- the deleted version is in git. */
.trx .trx-head,
.trx .trx-list {
  right: auto;
  width: max-content;
  max-width: calc(100% - 2 * clamp(0.9rem, 1.4vw, 1.3rem));
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  text-shadow: none;
  color: var(--ink);
}
/* Head and list share one left inset so their left edges line up down the
   band. With the plates gone the padding is no longer the plate's own inset,
   so it comes off and the offsets do the positioning on their own. */
.trx .trx-head {
  top: clamp(0.75rem, 1.2vw, 1.05rem);
  left: clamp(0.9rem, 1.4vw, 1.3rem);
  padding: 0;
}
.trx .trx-list {
  bottom: clamp(0.75rem, 1.2vw, 1.05rem);
  left: clamp(0.9rem, 1.4vw, 1.3rem);
  padding: 0;
}
/* The secondary type, one step off black rather than a second colour -- the
   note beside the family word, the durations and the prices. Same rgb as
   --ink, so it stays in the family. */
.trx .trx-note,
.trx .trx-meta { color: rgba(42, 26, 12, .72); }
.trx .trx-note::before { color: rgba(42, 26, 12, .38); }
.trx .trx-row + .trx-row { border-top-color: rgba(42, 26, 12, .16); }

/* THE PRICE GOES UNDER THE NAME, AND THE PHOTOGRAPHS FORCED IT -- 2026-09-20,
   found by screenshot, not by reading the CSS.

   Name and price on one line measures about 315px on the longest row
   (`Multiorgasmic Tantra`, its numeral and its price). The clear white to the
   left of the figure is 31% of the band on the tantra photograph -- 232px at
   the widest the band ever gets. So the row overran the white by 85px and the
   price landed on a shoulder, unreadable, on two of the four bands. With a
   plate behind it this was invisible; without one it is the layout breaking.

   Stacked, the widest row is the name alone at about 207px and every band
   clears its own figure. The block gets taller, which the band can afford --
   three rows come to roughly 140px inside a 205px floor.

   THE ONE-LINE VERSION IS NOT WRONG, IT IS JUST WIDER THAN THIS PICTURE. If a
   photograph is ever swapped for one with more room on the left, deleting this
   rule restores it: `margin-left: auto` on .trx-meta is still what would push
   the price to the right edge. */
.trx .trx-row a {
  display: grid;
  grid-template-columns: 1.4rem auto;
  column-gap: 0.6rem;
  row-gap: 0.05rem;
  align-items: baseline;
  padding: clamp(0.3rem, 0.5vw, 0.45rem) 0;
}
.trx .trx-meta { grid-column: 2; margin-left: 0; }

/* --- THE FAMILY LABEL MOVED ONTO THE BAND, 2026-08-26 -------------------
   Greg, having seen the photographs land: "the images look good, this is the
   elegant effect I wanted. But now the animated line with the text glued to it
   doesn't look good any more. So we keep the line, the double line with the
   animation, but we remove the text, and this text has to move to the second
   column."

   INSIDE THE PICTURE RATHER THAN ABOVE IT, and he left the call open --
   "having it inside the image maybe will be better, but not necessary,
   whatever would look good. Help me make the decision."

   THE ARGUMENT THAT DECIDED IT IS THE ASYMMETRY, not taste. Every band's left
   edge now sits 50px from the curve at that band's own height, so the four
   edges step in and out by about 90px. A header set ABOVE a band has to align
   to that moving edge -- which it can, but then the words step left and right
   down the page with nothing visible explaining why, and it reads as
   accidental rather than composed. Inside, the header travels with its own
   band and the stepping is something the picture does, not something the type
   does.

   Two supporting reasons. The band already carries type at the bottom, so a
   header outside would split one family's identity across two zones. And the
   left column is now empty apart from the drawing -- a line of type floating
   in it would be the only thing there, which makes it look stranded rather
   than deliberate.

   TO PUT IT ABOVE INSTEAD: move .trx-head out of .trx-panel to just before
   .trx-frame, drop the two absolute-position lines and the scrim, and swap the
   two colours for their base inks. Nothing else has to change.

   ONE LINE WITH A DASH, which is Greg's own format: "when we have Tantra, then
   we have a dash, and then three levels, using the styling and colours." The
   dash is generated rather than typed, so a translator never has to carry
   punctuation. */
.trx-head {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 1;
  margin: 0;
  /* The generous BOTTOM padding is the scrim's fade-out room, the same way the
     list's big top padding is its fade-in room. Anchored to the type, so it
     holds at any band height -- see the note on .trx-list. */
  padding: clamp(0.75rem, 1.2vw, 1.1rem) clamp(1rem, 1.6vw, 1.5rem) 2.5rem;
  /* DENSER THAN IT FIRST WAS, AND THE TEAL FAMILY IS WHY. `For the Body`
     sits on a photograph whose top-left corner is bright sky, and the lifted
     teal -- already raised in lightness so it survives a dark wash -- washed
     out completely against it. Measured on screen, not reasoned about. The
     bottom wash under the service names is .93 at its densest for exactly the
     same reason; this now sits in the same range where the type actually is,
     and still reaches zero before the middle of the band. */
  background: linear-gradient(to bottom,
    rgb(46 50 49 / var(--trx-wash, 0.74)) 0%,
    rgb(46 50 49 / calc(var(--trx-wash, 0.74) * 0.89)) 40%,
    rgb(46 50 49 / calc(var(--trx-wash, 0.74) * 0.42)) 72%,
    rgb(46 50 49 / 0) 100%);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  line-height: 1.4;
}
/* The same 12px / 0.11em the label had in the left column, so nothing about
   the family word changed except where it sits. */
.trx-fam { color: rgb(var(--trx-accent)); }
.trx-note {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: rgba(255, 255, 255, .78);
}
.trx-note::before {
  content: '\2014';
  padding: 0 0.55rem;
  color: rgba(255, 255, 255, .45);
}

/* --- THE BANDS ARE MEASURED ONTO THE CURVE NOW -------------------------
   Greg: "take the vertical centre of the image and calculate the distance from
   the line to the edge, and place it at, let's say, fifty pixels from the
   line. Take the second picture, take its vertical centre, and calculate the
   fifty pixel gap. So this will cause the asymmetry of placements of those
   images... this allows us for asymmetry and somehow gluing those images to
   the line while holding the gap."

   SO THE LEFT COLUMN IS GONE, not emptied. There is nothing left to put in it
   -- the label moved onto the picture -- and a 272px reserved column holding
   only a drawing was what made the bands narrow. Each band now runs from its
   own measured left edge to the right margin, so it is both wider and a
   different width from its neighbours.

   --band-x IS WRITTEN BY main.js, one value per family, using the same
   function that put the old labels 10px from the curve -- read at the BAND's
   own vertical centre, with the gap at 50 instead of 10. The fallback of 96px
   is the curve's widest point plus the gap, so without JavaScript every band
   clears the line at the cost of the asymmetry. */
.trx .tri-fam {
  display: block;
  /* ===== THE ONE NUMBER THAT DRIVES BOTH WASHES =====================
     Greg, 2026-08-26: "let's try those values at fifty and at seventy-four
     percent at the other extreme." 0.50 and 0.74 were the two MIDDLE stops of
     the bottom gradient; he wants them tried as the EXTREME -- the densest
     point, at the picture's edge, which had been 0.84 below and 0.90 above.

     CHANGE THIS ONE VALUE AND BOTH GRADIENTS FOLLOW. Every stop below is a
     ratio of it, so the shape of the fade is preserved and only its depth
     moves. 0.74 is live. To see the other, make it 0.50.

     THE RATIOS ARE CHOSEN SO THAT 0.74 REPRODUCES THE STOP THAT WAS MEASURED
     SAFE. The bottom gradient's 74% stop is what governs legibility -- it is
     where the topmost service name sits -- and at this setting it computes to
     0.503, which is the 0.50 that measured 5.28:1 against the 4.5:1 minimum.
     At 0.50 that same stop falls to 0.34, and 0.32 was measured at 3.96:1.
     So the lighter option is expected to fail on the Tantra band; it is
     included because Greg asked to see it, not because it is safe. */
  --trx-wash: 0.74;

  /* BACK TO WHAT IT WAS BEFORE THE RULES, at Greg's instruction. It was
     opened to clamp(1.5rem, 2.6vw, 2.4rem) for one reason only -- to keep the
     next band's hover rule off the previous band's photograph -- and with the
     rules gone the reason went with them. Two paddings meet between any two
     bands, so this is 22-38px of daylight between pictures. */
  padding-block: clamp(0.7rem, 1.4vw, 1.2rem);
  position: relative;
}

/* NO HOVER RULE ABOVE THE BANDS. One was built on 2026-08-26 -- the FAQ's
   animated hairline, drawn left to right in the family's colour -- first
   starting on the curve and then, at Greg's correction, on the picture's own
   left edge with 18px of daylight above it. He cut it the same afternoon:
   "those animated lines on hover, it's too much of animations there. So we
   remove all lines, reduce the gaps between the images like it was before the
   lines." The bands already displace, brighten and go to colour on hover; a
   fourth thing arriving at the same moment was one too many.

   THE TECHNIQUE IS NOT LOST. .svc-rule on the FAQ is the same effect, still
   live, and references/web-effects/README.md carries the row. What was
   specific to here -- an animated rule anchored to a curve rather than to a
   box -- is in the history of this file and in decisions/log.md. */

.trx .trx-frame {
  margin-left: var(--band-x, 176px);
  /* ONE WIDTH FOR ALL FOUR, SO THE ASYMMETRY LANDS ON THE RIGHT EDGE --
     Greg, 2026-08-26: "the images should have the same width, which will
     create asymmetry on the right edge. You can make those images twenty
     percent smaller and make the offset from the line eighty pixels instead of
     fifty."

     Before this the width was `auto`, so each band ran from its own measured
     left edge to the right margin and no two were the same size. That put the
     ragged edge on the LEFT, where the line already is, and left the right
     margin as the only straight edge on the page. Turning it round puts the
     variation on the empty side and makes the four pictures identical objects.

     72%, WHICH IS THE 20% HE ASKED FOR. The auto bands measured 1001-1052,
     averaging 1024; a fifth off that is 819, and 819 of the 1136 container is
     72%. A percentage rather than a fixed number so it tracks the container
     down without needing a breakpoint.

     IT CANNOT OVERFLOW, and the check is one line: the curve's widest point is
     96px, so the largest offset this can ever produce is 96 + 80 = 176, and
     176 + 72% of 1136 is 994 against a 1136 row. 142px of headroom at the
     worst case. The 176 fallback is that same worst case, so a band without
     JavaScript still clears the line rather than crossing it.

     `min-width: 0` STAYS, and the explicit width is what stops aspect-ratio
     deriving one -- the fault that bit twice on 2026-08-26. */
  width: 72%;
}

/* NO DIVIDERS BETWEEN THE BANDS. The rule that hangs off the curve is right
   for a list of text -- it is what separates one family's rows from the next
   family's. Four photographs separate themselves, and a hairline crossing the
   gap between two pictures reads as a repair. The rule still exists for the
   parked text version, which needs it. */
.trx .tri-fam + .tri-fam::before { display: none; }
/* THE TOP BORDER IS BACK -- Greg, 2026-08-27. It came off when the first
   band's animated rule was going to take its place and its level; that rule was
   then cut for being too much animation, and nothing replaced the border. So
   the blue curve was beginning in empty space, and the left corner of that
   divider is the origin the whole line language was built around -- the bottom
   divider was still there, which is what made the loss visible.

   Restoring it is the deletion of `.tr-index.trx { border-top: 0 }`: the base
   .tr-index rule already carries both hairlines, and the parked text version
   above the footer never lost either of them. Nothing else changes. */

/* --- the frame around the band -----------------------------------------
   A BOX THAT CLIPS ITSELF CUTS ITS OWN SHADOW OFF. .trx-panel has to keep
   overflow:hidden -- it is what crops the photograph and what the wipe clips
   against -- so the shadow cannot live on it. This wrapper is unclipped and
   carries the shadow, and it is also the thing that moves in the slide
   variant, so the shadow travels with the picture instead of staying behind.

   The site already had this exact problem and this exact answer: .shot-clip
   further up the file exists for the same reason under .treatment-shot.

   IT IS IN BOTH VARIANTS EVEN THOUGH ONLY ONE USES IT, so the two sections
   are identical markup and the comparison is honest -- whichever Greg keeps,
   the other is a deleted section rather than a merge. */
.trx-frame {
  position: relative;
  /* AND IT NEEDS THE SAME TWO LINES .trx-panel NEEDS, FOR THE SAME REASON --
     which is worth saying plainly, because adding this wrapper reintroduced a
     fault that had already been found and fixed one element lower down.

     A grid item defaults to `min-width: auto`, so it refuses to shrink below
     its content's minimum. Its content is the band, whose aspect-ratio plus
     min-height give it a derived preferred width -- so at a 360px viewport the
     frame came out 469px wide inside a 305px column. The transform was
     identity; nothing was sliding. It was width, hidden by the section's own
     overflow rule rather than reported, exactly as before.

     THE LESSON, AND IT IS NOT A CSS ONE: measure the new element, not the one
     that used to be wrong. The old check was the panel against the index and
     it passed, because the panel really is 100% -- of a frame that was too
     wide. */
  width: 100%;
  min-width: 0;
}

/* --- the slide prototype (#menu-slide) ----------------------------------
   Greg, 2026-08-26: "we are using the effect of sliding from left and right
   [everywhere else], but here we have a line which we don't want to touch, so
   this could be a delayed sliding of each section from the right to left."

   64px, BETWEEN THE SITE'S TWO EXISTING THROWS. .reveal--left and
   .reveal--right are 34px for ordinary elements and 72px for the service
   blocks, which are the largest things on the site that move. A band is in
   that second class, and 64 is the distance at which it still clears the
   label column without the movement reading as a jump.

   THE LINE IS NOT TOUCHED, WHICH IS THE POINT. The transform is on the band's
   own wrapper, not on the index, so the curve and the four labels measured
   onto it never move. This is why it can be a transform at all -- buildWave
   reads getBoundingClientRect on the LABELS, and a transform on a sibling
   changes nothing it looks at.

   IT NEEDS THE SECTION'S overflow:hidden AND ALREADY HAS IT. .section--lined
   sets it, which is why a band starting 64px to the right of its column does
   not widen the document. Any future home for this effect needs the same. */
@keyframes trx-slide-in {
  from { opacity: 0; transform: translateX(64px); }
  to   { opacity: 1; transform: none; }
}
/* Gated on `.is-in` for the same reason the wipe is -- see the long note in
   index-reveal.css. Two sections carry this effect now and only the top one
   would ever be on screen when a page-load timer fired. */
.trx--slide .trx-frame { opacity: 0; transform: translateX(64px); }
.trx--slide.is-in .trx-frame {
  animation: trx-slide-in 900ms var(--ease-out) both;
}
/* nth-of-type counts divs, so the two line svgs are not counted -- see the
   same note in index-reveal.css. THE FOUR DELAYS ARE THE WIPE'S OWN, kept
   identical on purpose: the only variable between the two sections is meant
   to be the gesture. They are already asymmetric -- gaps of 0.7, 0.5, 0.5. */
.trx--slide .tri-fam:nth-of-type(1) .trx-frame { animation-delay: 1.0s; }
.trx--slide .tri-fam:nth-of-type(2) .trx-frame { animation-delay: 1.7s; }
.trx--slide .tri-fam:nth-of-type(3) .trx-frame { animation-delay: 2.2s; }
.trx--slide .tri-fam:nth-of-type(4) .trx-frame { animation-delay: 2.7s; }

/* THE WIPE IS OFF IN THIS VARIANT. Both would otherwise run -- the wipe is on
   .trx-panel and the slide on its parent -- and the band would open while it
   travelled, which is a third effect and not either of the two being judged. */
.trx--slide .trx-panel { animation: none; clip-path: none; }

/* --- the shadow, and it is Greg's own -----------------------------------
   Greg, 2026-08-26: "add the shadow effect on the images like we're using
   across the whole page."

   IT IS THE SAME PNG, NOT A box-shadow THAT LOOKS LIKE IT. The site's picture
   shadow is frames/shadow-only-landscape.png -- the 2014 frame with its
   white card knocked transparent -- applied with border-image so it keeps its
   true 25px at the edge and stretches only its soft middle. .treatment-shot
   and .gal-shot already use exactly this rule. A CSS shadow would be close and
   would not match, which is worse than not having one.

   SCOPED TO THE PROTOTYPE. If the shadow is the half worth keeping and the
   slide is not, this selector loses `--slide` and nothing else changes. */
.trx--slide .trx-frame::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 0 25px 0;
  border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 25px 0 stretch;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .trx--slide .trx-frame,
  .trx--slide.is-in .trx-frame { animation: none; opacity: 1; transform: none; }
}

/* --- and the one breakpoint it needs ------------------------------------
   THE BAND SETS IT, NOT THE TYPE. Below this the two columns leave the band
   too narrow for its own list: the label column is 272px and does not shrink,
   so the band runs out before the words do. One column from here -- the label
   becomes a heading and the band takes the full width, which is more
   photograph than it had at 900.

   THIS IS THE ONLY ONE IT NEEDS. The text version parked at the foot of this
   file needs four. That difference is the point of the variant. */
@media (max-width: 780px) {
  .trx .trx-frame { width: 100%; }
  /* THE BANDS GO FULL WIDTH AND THE ASYMMETRY GOES WITH THE LINE. --band-x is
     an indent measured off a curve; with no curve on screen it is just four
     different left margins for no reason a reader could see. main.js drops the
     inline values at the same width -- it asks whether the line is displayed
     rather than naming a number -- and this kills the 96px fallback
     underneath them, which would otherwise still indent every band. */
  .trx .tri-fam { --band-x: 0px; }
  /* The rule stays: it belongs to the picture, not to the curve, so it still
     means something with the line gone. */
  /* AND THE LINE GOES WITH THE COLUMN. The general exemption further up keeps
     .tr-index's lines alive down to 561, which is right for the text version
     parked above the footer -- it holds two columns that far. This one does
     not, and a curve still being drawn behind labels that have stopped being
     measured onto it is the exact fault Greg reported at 1024: the drawing is
     painted, and it has stopped meaning anything. Three classes to the
     exemption's two, so it wins without an !important. */
  .tr-index.trx .section-lines { display: none; }
  /* Wider than tall is still the shape, but a full-width 16/5 band on a phone
     is a 100px letterbox. */
  .trx-panel { aspect-ratio: 16 / 7; }
}

/* --- STEP ONE: THE PRICE GOES UNDER THE NAME, THE COLUMNS STAY -------------
   Greg, 2026-08-26: "before hiding the lines, we could on the narrow screen
   push the prices under each name, which will allow us to maintain the
   animated line for longer on the narrower screens."

   AND THAT IS EXACTLY WHAT IT BUYS. The old ladder had one step: two columns
   with the price beside the name, or nothing. The moment the price would not
   fit beside the name the whole index flattened, the label stopped riding the
   curve and the drawing was thrown away -- at 870px, on a tablet. Stacking the
   price is the cheap intermediate: the label column, the wave and the line all
   survive, and the only thing that changes is that each row is two lines
   instead of one.

   THE NAME'S RESERVED WIDTH GOES TO ZERO HERE, and it is the largest part of
   what this step recovers. That min-width exists to give every price a common
   left edge to align on. With the price on its own line there is no such edge
   to hold, so reserving for it is pure empty column on every row. */
@media (max-width: 870px) {
  .tri a { flex-wrap: wrap; }
  .tri-name { min-width: 0; }
  /* The dash says "and here is the price". On its own line the price does not
     need announcing, and a hairline hanging off the end of a name reads as a
     mistake. */
  .tri-rule { display: none; }
  /* Indented to the numeral column's right edge, so the price starts under the
     first letter of its name rather than under the level mark. One number, and
     it is the numeral's own basis plus the row gap, less the letter-spacing
     ghost the numeral gives back. */
  /* AND IT MAY WRAP HERE, WHICH IT MAY NOT ANYWHERE ELSE. `white-space:
     nowrap` on the base rule is what keeps a duration and its price on one
     line beside a name; on its own line under the name there is nothing to
     break away from, and the alternative is a hard floor where the price
     simply runs out past the right edge. Measured, it does not actually wrap
     until about 570px -- this exists so that the last few pixels before the
     one-column layout degrade instead of breaking. */
  .tri-meta { flex-basis: 100%; padding-left: 1.85rem; margin-top: 0.15rem;
              white-space: normal; }
}

/* --- STEP TWO: THE LABEL BECOMES A HEADING AND THE CURVE GOES --------------
   PAIRED WITH WAVE_FLAT_MAX IN main.js -- 560 in both places, and both halves
   must move together. See the note inside buildWave for what a mismatch looks
   like: nothing throws, nothing overflows, and every label sits at the wrong
   distance from a curve it is supposed to be measured onto.

   THIS IS 310px LOWER THAN IT WAS THIS MORNING. It was 870, and before that
   900. What moved it is the step above plus the two notes Greg shortened --
   `emotions & sexual energy` for `emotions, and sexual energy`, and
   `food, movement, breathing, sleep` broken over two lines. The label column's
   floor is the longest note plus the curve's own indent, and both halves of
   that came down. */
@media (max-width: 560px) {
  /* The family label stops being a column and becomes a heading above its
     rows. Without this the label column squeezes the names at the width where
     they are already wrapping. */
  .tri-fam { grid-template-columns: 1fr; gap: 0.4rem; }
  /* The wave is a two-column figure. In one column the label sits above its own
     rows and an indent read off a two-column layout means nothing. */
  .tri-fam-name { padding-top: 0; padding-left: 0; }
  /* And the divider goes back to the margin, because there is no longer a
     curve for it to clear. main.js drops the measured value at the same width;
     this kills the stylesheet fallback underneath it, which would otherwise
     leave three rules starting 44-88px in for no visible reason. */
  .tri-fam { --divider-x: 0px; }
}


/* ==========================================================================
   GALLERY (gallery.php) -- built 2026-08-25
   ==========================================================================
   The one page on this site whose whole content is photographs, which makes
   it the page most at risk of reading as a contact sheet. Two rules keep it
   from doing that.

   1. TWO GROUPS, NOT ONE WALL. The place and the work. A man deciding
      whether to come here is answering two different questions and a single
      grid answers neither.

   2. THE GRID IS EDITORIAL. Six columns; a picture takes four of them or
      two, so rows fall wide-then-tall, three talls, tall-then-wide. Nothing
      repeats on a cycle -- the same rule the testimonial offsets follow. A
      uniform tile grid is easier to build and looks like an estate agent's
      listing.

   The caption sits UNDER the picture, never over it. An overlaid caption
   needs a scrim, a scrim darkens the photograph, and these photographs are
   the thing being sold. */

.gal-group { padding-block: clamp(2.5rem, 4vw, 5rem); }
.gal-group-h {
  max-width: 52ch;
  margin-bottom: clamp(1.75rem, 3vw, 2.75rem);
  padding-bottom: clamp(1.25rem, 2vw, 1.75rem);
  border-bottom: 1px solid var(--hairline);
}
.gal-group-h .eyebrow { margin: 0 0 0.75rem; }

.gal-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: clamp(1rem, 2.2vw, 2.25rem);
}
/* Two columns by default, four when wide. The span IS the rhythm, so it is
   written on the item rather than derived from nth-child -- the same lesson
   the testimonial offsets produced on 2026-08-25: a rule keyed to position
   reassigns itself the moment a picture is added or dropped, and this list
   will change every time Greg shoots. */
.gal { grid-column: span 2; margin: 0; }
.gal--wide { grid-column: span 4; }

/* .gal-media IS THE FRAME'S BOX NOW and no longer clips -- the same change the
   wall needed, for the same reason: the shadow hangs 17-25px BELOW the picture
   and overflow: hidden cut every one of them off at the edge.

   THE ASPECT-RATIO MOVED OFF THE IMG AND ONTO THIS BOX. It had to: .gal-clip is
   absolutely positioned to inset 0, and it can only fill a parent that has a
   height of its own. Before this the img's own aspect-ratio was what gave the
   container its height, which stops being true the moment the img is taken out
   of the flow. */
.gal-media {
  position: relative;
  background: var(--surface);
  aspect-ratio: 3 / 4;
}
.gal--wide .gal-media { aspect-ratio: 3 / 2; }
.gal-clip { position: absolute; inset: 0; overflow: hidden; }
.gal-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Black and white at rest, colour on hover -- Greg, 2026-08-25. The filter
     is transitioned, NOT the custom property: custom properties do not
     animate, but the filter's own computed value does, and because both ends
     are grayscale() contrast() brightness() in the same order the browser
     interpolates them function by function. Changing --photo-gray on the
     hover is what moves it.

     0.7s is slower than a hover usually gets. It matches .gal:hover's 1.1s
     scale rather than racing it, and a gallery that snaps to colour reads as
     a web page -- this should read as a print portfolio that happens to
     breathe. */
  filter: var(--photo-filter);
  transition: filter 0.7s var(--ease-out), transform 1.1s var(--ease-out);
}
/* the wide tile's ratio now lives on .gal-media, above */
/* Slow, and only a little. A gallery that leaps under the pointer reads as a
   web page; this should read as a print portfolio that happens to breathe. */
.gal:hover .gal-media img { transform: scale(1.035); }

/* THE COLOUR REVEAL. Guarded on (hover: hover) because a touch screen has no
   hover to leave: without the guard the first tap latches a picture into
   colour and nothing takes it out again, so a phone would end up with one
   coloured tile in a black-and-white grid. On a phone the gallery is simply
   black and white, which is the design.

   NO :focus-within COMPANION RULE, and that is checked rather than assumed:
   these figures contain no link, button or input, so there is nothing inside
   one to focus and the rule would never fire. If the tiles ever become links
   -- a lightbox, or a jump to the matching service -- add it then, and it
   will work because .gal will finally have something focusable in it. */
@media (hover: hover) {
  .gal:hover .gal-media img { --photo-gray: 0; }
}

.gal figcaption {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-top: 0.9rem;
}
.gal-eyebrow {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

/* --- the eyebrows carry the four card colours ---------------------------
   Greg, 2026-08-26: the home page's four service cards each have a colour, and
   the gallery's labels should use them by subject -- tantra burgundy, the
   studio and its objects blue, anything outdoors orange, the treatments given
   inside brown. Eighteen grey labels were eighteen labels nobody read.

   THESE ARE NOT THE CARDS' OWN VALUES AND THEY CANNOT BE. Those are lifted
   variants -- 219,124,113 and 77,166,190 -- raised in lightness specifically to
   survive on top of a dark photograph. Measured against this page's ground they
   come out at 2.94:1 and 2.79:1, well under the 4.5 minimum, and this is 11px
   uppercase, which is the least forgiving type on the site.

   So each one uses the SAME HUE in the form the palette already keeps for type:

     tantra   --red   #A23A33   6.58:1
     studio   --teal  #00637D   6.82:1
     outside  gold    #9A6D00   4.60:1
     work     brown   #5C2C05  11.55:1

   THE ORANGE CANNOT BE THE CARD'S ORANGE, and the number settles it rather
   than any opinion: --saffron is #F2A809 and measures 2.02:1 as text on white.
   The stylesheet already knew -- .eyebrow--warm reaches for --saffron-ink and
   not --saffron, and this is why. Nor is there room to move toward it: the
   BRIGHTEST colour on saffron's own hue that still clears 4.5:1 is #9D6D06,
   which is the same colour as the --saffron-ink it would replace.

   SO THE LAST TWO ARE PULLED APART FROM THE BROWN END. Greg saw them merging
   and he was right; the cause was that --saffron-ink and --brown sit 11 degrees
   of hue and 5 points of lightness apart, which at 11px uppercase is nothing.
   Every degree of freedom is on the dark side, because only the orange has a
   contrast ceiling over it:

     was   #9C6604 hue 39 light 31   vs  #7D4009 hue 28 light 26
     now   #9A6D00 hue 42 light 30   vs  #5C2C05 hue 27 light 19

   Fifteen degrees and eleven points -- both roughly doubled. The gold went as
   yellow as 4.5:1 allows and gave up almost nothing in lightness; the brown
   went to a deep chocolate, which is also what keeps it from reading as another
   shade of the orange rather than as its own thing.

   THESE TWO ARE WRITTEN AS VALUES, NOT TOKENS, ON PURPOSE. --saffron-ink and
   --brown are used across every page of the site, and this is a gallery caption
   problem; moving the tokens to fix eighteen labels would repaint the whole
   build. If these ever earn their place site-wide they become tokens then.

   The deep brown must also stay clear of --ink #2A1A0C at light 11, which is
   the colour of the title directly beneath it. Eight points and a warmer hue,
   plus the eyebrow is uppercase and letterspaced where the title is serif.

   Grey stays the default. A label with no class is not a mistake, it is a
   picture that belongs to none of the four. */
.gal-eyebrow--tantra  { color: var(--red); }
.gal-eyebrow--studio  { color: var(--teal); }
.gal-eyebrow--outside { color: #9A6D00; }
.gal-eyebrow--work    { color: #5C2C05; }
.gal-title {
  font-family: var(--serif);
  font-size: clamp(1.1rem, 1.5vw, 1.35rem);
  line-height: 1.15;
  color: var(--ink);
}

/* --- the close ----------------------------------------------------------- */
/* TIGHTENED 2026-08-26, Greg's call, once the beach frame closed the wall.
   Measured before and after: the run from the last caption to the hairline was
   56px at 1440 -- 3.5vw of section padding on top of the wall's own -- and the
   picture and the sentence read as two pages. 16px now. The air under the rule
   is untouched at 48px, because that gap is doing a different job: it separates
   the rule from the eyebrow, and closing it would crowd the heading instead. */
.gal-close {
  padding-top: clamp(0.5rem, 1.1vw, 1.5rem);
  /* the ground the texture below needs; it carries no offsets of its own */
  position: relative;
}

/* THE TEXTURE, the same file the home page uses. Greg, 2026-08-26.

   FIRST, WHAT THE FILE ACTUALLY IS, because Greg described it as a gradient
   running opaque at the top to transparent at the bottom and it is not that.
   Profiled row by row: 1920x1080, and rows 0-24% and 58-100% are COMPLETELY
   empty. Everything lives in a band between 24% and 57%, peaking at 40%. It is
   a strip of speckle with its own fade at each end, floating in a transparent
   file.

   BUT HIS INSTINCT WAS RIGHT, because the band is not symmetric. Coverage by
   depth: 0.6, 2.5, 4.2, 9.8, 7.3, 3.5, 0.4 percent at 25/30/35/40/45/50/55.
   The rise into the peak takes 15% of the height and the fall out of it takes
   17, and the fall is much gentler in its first stretch -- 7.3 against 4.2 at
   the same distance either side. So the band has a FIRM edge on top and a LONG
   TAIL below.

   scaleY(-1) TURNS IT OVER. Firm edge low, tail running upward: the grain
   gathers near the footer and dissolves as it climbs. That is the effect Greg
   asked for, and it is one line rather than a second copy of a 1920x1080 file.

   AND IT REACHES INTO THE SECTION ABOVE, which was the other half of the ask.
   -70vh puts the element's top well inside the wall, so the tail crosses the
   boundary and fades out over the last photograph instead of stopping dead at
   a section edge. z-index -1 keeps it behind the pictures, so it shows in the
   gaps between them rather than over them -- which is the same behaviour the
   home page relies on where this runs into About.

   NO MASK, unlike the home page, and it is not an oversight. That one needs a
   fade because its box is short enough that `cover` crops into the band and
   leaves a hard edge. This box is far taller than 16:9, so cover scales to the
   HEIGHT and the full band is visible with no vertical crop at all -- the fades
   are the file's own. It also means background-position has no vertical effect
   here, which is what makes the flip safe to do with a transform: there is no
   flipped coordinate left for anyone to reason about later. */
.gal-close::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -70vh;
  /* 120px BELOW THE SECTION, A CONSTANT, NOT A SHARE OF THE VIEWPORT. It was
     +90vh against a -70vh top, so the overrun past the section was 0.20 x vh --
     and the footer that has to hide it is a fixed 242px on desktop. Measured:
     180px of overrun at vh 900, 216px at 1080, and 240px at vh 1200, which is
     past the bottom of a 242px footer and the grain reappears under it. That is
     exactly why Greg saw it on the desktop and not on the phone -- there the
     footer stacks to 439px and swallows anything this rule can produce.

     A constant cannot outgrow the footer no matter how tall the window is, and
     120px still leaves the cut well inside it, so the hard edge never shows at
     the footer's elliptical top corners. */
  height: calc(100% + 70vh + 120px);
  background: url(../images/texture.png) center / cover no-repeat;
  transform: scaleY(-1);
  /* GREG, 2026-08-26: opaque at the bottom, transparent at the top, so the
     grain thins out before it reaches the words.

     `to bottom` AND IT PRODUCES A TOP-TRANSPARENT RESULT. This is the flipped
     coordinate I said above there was none of, and adding a mask is exactly
     what brings it back, so it is written down rather than left to be
     rediscovered: a mask is painted in the element's OWN space and the
     transform is applied to the result. scaleY(-1) maps local top to visual
     bottom. So the opaque end of this gradient has to be written at the LOCAL
     top to arrive at the VISUAL bottom. Verified by sampling the rendered
     page rather than by reading this rule: grain coverage down the left margin
     measures 0.00% above the wall, 1.97% where the close section begins, and
     4.1% by the footer edge. Written the other way round it would have piled
     the speckle onto the heading and cleaned off the empty end.

     If the transform is ever removed, this gradient reverses with it. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.45) 55%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.45) 55%, transparent 100%);
  pointer-events: none;
  z-index: -1;
}
.gal-close-in {
  border-top: 1px solid var(--hairline);
  padding-top: clamp(2rem, 3.5vw, 3rem);
}
.gal-close-in .eyebrow { margin: 0 0 0.75rem; }
.gal-close-in .display-sm { margin-bottom: 1rem; }
.gal-close-in p {
  margin: 0 0 1.5rem;
  max-width: 54ch;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
.gal-close-in a:not(.btn) { color: var(--saffron-ink); }

/* THE INLINE LINK GETS THE SITE'S UNDERLINE. Greg, 2026-08-26: the pointer
   turns into a hand and nothing else says this is a link, which on a page whose
   only other call to action is a button is not enough.

   IT IS THE SAME UNDERLINE .nav a AND .btn--line ALREADY DRAW -- 1px, saffron,
   growing from the left, 0.45s on --ease-out. Reused rather than invented,
   because a second underline behaviour on one site is a second thing to keep in
   step.

   THE TECHNIQUE DIFFERS AND IT HAS TO. Those two animate a positioned ::after
   with scaleX. That works on a nav item, which is a block-ish inline that never
   breaks; this link sits mid-sentence in a paragraph capped at 54ch, so it CAN
   wrap across two lines, and an absolutely positioned ::after on a fragmented
   inline box resolves against one containing block and draws a single bar
   across the gap. A background-size transition is drawn per line box instead,
   so a wrapped link underlines both halves and neither joins the other.

   0% to 100% of the box rather than 0 to a pixel width: the growth is
   proportional, so it reads at the same speed whatever the link's length.

   THE :not(.btn) IS A REAL BUG FIX, not tidying. This section's call to action is
   <a class="btn btn--solid"> inside <p class="gal-ask">, so a bare
   `.gal-close-in p a` selected the BUTTON as well as the sentence's link -- and
   at (0,2,1) it beat .btn--solid's (0,1,0). Measured: the button came out 39px
   tall against 54 everywhere else on the site, because padding-bottom: 2px had
   replaced its 16, and it was also being handed an underline gradient that
   .btn--solid's `background:` shorthand could not override. Found by comparing
   button heights across pages, not by reading this rule. */
.gal-close-in p a:not(.btn) {
  /* currentColor, NOT --saffron. Greg, 2026-08-26: the line was arriving a
     different colour from the words it belongs to. .nav a can use the bright
     saffron because its text is --ink and the line is a separate accent; here
     the text is already --saffron-ink, so a second, brighter orange 1px below
     it read as a mismatch rather than as an accent.

     AND WHY THE LINE MAY BE BRIGHT WHERE A LABEL MAY NOT -- Greg asked exactly
     this about "The ocean". --saffron #F2A809 is 2.02:1 on white, so it is
     unreadable as TEXT and the gallery eyebrows cannot have it. A 1px rule is
     not text: nobody has to resolve its shape, so contrast rules do not bind
     it. That is why the same colour is fine under a link and wrong on a label.

     currentColor also means the two can never drift apart again. */
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  /* clears the descenders in "services page" -- without it the rule cuts
     through the p and the g */
  padding-bottom: 2px;
  transition: background-size 0.45s var(--ease-out);
}
/* The colour no longer shifts on hover. It used to go to --ink, which with a
   currentColor rule would drag the line dark with it -- and the line arriving
   IS the feedback, so the colour change was never carrying its weight. */
.gal-close-in p a:not(.btn):hover,
.gal-close-in p a:not(.btn):focus-visible { background-size: 100% 1px; }
.gal-ask {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2vw, 1.75rem);
  flex-wrap: wrap;
  margin: 0;
}
.gal-tel {
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.35s var(--ease-soft), border-color 0.35s var(--ease-soft);
}
.gal-tel:hover { color: var(--ink); border-bottom-color: var(--hairline); }

/* --- narrow ---------------------------------------------------------------
   The phone layout of this site is a SEPARATE design and is not being made
   yet (BUILD-BRIEF.md, desktop first from 2026-08-21). This is only enough
   that the page does not break while the desktop layout is judged: six
   columns become two, and a wide picture takes both. */
@media (max-width: 900px) {
  .gal-grid { grid-template-columns: repeat(2, 1fr); }
  .gal { grid-column: span 1; }
  .gal--wide { grid-column: span 2; }
}


/* ==========================================================================
   CONTACT — restyled 2026-08-25
   ==========================================================================
   This page was built before the site's style settled and it showed: plain
   fades where every other page wipes and rises, no line at all, no hairline
   structure, and form fields that were rounded boxes with a teal glow — the
   least premium thing on any of the six pages.

   Nothing here is new vocabulary. The reach rows are the services page's
   price rows, the block headings are the group headings, the line is the one
   #studio already uses on the home page. The page was not short of ideas; it
   was short of the ones the rest of the site had already settled on.

   Everything below is contact-only — .field and .contact-* appear on no
   other page, checked before touching them. */

/* .contact-form was display:grid with a 1.4rem gap, which would double up
   with the field margins below. Blocks, so one thing governs the rhythm. */
.contact-form { display: block; }
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 5rem);
  align-items: start;
}
/* The colour is restated, not just the margin. The eyebrow here is a <p>,
   and an older `.contact-details p { color: var(--body) }` three layers up
   is MORE specific than .eyebrow--red -- so the left eyebrow came out brown
   while the identical one over the form came out red. */
.contact-details .eyebrow,
.contact-form .eyebrow { margin: 0 0 1.25rem; }
.contact-details .eyebrow--red,
.contact-form .eyebrow--red { color: var(--red); }

/* --- the two channels ----------------------------------------------------
   STACKED, NO LEADER RULES — Greg, 2026-08-25. The first version used the
   services page's price row: label, a rule stretching across, value. That
   shape earns its place on a price list, where two columns have to be read
   across a wide gap. Here there are two facts and no table, so the rule was
   pointing at something already obvious, and it put four more hairlines on a
   page that already has a form made of them.

   What is left is one hairline under the eyebrow. The label sits above its
   value and the whole thing is a link. */
.reach-list {
  list-style: none;
  margin: 0 0 clamp(2rem, 3.5vw, 3rem);
  padding: 1.25rem 0 0;
  border-top: 1px solid var(--hairline);
}
.reach + .reach { margin-top: 1.5rem; }
.reach a {
  display: block;
  text-decoration: none;
  color: inherit;
}
.reach-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color 0.35s var(--ease-soft);
}
.reach-value {
  display: inline-block;
  font-family: var(--serif);
  font-size: clamp(1.25rem, 1.9vw, 1.6rem);
  line-height: 1.15;
  color: var(--ink);
  transition: color 0.35s var(--ease-soft);
}
/* The address is long and would otherwise set the column's width. */
.reach-value--sm { font-size: clamp(0.95rem, 1.25vw, 1.1rem); }
.reach a:hover .reach-value,
.reach a:focus-visible .reach-value { color: var(--red); }
.reach a:hover .reach-label,
.reach a:focus-visible .reach-label { color: var(--ink); }

/* THE FLAGS SIT UNDER THE NUMBER, inside its item — Greg, 2026-08-25. They
   say which languages a call can be made in, so they belong to the number
   rather than to the page. */
.contact-langs {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 0.85rem;
}
.contact-langs img { display: block; width: 24px; height: 24px; }
/* WhatsApp is the only link in the row, so it is the only thing needing a tap
   target. Padded out to 44x44 and pulled back by the same amount, so it grows
   for a thumb without moving a pixel on screen. */
.contact-wa {
  display: block;
  line-height: 0;
  padding: 9px;
  margin: -9px -9px -9px -1px;
}
.contact-wa img { display: block; width: 26px; height: 26px; }

.contact-block + .contact-block { margin-top: clamp(1.75rem, 3vw, 2.5rem); }
.contact-langs + .contact-block { margin-top: clamp(1.75rem, 3vw, 2.5rem); }
.contact-details .contact-h {
  /* text-transform and letter-spacing are declared, not inherited: the older
     .contact-h three layers up sets uppercase with wide tracking, and left
     alone it turned these into a THIRD heading style -- serif capitals --
     beside the eyebrow and the display-sm the rest of the site uses. */
  text-transform: none;
  letter-spacing: -0.01em;
  margin: 0 0 0.75rem;
  padding-bottom: 0.65rem;
  border-bottom: 1px solid var(--hairline);
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.2rem, 1.6vw, 1.45rem);
  line-height: 1.15;
  color: var(--ink);
}
.contact-details .contact-line {
  margin: 0 0 0.9rem;
  max-width: 46ch;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
.contact-details .contact-line:last-child { margin-bottom: 0; }
.contact-faq a { color: var(--saffron-ink); }

/* --- the form ------------------------------------------------------------
   HAIRLINES, NOT BOXES. The old fields were 12px-radius rectangles with a
   3px teal ring on focus — a generic web form dropped into a page made of
   rules and serif type. A field is now the same hairline as everything else
   and the focus state is the warm accent thickening it, which is the only
   moving part.

   The label stays above the input rather than floating into it. A floating
   label is a nice trick and it costs the one thing a form on this page
   cannot lose: the question still readable while the answer is being typed. */
.contact-form .field { display: block; margin-bottom: 1.6rem; }
.field label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 0.55rem;
  transition: color 0.35s var(--ease-soft);
}
.field input,
.field textarea,
.field select {
  width: 100%;
  font: inherit;
  font-size: 1.05rem;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--hairline);
  border-radius: 0;
  padding: 0.7rem 0 0.65rem;
  transition: border-color 0.35s var(--ease-soft);
}
.field textarea { min-height: 130px; resize: vertical; line-height: 1.6; }
/* The select needs its own arrow once the browser chrome is taken off it.
   An inline SVG as a data URI, in --muted, so it matches the labels. */
.field select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%237A6957' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.2rem center;
  padding-right: 2rem;
}
.field input:focus,
.field textarea:focus,
.field select:focus {
  outline: none;
  border-bottom-color: var(--saffron);
  box-shadow: 0 1px 0 0 var(--saffron);   /* the hairline thickening, not a ring */
}
.field:focus-within label { color: var(--saffron-ink); }

.contact-form .btn { margin-top: 0.5rem; }

/* --- the send button -----------------------------------------------------
   GREG, 2026-08-25: the pale teal pill "is a little bit not following the
   style", and he is right. It was the site's general-purpose button dropped
   into a form made of hairlines — a filled shape and a 999px radius on a page
   whose every other edge is one pixel of --hairline and whose map frame is
   squared to 2px.

   THE FIX IS TO MAKE IT THE LAST FIELD RATHER THAN A NEW OBJECT. Every input
   above it is a hairline that turns saffron and thickens when it is active.
   So the button is a hairline rectangle, and pressing towards it draws that
   same saffron line across its foot. The gesture is already learned by the
   time anyone reaches it, which is why this reads as belonging and the pill
   did not.

   Of the directions Greg floated: the fill is gone, the corners are squared
   to the map's 2px, and the accent is the saffron from the logo rather than
   the blue. A full animated border was the other candidate and was not taken
   — a line travelling around four sides is a bigger gesture than a send
   button on a quiet page should make, and it does not rhyme with anything
   else here. Both alternatives are a few lines away if he wants to see them. */
/* ===== NO LONGER USED, AND KEPT ON PURPOSE =================================
   Greg, 2026-08-26: "make every button on the page the same style as the
   gallery page." The contact form's Send and the 404's Back to the start were
   the only two carrying this variant, and both moved to .btn--solid, so nothing
   on the site selects .btn--line today.

   IT IS KEPT RATHER THAN CUT because the reasoning above it is the record of a
   real decision, and because that decision was reversed by a change to its own
   premise rather than by being wrong. The argument for the hairline was that
   "the pill did not belong" -- and the pill is gone: .btn is 4px now, so the
   solid button is a rectangle too, and the thing this variant existed to avoid
   no longer exists. Deleting the block would delete the only place that is
   written down.

   If it is still unused when the site uploads, cut it then -- dead CSS ships to
   every page -- and put it in archives/ with the WHY.md the project rule asks
   for. Until then it costs a few hundred bytes and holds the history.
   ======================================================================== */
.btn--line {
  position: relative;
  overflow: hidden;
  background: transparent;
  border: 1px solid var(--hairline);
  border-radius: 2px;
  padding: 1.05rem 1.9rem;
  color: var(--ink);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 0.4s var(--ease-soft), color 0.4s var(--ease-soft);
}
/* The saffron underline, drawn from the left — the same direction and the
   same colour as a field taking focus. */
.btn--line::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: var(--saffron);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.55s var(--ease-out);
}
.btn--line:hover,
.btn--line:focus-visible { border-color: var(--ink); }
.btn--line:focus-visible { outline: none; }
.btn--line:hover::after,
.btn--line:focus-visible::after { transform: scaleX(1); }
/* .btn lifts 2px on hover. A shape with a line being drawn along its bottom
   edge should not also be floating away from it. */
.btn--line:hover { transform: none; }
.btn--line svg { transition: transform 0.4s var(--ease-out); }
.btn--line:hover svg,
.btn--line:focus-visible svg { transform: translateX(4px); }
.form-note {
  margin: 1.1rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}

/* The honeypot, moved off-screen rather than hidden — see the markup. */
.hp-field {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- the map -------------------------------------------------------------
   A LINK, NOT A PICTURE WITH A LINK UNDER IT. The whole frame is the target,
   because a map is the one element on a page that everybody already expects
   to be tappable. */
/* THREE GAPS, ALL TIGHTENED — Greg, 2026-08-25.

   1. ABOVE the eyebrows. The section carried padding-top:0 inline, so "How to
      reach me" started immediately under the beige band and the two read as
      one block. A little air, not a lot: this is the gap he asked to be
      "elegant", which means noticed only if it were missing.
   2. BELOW the send button. The reach section's default bottom padding plus
      the map section's top padding stacked into roughly 160px of nothing at
      1440. They are now about a third of that between them.
   3. BETWEEN the heading and the map. The rule under "Where" is what
      separates them; the gap was doing the same job twice. */
.contact-reach-sec {
  padding-top: clamp(1.5rem, 2.6vw, 2.5rem);
  padding-bottom: clamp(1.25rem, 2.2vw, 2rem);
}
.contact-map-sec { padding-top: clamp(0.75rem, 1.4vw, 1.5rem); }
.map-head {
  margin-bottom: clamp(0.75rem, 1.2vw, 1rem);
  padding-bottom: clamp(1rem, 1.8vw, 1.5rem);
  border-bottom: 1px solid var(--hairline);
}
.map-head .eyebrow { margin: 0 0 0.6rem; }

.contact-map {
  position: relative;
  /* The v5 sheet still carries a .contact-map written for the iframe that
     never shipped: 18px radius and grayscale(1). The greyscale would drain
     the saffron out of the pin, so it is turned off explicitly here rather
     than deleted there -- that block is three layers up in a stylesheet the
     whole site shares. */
  filter: none;
  display: block;
  overflow: hidden;
  border-radius: 2px;
  background: var(--surface);
  /* MUST BE STATED, and it is not cosmetic. The v5 sheet's .contact-map --
     written for the iframe that never shipped -- pins this box to 16/11. The
     image inside is 19/8 with object-fit: cover, so the container's ratio was
     silently CROPPING the map left and right while the pins, which are
     positioned as a percentage of the container, went on pointing at where
     things were in the uncropped picture. The pins measured correct and were
     not. Both ratios have to be the same number. */
  aspect-ratio: 19 / 9;
}
.contact-map img {
  display: block;
  width: 100%;
  height: auto;
  /* 19:9 — the frame this walk needs. It was 16:11 when the map was a
     single pin. The route through Avenida de España climbs north before it
     turns east, so it spans 1126 x 508px and needs more height than the
     first, flatter route did. */
  aspect-ratio: 19 / 9;
  object-fit: cover;
  /* NO --photo-filter here. The Voyager tiles are already the site's warm
     cream, which is most of why that style was chosen — greyscaling them
     would throw away the one thing that made them fit. */
  transition: transform 1.2s var(--ease-out);
}
.contact-map:hover img { transform: scale(1.02); }

/* --- the two pins --------------------------------------------------------
   POSITIONS COME FROM THE MARKUP, not from here. --x and --y are the route's
   own endpoints projected into the frame, so they are data about a
   photograph rather than a design decision, and re-shooting the map changes
   two numbers in the HTML and nothing in this file.

   THE TWO ENDS ARE NOT THE SAME WEIGHT, deliberately. The studio is a solid
   saffron dot; Yumbo is a hollow ring in the line's own teal. One is a
   destination and one is a landmark you already know, and a map with two
   identical markers makes the reader work out which is which. */
.map-pin {
  position: absolute;
  top: var(--y);
  left: var(--x);
  width: 46px;
  height: 46px;
  margin: -23px 0 0 -23px;
  border-radius: 50%;
  background: rgba(var(--saffron-rgb), 0.22);
  display: grid;
  place-items: center;
}
.map-pin-dot {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--saffron);
  box-shadow: 0 0 0 3px var(--white), 0 2px 6px rgba(42, 26, 12, 0.28);
}
.map-pin--start {
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  background: rgba(var(--teal-rgb), 0.16);
}
.map-pin-ring {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--white);
  box-shadow: 0 0 0 3px var(--teal), 0 2px 6px rgba(42, 26, 12, 0.22);
}

/* The tags sit ABOVE their pins and clear of them, so the markers keep the
   point and the words never cover the street they are on. */
.map-tag {
  position: absolute;
  top: var(--y);
  left: var(--x);
  transform: translate(-50%, calc(-100% - 30px));
  white-space: nowrap;
  padding: 0.4rem 0.75rem;
  background: var(--white);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
  box-shadow: 0 2px 10px rgba(42, 26, 12, 0.12);
}
.map-tag--start {
  transform: translate(-50%, calc(-100% - 24px));
  color: var(--teal);
}

/* The walking time, on the long straight stretch of the route. Not a tag on
   a pin — it belongs to the LINE, so it is set lighter and without a box, and
   it sits above the path rather than on it. */
.map-walk {
  position: absolute;
  top: var(--y);
  left: var(--x);
  transform: translate(-50%, -50%);
  white-space: nowrap;
  padding: 0.35rem 0.7rem;
  background: rgba(255, 255, 255, 0.86);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--teal);
}

/* The call to action rides in the corner rather than under the frame: the
   frame IS the link, and a label outside it would suggest the picture is
   not. */
.map-cta {
  position: absolute;
  right: clamp(0.75rem, 1.5vw, 1.25rem);
  bottom: clamp(0.75rem, 1.5vw, 1.25rem);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.7rem 1.15rem;
  background: var(--white);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--teal);
  box-shadow: 0 2px 12px rgba(42, 26, 12, 0.14);
  /* GREG, 2026-08-26: "make it maybe green borders to point it clearly that
     they have to click in there." The border is the fix -- a white chip on a
     pale map reads as a caption, and a caption is not a thing you press.

     IT IS --teal AND NOT A NEW GREEN, deliberately, and it is worth saying
     why rather than quietly substituting. This site has no green token; the
     palette is still three candidates on the table (PALETTES.html) and adding
     a fourth colour to settle a button would prejudge that. Teal is the
     nearest thing the site owns, it already means "this opens something"
     here, and -- the reason it is actually right -- IT IS THE ROUTE'S OWN
     COLOUR. The line drawn across the map is this exact teal, so the button
     now looks like the thing it opens. If Greg wants a true green, this is
     one value and the beach link's to match. */
  border: 2px solid var(--teal);   /* 1.5 rounds to 1px at DPR 1, which is a hairline, not a button */
  transition: color 0.35s var(--ease-soft), background-color 0.35s var(--ease-soft);
}
.map-cta svg { transition: transform 0.4s var(--ease-out); }
/* Hover FILLS rather than dimming. The old rule went teal -> ink, which is a
   button getting quieter as you reach for it. */
.contact-map:hover .map-cta { background: var(--teal); color: var(--white); }
.contact-map:hover .map-cta svg { transform: translateX(4px); }

/* REQUIRED, NOT DECORATIVE. OpenStreetMap's licence and CARTO's terms both
   ask for this line. Do not remove it. */
.map-credit {
  margin: 0.9rem 0 0;
  max-width: 78ch;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--muted);
}
.map-credit a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
.map-credit a:hover { color: var(--ink); }

/* --- narrow ---------------------------------------------------------------
   The phone layout of this site is a SEPARATE design and is not being made
   yet (BUILD-BRIEF.md, desktop first from 2026-08-21). This is only enough
   that the page does not break while the desktop layout is judged. */
@media (max-width: 900px) {
  .contact-grid { grid-template-columns: 1fr; }

  /* THERE IS NO aspect-ratio OVERRIDE ON THE IMAGE HERE, AND THERE MUST NOT BE.
     It read `aspect-ratio: 4 / 3` while the frame above stayed 19/9, which is
     the exact fault the comment on .contact-map warns about, re-made one
     breakpoint later. Measured at 390px, frame 350 x 165.8:

       - the image laid out 350 x 262.5, so object-fit: cover scaled the
         1900px source to 554px wide and threw away 102px OFF EACH SIDE;
       - overflow: hidden then cut 97px off the bottom.

     The pins are a percentage of the FRAME, so both of them went on pointing
     at where those places were in the uncropped picture. Yumbo's ring sat
     ~52px right of the Yumbo, and the studio's real position was x 353 / y 205
     in a 350 x 166 box -- off the picture in both directions, which is exactly
     what it looked like. The frame and the image are one ratio at every width;
     the map is a photograph and it scales, it does not re-crop. */

  /* The CTA STAYS ABSOLUTE. `position: static` put it in flow beneath the
     image, at y 262.5 inside a box clipped at 165.8 -- so "Open directions"
     was not merely misplaced below 900px, it was invisible. Centred along the
     bottom instead, where there is no route under it at any width. */
  .map-cta {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    padding: 0.6rem 1rem;
    font-size: 11px;
  }
}

/* The furniture was sized for a 1136px-wide frame. On a 350px one the same
   pins and labels are 3.2x the map, so they cover the route they annotate.
   Sizes only -- every position is still the percentage in the markup, and each
   negative margin is half its own box so the pin stays centred on its point.
   Same 620px breakpoint the Yumbo photograph already uses, so the map has one
   story and not two. */
@media (max-width: 620px) {
  .map-pin { width: 30px; height: 30px; margin: -15px 0 0 -15px; }
  .map-pin-dot {
    width: 11px; height: 11px;
    box-shadow: 0 0 0 2px var(--white), 0 2px 5px rgba(42, 26, 12, 0.28);
  }
  .map-pin--start { width: 22px; height: 22px; margin: -11px 0 0 -11px; }
  .map-pin-ring {
    width: 9px; height: 9px;
    box-shadow: 0 0 0 2px var(--teal), 0 2px 5px rgba(42, 26, 12, 0.22);
  }
  .map-tag {
    font-size: 9px;
    letter-spacing: 0.1em;
    padding: 0.3rem 0.5rem;
    transform: translate(-50%, calc(-100% - 17px));
  }
  .map-tag--start { transform: translate(-50%, calc(-100% - 13px)); }

  /* THE WALKING TIME GOES, it does not shrink. Measured at 390 it still
     clipped the corner of the Yumbo tag, and the credit line directly under
     the frame already says "About 8 minutes on foot from Yumbo Centrum" -- so
     this is the one label on the map that costs nothing to drop. */
  .map-walk { display: none; }

  /* AND THE CTA MOVES TO THE BOTTOM-LEFT CORNER. Centred, it ran into the
     studio tag at 320 (x 190-210) once the frame got short enough to push it
     up into the picture. The route climbs from mid-left to bottom-right, so
     the bottom-left corner is the one part of this map with nothing drawn in
     it at any width. */
  .map-cta {
    left: clamp(0.6rem, 2.5vw, 1rem);
    transform: none;
    padding: 0.5rem 0.85rem;
    font-size: 10px;
    letter-spacing: 0.09em;
  }
}


/* ==========================================================================
   404 (404.php) -- built 2026-08-25
   ==========================================================================
   THE NUMBERS FALL AND LAND, and Greg's question was whether CSS can do it.
   It can, with one boundary worth stating because it decides where else this
   technique is usable:

     REAL   -- the fall, the acceleration, the squash on impact, two decaying
               bounces, and each glyph resting at its own angle. Keyframes on
               a gravity curve, nothing exotic.
     FAKED  -- the randomness. CSS has no random() and no collision, so every
               glyph carries a hand-set --delay, --drift and --rest. Three
               characters read as chance. Fifty objects landing in a heap
               would not, and that is a JavaScript job.

   WHERE IT SHOULD AND SHOULD NOT BE REUSED. This is the right page for it
   precisely because it is a dead end -- nobody is trying to read anything, so
   the page can have a personality the others cannot. The rest of the site's
   motion is scroll-linked line DRAWING: slow, continuous, and tied to what
   the reader is doing. Dropping a bouncing object into that vocabulary would
   read as a different site. Keep it here. */

/* THE HEADER IS FIXED AND main STARTS AT ZERO, so the first thing on any
   page has to make its own room or it renders UNDERNEATH the bar. Every other
   page gets that for free from .page-head; this page has no beige band, so it
   borrows the same calc rather than inventing a number. Measured before the
   fix: the eyebrow sat 4px above the header's bottom edge. */
.nf {
  padding-block: calc(var(--header-h) + clamp(2.5rem, 5vw, 4rem)) clamp(3rem, 6vw, 7rem);
  /* CENTRED — Greg, 2026-08-25. text-align carries the eyebrow, the heading
     and the prose; the three flex children (the stage, the link rows and the
     button row) each need telling separately, because text-align does not
     reach a flex container's items. */
  text-align: center;
}
.nf .eyebrow { margin: 0 0 clamp(1.5rem, 3vw, 2.5rem); }

/* The stage is what the glyphs fall INTO. overflow:hidden is what lets them
   start above the page without lengthening it -- without this, a -110vh
   translate adds a screen of scroll before the animation has even run. */
.nf-stage {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  gap: clamp(0.5rem, 1.5vw, 1.25rem);
  height: clamp(190px, 27vw, 350px);
  margin-bottom: clamp(2rem, 4vw, 3.5rem);
  overflow: hidden;
}

/* The ground. A hairline, the same one everything else on the site uses, so
   the numbers land on the page's own structure rather than on an invented
   surface. */
.nf-ground {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--hairline);
}

.nf-glyph {
  display: block;
  font-family: var(--serif);
  font-size: clamp(5rem, 17vw, 14rem);
  line-height: 0.78;
  letter-spacing: -0.04em;
  color: var(--ink);
  /* The transform origin sits at the FOOT of the glyph. Rotating about the
     centre makes a resting numeral sink through the ground on one corner and
     float above it on the other; about the foot it pivots where it is
     actually touching. */
  /* PIVOT ON THE CORNER THAT IS TOUCHING, not the middle of the foot.
     About 50% 100% a tilted numeral drives its lower corner through the
     ground line — measured at 8px below on the 9deg glyph. Each one pivots
     on the corner it leans onto instead, so the contact point stays put. */
  transform-origin: var(--origin, 50% 100%);
  /* linear here on purpose — EVERY segment sets its own curve below. A
     timing function on the shorthand governs each segment individually, so
     one curve for the whole thing cannot be both a fall and a bounce. */
  /* BOTH, not backwards. `backwards` holds the 0% state through the delay,
     which is what stops the glyph flashing at ground level before it drops —
     but it does NOT hold the 100% state afterwards, so every numeral snapped
     upright and lost its drift and resting angle the instant the animation
     ended. The tilt only existed while it ran. `both` does both ends. */
  animation: nf-drop 1.6s linear var(--delay, 0s) both;
  will-change: transform;
}
/* The middle glyph is the warm accent — one spot of colour, the same rule the
   wordmark follows, rather than three numerals all shouting. */
.nf-glyph:nth-child(2) { color: var(--saffron-ink); }

/* GRAVITY, THEN TWO BOUNCES THAT DIE.

   THE CURVE GOES IN THE KEYFRAME THAT STARTS THE SEGMENT, and getting that
   wrong is what made the first attempt look like a lift descending: the
   accelerating curve had been written into the 55% block, where it governs
   what happens AFTER the landing rather than the fall itself. The fall used
   the shorthand's curve, which eased OUT — so the numbers slowed down as
   they approached the ground, which is the opposite of falling.

   So: ease-IN on anything travelling downward (0%, 72%, 92%), ease-OUT on
   anything travelling up off the ground (58%, 84%), and a fast curve on the
   impact itself. */
@keyframes nf-drop {
  /* the fall — slow at the top, fast at the bottom */
  /* STARTS JUST ABOVE THE STAGE, not off the screen. It was -115vh, which
     is 1018px at this size — but the stage clips at 350px, so five sixths of
     the drop happened out of sight and the visible part was over in about a
     tenth of a second. The numerals appeared rather than fell. Expressed in
     the glyph's OWN height so it holds at every width. */
  0%   { transform: translate3d(0, -115%, 0) rotate(calc(var(--rest) * 3));
         animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
  /* contact */
  52%  { transform: translate3d(0, 0, 0) rotate(0deg);
         animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  /* squashed wide and short, and shunted sideways by its own momentum */
  58%  { transform: translate3d(var(--drift), 0, 0) scale(1.12, 0.8) rotate(calc(var(--rest) / 3));
         animation-timing-function: cubic-bezier(0.25, 0.6, 0.4, 1); }
  /* first bounce, stretched thin at the top of the arc */
  72%  { transform: translate3d(var(--drift), -16%, 0) scale(0.97, 1.05) rotate(calc(var(--rest) / 1.6));
         animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
  84%  { transform: translate3d(var(--drift), 0, 0) scale(1.05, 0.92) rotate(var(--rest));
         animation-timing-function: cubic-bezier(0.25, 0.6, 0.4, 1); }
  /* second bounce, a quarter of the first, and it is what sells the weight */
  92%  { transform: translate3d(var(--drift), -4%, 0) scale(0.99, 1.01) rotate(var(--rest));
         animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19); }
  100% { transform: translate3d(var(--drift), 0, 0) scale(1, 1) rotate(var(--rest)); }
}

/* --- the copy ------------------------------------------------------------ */
.nf-copy { max-width: 56ch; margin-inline: auto; }
.nf-copy .display-sm { margin: 0 0 1rem; }
.nf-copy p {
  margin: 0 0 1.1rem;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}

/* THE FOUR WAYS OUT — THE ONE THING ON THIS PAGE THAT IS NOT CENTRED.
   Greg, 2026-08-25: centred, they got lost. He is right, and the reason is
   worth keeping. Centred text has no common left edge, so four items of
   different lengths start in four different places and the eye has to find
   the beginning of each one. That is fine for two lines of prose and bad for
   a list somebody is scanning to choose from — and on this page the list IS
   the content, since everything else is an apology.

   So the BLOCK is centred and its CONTENTS are left-aligned: one shared left
   edge to run down, sitting in the middle of a centred page. The rest of the
   page stays as it was.

   The description follows an em dash on the same line, Greg's suggestion. It
   is set sentence-case rather than the small-caps it was: after a serif name
   and a dash, uppercase reads as a second heading competing with the first
   rather than as a gloss on it. */
.nf-links {
  list-style: none;
  max-width: 44ch;
  margin: 0 auto clamp(1.75rem, 3vw, 2.5rem);
  padding: 0;
  text-align: left;
  border-top: 1px solid var(--hairline);
}
.nf-links a {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--hairline);
  text-decoration: none;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.7vw, 1.4rem);
  color: var(--ink);
  transition: color 0.35s var(--ease-soft);
}
.nf-meta {
  font-family: var(--sans);
  font-size: 0.92rem;
  line-height: 1.4;
  color: var(--muted);
  transition: color 0.35s var(--ease-soft);
}
/* The dash lives here rather than in the markup: it is punctuation between
   two things the HTML already separates, so the page source stays a name and
   a description. */
.nf-meta::before { content: "\2014"; margin-right: 0.45rem; }
/* Pushed to the far right by the auto margin, which is what gives the rows a
   right edge to line up on as well as a left one. */
.nf-arrow {
  margin-left: auto;
  flex: 0 0 auto;
  font-family: var(--sans);
  font-size: 0.95rem;
  color: var(--muted);
  transform: translateX(0);
  transition: transform 0.4s var(--ease-out), color 0.35s var(--ease-soft);
}
.nf-links a:hover,
.nf-links a:focus-visible { color: var(--red); }
.nf-links a:hover .nf-meta,
.nf-links a:focus-visible .nf-meta { color: var(--body); }
.nf-links a:hover .nf-arrow,
.nf-links a:focus-visible .nf-arrow { transform: translateX(4px); color: var(--red); }

.nf-ask {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 2vw, 1.75rem);
  flex-wrap: wrap;
  margin: 0;
}
.nf-tel {
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 0.35s var(--ease-soft), border-color 0.35s var(--ease-soft);
}
.nf-tel:hover { color: var(--ink); border-bottom-color: var(--hairline); }

@media (max-width: 900px) {
  .nf-links a { flex-wrap: wrap; }
  .nf-meta { flex-basis: 100%; }
  .nf-meta::before { content: none; }
  .nf-arrow { display: none; }
}


/* ===========================================================================
   PRIVACY POLICY — privacy.php, built 2026-08-25
   ===========================================================================
   The only long-form reading page on the site. Everything else is a heading
   and a short paragraph; this is a document somebody scans for one answer.

   ONE COLUMN, 68ch, LEFT. Not centred. A centred measure is for a statement
   the reader is meant to dwell on — this is reference text, and the eye wants
   a fixed left edge to run back to on every line.

   The type is 16px, NOT --read-size. --read-size is a display size that
   reaches 1.6rem at width; at 68ch of continuous prose that is a wall. This
   is the one place on the site that reads like a page rather than a poster,
   and it is deliberate rather than an oversight.
   ======================================================================== */

.legal { max-width: 68ch; }

/* The summary. It exists because almost nobody reads a privacy policy, and on
   this site the honest summary is genuinely short — no cookies, no analytics,
   no third parties. Burying that under nine headings would be a small lie of
   emphasis. Saffron rule on the left, the same mark the testimonials use for
   a letter worth stopping on. */
.legal-short {
  border-left: 2px solid var(--saffron-ink);
  padding: 0.25rem 0 0.25rem 1.5rem;
  margin: 0 0 3.5rem;
}
.legal-short p { margin: 0 0 0.9rem; font-size: 17px; line-height: 1.6; color: var(--ink); }
.legal-short p:last-child { margin-bottom: 0; }
.legal-short strong { font-weight: 600; }

.legal h2 {
  font-family: var(--serif);
  font-size: clamp(1.35rem, 2.2vw, 1.7rem);
  line-height: 1.25;
  font-weight: 400;
  color: var(--ink);
  margin: 3rem 0 1rem;
}
.legal h2:first-of-type { margin-top: 0; }

.legal h3 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 2rem 0 0.75rem;
}

.legal p { margin: 0 0 1.1rem; font-size: 16px; line-height: 1.65; color: var(--body); }
.legal ul { margin: 0 0 1.1rem; padding-left: 1.1rem; }
.legal li { font-size: 16px; line-height: 1.65; color: var(--body); margin-bottom: 0.5rem; }

/* Inline links get an underline here and nowhere else on the site. In a
   legal document a link is information — an address you may need to write
   down — not decoration, and it has to survive being printed. */
.legal a {
  color: var(--teal);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}
.legal a:hover { color: var(--ink); }

/* The dates. Small, muted, and at the top rather than buried at the bottom —
   the first thing a person checking a policy wants to know is whether it is
   current. */
.legal-dates {
  font-size: 13px;
  letter-spacing: 0.03em;
  color: var(--muted);
  margin: 0 0 2.5rem;
}

/* Contact details set as a block rather than a paragraph, so the address can
   be copied cleanly and read as data. */
.legal-who {
  background: var(--surface);
  border-radius: 14px;
  padding: clamp(1.25rem, 2.5vw, 1.75rem);
  margin: 0 0 1.1rem;
}
.legal-who p { margin: 0 0 0.35rem; }
.legal-who p:last-child { margin-bottom: 0; }

@media (max-width: 700px) {
  .legal-short { padding-left: 1.1rem; }
}


/* ===========================================================================
   THE GALLERY WALL — gallery.php #the-work, built 2026-08-25
   ===========================================================================
   Greg's nine new photographs, and his brief: asymmetrical, unusual, a wow
   without lowering the elegance, easy to navigate, and NO lightbox.

   WHY THIS IS NOT A MASONRY, which is what he asked for by name. Two reasons,
   both from measuring his files rather than from taste:

   1. MASONRY FLOWS DOWN COLUMNS, NOT ACROSS ROWS. Item 2 lands under item 1
      instead of beside it, so reading order and visual order come apart. That
      is the opposite of "easy to navigate", and on a page where the pictures
      tell an order -- the room, the ritual, the work, outside -- it scrambles
      the story.
   2. IT WOULD NOT HAVE LOOKED ASYMMETRIC ANYWAY. Eight of the nine files are
      16:9 and the ninth is 3:4. Masonry gets its raggedness from unequal
      NATIVE heights; feed it eight identical ratios and it returns a regular
      grid with one odd tile.

   SO THE ASYMMETRY IS BUILT FROM THREE THINGS INSTEAD, and the third is the
   one that actually does the work:

     · WIDTH -- twelve columns, tiles spanning 3, 4, 5, 6, 7 or 8 of them.
     · CROP -- the same photograph cut 16:9, 4:3, 3:4 or 21:9. This is where
       the height variety comes from, and it is the reason eight files of one
       ratio can still make a ragged wall.
     · DRIFT -- a hand-set top offset per tile, so the row baseline breaks.

   NO REPEATING CYCLE IN ANY OF THE THREE. That is the lesson the testimonials
   produced on 2026-08-24: a four-step cycle reads as a staircase, and the eye
   solves a staircase in two items and then calls it mechanical. Greg's word
   for it was "too strong, too much". The offsets below are hand-set for the
   same reason his letter indents are.

   THE OFFSETS ARE POSITIVE ONLY. A negative margin-top would pull a tile up
   into a row whose height was computed without it, and pictures would overlap
   at some widths and not others. Pushing down can never collide.
   ======================================================================== */

.gw-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: clamp(0.75rem, 1.6vw, 1.75rem);
  align-items: start;          /* never stretch a tile to its row's height */
}

.gw { margin: 0; grid-column: span 6; }

/* Spans. Written on the item, not derived from nth-child -- the same rule the
   original .gal grid follows, and for the same reason: a rule keyed to
   position reassigns itself the moment a picture is added or dropped, and
   this list changes every time Greg shoots. */
.gw--3 { grid-column: span 3; }
.gw--4 { grid-column: span 4; }
.gw--5 { grid-column: span 5; }
.gw--6 { grid-column: span 6; }
.gw--7 { grid-column: span 7; }
.gw--8 { grid-column: span 8; }
/* THE FULL ROW. Added 2026-08-26 for the beach frame, and it is the first tile
   on this wall that takes all twelve. It is NOT .gw-bleed: that one is a 32/9
   band running edge to edge, and 32/9 on a 1492x900 source keeps 47% of the
   height -- it would cut the head off the man doing the massage. This stays
   inside the grid and inside its frame, and just spans the whole of it. */
.gw--12 { grid-column: span 12; }

/* CENTRED, for a wide tile that is ALONE in its row. Greg, 2026-08-26, on
   "Under the tree": the note tile that used to sit beside it moved down, and a
   lone gw--8 left in column 1 reads as a tile with a hole next to it rather
   than as a picture. 3 / span 8 puts the same two columns either side.
   Deliberately explicit and deliberately narrow in scope -- it is a placement,
   not a span, and it is only correct while nothing shares its row. */
.gw--mid8 { grid-column: 3 / span 8; }

/* A COLUMN INSIDE THE COLUMN. Greg, 2026-08-26: he wanted the "tantra,
   relaxation and deep strong work" note in "the empty space that opens between
   the gong bath and the beach".

   THAT SPACE IS NOT A GRID CELL, which is the whole reason this exists. The
   grid sets align-items: start, so a row is as tall as its tallest tile and a
   short one simply stops early. The bowl beside it is a 3/4 portrait running
   676px; the gong is 16/9 and stops at 430. The 246px underneath it belongs to
   no cell at all, so no amount of grid-column moves anything into it -- an item
   placed after the gong starts a new row BELOW the bowl, which is past the gap
   rather than in it.

   So the gong and the note become one 7-column item with the two stacked
   inside it. The stack is FLEX on purpose: the children still carry .gw, and
   .gw sets grid-column: span 6, which inside a one-column grid would generate
   five implicit columns. Flex ignores it. Same gap as the wall, so the seam is
   invisible. */
.gw-col {
  display: flex;
  flex-direction: column;
  gap: clamp(0.75rem, 1.6vw, 1.75rem);
}

/* Crops. object-fit: cover does the cutting, so a 16:9 original can be a tall
   tile without being squashed. */
/* .gw-media IS THE FRAME'S BOX and no longer clips. The frame hangs below
   the picture -- 25px on the classic, 17px on the modern -- and the old
   overflow: hidden on this element cut every one of them off. The clipping
   the hover zoom needs moved inward to .gw-clip. */
.gw-media { position: relative; background: var(--surface); }
.gw-clip { position: absolute; inset: 0; overflow: hidden; }
.gw-media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--photo-filter);
  transition: filter 0.7s var(--ease-out), transform 1.1s var(--ease-out);
}
.gw--wide  .gw-media { aspect-ratio: 16 / 9; }
.gw--box   .gw-media { aspect-ratio: 4 / 3; }
.gw--tall  .gw-media { aspect-ratio: 3 / 4; }
.gw--cine  .gw-media { aspect-ratio: 21 / 9; }

/* Drift. Hand-set, no cycle, positive only. In rem so they scale with the
   type rather than staying fixed while everything else grows. */
.gw--dA { margin-top: 3.5rem; }
.gw--dB { margin-top: 6rem; }
.gw--dC { margin-top: 2rem; }
.gw--dD { margin-top: 4.5rem; }
.gw--dE { margin-top: 1.5rem; }

/* THE WHOLE FRAME GROWS ON HOVER, Greg 2026-09-19 -- 3% over 1.5s. The frame
   is .gw-media::after, so it grows with the picture. The picture no longer
   zooms inside it: Greg tried both together and both in sequence the same day
   and dropped them -- the double movement did not look good.
   .gw only, NOT .gw-bleed: that one is full width, and 3% on it would push
   past the window. */
.gw .gw-media {
  transition: transform 1.5s var(--ease-out);
}
.gw:hover .gw-media { transform: scale(1.03); z-index: 2; }
/* COLOUR UNDER THE POINTER -- and it never worked until 2026-09-19. This set
   --photo-gray: 0 on the img, but --photo-filter is declared on :root, where
   its var() is resolved against the ROOT's --photo-gray and inherited already
   flattened to grayscale(1). Changing --photo-gray further down reaches
   nothing. Measured: the filter read grayscale(1) before and during hover.
   So the hover writes the filter itself: the same grade with the grey out. */
@media (hover: hover) {
  .gw:hover .gw-media img { filter: grayscale(0) contrast(1.06) brightness(1.04); }
}

.gw figcaption {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding-top: 0.9rem;
}

/* --- the full-bleed band ------------------------------------------------
   THE ONE SURPRISE ON THE PAGE, and it is deliberately the only one. Greg
   ruled out a lightbox, so the moment where a picture gets big has to happen
   in the flow of the page instead of on top of it.

   IT BREAKS OUT BY SITTING OUTSIDE .container, NOT BY USING 100vw. The
   100vw + translateX(-50%) trick is the usual way and it is wrong here: body
   carries overflow-x: hidden and html is visible, so body's overflow
   propagates to the viewport -- 100vw counts the scrollbar, and the page
   would gain a horizontal scroll of exactly the scrollbar's width. As a
   direct child of the section it is simply full width, with no arithmetic and
   nothing to get wrong at a particular window size.

   tantra-massage-heart-connection is the file for it: it is already a tight
   centred crop of two hands on a chest, so a 32:9 band takes it further in
   the direction it was shot rather than fighting it. It is also the one
   picture here that is intimate without being explicit, which is what a
   full-width moment on this site has to be. */
.gw-bleed { margin: clamp(3rem, 6vw, 6.5rem) 0; }
.gw-bleed .gw-media { aspect-ratio: 32 / 9; }
.gw-bleed .gw-media img { filter: var(--photo-filter); }
/* NO COLOUR ON HOVER HERE -- Greg, 2026-09-19: the parallax band stays black
   and white at all times. Its old hover rule never fired anyway (the same
   --photo-gray fault as .gw:hover); it was removed rather than fixed. */
.gw-bleed figcaption { padding-top: 0.9rem; }

/* --- narrow -------------------------------------------------------------
   NOT the phone design, which is a separate job. This does one thing: stop
   the twelve-column layout from turning nine photographs into nine slivers.
   Two up, then one up, and every drift offset comes off -- a hand-set 6rem
   gap is composition on a wide screen and dead space on a narrow one. */
@media (max-width: 900px) {
  .gw-grid { grid-template-columns: repeat(2, 1fr); }
  .gw, .gw--3, .gw--4, .gw--5, .gw--6, .gw--7, .gw--8 { grid-column: span 1; }
  /* the full-row tile keeps the full row -- span 2 of the 2 columns here,
     otherwise the one picture built to run the width becomes a half-width
     tile like every other one and the beat it exists for is gone */
  .gw--12 { grid-column: span 2; }
  /* both placements are twelve-column ideas and neither survives two columns */
  .gw--mid8 { grid-column: span 1; }
  .gw--dA, .gw--dB, .gw--dC, .gw--dD, .gw--dE { margin-top: 0; }
  .gw-bleed .gw-media { aspect-ratio: 21 / 9; }
}
@media (max-width: 560px) {
  .gw-grid { grid-template-columns: 1fr; }
  /* back to 1: span 2 against a single-column grid would generate an implicit
     second column and push the wall off screen */
  .gw--12 { grid-column: span 1; }
  .gw-bleed .gw-media { aspect-ratio: 16 / 9; }
}

/* --- where the crop lands ------------------------------------------------
   FOUND BY RENDERING THE WALL, NOT BY READING IT, and it is the reason the
   crop classes above needed a companion.

   object-fit: cover crops from the CENTRE by default. On a standing figure
   that centre is the hips. "The opening" is a 16:9 frame of a man unwrapping
   a sarong -- upper body, hands and cloth -- and a centred 3:4 cut of it threw
   away the hands and the face and kept only the part that made the picture
   read as something this site is explicitly not selling. The source frame was
   fine. The CROP was the problem, which is a thing only a render shows.

   So: a shallower cut, biased upward, keeping hands and chest.
   content-guardrails.md -- therapeutic, never sexual or erotic. */
.gw--top .gw-media img { object-position: center 22%; }


/* ===========================================================================
   GREG'S FRAMES ON THE WALL — 2026-08-25
   ===========================================================================
   His own assets from frames/, already carrying the hero and the About
   pair on the home page. Half the wall gets each, so the two can be judged
   against each other on one screen.

   WHAT THE TWO ACTUALLY ARE, measured off the alpha rather than assumed:

     CLASSIC  shadow-only-landscape.png (449x271) -- opaque only in the bottom
              25 rows. A broad, soft, bottom-only shadow. No outline at all.
              The portrait cut adds 6px down the right edge.

     MODERN   shadow-only-modern-landscape.png (293x179) -- a 1px hairline on
              the top, 2px down each side, and a SHORTER 17px shadow below.
              So "modern" is not a different shadow, it is a hairline plus a
              tighter one. Peak alpha 150, against the classic's soft spread.

   border-image, not a background, for the reason already written at
   .hero-shot: these tiles are percentages of a fluid column, and border-image
   holds each edge's true thickness while stretching only the soft middle.
   Stretching a soft shadow is invisible. Stretching a 1px hairline would not
   be -- which is exactly why the modern one needs slices on all four sides
   and the classic only needs one.

   ::after with negative insets, so the frame costs the layout nothing and
   cannot change the aspect-ratio the crop depends on. border-style must be
   set or border-image is never painted.

   SQUARE CORNERS, AND THAT IS NOT AN OVERSIGHT. Greg asked for the modern one
   as "rounded". These PNGs cannot be: the hairline is painted into the file
   with square corners, so a border-radius makes the drawn corner and the
   clipped corner disagree at all four -- which is the exact fault that took
   the radius off the home page's frames on 2026-08-22. Rounded is available,
   but only by dropping the PNG and drawing the hairline and the shadow in
   CSS, which is a different look and a separate decision.
   ======================================================================== */

.fr-classic .gw-media::after,
.fr-classic .gal-media::after,
.fr-modern  .gw-media::after,
.fr-modern  .gal-media::after {
  content: '';
  position: absolute;
  border-style: solid;
  border-color: transparent;
  pointer-events: none;
}

/* CLASSIC — the broad bottom shadow. Same slices as the hero. */
.fr-classic .gw-media::after,
.fr-classic .gal-media::after {
  top: 0; left: 0; right: 0; bottom: -25px;
  border-width: 0 0 25px 0;
  border-image: url(../frames/shadow-only-landscape.png) 0 0 25 0 / 0 0 25px 0 stretch;
}
/* the one tall tile — the portrait cut carries 6px down the right edge too */
.fr-portrait .gw-media::after,
.fr-portrait .gal-media::after {
  right: -6px;
  border-width: 0 6px 25px 0;
  border-image: url(../frames/shadow-only-portrait.png) 0 6 25 0 / 0 6px 25px 0 stretch;
}

/* MODERN — hairline all round, shorter shadow. The 1/2/17/2 slice is measured
   off the file: row 0 alpha 38, cols 0-1 alpha 20 then 91, and the shadow
   running 17 rows up from the bottom. */
.fr-modern .gw-media::after,
.fr-modern .gal-media::after {
  top: 0; left: 0; right: 0; bottom: -17px;
  border-width: 1px 2px 17px 2px;
  border-image: url(../frames/shadow-only-modern-landscape.png) 1 2 17 2 / 1px 2px 17px 2px stretch;
}

/* The drift offsets already give each row room, but a framed tile now hangs
   17-25px lower than an unframed one. Nothing below it moves, because the
   frame is on ::after with negative insets and takes no layout height -- so
   the gap under a framed tile reads slightly tighter. That is the frame doing
   its job, and it is why the caption padding is not reduced to match. */

/* ===========================================================================
   THE WALL REVEALS, AND THE BAND'S PARALLAX — 2026-08-25
   ===========================================================================
   FOUR DIRECTIONS, NO CYCLE. Greg asked for left, right, up and down, arriving
   as you scroll. Three of the four already existed; .reveal--fall is the one
   that did not.

   AND THE REASON THE WALL LOOKED STILL BEFORE THIS, which is worth knowing
   because it is not a missing feature but a misapplied class: every gallery
   figure carried .reveal--rise, and .reveal--rise sets `transform: none`. It
   is the STATEMENT's animation -- a blockquote whose saffron border scales up
   and the words follow -- and its own comment says it belongs to the statement
   and to nothing else. On a figure with no such border it degrades to a plain
   fade. The pictures were not arriving from anywhere; they were appearing.

   THE THROW IS BIGGER HERE, and for the reason already recorded for the four
   service blocks: 28px is right for a caption and invisible on a tile 737px
   wide. These are the biggest elements on the page and they get 52 / 64.
   ======================================================================== */

.reveal--fall { transform: translateY(-28px); }
.reveal--fall.is-in { transform: none; }

.gw.reveal        { transform: translateY(52px); }
.gw.reveal--fall  { transform: translateY(-52px); }
.gw.reveal--left  { transform: translateX(-64px); }
.gw.reveal--right { transform: translateX(64px); }
.gw.reveal.is-in  { transform: none; }

/* --- the full-bleed band's parallax -------------------------------------
   IT IS BIG ENOUGH, and the number is exact rather than comfortable-looking:
   the band is 32:9 and the file is 16:9, so an image scaled to cover the
   width is ALWAYS EXACTLY TWICE the band's height, at every viewport. The
   whole band height is spare. 405px of slack at 1440, 540px at 1920.

   So the travel is a taste decision, not a headroom one. 22% of the band
   height -- 89px at 1440 -- uses barely a fifth of what is there, which means
   no edge can ever be exposed at any width or any scroll position.

   The image is 200% tall at top:-50%, so it sits centred with half a band
   height hidden above and below. Travel stays inside +/-11%.

   translate3d, not top: a transform does not touch layout and is composited,
   which is what keeps this smooth against Lenis's interpolated scroll. The
   driver is bleedFrame() in main.js, on the permanent frame loop rather than
   the scroll handler -- Lenis keeps moving after scrollY has stopped, and a
   scroll-event driver would freeze the image while the page was still gliding. */
.gw-bleed .gw-media img {
  position: absolute;
  left: 0;
  width: 100%;
  height: 200%;
  top: -50%;
  object-fit: cover;
  transform: translate3d(0, var(--bleed-y, 0px), 0);
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .gw-bleed .gw-media img { transform: none; }
}

/* The lower groups arrive the same way the wall does. Their tiles are 405px
   rather than 737px, so the throw is a little shorter -- 40 / 48 against the
   wall's 52 / 64. Same principle as everywhere else on this page: the movement
   has to be a real fraction of the thing moving or it reads as a jitter. */
.gal.reveal        { transform: translateY(40px); }
.gal.reveal--fall  { transform: translateY(-40px); }
.gal.reveal--left  { transform: translateX(-48px); }
.gal.reveal--right { transform: translateX(48px); }
.gal.reveal.is-in  { transform: none; }

/* ===========================================================================
   THE NOTE TILE — a block of words WHERE A PICTURE WOULD GO, 2026-08-25
   ===========================================================================
   Greg's problem, and it is a real one rather than a styling preference: he
   wants the photographs MIXED -- tantra beside the room beside the garden --
   because the mixing is what makes the wall read as composed rather than
   catalogued. But a section heading over a mixed set is a promise the content
   does not keep. "The work / Long sessions, warm oil, and instruments" tells
   the reader the next block is about that, and then the next block is a
   fountain. The heading was organising something that is deliberately not
   organised.

   So the accent stops being a HEADING and becomes a TILE. It takes a cell in
   the same twelve-column grid, carries the same drift offset as its
   neighbours, and sits IN the mix instead of above it. It labels nothing.
   Greg's phrase survives -- it just stops making a claim about what follows.

   IT IS ALSO WHERE THE SEARCH TERMS LIVE. A note can say "tantra massage,
   relaxation massage and XL Man massage, in Playa del Ingles, Maspalomas"
   without a caption having to pretend a photograph is about that. Greg's own
   suggestion, and it is the honest place for it: prose a reader will actually
   read, rather than words hidden in an alt attribute.

   No background and no border box. It is air with words in it -- a panel
   would turn it back into a container, which is the thing being removed.
   ======================================================================== */

.gw-note {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0.25rem 0 1.5rem;
}
/* The saffron mark, the page's one warm accent, at the weight the eyebrows
   use. It is what tells the eye this cell is deliberate and not a gap. */
.gw-note::before {
  content: '';
  display: block;
  width: 44px;
  height: 2px;
  background: var(--saffron-ink);
  margin-bottom: 1.35rem;
}
.gw-note p {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.7vw, 1.55rem);
  line-height: 1.28;
  color: var(--ink);
  max-width: 24ch;
}
.gw-note .gw-note-sub {
  margin-top: 0.9rem;
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 34ch;
}

@media (max-width: 900px) {
  .gw-note { padding-bottom: 0.5rem; }
  .gw-note p, .gw-note .gw-note-sub { max-width: none; }
}

/* --- the beach arrival, on the contact page ------------------------------
   It shares .map-credit's measure and rhythm but NOT its voice. The credit
   above it is attribution and cookie housekeeping -- small, grey, the kind of
   text a reader's eye is right to skip. This paragraph is an invitation, and
   at the same size and colour it would be skipped with the licence text.

   So: the body colour rather than the muted grey, and a hairline above it to
   break the two apart. Still smaller than body copy, because it is a note
   under a map rather than a section of the page. */
.map-credit--beach {
  margin-top: 1.1rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline);
  font-size: 0.92rem;
  color: var(--body);
}
.map-credit--beach strong { color: var(--ink); font-weight: 600; }

/* The beach directions link. Inline at the end of its paragraph rather than a
   second big CTA on a second map -- see the comment beside it in contact.php
   for why there is no second map. Teal, like the map's own CTA, because on
   this site teal means "this opens something". */
.map-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  /* BASELINE ALIGNMENT WAS COSTING A LINE'S WORTH OF HEIGHT. The link is an
     inline-flex box with 0.85rem of top padding on it (the 44x44 tap target),
     so its baseline sits near its bottom -- and aligning that baseline to the
     sentence's hoists the whole box above the line, inflating the paragraph
     from 22.8px to 41.4px for a sentence that fits on one line. Centring it
     on the line is what Greg actually asked for when he asked for the link on
     the same line: it now costs nothing. */
  vertical-align: middle;
  /* 0.6rem and NOT a space in the markup. Greg asked for the link on the same
     line as the sentence, so the newline that used to separate them in the
     HTML is gone -- and with it the collapsing whitespace that was doing the
     spacing. The gap is a margin now, which is the only kind that survives
     being written tight against the full stop. */
  margin-left: 0.6rem;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--teal);
  border-bottom: 1px solid transparent;
  transition: color 0.35s var(--ease-soft), border-color 0.35s var(--ease-soft);
}
/* .map-credit a HAS TO BE OUT-SPECIFIED, and this is a bug fix rather than a
   tidy-up. That rule -- `.map-credit a { color: var(--muted); text-decoration:
   underline; }` -- is (0,1,1); the .map-link block above is (0,1,0), so the
   licence links' grey underline has been winning here since the day the beach
   line was written. The comment above claiming "teal, like the map's own CTA"
   described an intention the cascade was quietly overruling: it rendered as
   small grey underlined text, indistinguishable from the OpenStreetMap credit
   directly above it.

   It matters more now than it did. Greg asked for the map's CTA to be edged
   so it is obviously pressable; leaving the OTHER way into Maps looking like
   attribution would have made the two read as different kinds of thing. */
.map-credit a.map-link {
  color: var(--teal);
  /* UNDERLINE, NOT border-bottom. The border draws on the PADDING box, and
     this link carries 0.85rem of it top and bottom to make the 44px tap
     target -- so the rule landed 14px under the words, reading as a stray
     line rather than as part of the link. text-decoration follows the text
     itself and is unaffected by the padding. */
  text-decoration: underline;
  text-decoration-color: rgba(var(--teal-rgb), 0.4);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  border-bottom: 0;
}
.map-credit a.map-link:hover {
  color: var(--ink);
  text-decoration-color: var(--ink);
}
/* 44x44 minimum, padded out and pulled back so nothing moves on screen --
   the same trick every other tap target on this site uses. */
.map-link { padding: 0.85rem 0; margin-block: -0.85rem; }

/* --- the Yumbo sign on the map -------------------------------------------
   Greg, 2026-08-25. A visitor recognises the Yumbo BY SIGHT well before a
   street layout means anything to him, so the photograph does the orienting
   the text label was asking him to do from memory.

   IT HANGS OFF THE START PIN'S OWN --x/--y, not its own numbers, so the two
   cannot drift apart if the map is ever re-shot and the percentages change.
   The offset is in the margin, which is the same trick .map-pin uses to
   centre itself on its coordinate.

   Small on purpose: it is an aid to recognition, not a second picture
   competing with the map. 118px wide at its largest, and it shrinks with the
   viewport so it never eats the route on a narrow window. */
.map-shot {
  position: absolute;
  top: var(--y);
  left: var(--x);
  width: clamp(74px, 8.2vw, 118px);
  margin: 26px 0 0 -26px;
  border-radius: 3px;
  overflow: hidden;
  background: var(--white);
  padding: 4px;
  /* the same lift the map CTA uses, so the two read as one family */
  box-shadow: 0 2px 12px rgba(42, 26, 12, 0.18);
  transform: rotate(-1.6deg);   /* a pinned photograph, not a UI chip */
  transition: transform 0.5s var(--ease-out);
}
.contact-map:hover .map-shot { transform: rotate(0deg) scale(1.04); }
.map-shot img { display: block; width: 100%; height: auto; border-radius: 1px; }

@media (max-width: 620px) {
  /* the map is small enough here that the sign starts covering the route */
  .map-shot { display: none; }
}

/* ==========================================================================
   TANTRIC COACHING — coaching.php, built 2026-08-26
   ==========================================================================
   The page is built almost entirely out of components that already existed:
   .page-head, .tr-note, .tr / .tr-grid / .tr-fig / .tr-body, .tr-rates,
   .tr-seam, .faq, .tr-close, .quote--lead. What follows is only the things
   the stylesheet genuinely had no pattern for.

   WHY .quote--lead IS BEING USED RATHER THAN EXTENDED. It was written for the
   testimonials page and then never used there — dead CSS carrying exactly the
   right shape for a pullquote. Three of them appear on this page and each one
   gets a different treatment on purpose: three identical framings in one
   scroll read as a tic rather than as a voice. */

/* Brown is the coaching family on the home page, the FAQ and the Services
   index, and this is the first time it is asked for as an eyebrow. 8.05:1 on
   white, so unlike saffron it needs no darkened variant. */
.eyebrow--brown { color: var(--brown); }

/* The two halves set --fam once and the topic list's heading and mark read it.
   Deliberately NOT .tri-fam--brown: that class belongs to the Services index
   and also carries --wave offsets measured off that page's path. Borrowing it
   here would make a later change to the index silently reach this page. */
/* --fam-wash rides with --fam so .tp-out takes its ground from the half it
   sits in. Brown has no wash token in :root and deliberately does not get one
   here -- a literal keeps the addition inside this page's block. 0.10 rather
   than red's 0.12: --brown is the darker and more saturated of the two, so the
   same alpha reads heavier. Body copy on it measures 7.5:1, brown 6.9:1. */
.fam-brown { --fam: var(--brown); --fam-wash: rgba(125, 64, 9, 0.10); --fam-hair: rgba(125, 64, 9, 0.28); }
.fam-red   { --fam: var(--red);   --fam-wash: var(--red-wash);           --fam-hair: var(--red-hair); }

/* --- the three pullquotes ------------------------------------------------ */
/* A <blockquote> brings its own 1em/40px margin and .quote--lead only ever
   styled a bare <p>. Two of the three quotes run to three paragraphs. */
.quote--lead blockquote { margin: 0; }
.quote--lead p + p { margin-top: clamp(0.9rem, 1.7vw, 1.4rem); }
/* Standalone on this page: the section padding is the spacing, so the
   component's own trailing margin would double it.
   .quote--thesis came out of the list on 2026-08-27 with the #love section it
   belonged to -- the thesis is the band's quote now and .band styles it. */
/* .quote--water and .quote--degree left the site on 2026-08-27 with the two
   sections they styled: the clean-water quote is banked for marketing in
   references/quotes-greg.md, and the one-degree quote is a band on the
   Services page now, where .band styles it. Only the thesis rule is left, and
   .quote--lead keeps its own. */


/* --- the four questions -------------------------------------------------- */
/* THE ONE GENUINELY NEW COMPONENT. Nothing in the stylesheet does a list of
   large UNANSWERED questions: .faq-q is an accordion control and these open
   nothing, .tr-rates is a term-and-value row, and a plain <ul> reads as topics
   rather than as somebody asking you something.

   NUMBERED, because the numeral is what stops them reading as four similar
   things and makes them read as a sequence a man is taken through.

   PLACED EXPLICITLY IN A GRID rather than with an absolutely positioned
   ::before, and baseline-aligned. The numeral has to sit on the first line of
   a question that wraps to two at narrow widths, which an absolute `top` in em
   only gets right at one font size. */
.cq-head .eyebrow { margin: 0 0 0.75rem; }
.cq-head .display-sm { margin: 0 0 0.9rem; }
.cq-intro {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--muted);
  max-width: 52ch;
}
.cq-list {
  list-style: none;
  margin: clamp(1.75rem, 3.5vw, 2.75rem) 0;
  padding: 0;
  counter-reset: cq;
}
.cq-list li {
  counter-increment: cq;
  display: grid;
  grid-template-columns: clamp(1.8rem, 3.2vw, 2.6rem) minmax(0, 1fr);
  align-items: baseline;
  gap: 0 0.7rem;
  padding: clamp(0.95rem, 2vw, 1.35rem) 0;
  border-top: 1px solid var(--hairline);
  font-family: var(--serif);
  font-size: clamp(1.2rem, 2.2vw, 1.65rem);
  line-height: 1.28;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.cq-list li:last-child { border-bottom: 1px solid var(--hairline); }
.cq-list li::before {
  content: counter(cq);
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.07em;
  color: var(--brown);
}
.cq-after p {
  margin: 0 0 1.15rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--body);
}
.cq-after p:last-child { margin-bottom: 0; }

/* --- the topic lists ----------------------------------------------------- */
/* THESE ARE THE RECOGNITION MECHANISM AND THEY ARE NEVER FOLDED. A man scans
   until he finds his own sentence — "sex is easy to find and closeness is
   not", "living with HIV" — and that is what tells him the page is about him.
   A fold headed "what men bring me" does not do that job.

   They are also where the explicit search words live, on Greg's instruction
   2026-08-26: "they need to be somewhere there so that crawlers can pick it
   up." Which is the other half of the clean-water rule — the metaphor keeps
   its distance because these words are here instead of beside it.

   The mark is a short rule in the family colour, not a bullet: the site has no
   round bullets anywhere and the leader-dash is already its idiom, on
   .trr-rule and on the index rows. */
.tp { margin: clamp(1.5rem, 3vw, 2.25rem) 0 0; }
.tp-h {
  margin: 0 0 0.85rem;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--fam, var(--muted));
}
.tp-list { list-style: none; margin: 0; padding: 0; }
.tp-list li {
  position: relative;
  max-width: 60ch;
  padding: 0.4rem 0 0.4rem 1.2rem;
  font-size: 1rem;
  line-height: 1.55;
  color: var(--body);
}
.tp-list li::before {
  content: "";
  position: absolute;
  left: 0;
  /* on the first line, not the top of the box */
  top: 0.95em;
  width: 8px;
  height: 1px;
  background: var(--fam, var(--muted));
  opacity: 0.8;
}
/* Inside an open panel. --fam is inherited from the half, so a list in a brown
   panel keeps brown marks with no second class. Sized to .faq-a p. */
.tp-list--inset { margin: 0 0 1.6rem; }
.tp-list--inset li { font-size: 15px; padding-top: 0.28rem; padding-bottom: 0.28rem; }

/* The line between the open list and the folds. .faq brings its own generous
   top margin, which after a single line of type is one gap too many. */
.tp-more {
  margin: clamp(1.4rem, 2.8vw, 2rem) 0 0;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.tp-more + .faq { margin-top: clamp(0.9rem, 1.8vw, 1.35rem); }

/* --- the outcome block --------------------------------------------------- */
/* THE ONE BLOCK IN THE HALF THAT IS AN ANSWER RATHER THAN A PROBLEM, and it
   is styled so it cannot be read as a fifth fold. Greg, 2026-08-27: as the
   last item in the accordion it was getting lost among the problems.

   Three signals the folds do not have: a ground, a rule down the left, and a
   SERIF heading where every fold has a sans-serif button. The idiom is the
   FAQ notice's, on purpose -- that is already the site's shape for a block
   with no control that must not be scrolled past. Colour comes from --fam, so
   the tantric half would get the red version with no second rule if it ever
   grows an outcome block of its own. */
.tp-out {
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 0;
  padding: clamp(1.2rem, 2.4vw, 1.9rem) clamp(1.2rem, 2.6vw, 2rem);
  border-left: 3px solid var(--fam, var(--brown));
  background: var(--fam-wash, rgba(125, 64, 9, 0.10));
}
.tp-out-eyebrow {
  margin: 0 0 0.45rem;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fam, var(--brown));
}
.tp-out-h {
  margin: 0 0 0.7rem;
  font-family: var(--serif);
  font-size: clamp(1.3rem, 2.4vw, 1.75rem);
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.tp-out-lead {
  margin: 0 0 0.9rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
.tp-out .tp-list { margin: 0 0 1.1rem; }
/* the marks are on a tinted ground now, not on white */
.tp-out .tp-list li::before { opacity: 1; }
.tp-out-note {
  margin: 0;
  max-width: 60ch;
  font-size: 15px;
  color: var(--muted);
}
/* The step after. Divided from the outcome by the family hairline rather than
   by a gap: it is a different subject, and the same device as .faq-notice-sig
   for the same reason. Body colour, not muted -- it is not a footnote. */
.tp-out-next {
  margin: 1.4rem 0 0;
  padding-top: 1.1rem;
  border-top: 1px solid var(--fam-hair, rgba(125, 64, 9, 0.28));
}
.tp-out-next p:not(.tp-out-eyebrow) {
  margin: 0 0 0.75rem;
  max-width: 60ch;
  font-size: 15px;
  line-height: 1.6;
  color: var(--body);
}
.tp-out-next p:last-child { margin-bottom: 0; }

/* --- the coaching page's band -------------------------------------------- */
/* The home page's band component, unchanged. .band--thesis carries NO RULES
   TODAY and is not a leftover: it is the only hook that separates this band
   from index.php's, both of which are #band, and the one override likely to
   be wanted is `#band.band--thesis .band-media img` -- an id selector at line
   ~3785 keeps every band's photograph in colour, and this page's is the one
   placeholder on a page of grey ones.

   The paragraph rules that were here on 2026-08-27 came out with the quote's
   other two paragraphs the same evening. If they go back in, both bands hold
   bare text inside the blockquote today, so the rule stays scoped here rather
   than moving to .band -- on the other two it would be inert now and would
   start applying silently the first time either grows a paragraph:
     .band--thesis blockquote p { margin: 0; }
     .band--thesis blockquote p + p { margin-top: clamp(0.9rem, 2vw, 1.5rem); } */

/* THE TWO LINES TAKE THE PALETTE HERE. Greg, 2026-08-27: the pair the home
   page's hero runs -- teal on one, saffron on the other -- rather than the
   band's white. Same order as .hero-paths and .spine-lines: the bright line is
   the blue one, the ghost is the warm one.

   SCOPED TO THIS BAND. index.php's band is #band as well and it is finished;
   editing .band-lines at source would have coloured the home page as a side
   effect, which is the same trap the seam band's scrim avoided on treatments.

   THE OPACITIES ARE NOT INHERITED AND BOTH ARE MEASURED against the photograph
   under the line, at the 0.55 scrim, across the right-hand strip the svg
   actually occupies. Colour costs contrast, so both are opened up to pay for
   it:
     bright  white 0.85 -> 8.20:1   is now teal-lift 1.00 -> 4.98:1
     ghost   white 0.32 -> 2.54:1   is now saffron   0.55 -> 2.66:1
   The ghost lands within 0.12 of the weight it had, which is the point: it is
   an echo of the first line and stops being one if it draws level. */
/* THE ONE-DEGREE BAND on the Services page. Same short geometry as the
   coaching page's compass band and the same signed italic setting -- what
   differs is the ground and the fact that it holds two paragraphs.

   0.62, AND IT IS THE ONE PLACE ON THIS SITE THAT SITS BELOW 4.5:1 ON PURPOSE.
   Measured on the filtered file, across the crop the band actually shows and
   the vertical third the two paragraphs land in: 3.99:1 worst case, 4.03:1 at
   the p99, 6.25:1 at the median. 0.68 gives 4.74:1 and was tried first --
   compared on screen, and it puts the mist out. This photograph is bright
   exactly where the type sits and dark where it does not, so every point of
   contrast is paid for in the only thing the picture has: at 0.68 the fork in
   the path stops reading and the band is a grey rectangle.

   WHAT MAKES IT SAFE ANYWAY: the type is 32px, which is large text, where the
   threshold is 3:1 rather than 4.5:1. So it clears its actual requirement by a
   third. If Greg wants the site's usual margin back it is one number.

   The paragraph rules are the ones written for the coaching band on 2026-08-27
   and commented out the same evening when that quote came down to one
   paragraph. They are live again here, and still scoped rather than moved to
   .band -- the seam band and the home page's reviews hold bare text, where the
   rule would be inert now and would start applying the day either grows a
   paragraph. */
.band--degree blockquote p { margin: 0; }
.band--degree blockquote p + p { margin-top: clamp(0.9rem, 2vw, 1.5rem); }
.band--degree .band-scrim { background: rgba(46, 50, 49, 0.62); }
/* it sits between two articles inside one .tr-group, so it brings its own air */
.band--degree { margin-block: clamp(2rem, 4vw, 4rem); }

/* --- the short band ------------------------------------------------------ */
/* THE SERVICES PAGE'S SEAM BAND GEOMETRY, LIFTED BY SELECTOR RATHER THAN
   COPIED. .band--seam already solves "full width, not tall": a clamped height
   instead of the tall band's 120vh, and an image that still travels 35% of
   itself so the parallax survives the shorter box. Adding .band--short to
   those selector lists is what makes the two bands provably the same
   component; a second set of declarations is how two of them drift.

   WHAT IT DOES NOT TAKE is the seam's 0.25 scrim and its object-position --
   both were measured on long-massage.jpg, which Greg toned himself. This band
   sets its own below. */
.band--short {
  min-height: clamp(340px, 37vw, 545px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.band--short .band-media { overflow: hidden; }
.band--short .band-media img {
  position: absolute;
  top: -17.5%;
  left: 0;
  height: 135%;
  will-change: transform;
}

/* THE GAP ABOVE, MATCHED TO THE ONE BELOW -- Greg, 2026-08-27. There was no
   gap at all above the band, and the cause is a rule written for another page:
   `#what-this-is { padding-bottom: 0 }` at line ~2828 belongs to index.php,
   where that section gives up its bottom padding to a hairline, and it reaches
   this page because it is keyed to an id rather than to a page.

   Rather than fight that rule, the band carries its own gap, and it is THE
   SAME EXPRESSION the next section's padding-top uses -- `.section` is
   clamp(3rem, 3vw, 6.5rem), 48px at 1440 -- so the two stay equal at every
   width instead of matching at the one width they were measured on. */
.band--course { margin-top: clamp(3rem, 3vw, 6.5rem); }

/* THE COMPASS BAND. Scrim measured on heart-compass.jpg: at the base 0.55 the
   sunrise sky puts white type at 3.71:1, which is legible at 32px and thinner
   than anything else on this site. 0.62 takes it to 4.44:1 worst case and
   4.75:1 at the p99, with the median at 9.46:1. Scoped to this band, never to
   .band-scrim, which is shared with the finished home page.

   Italic and the drawn signature come from .band--thesis's rules below, which
   this class is added to -- one treatment, two bands, one place to change it. */
.band--course .band-scrim { background: rgba(46, 50, 49, 0.62); }
/* IN COLOUR, LIKE EVERY OTHER BAND ON THE SITE. This is not an exemption from
   the placeholder grade so much as a correction: `#band .band-media img
   { filter: none }` keeps the home and Services bands in colour, and an id
   selector cannot reach this one because two bands cannot share an id. Left
   alone, coaching.php would have shown a grey band above a colour band on the
   same scroll. The measured scrim above assumes the colour file. */
.band--course .band-media img { filter: none; }

/* ITALIC AND SIGNED, the treatment Greg asked for on 2026-08-27 after seeing
   it on the Services band: "italic style, and underneath has a dash and Greg."

   THE MEASURE IS DELIBERATELY NOT COPIED WITH IT. .band--seam caps its
   blockquote at 52ch because that quote is two sentences and 190 characters;
   this one is 130 and already sets in two lines across the container. Capping
   it would push it to three and make the block taller than the thing it is
   quoting -- the italic and the signature are the treatment, the width is that
   band's own arithmetic. */
.band--thesis blockquote,
.band--course blockquote,
.band--degree blockquote,
.band--effect blockquote { font-style: italic; }

.band--thesis .band-lines path { stroke: var(--teal-lift); opacity: 1; }
.band--thesis .band-lines path.is-ghost { stroke: var(--saffron); opacity: 0.55; }

/* The rate inside the opening note, where there is no .tr-body to space it. */
.tr-rates--note { margin: clamp(1rem, 2.2vw, 1.5rem) 0 0; }

/* --- the two plain prose sections ---------------------------------------- */
/* THE CHILD COMBINATOR AND THE TWO :not() ARE LOAD-BEARING. .eyebrow is 0,1,0
   and would lose its size to a bare `.prose-block p`; and #how-it-works nests
   .step-list inside, whose paragraphs are grid items placed in column two and
   must keep their own rule. Both are excluded explicitly rather than patched
   afterwards with specificity. */
.prose-block .eyebrow { margin: 0 0 0.75rem; }
.prose-block .display-sm { margin: 0 0 1.25rem; }
.prose-block > p:not(.eyebrow):not(.hw-where) {
  margin: 0 0 1.15rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--body);
}
.prose-block > p:not(.eyebrow):not(.hw-where):last-child { margin-bottom: 0; }

/* --- how it works, as four steps ----------------------------------------- */
/* Same explicit placement as .cq-list, and for the reason recorded on
   .svc-head: taking one child out of the auto flow moves the cursor for every
   child after it, so the heading and the paragraph are put in column two by
   hand rather than left to land there. */
.step-list {
  list-style: none;
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 clamp(1.6rem, 3vw, 2.35rem);
  padding: 0;
  counter-reset: st;
}
.step-list li {
  counter-increment: st;
  display: grid;
  grid-template-columns: clamp(1.8rem, 3.2vw, 2.6rem) minmax(0, 1fr);
  gap: 0 0.7rem;
  padding: clamp(0.95rem, 2vw, 1.3rem) 0;
  border-top: 1px solid var(--hairline);
}
.step-list li:last-child { border-bottom: 1px solid var(--hairline); }
.step-list li::before {
  content: counter(st);
  grid-column: 1;
  grid-row: 1;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.07em;
  color: var(--brown);
}
.step-list h3 {
  grid-column: 2;
  grid-row: 1;
  margin: 0 0 0.35rem;
  font-family: var(--sans);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.step-list p {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  max-width: 58ch;
  font-size: 1rem;
  line-height: 1.6;
  color: var(--body);
}
/* THE CLOSING BLOCK ON THE COACHING PAGE. The .tp-out idiom -- family wash,
   3px family rule, no control -- reused rather than re-declared in spirit, and
   italic on Greg's instruction so the last paragraphs of a long page do not
   read as more of the same body copy.

   THE PARAGRAPH RULES ARE NOT INHERITED AND THAT IS THE TRAP HERE. The prose
   setting is `.prose-block > p`, a CHILD combinator, so wrapping these two in a
   div silently drops their size, measure and colour. They are restated below;
   they are not a second opinion about how prose should look. */
.hw-close {
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 clamp(1.2rem, 2.4vw, 1.75rem);
  padding: clamp(1.2rem, 2.4vw, 1.9rem) clamp(1.2rem, 2.6vw, 2rem);
  border-left: 3px solid var(--fam, var(--brown));
  background: var(--fam-wash, rgba(125, 64, 9, 0.10));
  font-style: italic;
}
.hw-close p {
  margin: 0 0 1.15rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.65;
  color: var(--body);
}
.hw-close p:last-child { margin-bottom: 0; }

.hw-where {
  margin: 0;
  font-size: 13px;
  letter-spacing: 0.04em;
  color: var(--muted);
}


/* ===========================================================================
   FAQ — THE TEXTURE UNDER THE TURN-IT-AROUND STATEMENT — 2026-08-27
   ===========================================================================
   Greg asked for the texture on the second section of the FAQ, which is
   #statement -- the erotic-massage paragraph. images/texture.png, THE SAME FILE
   THE HOME PAGE AND THE GALLERY USE. It went in as texture-spots.png first and
   that was wrong; his correction, same session. texture-spots.png stays in
   images/, still unused on any page.

   WHAT THE FILE IS, and it is the reason for the positioning below. Profiled
   row by row when it went on the home page: 1920x1080, and the grain is a BAND,
   not a wash -- rows 0-24% and 58-100% are completely empty, everything lives
   between 24% and 57%, peaking at 40%, with a firm edge on top and a longer
   tail below.

   SO IT IS CENTRED, NOT TOP-ANCHORED, and that is arithmetic rather than taste.
   The section is 419px at 1440; `cover` on a 16:9 file at that width draws it
   801px tall, so 382px of it are outside the box. Anchored top, the visible
   window would be the file's top 52% -- which cuts the band off just past its
   peak and leaves the grain at its heaviest against the section's bottom edge.
   Centred, the offset is -191px, and the band lands at 1px, peaks at 129px and
   has run out by 265px: the whole thing sits inside the statement and dissolves
   about two-thirds of the way down, on the file's own tail. No bottom mask, for
   the same reason the gallery needs none -- there is nothing left to cut off.

   THE TOP IS THE ONE EDGE THAT NEEDS HELP. The band starts at the very top of
   the box and starts hard, and a horizontal line of grain across the page reads
   as a mistake -- the same fix as #what-this-is on the home page. 12% is 50px,
   which softens the entry and still clears the peak at 129px.

   z-index: .section--lined already puts > .container at 1, so the words are
   clear of this. The mirrored practitioner pair is z-index 0 too and ::before is
   generated ahead of it in the DOM, so the drawing paints over the grain rather
   than under it, which is the right way round for the section's one deliberate
   mark. */
#statement::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 100%;
  background: url(../images/texture.png) center / cover no-repeat;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 12%);
  pointer-events: none;
  z-index: 0;
}


/* ===========================================================================
   SERVICES — THE TEXTURE ACROSS THE SEAM INTO THE INDEX — 2026-08-27
   ===========================================================================
   Greg: put the texture in the second section and let it bleed into the third,
   the one with the images. Second section is #how-time, third is #menu, the
   four photographic bands. images/texture.png, the same file as the home page,
   the gallery and the FAQ's statement.

   IT HANGS FROM #how-time AND IS SIZED TO REACH, which is the home page's
   #what-this-is::after construction. The section is a 167px strip of text at
   1440, so the grain cannot live inside it -- there is not enough height for
   the band to open and close. It has to cross the boundary, which is what Greg
   asked for anyway.

   background-position: center 37%, AND IT IS A PERCENTAGE FOR A REASON. The
   band in this file sits between 24% and 57% of its own height, and `cover`
   scales the file by WIDTH, so at 1440 the band lands 192px down a box that
   starts at the top of a 167px section -- entirely inside the index, with
   nothing in the section Greg named. A fixed negative offset fixes that at one
   width and breaks it at every other: the same -140px that is right at 1440 is
   past the top of the file at 780, and the band arrives already cut, as a hard
   horizontal line across the page. A percentage is resolved against
   (box height - image height), so it tracks the scale instead of fighting it.
   Measured at 1425: the grain starts about halfway through the text, peaks 37px
   inside the index and has died 174px in. It holds its shape from 780 to 2560.

   THERE IS NO MASK HERE, AND THAT IS THE POINT. Two went on and both came off.
   Greg, 2026-08-27: "the image was so constructed that you don't really need to
   apply any kind of mask on it if you position it correctly." He is right, and
   the profile proves it -- coverage falls to 0.6% at the band's top edge and
   0.4% at its bottom, over a ramp of about 15% of the file's height at each
   end. The file dissolves itself at BOTH ends. Nothing needs manufacturing.

   THE ONE CONDITION IS THAT THE WHOLE BAND FITS IN THE BOX, and that is what
   was failing -- see the height below. A mask was twice the wrong answer to a
   sizing fault: it hid the symptom at the width I checked and left it at the
   widths I did not, which is exactly how it survived two passes.

   FLIPPING WAS THE OTHER SUGGESTION AND IT IS THE WRONG TOOL HERE, though the
   asymmetry Greg reasoned from is real: the top ramp is steeper than the
   bottom. But both ends fade, so a flip cannot fix a cropped band -- it only
   swaps which end is gentler. It is right on .tr-close::before, where the grain
   has to dissolve UPWARD away from the footer, and that is the only reason the
   two rules differ.

   z-index: -1 puts it behind everything in the flow while staying above the
   white ground, so the photographs and the price rows sit on top and the grain
   shows in the gaps -- the same behaviour the home page relies on. #menu is
   position: relative with no z-index, so it paints in the positioned layer,
   which is above this. Its own overflow: hidden is irrelevant here: this
   element belongs to #how-time and is not its descendant. */
/* IT HANGS OFF .page-head, NOT OFF #how-time, AND THAT IS THE FIX FOR THE GAP.
   Greg, 2026-08-27: "there is a gap between the beige first section and the
   second section -- position the image exactly at the border with the previous
   section without the margin." He is right, and the cause is not a margin:
   .page-head's box ends at 348 and #how-time's begins at 388, with 40px of
   space belonging to <main> and to neither section. Probed both margins to
   zero and the gap did not move; elementFromPoint in it returns <main>.

   So the element is anchored to the beige band's own bottom edge with
   top: 100%, which starts it exactly at the border BY CONSTRUCTION. A negative
   top of -40px on #how-time would have looked identical at 1440 and drifted at
   every other width, because nothing tells us that 40px is a constant.

   :has(+ #how-time) SCOPES IT TO THIS PAGE. .page-head is on every inner page
   and none of the others wants a texture; #how-time exists only on
   treatments.php, so the relational selector says exactly "the beige band on
   the services page" without inventing a page class. */
.page-head:has(+ #how-time) { position: relative; }
.page-head:has(+ #how-time)::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  /* 34vw, NOT A CONSTANT, AND THIS IS WHAT RETIRED THE MASK. `cover` scales the
     file by WIDTH, so the band's height on screen is always about 0.186 x the
     viewport width -- 265px at 1440, 476px at 2560, 640px at 3440. The box was
     built from `100% + 320px`, which grows on a constant while the band grows on
     width, so past roughly 2500px the band stopped fitting and was cut at the
     top. That cut is a hard edge, and it is the one Greg saw. Tie the box to
     width as well and the two grow together at every screen. 34vw is the band
     plus the slack the position below needs. */
  height: 34vw;
  /* 56%, RAISED FROM 37% WITH THE ANCHOR CHANGE. Moving the box to the beige
     edge closed the structural gap, but the grain itself still began 77px lower
     -- the file's top quarter is empty, and that emptiness was landing in the
     gap Greg was pointing at. This puts the band's first row about 14px below
     the border, so the ramp starts at the join instead of after it.

     Derived, not nudged, so it holds at every width. With boxH = 0.34W and
     imgH = 0.5625W, the band's top sits at 0.135W - P x 0.2225W. Solving for a
     top of ~0.01W gives P = 56%: the grain runs 14px to 282px below the beige
     at 1440, and 26px to 501px at 2560, always inside a box of 490 and 870. */
  background: url(../images/texture.png) center 56% / cover no-repeat;
  pointer-events: none;
  z-index: -1;
}


/* ===========================================================================
   SERVICES — THE FAMILY WASH AND THE DRAWN RULE ON THE GROUP HEADERS
   =========================================================================== 2026-08-27
   Greg: the FAQ's blocks carry a subtle background in the family's colour and a
   line that animates, and the three group headers on this page should do the
   same. IT IS THE FAQ'S BEHAVIOUR, COPIED, NOT AN ADAPTATION OF IT -- he had to
   say so twice, so it is written down here rather than left to judgement:

     - the wash appears ON HOVER and is not there at rest
     - at rest the header carries its plain grey hairline, nothing else
     - the family colour DRAWS ACROSS THAT HAIRLINE, left to right, on hover

   No scroll-reveal trigger, no resting tint, no drawn line waiting on the page.
   .svc-head::before and .svc-rule are the two rules being mirrored and the
   numbers below are theirs.

   THE ONLY DELIBERATE DIFFERENCE IS THE WIDTH, and Greg approved it: the FAQ's
   rule runs the full container because .svc-head does, while .tr-group-h is
   capped at 56ch, so here the line draws halfway across the page and stops with
   the heading. "In the treatments it is only half way. So only use half way
   animation."

   THE COLOURS MATCH THE FAQ BLOCK OF THE SAME NAME, which is the point of doing
   it at all: tantra is red there and red here, massage saffron and saffron.

   #off-the-table IS THE ONE JUDGEMENT CALL. It holds three blocks the FAQ splits
   across two colours -- A Body That Works is teal there, Coaching brown. Brown,
   because two of the three are coaching and the section's own lede is about what
   a massage does not answer. One line to change if he reads it as the programme.

   THE GREY HAIRLINE STAYS AND IS NOT REPLACED. .tr-group-h's border-bottom is
   the resting state; the coloured rule sits on top of it at bottom: -1px, the
   same trick .svc-rule uses, so the header always has a line under it and the
   hover adds colour rather than adding a line.

   THE WASH FADES OUT TO THE RIGHT instead of ending in a hard edge, because the
   header is capped at 56ch inside a much wider container and a rectangle
   stopping at the text measure reads as a mistake. That fade is also why the
   tint starts at 0.06 rather than the FAQ's flat 0.05 -- half of this gradient
   is already below the FAQ's value by the time it reaches the middle. */
.tr-group-h {
  position: relative;
  /* how far the wash reaches above the eyebrow; the bottom is the hairline */
  --gh-lift: clamp(1rem, 2vw, 1.75rem);
}
#tantra        .tr-group-h { --sv-line: var(--red);     --sv-rgb: var(--red-rgb); }
#massage       .tr-group-h { --sv-line: var(--saffron); --sv-rgb: var(--saffron-rgb); }
/* no --brown-rgb exists; .svc--brown carries the same literal */
#off-the-table .tr-group-h { --sv-line: var(--brown);   --sv-rgb: 125, 64, 9; }

.tr-group-h::before {
  content: '';
  position: absolute;
  left: calc(var(--gutter) * -0.5);
  right: 0;
  top: calc(var(--gh-lift) * -1);
  bottom: 0;
  background: linear-gradient(to right, rgba(var(--sv-rgb), 0.06), rgba(var(--sv-rgb), 0));
  opacity: 0;
  transition: opacity 0.55s var(--ease-soft);
  pointer-events: none;
  z-index: -1;
}
.tr-group-h::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--sv-line);
  opacity: 0.7;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.9s var(--ease-out), opacity 0.4s var(--ease-soft);
  pointer-events: none;
}
.tr-group-h:hover::before { opacity: 0.5; }
.tr-group-h:hover::after  { transform: scaleX(1); opacity: 0.4; }


/* ===========================================================================
   THE SEAM BAND — treatments.php, between tantra and massage — 2026-08-27
   ===========================================================================
   The home page band, made short. It reuses .band, .band-media, .band-scrim
   and .band blockquote unchanged; only the height and two details differ.

   SHORT ON PURPOSE. Greg: "not so high, because we don't want to create space
   between the treatments, but we want to have a placement for the quote." The
   tall variant is min-height: 120vh, which is room for the scroll-drawn lines
   to complete -- there are no lines here, so that reason is gone with them.
   460px at 1440 is about half a screen: the quote runs four lines at 32px in
   the 1136 container, the band's own padding is 100px top and bottom, and the
   sum is around 354px. The min-height gives it air without opening a hole.

   THE PARALLAX RULES ARE THE TALL VARIANT'S, COPIED RATHER THAN SHARED, and
   the 135% is load-bearing: main.js writes the transform from
   PARALLAX_RANGE = 0.35, which is exactly the 35% of travel that 135% height
   at top: -17.5% provides. Change one and the picture's edge comes into the
   frame. They are not merged into one selector list because .band--tall also
   carries min-height: 120vh, and a shared block would have to unset it here.

   WHAT THE CROP KEEPS. long-massage.jpg is 2976x1680, 16:9. The image box is
   135% of a 460px band, so 621px in a 1425px-wide row -- a 2.3:1 window. Cover
   scales the file to the width and centres, so roughly the top and bottom 13%
   of the photograph are outside the frame. object-position would move that if
   the composition needs it.

   NO --photo-filter, AND IT IS THE ONLY PICTURE ON THE SITE EXEMPT FROM IT.
   Greg toned this one to a sepia monochrome himself before handing it over.
   The site-wide greyscale exists so that 2022 placeholders are not judged as
   photographs; this one is a choice, and greying it would throw away the
   toning. Do not "fix" the inconsistency by adding the filter back. */
.band--seam {
  /* TALLER, 2026-08-27, and the reason is the picture rather than the type.
     Greg wants more of the bottom of the frame, which is the table. 460 -> 545
     changes how much survives the crop: the image box is 135% of the band, so
     at 460 it was 621px against the 804px the source scales to at 1425 wide --
     23% of the photograph outside the frame. At 533 (37vw at 1440) the box is
     720px and only 84px is cropped, about 5% at each end. Nearly the whole
     frame is now in view. */
  min-height: clamp(340px, 37vw, 545px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.band--seam .band-media { overflow: hidden; }
.band--seam .band-media img {
  position: absolute;
  top: -17.5%;
  left: 0;
  height: 135%;
  will-change: transform;
  filter: none;
  /* 80%, NOT THE DEFAULT 50%. With only 84px of crop left there is little
     slack, and Greg asked for it to come off the top: this puts about 67px of
     the loss above and 17px below, so the table edge at the foot of the frame
     stays in. */
  object-position: 50% 80%;
}

/* THE SCRIM IS SCOPED HERE AND NOT CHANGED AT SOURCE. Greg asked the right
   question -- .band-scrim is on index.php's testimonial band as well, and that
   page is finished, so editing the base rule at line ~2138 would have darkened
   the home page as a side effect. This overrides it for the seam band only.

   0.55 -> 0.25, AND THE NUMBER IS MEASURED. Sampled the luminance of
   long-massage.jpg across the horizontal span the 52ch quote occupies:

     p50 66 · p90 78 · p99 112 · p99.9 169 · max 212

   Against white type that is 4.95:1 at the p99 with NO scrim at all, so the
   photograph is already legible on its own -- it is mid-dark almost everywhere,
   which is what Greg's sepia toning did. The 0.55 was buying very little: even
   at full strength the brightest pixel, a specular glint on the singing bowl's
   rim, only reaches 4.30:1 and still fails. A flat scrim cannot fix a specular
   highlight; it can only dim the whole picture trying.

   0.25 holds the p99 at 6.29:1 with the image visibly lighter. What it does not
   cover is the top 0.1% of pixels -- a few glints on the bowl. If a letter ever
   lands on one it is a local softening, not an unreadable line, and the honest
   fix for that is the crop or the type position, not more darkness. */
.band--seam .band-scrim { background: rgba(46, 50, 49, 0.25); }
/* The home page band is a review with a cite under it and a drawn line above.
   This is Greg speaking on his own page, so there is nothing to attribute and
   the figure carries the blockquote alone. A slightly tighter measure than the
   full container keeps it from running the whole 1136 at three lines -- 77
   characters a line is at the outer edge of readable, and the home page only
   gets away with it because that block is a review being scanned rather than
   an argument being made. 52ch is about 765px at 32px, which puts this quote
   on four lines. It is the one number to move if the band reads too tall or
   too wide.

   ITALIC, AND SIGNED. Greg, 2026-08-27: "what about if we put my name
   underneath it and maybe make the text italic -- the quote on the home page
   is like a testimonial, and this is just my own idea." That is the right
   distinction and it is the one thing that keeps the two bands from reading as
   the same device twice on one site: the home page band is somebody else's
   words ABOUT him, upright, with the reviewer's name under it. This is his own
   words, so it leans.

   A REAL ITALIC EXISTS, which is why this is safe to ask for at 32px --
   crimson-italic-latin.woff2 is declared at the head of this file. Without it
   the browser would synthesise an oblique by shearing the upright, and a
   sheared serif at display size is visible from across the room.

   THE CITE KEEPS THE CAPTION SETTING and is not given a treatment of its own.
   12px uppercase at 0.11em is what every caption on this site uses, the band's
   own reviewer credits included, so the family holds; the italic above it is
   doing all the work of saying who is speaking. It is also the first signed
   line of visible copy anywhere on the site -- the pages are first person
   throughout and none of them sign off. "Greg" rather than "Greg Wiater",
   which appears only in privacy.php where it is a legal identification. */
.band--seam blockquote {
  max-width: 52ch;
  font-style: italic;
}
/* THE DASH BESIDE THE NAME. Greg, 2026-08-27: "adding something like a dash
   next to my name on one side would make it look a little bit more elegant or
   stand out better."

   A DRAWN RULE RATHER THAN AN EM DASH, and the reason is that this site already
   has a language for short horizontal marks -- .svc-rule, .trr-rule between a
   duration and its price, .band-line above the home page's reviews. A typed
   "—" is a punctuation mark that happens to look like a line; this is the same
   mark the rest of the site draws, at the size the caption asks for.

   2.4em rather than a pixel width, so it tracks the type: at 12px it renders
   28.8px, and it stays proportionate if the caption size is ever changed.
   currentColor takes the cite's own rgba white, so the rule can never drift out
   of step with the text beside it.

   vertical-align: 0.3em, NOT `middle`. The cite is uppercase, and `middle`
   aligns to half the x-height, which sits below the optical centre of a line of
   capitals -- close enough to look like a mistake rather than a choice. 0.3em
   puts it on the middle of the cap height.

   ONE SIDE ONLY, as asked: the mark leads into the name and nothing closes it.
   A rule on both sides would make it a label instead of a signature.

   TWO BANDS SHARE THIS RULE, added 2026-08-27 when Greg asked for the same
   signature on the coaching band. It is one selector list rather than a second
   copy of the mark: the whole argument above is that the site draws one kind of
   short horizontal rule, and a duplicated declaration is how two of them start
   drifting apart. index.php's band is not in the list -- its cites are
   "Robert · Travel Gay" and a Google review, which are sources rather than
   signatures. */
.band--seam cite::before,
.band--thesis cite::before,
.band--course cite::before,
.band--degree cite::before,
.band--effect cite::before {
  content: '';
  display: inline-block;
  width: 2.4em;
  height: 1px;
  margin-right: 0.9em;
  vertical-align: 0.3em;
  background: currentColor;
}


/* ===========================================================================
   SERVICES — THE CLOSE: THE GAP ABOVE IT, AND THE TEXTURE UNDER IT
   =========================================================================== 2026-08-27
   Greg: "the last section called Choosing has too much gap between the elements
   above it," and it should carry texture.png the way gallery.php does.

   THE GAP WAS 187px from the last Ask button to the hairline, measured. Two
   paddings stacked to make it and neither was doing anything on its own: the
   last .tr-group's 57.6px bottom, and .tr-close's own 57.6px top. Both are
   trimmed rather than one, because zeroing a single one leaves the other
   looking like the whole problem. Now about 102px, which is a button's worth of
   air instead of a hole. The gallery had the identical fault and the identical
   fix -- .gal-close went to clamp(0.5rem, 1.1vw, 1.5rem), the value reused here.

   :has(+ .tr-close) RATHER THAN AN ID OR :last-of-type. Scoping it to
   #off-the-table would break the day a group is renamed or reordered, and
   .tr-group:last-of-type matches nothing at all -- :last-of-type counts every
   <section> in <main>, and the last one is .tr-close, not a .tr-group. The
   relational selector says the actual condition: the group that sits directly
   above the close.

   THE TEXTURE IS .gal-close::before, COPIED, and the numbers are that rule's
   own -- see it for the reasoning behind every one:
     - scaleY(-1) turns the file's asymmetric band over, so the firm edge sits
       low and the long tail climbs. Grain gathers near the footer and thins out
       before it reaches the words.
     - top: -70vh reaches up into the section above, so the tail crosses the
       boundary instead of stopping at a section edge.
     - +120px is A CONSTANT, NOT A SHARE OF THE VIEWPORT. The footer has to hide
       the hard bottom cut and it is a fixed 242px on desktop; a vh-based
       overrun grows past that on a tall window and the grain reappears below
       the footer's elliptical corners.
     - THE MASK LOOKS BACKWARDS AND IS NOT. It is painted in the element's own
       space and the transform is applied to the result, so scaleY(-1) maps
       local top to visual bottom -- the opaque end has to be written at the
       LOCAL top to arrive at the VISUAL bottom. Remove the transform and this
       gradient reverses with it.

   THIS IS THE PAGE'S SECOND TEXTURE. #how-time carries one that bleeds down
   into the index; this one climbs from the footer. They are at opposite ends of
   a very long page and never share a screen, which is the only reason two do
   not read as a pattern. */
.tr-group:has(+ .tr-close) { padding-bottom: clamp(0.5rem, 1vw, 1.25rem); }
.tr-close {
  padding-top: clamp(0.5rem, 1.1vw, 1.5rem);
  /* the ground the texture needs; it carries no offsets of its own */
  position: relative;
}
.tr-close::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -70vh;
  height: calc(100% + 70vh + 120px);
  background: url(../images/texture.png) center / cover no-repeat;
  transform: scaleY(-1);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.45) 55%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.45) 55%, transparent 100%);
  pointer-events: none;
  z-index: -1;
}


/* ===========================================================================
   A BODY THAT WORKS — the four rules the page needed
   =========================================================================== 2026-08-27

   Everything else on a-body-that-works.php is existing components used as
   written: .page-head, .tr-note, .tr-rates, .band--short, .tr / .tr-grid /
   .tr-fig / .tr-body, .tp / .tp-list, .faq, .prose-block, .step-list, .tp-out
   and .tr-close. Four things did not exist. */

/* --- .band--effect: the thesis band --------------------------------------
   Greg's own quote, the one he asked for high on the page with a photograph:
   "losing weight is not my objective." Same component as the compass band on
   coaching.php and the degree band on the Services page — .band--short does
   the height and the parallax, this adds only what belongs to THIS photograph.

   THE GAP ABOVE IS THE SAME EXPRESSION .section uses for its padding, copied
   from .band--course for the same reason it exists there: `#what-this-is
   { padding-bottom: 0 }` is keyed to an id and reaches every page carrying
   that id, so the band has to bring its own gap. Matching the expression
   rather than a measured pixel keeps the two equal at every width.

   SCRIM 0.62, MEASURED ON xl-man-massage2.jpg and not copied from the compass
   band, which happens to land on the same number. Sampled across the middle
   70% of the visible crop, inside the container width, 2px blur: white type is
   4.48:1 worst case, 5.18:1 at the p1, 10.47 median. The base 0.55 gives 3.74
   worst and fails. THE NUMBER BELONGS TO THE FILE — re-measure if the
   photograph changes.

   filter: none for the same reason .band--course carries it. `#band
   .band-media img { filter: none }` keeps the home and Services bands in
   colour and an id selector cannot reach a second band on the same page.
   Without this the page would show a grey band above a colour one. */
.band--effect { margin-top: clamp(3rem, 3vw, 6.5rem); }
.band--effect .band-scrim { background: rgba(46, 50, 49, 0.62); }
.band--effect .band-media img { filter: none; }

/* --- .band--plate: a photograph, and no type -----------------------------
   The hinge between what the programme contains and why anyone would listen
   to Greg about it. It carries no blockquote, so no contrast measurement
   applies and the scrim is doing one job only: keeping the picture in the
   same tonal family as the two bands that do carry type.

   DELIBERATELY NOT .band--seam. That class looks like the obvious reuse and
   is not: its 0.25 scrim and its `object-position: 50% 80%` were both
   measured on long-massage.jpg, which Greg toned himself, and both are wrong
   for any other file. Taking .band--short and adding two lines is what keeps
   the seam's numbers attached to the photograph they were derived from. */
.band--plate { margin-block: clamp(3rem, 3vw, 6.5rem); }
.band--plate .band-scrim { background: rgba(46, 50, 49, 0.18); }
.band--plate .band-media img { filter: none; }

/* --- .seam-free: the same seam, with nothing behind the picture -----------
   Written 2026-09-20 for the ABTW page's second seam, where the photograph
   became a 1624x508 PNG with the figure inside a transparent margin.

   IT IS A CUT-OUT, WHICH IS WORTH SAYING BECAUSE OPENING THE FILE DOES NOT
   SHOW IT. The room Greg shot in -- curtain, shelf, tiles -- is still in the
   RGB channels and is masked out by alpha, so any tool that ignores the alpha
   channel renders the whole room and any browser renders the man alone. Judge
   this file in a browser, not in a preview.

   EVERY DECLARATION HERE IS SOMETHING .band--plate DOES THAT A CUT-OUT CANNOT
   SURVIVE. It is not a variant of that component, which is why it does not
   share its class:

     - no absolute .band-media, so the picture sets the section's height
       instead of being cropped to it;
     - no object-fit: cover -- cropping a cut-out to a 480px band scales it to
       about 2074px wide and takes 634px off the sides, which is the man's
       head at one end and his feet at the other;
     - no 135% height and no transform, so main.js never finds a
       `.band-media img` here and the parallax leaves it alone;
     - no scrim, because a scrim over transparency is a grey rectangle the
       width of the page with a man floating in it.

   MAX-WIDTH IS THE FILE'S OWN WIDTH. Full-bleed and upscaling are the same
   decision at a wide viewport; capping at 1624 means the picture is shown at
   1:1 or smaller and never softened. Above that it centres, and the page's own
   ground runs to the edges exactly as it already does through the cut-out.

   AND IT TAKES NO PADDING, AND ALMOST NO MARGIN. The band it replaced needed
   padding because its photograph reached all four edges. A cut-out brings its
   own air -- the figure sits in the middle of a 1624x508 canvas and the rest
   is already empty -- so section spacing is added to space that is not there.
   Measured: with .band's clamp(3rem, 7vw, 6.5rem) the seam stood 652px tall
   at a 1440 viewport to show a 450px picture.

   THE MARGIN CAME DOWN AGAIN WHEN IT MOVED INTO THE PROSE -- Greg,
   2026-09-20: "don't make so much gap between the sections." It is no longer
   a seam between two sections, it is a picture between two paragraphs, so it
   takes a paragraph's gap and not a section's. clamp(1rem, 2vw, 2.25rem) is
   28.8px at 1440, against .prose-block's own 1.15rem between paragraphs --
   near enough that the picture reads as part of the run of text.

   THE COLOUR IS DELIBERATE AND INHERITED. .band--plate set `filter: none` on
   this slot -- the one photograph on the page that is not under the
   placeholder greyscale -- and that decision is kept rather than re-made.

   --- THE DRIFT ------------------------------------------------------------
   Greg, 2026-09-20: "I would like to add some kind of animation on it
   reactive on scrolling. So it's like we would apply some kind of slight
   parallax movement without creating a parallax as such."

   HE IS DESCRIBING THE FEEL OF PARALLAX WITHOUT ITS MECHANISM, and the
   distinction is real. A parallax needs an oversized image inside a clipping
   frame, so it has slack to travel through and an edge it must never expose.
   This picture has neither: it is shown whole, at its own ratio, with the
   page's ground visible straight through it. There is nothing to clip and no
   edge to protect, so the FIGURE ITSELF moves instead of a layer behind one.

   HORIZONTAL, AND THAT IS THE FIGURE'S OWN AXIS. The man is in a press-up,
   head left and legs extended right -- a vertical drift on a horizontal body
   reads as the picture sliding, a horizontal one reads as the body moving
   through the frame the way it is already pointing. He travels LEFT as the
   page scrolls down, which is the direction he is facing.

   3.5% OF THE WIDTH, ABOUT 50px AT 1440, AND IT IS FREE. Translating a
   cut-out cannot expose anything -- what is behind it is the page, which is
   what is behind it already. So the travel is chosen on how it reads rather
   than on what it would uncover, which is the opposite of every other
   parallax on this site.

   THE TRANSFORM IS DRIVEN BY --seam-x, WRITTEN FROM THE PERMANENT FRAME LOOP
   in main.js. Not from the scroll event: Lenis keeps interpolating for a few
   hundred milliseconds after scrollY stops changing, and a scroll-driven
   value freezes while the page is still gliding underneath it. .gw-bleed
   learned this first and this copies it exactly, --bleed-y and all. The
   default of 0 is what a browser with JavaScript off shows, which is the
   still picture and nothing lost. */
.seam-free {
  margin-block: clamp(1rem, 2vw, 2.25rem);
  overflow: hidden;
}
.seam-free img {
  display: block;
  width: 100%;
  max-width: 1624px;
  height: auto;
  margin-inline: auto;
  transform: translate3d(var(--seam-x, 0px), 0, 0);
  will-change: transform;
}
/* The travel is motion for its own sake -- the picture says the same thing
   standing still -- so it is the whole effect that goes, not a reduced one. */
@media (prefers-reduced-motion: reduce) {
  .seam-free img { transform: none; will-change: auto; }
}

/* --- .cq-list--plain: the same rows, without the numbers ------------------
   .cq-list is the coaching page's four questions and it counts them, which is
   right there: they are asked in an order. The six statements on this page are
   a man recognising himself, in any order at all, and numbering them would
   claim a sequence that does not exist.

   THE MARK IS THE ONE THE REST OF THE SITE DRAWS — the same 1px horizontal
   rule .tp-list puts beside a topic, at the larger size this type asks for,
   rather than a bullet or a dash character. Colour is --brown to match the
   counter it replaces.

   align-self: start IS LOAD-BEARING. The li is `align-items: baseline` and an
   empty ::before has no text to derive a baseline from, so browsers fall back
   to its bottom margin edge and the mark drops to the foot of a two-line row.
   Starting it and pushing it down puts it on the optical middle of the FIRST
   line, which is where .tp-list's absolute `top: 0.95em` puts its own mark for
   the same reason.

   AND `font-size: inherit` IS WHY THAT WORKS, which cost a measurement to
   find. The base rule sets `font-size: 13px` on this pseudo-element, because
   there it is a NUMBER and wants the caption size. There is no number here, so
   the declaration is invisible -- but `em` still resolves against it, and
   0.65em silently meant 8.5px against the 13, not 17px against the li's 26.4.
   The mark rendered at the cap line rather than the middle of it. Inheriting
   the li's own size is what makes the em mean what it reads as; measured at
   1440, the mark now lands 17.2px into a 33.8px line box. */
.cq-list--plain { counter-reset: none; }
.cq-list--plain li { counter-increment: none; }
.cq-list--plain li::before {
  content: '';
  font-size: inherit;
  width: clamp(14px, 1.6vw, 20px);
  height: 1px;
  background: var(--brown);
  opacity: 0.6;
  align-self: start;
  justify-self: end;
  margin-top: 0.65em;
}

/* --- a plain paragraph inside .tp-out ------------------------------------
   .tp-out was written as an outcome block on the coaching page, where every
   child has a class: -eyebrow, -h, -lead, .tp-list, -note. Standing alone as
   this page's filter it carries two ordinary paragraphs between the lead and
   the note, and an unclassed <p> inside it was inheriting the document default
   rather than the block's own measure and rhythm. Matched to .tp-out-lead so
   the four paragraphs set as one block.

   .tp-out--standalone rather than the bare element selector: the coaching
   page's outcome blocks must not acquire a rule they were not built with, and
   a "minor" fix on a shared component ships to every page using it. */
.tp-out--standalone > p:not([class]) {
  margin: 0 0 0.9rem;
  max-width: 60ch;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--body);
}
/* The closing paragraph was .tp-out-note -- 15px and muted, which is right for
   a footnote under a coaching outcome and wrong here: Greg, 2026-08-31, it is
   the fourth paragraph of one argument and must set like the other three. */
.tp-out--standalone > p:not([class]):last-child { margin-bottom: 0; }

/* === THE CEILING PLATES — a-body-that-works.php #ceiling ==================
   Built 2026-08-31. Two half-width plates rather than a second full-width
   parallax: #thesis on the same page is already that device, and repeating it
   costs the second one its weight. Also the honest container for two examples.

   ASPECT RATIO RATHER THAN A HEIGHT. Both pictures are 3:2 and the plate crops
   to 4:3, which keeps the horizon and drops the empty sky the generator likes
   to leave. A fixed height would break the moment either image is replaced.

   SCRIM 0.55, THE SAME AS .band-scrim, AND IT IS MEASURED. Across the caption
   zone -- the bottom 34% of the 4:3 crop, inset, with the gradient applied --
   white type is 9.44:1 at worst on the flight plate and 11.23:1 on the track,
   medians 15.6 and 18.6. Both are dark pictures, so the scrim is doing shape
   work rather than rescuing contrast, and there was no reason to invent a
   number the component does not need. The saffron figure lands at 4.67:1 at
   the worst pixel, against 3:1 for text this size. RE-MEASURE IF EITHER IMAGE
   IS SWAPPED -- both are placeholders until Greg generates his own, and a
   brighter sky is exactly what would break this.

   The gradient is on top of the flat scrim rather than instead of it: the flat
   layer protects the number at the top of the caption, the gradient carries
   the paragraph at the bottom where the pictures are darkest anyway. */
.ceil-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.4rem;
  margin-top: 2.6rem;
}

.ceil-plate {
  position: relative;
  margin: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  display: flex;
  align-items: flex-end;
}

.ceil-media { position: absolute; inset: 0; }
.ceil-media img {
  width: 100%; height: 100%;
  object-fit: cover;
  filter: var(--photo-filter);
}

.ceil-scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, rgba(20, 12, 5, 0.72) 0%, rgba(20, 12, 5, 0) 62%),
    rgba(30, 20, 10, 0.55);
}

.ceil-text {
  position: relative;
  z-index: 2;
  padding: 1.8rem 1.7rem 1.6rem;
}

/* Serif, not the Inter eyebrow: this is the loudest thing in the plate and it
   is a quantity, not a label. Saffron rather than white so it reads as the
   site's accent instead of a headline competing with the h2 above. */
.ceil-num {
  display: block;
  font-family: var(--serif);
  font-size: clamp(1.9rem, 3.1vw, 2.7rem);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--saffron);
  margin-bottom: 0.6rem;
}

.ceil-text p {
  margin: 0;
  max-width: 42ch;
  font-size: 1rem;
  line-height: 1.55;
  color: rgba(255, 255, 255, 0.9);
}

.ceil-after {
  margin-top: 2.2rem;
  max-width: 62ch;
}
.ceil-after p {
  margin: 0;
  font-size: 1.06rem;
  line-height: 1.65;
  color: var(--body);
}
.ceil-after em { font-style: italic; }

/* Narrow: the two plates stack. This is the desktop-first narrow rule that
   stops the layout breaking, NOT the phone design -- that is a separate design
   and it is job 3 in NEXT-SESSION.md. */
@media (max-width: 860px) {
  .ceil-grid { grid-template-columns: 1fr; gap: 1.1rem; }
  .ceil-plate { aspect-ratio: 3 / 2; }
}

/* --- the proof row, #ceiling — ONE DARK ELEMENT, TWO COLUMNS -------------
   Greg, 2026-08-31: photograph and quote inside a single element with a dark
   background, split into two columns only because the picture is small. So
   the dark ground is the element and the columns live inside it, rather than
   two things sitting next to each other on the page's cream.

   IT BREAKS THE CONTAINER DELIBERATELY. The ground has to run edge to edge or
   it reads as a card, and a card is the thing this is not. The negative
   margin is the page's own full-bleed idiom rather than a new one.

   NOT A .band, and this is the third quote device on the page for a reason:
   #thesis is a photograph with type over it, the plates above are two of
   those side by side, and this one is type BESIDE a photograph. Overlaying
   this picture would put words across the body that is doing the arguing.

   THE PHOTOGRAPH KEEPS ITS OWN COLOUR — no --photo-filter. It is a real
   photograph, not a 2022 placeholder, and greying the evidence is the one
   place on this page where the placeholder treatment fights the copy. */
.ceil-proof {
  margin: clamp(3rem, 6vw, 5rem) calc(50% - 50vw) 0;
  padding: clamp(2.6rem, 5vw, 4.5rem) 0;
  background: var(--proof-ground);
}

.ceil-proof-in {
  max-width: 1136px;
  margin: 0 auto;
  padding: 0 clamp(1.2rem, 4vw, 2.5rem);
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

/* THE SHADOW IS THE SITE'S OWN, INVERTED. frames/shadow-only-portrait.png
   is black ink with a drawn falloff, built for a white card on cream; on this
   ground it would be invisible. shadow-only-portrait-light.png is that exact
   file with the ink swapped to white and the alpha lifted 1.35x, because a
   white glow on near-black reads weaker than black on white at equal alpha.
   The falloff is not redrawn — it is the same shadow, turned inside out.

   Same border-image mechanics as .duo-frame::after, same slices: 6px on the
   right, 25px on the bottom, only the soft middle stretches. Negative insets
   so it costs the layout nothing. border-style must be set or border-image is
   never painted — that one has cost a round trip before, TRAPS.md. */
.ceil-proof-media {
  position: relative;
  margin: 0;
}
.ceil-proof-media::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: -6px; bottom: -25px;
  border-style: solid;
  border-color: transparent;
  border-width: 0 6px 25px 0;
  border-image: url(../frames/shadow-only-portrait-light.png) 0 6 25 0 / 0 6px 25px 0 stretch;
  pointer-events: none;
}
.ceil-proof-media img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}

.ceil-proof-text { margin: 0; }
/* Two paragraphs now, same size. The second is Greg's answer to the six-days-
   a-week objection and it is the same thought spoken on, not a footnote — the
   file already records his call on the two band reviews, that shrinking one of
   two true statements is a hierarchy picked rather than measured. */
.ceil-proof-text p + p { margin-top: 1.1rem; }
.ceil-proof-text p {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1.3rem, 2vw, 1.85rem);
  line-height: 1.32;
  letter-spacing: -0.01em;
  color: rgba(255, 255, 255, 0.94);
}

/* THE DASH BEFORE HIS NAME — Greg asked for it by pointing at the #thesis band
   and calling it "the double or triple dash on the left". It is the mark the
   rest of the site already draws: the same 1px rule .cq-list--plain and
   .tp-list put beside a line, not an em dash character, which would sit on the
   type's baseline and read as punctuation instead of a mark.

   Inline-block with a bottom offset rather than a flex row: the cite is one
   short word and a flex container would make a 12px label into a layout. */
.cite-dash {
  display: block;
  font-style: normal;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--cite-ink, var(--brown));
  margin-top: clamp(1.4rem, 2.4vw, 2rem);
}
.cite-dash::before {
  content: '';
  display: inline-block;
  width: clamp(22px, 2.4vw, 34px);
  height: 1px;
  background: var(--saffron);
  opacity: 0.85;
  vertical-align: middle;
  margin-right: 0.85rem;
}
/* The dark ground is the exception, so it sets the ink rather than the
   component carrying a colour that only one of its two homes wants. */
.ceil-proof-text { --cite-ink: rgba(255, 255, 255, 0.65); }

/* Narrow: the picture goes on top. It keeps its square crop — the shadow is
   drawn for an upright rectangle and a landscape crop would stretch the 25px
   bottom edge across a width it was never measured for. */
@media (max-width: 860px) {
  .ceil-proof-in { grid-template-columns: 1fr; gap: 2rem; }
  .ceil-proof-media { max-width: 380px; }
}

/* --- .gq: a quote in Greg's voice, text only -----------------------------
   Built 2026-08-31 for the certificates-or-pills quote at the foot of
   #ceiling. No picture, no ground, no card: Greg asked for it as text on the
   page while he decides what it becomes.

   NOT .t-quote, WHICH IS THE OBVIOUS REUSE AND THE WRONG ONE. That component
   is a client review on testimonials.php -- it carries a reviewer, a source
   and the semantics of somebody else vouching. This is the man himself, and
   dressing his own sentence as a testimonial is a claim about who is speaking.

   THE CITE IS SHARED, and that is the part worth reusing: .cite-dash is now
   one component in two homes, this and the dark proof row, taking its colour
   from --cite-ink. A change to the mark ships to both on purpose.

   Serif at the size the page gives a statement, but NOT the proof row's size
   -- that one is beside a photograph and has a column to fill; this one is
   alone on the cream with the full measure available, so it sets at the page's
   own reading size raised one step and lets the 62ch do the work. */
.gq {
  margin: clamp(2.6rem, 5vw, 4rem) 0 0;
  max-width: 62ch;
}
.gq p {
  margin: 0;
  font-family: var(--serif);
  font-size: clamp(1.15rem, 1.65vw, 1.5rem);
  line-height: 1.42;
  letter-spacing: -0.005em;
  color: var(--ink);
}

/* ==========================================================================
   FOOTER ON A PORTRAIT PHONE: THE NUMBER AND ITS ICONS ON ONE ROW
   Greg, 2026-09-19. The flags and WhatsApp sat on their own row under the
   number; a portrait phone has the width to put them beside it, and the
   footer is one row shorter for it. Wraps rather than overflows on the
   narrowest phones. Here, not in phone.css, because the footer is on every
   page -- shared furniture lives in this file.
   ========================================================================== */
@media (max-width: 768px) and (orientation: portrait) {
  .site-footer .f-reach {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: 0.9rem;
  }
  .site-footer .f-icons { margin-top: 0; }
}

/* ==========================================================================
   NOTHING SLIDES THE PAGE SIDEWAYS -- 2026-09-19
   Reveals arrive from the side (.reveal--right is translateX(34px), the
   services panels slide in from the right), and until they fire they sit
   past the right edge. A phone widens its layout to fit them, so a page
   loaded 410px wide on a 390 screen and could be dragged sideways. body's
   overflow-x: hidden does not stop that on a phone.
   `clip`, not `hidden`, and on main rather than html or body: clip makes no
   scroll container, so position: sticky -- the stacking service cards --
   keeps working. Horizontal only; nothing is cut top or bottom.
   ========================================================================== */
main { overflow-x: clip; }

/* ==========================================================================
   PHONE: THE DOTS AT THE GALLERY'S SIZE ON SERVICES AND THE FAQ -- 2026-09-19
   Greg: on a phone the grain on the top of Services and on the FAQ statement
   looked shrunk and spread out next to the gallery's. Measured: `cover` was
   rendering the file at 20% of its size on Services (a 133px box, so it scaled
   by width) and 46% on the FAQ, against 100% at the bottom of the gallery.
   The gallery does nothing special -- its box is far taller than 16:9, so
   cover scales by HEIGHT and lands on the file's own size.
   Here the file is simply drawn at that size (`auto` = 1920x1080), and the box
   is given the room the band needs. The band lives between 24% and 57% of the
   file (rows ~259-616, about 357px), so -250px puts its first row just under
   the top of the box. Phones only: the desktop was not part of the complaint.
   ========================================================================== */
@media (max-width: 900px) {
  .page-head:has(+ #how-time)::after {
    height: 420px;
    background-size: auto;
    background-position: center -250px;
  }
  #statement::before {
    background-size: auto;
    background-position: center -240px;
  }
}

/* ==========================================================================
   PHONE MENU: THE SERVICES SUBPAGES -- 2026-09-19
   Greg: the burger menu had Services but not the two pages the desktop bar
   hangs under it. They sit indented beneath it, smaller and with a saffron
   rule down the left, so they read as part of Services rather than as two
   more top-level pages.
   ========================================================================== */
.mobile-menu .mm-sub {
  display: flex;
  flex-direction: column;
  margin: 0.1rem 0 0.35rem;
  padding-left: 1rem;
  border-left: 1px solid var(--saffron-hair);
}
.mobile-menu .mm-sub a {
  font-size: clamp(1.25rem, 5.2vw, 1.7rem);
  padding-block: 0.3rem;
  color: var(--body);
}
@media (max-height: 520px) and (orientation: landscape) {
  .mobile-menu .mm-sub a { font-size: 1.1rem; padding-block: 0.2rem; }
}
