/* =====================================================================
   Mina Luxury Tan — Design System v4 · "Editorial, light"
   A print-magazine layout: high-contrast serif headlines, a tracked sans
   for reading and labels, hairline rules where cards and shadows used to
   be, square corners throughout, and the signature blue (#77d5dd) held
   back for outlines, hover fills, selected states and a few small
   uppercase tags — never body copy, headings or links.

   The public routes run on a white canvas. :root below still holds the
   ORIGINAL DARK palette and the four public pages opt into the light one
   with class="theme-light" on <body> — see the light theme block under
   :root for why that indirection exists.

   Two deliberate dark islands survive on those light pages: the footer,
   which anchors the foot of the page, and the nav while it sits over a
   hero photograph (/ and /services, via .has-hero). Both are handled by
   redeclaring tokens on the element rather than by writing a second set
   of rules.

   admin/admin.css reads these same tokens, so a change to :root reaches
   the admin panel too.
   ===================================================================== */

:root {
  /* — Canvas & surfaces — */
  --bg:          #0a0a0a;   /* neutral-950 */
  --bg-soft:     #0a0a0a;
  --surface:     #0f0f0f;
  --surface-2:   #151515;
  --surface-3:   #1c1c1c;

  /* — Rules: the hairlines that separate content zones — */
  --line:        #262626;   /* neutral-800 */
  --line-strong: #404040;   /* neutral-700 */

  /* — Type — */
  --text:        #ffffff;
  --strong:      #ffffff;   /* headings and emphasis; the light theme drops this to near-black */
  --muted:       #a3a3a3;   /* neutral-400: secondary copy and labels */
  --muted-2:     #737373;   /* neutral-500: disabled or deliberately quiet */

  /* — Signature accent — */
  --accent:      #77d5dd;
  --accent-hi:   #a2e8ee;
  --accent-dim:  #4fb9c2;
  --accent-ink:  #0a0a0a;   /* text sitting on an accent fill */
  --accent-soft: rgba(119, 213, 221, 0.10);

  /* — Fonts — */
  --font-display: "Playfair Display", "Iowan Old Style", Baskerville, Georgia, "Times New Roman", serif;
  --font-body:    "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* The masthead word MINA alone — "Luxury Tan" beneath it keeps --font-body */
  --font-brand:   "Montserrat", "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;

  /* — Tracking — */
  --track-body:  0.025em;   /* tracking-wide: print-column reading */
  --track-label: 0.24em;    /* uppercase kickers, nav links, sub-labels */

  /* — Shape & motion — */
  --radius:      0px;       /* every corner is square; the radius tokens stay */
  --radius-sm:   0px;       /* (at zero) because admin.css reads them */
  --radius-pill: 0px;
  --maxw:        1280px;
  --gutter:      clamp(1.25rem, 5vw, 3.5rem);
  --ease:        cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);

  /* — Space — */
  --section-y:       clamp(6rem, 12vw, 10rem);   /* py-24 up to py-40 */
  --section-y-tight: clamp(6rem, 9vw, 8rem);     /* py-24 up to py-32 */
  --col-gap:         clamp(2rem, 5vw, 5rem);     /* air either side of a column rule */

  /* — Fluid type scale — */
  --fs-display:  clamp(3rem, 8.2vw, 7.25rem);
  --fs-h2:       clamp(2.4rem, 5vw, 4.25rem);
  --fs-h3:       clamp(1.5rem, 2.2vw, 1.95rem);
  --fs-lead:     clamp(1.02rem, 1.25vw, 1.14rem);
  --fs-eyebrow:  0.7rem;
  --fs-label:    0.68rem;
}

/* ------------------------------------------------------------------ */
/* Light theme — the four public routes only                           */
/* ------------------------------------------------------------------ */
/* :root is left dark on purpose. admin/admin.css loads after this file
   and redefines no token of its own, so flipping :root would re-theme
   the admin panel as a side effect. /, /services, /about and /contact
   opt in with class="theme-light" on <body> instead, which leaves
   the admin panel exactly as it was. Only the palette moves — the accent, the
   type scale, the square corners and the spacing are all shared. */
body.theme-light {
  --bg:          #ffffff;
  --bg-soft:     #ffffff;
  --surface:     #f1f5f9;   /* slate-100: the marble body of a service card — cool, not warm grey */
  --surface-2:   #ffffff;   /* a card lifts to pure white under the cursor */
  --surface-3:   #ededed;

  --line:        #e5e5e5;   /* neutral-200 */
  --line-strong: #d4d4d4;   /* neutral-300 */

  --text:        #171717;   /* neutral-900 */
  --strong:      #0a0a0a;   /* headings and emphasis */
  --muted:       #525252;   /* neutral-600: secondary copy, readable on white */
  --muted-2:     #737373;   /* neutral-500 */
}

/* The footer is the deliberate exception and stays dark to anchor the
   page. Redeclaring the tokens on the element is the whole change: every
   rule inside the footer already reads them, so no rule needs a second
   light/dark variant. */
body.theme-light .footer {
  --bg:          #0a0a0a;
  --bg-soft:     #0a0a0a;
  --surface:     #0f0f0f;
  --surface-2:   #151515;
  --line:        #262626;
  --line-strong: #404040;
  --text:        #ffffff;
  --strong:      #ffffff;
  --muted:       #a3a3a3;
  --muted-2:     #737373;
  color: var(--text);
}

/* The bar is transparent at the top of every route. On the homepage that
   means transparent over the dark hero photograph, so it borrows the dark
   palette's type colours until the page moves; on the inner pages the
   canvas behind it is already white, so it keeps the light ones and stays
   legible. Once scrolled, .is-scrolled drops this rule and the bar is
   solid white with near-black type on every page. */
body.theme-light.has-hero .nav:not(.is-scrolled) {
  --text:        #ffffff;
  --strong:      #ffffff;
  --muted:       rgba(255, 255, 255, 0.78);
  --line-strong: rgba(255, 255, 255, 0.45);
  color: var(--text);
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.75;
  font-weight: 400;
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Flat, solid dark canvas — no ambient gradients */

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
::selection { background: var(--accent); color: var(--accent-ink); }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin: 0;
}
h1, h2 { text-wrap: balance; }

:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }

/* ------------------------------------------------------------------ */
/* Layout primitives                                                   */
/* ------------------------------------------------------------------ */
.container {
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  position: relative;
  z-index: 1;
}

.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: var(--section-y-tight); }

/* Consecutive sections are parted by a hairline drawn to the width of the
   text column rather than the window, the way a printed page keeps its
   margins. Full-bleed bands (.partners, .band) carry edge-to-edge rules. */
.section + .section {
  background-image: linear-gradient(var(--line), var(--line));
  background-repeat: no-repeat;
  background-position: top center;
  background-size: calc(min(100%, var(--maxw)) - 2 * var(--gutter)) 1px;
}

[id] { scroll-margin-top: 100px; }

.eyebrow {
  display: inline-block;
  font-family: var(--font-body);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--strong);
}

.lead {
  font-size: var(--fs-lead);
  color: var(--muted);
  line-height: 1.8;
  max-width: 44ch;
  font-weight: 300;
}

/* Section opener: kicker across the top, headline left, standfirst right,
   both hung from the same grid line. Stacks below 960px. */
.section-head {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  column-gap: var(--col-gap);
  align-items: end;
}
.section-head .eyebrow { grid-column: 1 / -1; justify-self: start; }
.section-head h2 { grid-column: 1; grid-row: 2; font-size: var(--fs-h2); margin-top: 1.5rem; }
.section-head .lead { grid-column: 2; grid-row: 2; margin: 0; }

/* ------------------------------------------------------------------ */
/* Buttons — thin square outlines that fill on hover                   */
/* ------------------------------------------------------------------ */
.btn {
  --_bg: transparent;
  --_fg: var(--strong);
  --_bd: var(--line-strong);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  /* the extra left padding offsets the tracking after the last letter */
  padding: 1rem 2rem 1rem calc(2rem + 0.22em);
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--_fg);
  background: var(--_bg);
  border: 1px solid var(--_bd);
  border-radius: 0;
  cursor: pointer;
  transition: background-color 0.35s var(--ease), color 0.35s var(--ease),
              border-color 0.35s var(--ease);
}
.btn svg { width: 14px; height: 14px; }

.btn--primary { --_bd: var(--accent); }
.btn--primary:hover { --_bg: var(--accent); --_fg: var(--accent-ink); }

.btn--ghost:hover { --_bd: var(--strong); }

.btn--lg { padding: 1.15rem 2.4rem 1.15rem calc(2.4rem + 0.22em); font-size: 0.74rem; }

/* ------------------------------------------------------------------ */
/* Navigation                                                          */
/* ------------------------------------------------------------------ */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  border-bottom: 1px solid transparent;
  transition: background-color 0.4s var(--ease), border-color 0.4s var(--ease);
}
.nav__inner { display: flex; align-items: center; justify-content: space-between; gap: 2rem; height: 84px; }
/* Solid canvas and a hairline once the page moves — no frosted glass */
.nav.is-scrolled { background-color: var(--bg); border-bottom-color: var(--line); }

/* Masthead */
.brand { display: inline-flex; align-items: baseline; gap: 0.6rem; line-height: 1; }
/* The logo is deliberately split: MINA is set in Montserrat, the sub-line
   "Luxury Tan" keeps the body sans it always had. Montserrat runs wider
   than the Playfair that used to sit here, so the weight comes up a step
   and the pair's gap comes down to keep the lockup balanced.
   MINA's tracking was narrowed from 0.14em; the margin hands back the
   trailing space the last letter used to carry, so "Luxury Tan" keeps
   exactly the distance from MINA it had before. */
.brand__mark {
  font-family: var(--font-brand);
  font-size: 1.45rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  margin-right: 0.1em;
  text-transform: uppercase;
}
.brand__sub {
  font-family: var(--font-body);
  font-size: 0.56rem;
  font-weight: 500;
  letter-spacing: 0.4em;
  text-transform: uppercase;
  color: var(--muted);
}

.nav__links { display: flex; align-items: center; gap: clamp(1.6rem, 3vw, 3rem); list-style: none; margin: 0; padding: 0; }
.nav__links a {
  position: relative;
  font-size: var(--fs-label);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted);
  padding-block: 0.5rem;
  transition: color 0.3s var(--ease);
}
.nav__links a::after {
  content: "";
  position: absolute; left: 0; bottom: 0;
  width: calc(100% - var(--track-label));   /* stop short of the trailing tracking */
  height: 1px;
  background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.4s var(--ease-out);
}
.nav__links a:hover,
.nav__links a[aria-current="page"] { color: var(--strong); }
.nav__links a:hover::after,
.nav__links a[aria-current="page"]::after { transform: scaleX(1); }

.brand, .nav__links a, .nav__cta > * { white-space: nowrap; }

.nav__cta { display: inline-flex; align-items: center; gap: 1.5rem; }
.nav__cta .btn { padding: 0.8rem 1.4rem 0.8rem calc(1.4rem + 0.22em); }
.nav__phone {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-size: 0.74rem; font-weight: 500; letter-spacing: 0.12em; color: var(--text);
  transition: color 0.3s var(--ease);
}
.nav__phone svg { width: 14px; height: 14px; color: currentColor; }
.nav__phone:hover { color: var(--strong); }

/* The admin entry point sits in the footer's bottom rule instead — see
   .admin-link further down — so the header carries only the client journey. */

.nav__toggle {
  display: none;
  width: 44px; height: 44px;
  border: 1px solid var(--line-strong);
  border-radius: 0;
  background: transparent;
  cursor: pointer; padding: 0; place-items: center;
}
.nav__toggle span, .nav__toggle span::before, .nav__toggle span::after {
  content: ""; display: block; width: 16px; height: 1px; background: var(--text);
  transition: transform 0.35s var(--ease), opacity 0.2s var(--ease);
}
.nav__toggle span { position: relative; }
.nav__toggle span::before { position: absolute; top: -5px; left: 0; }
.nav__toggle span::after  { position: absolute; top: 5px;  left: 0; }
body.menu-open .nav__toggle span { background: transparent; }
body.menu-open .nav__toggle span::before { transform: translateY(5px) rotate(45deg); }
body.menu-open .nav__toggle span::after  { transform: translateY(-5px) rotate(-45deg); }

/* ------------------------------------------------------------------ */
/* Hero — the cover                                                    */
/* ------------------------------------------------------------------ */
/* Three layers: the photograph, a scrim over it, and the type. The MINA
   masthead and the masked cut-out of the couple that used to sit between
   them were removed on 2026-09-19 — see index.html. */
.hero {
  position: relative;
  width: 100%;
  height: 100vh;
  height: 100svh;
  min-height: 600px;
  overflow: hidden;
  color: #ffffff;
}
.hero__bg {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
/* Darkened at the top so the nav and dateline read over the lobby, and
   weighted to the bottom-left, where the headline sits over the groom's
   white jacket. The couple in the middle of the frame keeps most of its
   light. */
.hero__overlay {
  position: absolute; inset: 0;
  background:
    linear-gradient(to bottom, rgba(10, 10, 10, 0.66) 0%, rgba(10, 10, 10, 0) 30%),
    linear-gradient(to top right, rgba(10, 10, 10, 0.7) 0%, rgba(10, 10, 10, 0.34) 38%, rgba(10, 10, 10, 0) 62%),
    linear-gradient(to top, rgba(10, 10, 10, 0.8) 0%, rgba(10, 10, 10, 0.3) 46%, rgba(10, 10, 10, 0.16) 100%);
}

.hero__cover {
  position: relative;
  height: 100%;
  width: 100%;
  max-width: var(--maxw);
  margin-inline: auto;
  /* clears the fixed 84px nav */
  padding: 84px var(--gutter) clamp(3rem, 8svh, 5.5rem);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
}

/* The dateline: a hairline across the measure under the nav, carrying the
   studio's claim the way a cover carries its issue line. */
.hero__dateline {
  margin: 0;
  padding-top: 0.9rem;
  border-top: 1px solid rgba(255, 255, 255, 0.32);
  font-family: var(--font-body);
  font-size: var(--fs-label); font-weight: 500; line-height: 1.4;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: rgba(255, 255, 255, 0.86);
  animation: hero-fade 1.4s var(--ease-out) 0.5s both;
}
/* The line is set in nowrap pieces, one per "│" segment, so on a phone
   it can only break at a separator — never between "Spray" and "Tan". */
.hero__dateline span { white-space: nowrap; }

/* The headline, at the foot of the cover. Three lines broken by sense
   rather than by the measure, and scaled against the viewport height too,
   so a short laptop screen still holds all three on the picture. */
.hero__title {
  grid-row: 3;
  align-self: end;
  position: relative;
  z-index: 1;
  margin: 0;
  color: #ffffff;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.9rem, min(7.4vw, 12.5svh), 7.5rem);
  line-height: 0.88;
  letter-spacing: -0.035em;
  animation: hero-in 1.2s var(--ease-out) 0.15s both;
}
.hero__title span { display: block; white-space: nowrap; }
/* The middle line steps in, so the headline reads as set type rather than
   a paragraph that happens to be large. */
.hero__title span:nth-child(2) { padding-left: 0.62em; }

@keyframes hero-in { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes hero-fade { from { opacity: 0; } to { opacity: 1; } }

@media (max-width: 760px) {
  .hero__dateline { font-size: 0.6rem; letter-spacing: 0.18em; }
  .hero__title span:nth-child(2) { padding-left: 0.45em; }
}

/* ------------------------------------------------------------------ */
/* Partner marquee (typographic, seamless)                             */
/* ------------------------------------------------------------------ */
/* A thin ribbon rather than a band: the block padding is roughly halved
   and the marks below are scaled down with it, so the strip reads as a
   credit line and stops competing with the hero above it. Tightened
   again on 2026-09-21 — the track now sits close to its own hairlines
   instead of floating between them, which is what made the ribbon read
   as a section in its own right rather than as a credit line. */
.partners { border-block: 1px solid var(--line); padding-block: clamp(0.45rem, 0.7vw, 0.6rem); overflow: hidden; }
/* The ribbon is flush to the hero above it — .hero is 100svh and carries
   no bottom padding — so the only gap that could hold it away from its
   neighbours is the one below, and a full --section-y-tight there left
   the strip stranded. The section that follows opens on a fraction of
   its usual facing padding instead; the marquee exists on index.html
   alone, so this reaches exactly one pair of sections. */
.partners + .section { padding-top: clamp(2.5rem, 4vw, 3.5rem); }
/* Kicker centred over the track. */
.partners__kicker {
  display: block;
  text-align: center;
  margin-bottom: clamp(0.4rem, 0.7vw, 0.55rem);
  font-family: var(--font-body);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--muted-2);
}
.marquee {
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
}
/* Composited outright: the track is the one thing on the page animating
   continuously, so it earns a layer of its own and moves on translate3d
   rather than on any property that would lay the row out again. */
.marquee__track {
  display: flex; width: max-content;
  animation: scroll-x 43s linear infinite;
  will-change: transform;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
.marquee__group { display: flex; align-items: center; flex: none; }
.partner {
  display: inline-flex; align-items: center;
  padding-inline: clamp(2.4rem, 4.6vw, 4.5rem);
  font-family: var(--font-display);
  font-size: clamp(0.85rem, 1.2vw, 1.05rem);
  font-weight: 400;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  transition: color 0.35s var(--ease);
}
/* Each partner is an outbound link. The duplicate marquee group is
   aria-hidden and its links carry tabindex="-1", so the row is tabbed
   through once, not twice. */
.partner:hover,
.partner:focus-visible { color: var(--strong); }
.partner__name { line-height: 1; }
/* Schäs House sets its own wordmark in Poppins rather than the display
   serif the other partners take, to match its real logo. Poppins is loaded
   on index.html only — the marquee exists nowhere else. */
.partner__name--brand {
  font-family: "Poppins", var(--font-body);
  font-weight: 500;
  letter-spacing: 0.14em;
}

/* The supplied logo is white artwork in a square canvas that is 98%
   transparent padding, so it is shown in a box shaped to the wordmark and
   cropped to it — sizing by height alone would render it microscopic.
   The artwork is white, which would be invisible on the white canvas, so
   brightness(0) crushes it to black the same way the credit-line marks
   are handled. Muted to sit with the partner names, full black on hover
   like them. */
.partner__logo {
  width: clamp(6rem, 9.5vw, 8rem);
  aspect-ratio: 5.3 / 1;
  height: auto;
  object-fit: cover;
  object-position: center;
  filter: brightness(0);
  opacity: 1;
}
/* Estetica's wordmark is cropped from their stacked logo to the lettering
   alone, so unlike the Noctis canvas it needs no cover crop: it is sized by
   height to sit at the cap height of the serif names beside it. */
.partner__logo--estetica {
  width: auto;
  height: clamp(0.6rem, 0.82vw, 0.72rem);
  aspect-ratio: 900 / 123;
  object-fit: contain;
  opacity: 0.68;
  transition: opacity 0.35s var(--ease);
}
/* Its strokes are heavier than the serif names, so it is held back a
   shade at rest and comes up to full black on hover like the rest. */
.partner:hover .partner__logo--estetica,
.partner:focus-visible .partner__logo--estetica { opacity: 1; }
@keyframes scroll-x { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-50%, 0, 0); } }

/* ------------------------------------------------------------------ */
/* Services — the menu / price list                                    */
/* ------------------------------------------------------------------ */
/* The homepage teaser: the section title sits on the image, and the whole
   banner is the way through to the menu on /services. */
.menu__banner {
  display: block;
  margin-top: clamp(2rem, 4vw, 3rem);
  position: relative;
  height: clamp(260px, 34vw, 440px);
  overflow: hidden;
}
/* The vignette was cut back so the photograph reads: brightness lifted
   from 0.6 to 0.82 and the gradient roughly halved. It still carries
   enough weight at the foot of the frame, where the caption sits, to hold
   white type — the hero's own overlay is untouched and stays as dark. */
.menu__banner img {
  width: 100%; height: 100%; object-fit: cover; filter: brightness(0.82) saturate(0.98);
  transition: transform 0.9s var(--ease-out), filter 0.6s var(--ease);
}
.menu__banner::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(10, 10, 10, 0.62) 0%, rgba(10, 10, 10, 0.16) 55%, rgba(10, 10, 10, 0.04) 100%);
}
/* Title and cue share one anchored block so the cue can sit under the
   title without either being positioned separately. */
.menu__banner-caption {
  position: absolute; z-index: 1;
  left: clamp(1.4rem, 3.5vw, 3rem);
  right: clamp(1.4rem, 3.5vw, 3rem);
  bottom: clamp(1.4rem, 3vw, 2.6rem);
}
.menu__banner-title {
  display: block;
  max-width: 16ch;
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(2rem, 4.6vw, 3.5rem); line-height: 1.05; letter-spacing: -0.02em;
  color: #ffffff;
}
/* The banner is already the link, so the cue is a span — a nested <a>
   would be invalid markup. It reads as a rule-underlined label. */
.menu__banner-cta {
  display: inline-block;
  margin-top: clamp(0.75rem, 1.4vw, 1.1rem);
  padding-bottom: 0.3rem;
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase;
  /* Text and rule both on the accent. This cue sits over the darkened
     banner photograph rather than the white canvas, so the accent has
     real contrast to work with here. */
  color: var(--accent);
  border-bottom: 1px solid rgba(119, 213, 221, 0.55);
  transition: border-color 0.35s var(--ease);
}
.menu__banner:hover .menu__banner-cta,
.menu__banner:focus-visible .menu__banner-cta { border-bottom-color: var(--accent); }
.menu__banner:hover img { transform: scale(1.02); filter: brightness(0.9) saturate(1); }

/* The menu renders on both / and /services: one ruled block per category,
   its title and description hung from the same line, then a grid of
   service cards. */
/* Tightened: the categories were reading as separate pages rather than one
   menu. Roughly half the previous vertical rhythm. */
.menu-page { margin-top: clamp(1.75rem, 3vw, 2.75rem); }
.menu-cat { padding-block: clamp(1.5rem, 2.6vw, 2.4rem); }
.menu-cat:first-child { padding-top: 0; }
.menu-cat + .menu-cat { border-top: 1px solid var(--line); }
/* Booking Policy is the exception on both routes: it sits under the last
   category on space alone, so /services sets it exactly as the homepage. */
.menu-cat:has(> .menu-policy) { border-top: 0; }

/* Title and standfirst used to hang from the same grid line, which left
   the description marooned on the far side of the block. It now sits
   directly under its own title as a subtitle, so each category reads as
   one unit above its cards. */
/* No rule under the title or its standfirst on either route: the space
   above the first row is what separates a category from its own copy. */
.menu-cat__head {
  display: block;
  padding-bottom: 0.9rem;
}
.menu-cat__head h2 { font-size: clamp(1.9rem, 3.4vw, 2.9rem); font-weight: 400; letter-spacing: -0.015em; }
.menu-cat__note {
  margin: 0.55rem 0 0;
  font-family: var(--font-body); font-style: italic;
  font-size: 0.875rem; font-weight: 300; line-height: 1.65;
  color: var(--muted-2); max-width: 54ch;
}

/* One card per service, and the whole box is the booking trigger — a far
   larger hit area than the hairline rows this replaced. Square corners and
   a neutral-800 hairline keep the cards inside the editorial system; the
   accent goes on the hover outline and glow, and on the small rinse-time
   note — never on the service name or price. */
.svc-grid {
  list-style: none; margin: 0; padding: 1.6rem 0 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
  gap: clamp(0.75rem, 1.4vw, 1.1rem);
}
.svc-grid > li { display: flex; }

.svc-card {
  width: 100%; margin: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 0.5rem;
  padding: clamp(1.5rem, 2.4vw, 2rem);
  /* The marble itself is applied further down, to both routes. This is the
     slate body it sits on, and the fallback if the photograph never
     arrives. */
  background-color: var(--surface);
  border: 1px solid var(--line);       /* neutral-800 */
  border-radius: var(--radius);
  font: inherit; letter-spacing: inherit; color: inherit;
  text-align: left; cursor: pointer;
  transition: border-color 0.35s var(--ease), background-color 0.35s var(--ease),
              box-shadow 0.35s var(--ease), transform 0.35s var(--ease-out);
}
.svc-card__name {
  display: block;
  font-family: var(--font-display); font-size: 1.35rem; font-weight: 400;
  line-height: 1.2; letter-spacing: -0.01em; color: var(--text);
}
/* The rinse-time line carries the accent so it lifts off the marble. */
.svc-card__note {
  display: block;
  font-size: 0.62rem; font-weight: 500; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--accent);
}
.svc-card__foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  width: 100%; margin-top: auto; padding-top: 1.4rem;
}
.svc-card__price {
  font-family: var(--font-display); font-size: 1.5rem; font-weight: 400; color: var(--text);
  font-variant-numeric: lining-nums tabular-nums;
}
.svc-card__price .alt { font-family: var(--font-body); color: var(--muted); font-size: 0.75rem; font-weight: 300; }
.svc-card__cue {
  flex: none;
  font-family: var(--font-body); font-size: 0.6rem; font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
  opacity: 0; transform: translateX(-5px);
  transition: opacity 0.3s var(--ease), transform 0.35s var(--ease-out), color 0.3s var(--ease);
}
.svc-card:hover,
.svc-card:focus-visible {
  background-color: var(--surface-2);   /* background-color, so the veining survives the hover */
  border-color: var(--accent);
  box-shadow: 0 0 0 1px rgba(119, 213, 221, 0.4), 0 22px 60px -28px rgba(119, 213, 221, 0.55);
  transform: translateY(-2px);
}
.svc-card:hover .svc-card__cue,
.svc-card:focus-visible .svc-card__cue { opacity: 1; transform: none; color: var(--strong); }
.svc-card:focus-visible { outline: 1px solid var(--accent); outline-offset: 4px; }

@media (prefers-reduced-motion: reduce) {
  .svc-card, .svc-card:hover, .svc-card:focus-visible { transform: none; }
}

/* On the homepage the menu is a scannable index rather than a showcase, so
   the price comes up beside the title instead of sitting under it and the
   card loses roughly half its height. The markup is untouched — both
   routes render through one renderMenu() — so this is purely a re-flow of
   the same three spans onto a two-column grid: title and note stack in the
   first column, the price spans both rows in the second. /services keeps
   the taller cards. */
/* A wider minimum than /services asks for. The compact row carries the
   name and the price on one line, so the track has to hold both: at the
   shared 268px the longest pair — "Half Up / Half Down" against
   "$150 · w/ blowout $190" — drove the title four lines deep. */
body.is-home .svc-grid { grid-template-columns: repeat(auto-fill, minmax(336px, 1fr)); }
body.is-home .svc-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: clamp(0.75rem, 1.6vw, 1.25rem);
  row-gap: 0.15rem;
  padding: clamp(0.95rem, 1.7vw, 1.25rem) clamp(1.1rem, 2vw, 1.5rem);
}
body.is-home .svc-card__name { grid-column: 1; font-size: 1.1rem; }
body.is-home .svc-card__note { grid-column: 1; }
body.is-home .svc-card__foot {
  grid-column: 2; grid-row: 1 / -1;
  width: auto; margin-top: 0; padding-top: 0;
  align-items: center; justify-content: flex-end;
}
/* The alternative price ("· w/ blowout $190") drops beneath the main one
   and right-aligns, so a two-part price cannot squeeze the title. */
body.is-home .svc-card__price { font-size: 1.2rem; text-align: right; }
body.is-home .svc-card__price .alt { display: block; margin-top: 0.15rem; }
/* The compact row does not need the hover cue: the accent outline and the
   cursor already say the whole box is a booking trigger, and dropping it
   keeps the price hard against the right edge. */
body.is-home .svc-card__cue { display: none; }

/* Literal white marble on the service cards of BOTH routes — /services was
   left on the old CSS veining and had drifted out of sync. The photograph
   is a fine-grained Carrara, chosen over the more dramatic Calacatta
   options because heavy veining fights the type (Pexels 1323712, self-hosted
   since 2026-10-10 like every other image), and it sits under a white
   scrim so names and prices keep their contrast. The scrim thins on hover,
   letting the stone come forward while the accent outline and glow do the
   real signalling. The URL lives in a custom property so the two states
   cannot drift. Every card gets it from the class alone, so a service added
   in the admin panel is on marble the moment it renders. Only the texture is shared: the compact grid above stays
   scoped to the homepage, so /services keeps its taller boxes. */
.svc-card {
  --marble: url("/assets/services/marble.jpg");
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.74), rgba(255, 255, 255, 0.74)),
    var(--marble);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.svc-card:hover,
.svc-card:focus-visible {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.58), rgba(255, 255, 255, 0.58)),
    var(--marble);
}

.menu-extra {
  margin: 1.6rem 0 0; padding-top: 1.3rem; border-top: 1px solid var(--line);
  font-size: 0.85rem; color: var(--muted); line-height: 2;
}
.menu-extra b {
  color: var(--text); font-weight: 500;
  font-size: var(--fs-label); letter-spacing: var(--track-label); text-transform: uppercase;
}
.menu-extra .sep { color: var(--strong); margin-inline: 0.5rem; }

/* On the homepage the categories are parted by space alone: the rules
   above Mobile Hair, Mobile Makeup, Booking Policy and the Enhancements
   line are all dropped. /services keeps them — it is the dense reference
   page, where the rules help the eye track across a long list. Both
   routes still render through one renderMenu(); only this hairline
   differs between them. */
body.is-home .menu-cat + .menu-cat { border-top: 0; }
body.is-home .menu-extra { border-top: 0; padding-top: 0.5rem; }

.menu-policy { list-style: none; margin: 0; padding: 0; }
/* The policy lines drop the muted grey for --text, which is the exact
   value the h2 above them inherits, so the block reads at one weight of
   black rather than two. They are parted by space alone — the hairlines
   between them came out on 2026-09-19 — so the padding carries the
   separation on its own. */
/* Since 2026-10-08 the lines sit close together as one compact block,
   each led by a small teal dot instead of resting on space alone. */
.menu-policy li {
  position: relative;
  padding: 0.2rem 0 0.2rem 1.15rem;
  font-size: 0.92rem; color: var(--text); line-height: 1.75;
}
.menu-policy li::before {
  content: ""; position: absolute; left: 0; top: calc(0.2rem + 0.875em - 3px);
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}
.menu-policy b { color: var(--text); font-weight: 500; }

/* Booking Policy reads as plain text: nothing between its lines, and no
   rule under the block either — the section rule that normally parts two
   sections is dropped after the menu on both routes. */
#services + .section,
#menu + .section { background-image: none; }

/* ------------------------------------------------------------------ */
/* Experience — we come to you                                         */
/* ------------------------------------------------------------------ */
/* Booking Policy closes the menu, How it works follows it, and "Our work
   was seen on" follows that. Full section rhythms between them left dead
   zones of 200px and more, so on the homepage all three give up most of
   their facing padding: the hairline over How it works sits about equally
   spaced between the policy and the kicker, and the Featured kicker comes
   in a short gap after the steps. */
body.is-home #services { padding-bottom: clamp(0.5rem, 1vw, 0.75rem); }
body.is-home #experience {
  padding-top: clamp(2.25rem, 3.5vw, 3.25rem);
  padding-bottom: clamp(1.5rem, 2.2vw, 2rem);
}
body.is-home .seen { padding-top: clamp(1.5rem, 2.2vw, 2rem); }

/* A feature spread. The left-hand page carries the story — display
   headline, drop-capped standfirst, the steps as narrow columns — and the
   right-hand page is the photograph, dropped to the foot of the spread so
   headline and picture sit on a diagonal. Everything is flush left and
   ragged right, as set type would be. */
.spread__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(15rem, 27vw, 24rem);
  column-gap: clamp(3rem, 7vw, 8rem);
}
.spread__kicker { grid-column: 1 / -1; justify-self: start; }

.spread__title {
  grid-column: 1; grid-row: 2;
  margin-top: clamp(1.5rem, 2.8vw, 2.5rem);
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 5.3vw, 5.5rem);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--strong);
}
.spread__title span { display: block; white-space: nowrap; }

/* The standfirst: small, tracked sans in a tight measure, opened by a
   three-line drop cap in the display serif. The cap is floated rather
   than set with initial-letter so it lands identically in every browser;
   its size is the depth of three lines of this text, so the top of the
   cap aligns with the cap height of the first line and its foot with the
   baseline of the third. */
.spread__standfirst {
  grid-column: 1; grid-row: 3;
  margin: clamp(2rem, 3.4vw, 3rem) 0 0;
  max-width: 19.5rem;
  font-family: var(--font-body);
  font-size: 0.9rem; font-weight: 400; line-height: 1.9;
  letter-spacing: 0.045em;
  color: var(--text);
}
.spread__standfirst::first-letter {
  float: left;
  margin: 0.035em 0.08em 0 -0.03em;
  font-family: var(--font-display);
  font-size: 9.1em; font-weight: 400;
  line-height: 0.72;
  letter-spacing: 0;
  color: var(--strong);
}

/* The steps: three narrow columns parted by column rules, under a rule
   of their own, set close beneath the standfirst that introduces them. No
   numerals — the step name keeps its accent label and the copy is small
   tracked sans. */
.spread__steps {
  grid-column: 1; grid-row: 4;
  list-style: none;
  margin: clamp(1.5rem, 2.4vw, 2.1rem) 0 0; padding: clamp(1.5rem, 2.2vw, 1.9rem) 0 0;
  border-top: 1px solid var(--line);
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
}
.spread__steps li { padding-inline: clamp(1.1rem, 2vw, 1.9rem); }
.spread__steps li:first-child { padding-left: 0; }
.spread__steps li:last-child { padding-right: 0; }
.spread__steps li + li { border-left: 1px solid var(--line); }
/* Book / We arrive / You glow take the accent. Size and weight are
   deliberately unchanged — only the colour moves. */
.spread__steps h3 {
  margin-top: 0;
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500; line-height: 1.6;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--accent);
}
.spread__steps p {
  margin: 0.7rem 0 0;
  max-width: 26ch;
  font-size: 0.78rem; font-weight: 400; line-height: 1.9;
  letter-spacing: 0.05em;
  color: var(--muted);
}

/* The photograph on the right-hand page, cropped to a print portrait. */
.spread__figure {
  grid-column: 2; grid-row: 2 / 5;
  align-self: end;
  margin: 0;
}
.spread__figure img {
  width: 100%; height: auto; aspect-ratio: 2 / 3; object-fit: cover;
  filter: brightness(0.94) saturate(0.95);
}


/* ------------------------------------------------------------------ */
/* Social proof — "Our work was seen on"                               */
/* ------------------------------------------------------------------ */
/* Took the place of the old 24/7 concierge feature band. A ruled index of
   client marks rather than a carousel, so it reads as a masthead credit
   line and not as advertising. */
.seen { padding-block: var(--section-y-tight); }
.seen__title {
  font-family: var(--font-display); font-size: var(--fs-h2); font-weight: 400;
  letter-spacing: -0.02em; line-height: 1.05; margin: 1.5rem 0 0; max-width: 18ch;
}
/* Unruled: the marks are separated by space alone now, so the colour in
   them is the only thing framing the row. Four to a row, and a short last
   row is centred under the full one rather than left hanging. */
.seen__grid {
  list-style: none; margin: clamp(3rem, 6vw, 4.5rem) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center;
}
.seen__item {
  flex: 0 0 25%;
  display: flex; align-items: center; justify-content: center;
  min-height: clamp(112px, 13vw, 152px);
  padding: 1.5rem clamp(1rem, 2.5vw, 2rem);
}

/* The marks run in their own brand colours. UFC is its red, Playboy its
   black wordmark. WWE and The Dance Awards both shipped white lettering
   that would have disappeared against this canvas, so the white in our
   self-hosted copies was recoloured to near-black — the two-colour form
   brands use on light backgrounds — and their other colours were left
   alone. */
.seen__logo {
  width: auto; height: clamp(26px, 3.1vw, 38px); max-width: 100%;
  opacity: 1;
}
/* The Dance Awards stacks its name over three lines, so at the one-line
   height of the others it would read at a third of their size. */
.seen__logo--stacked { height: clamp(52px, 5.6vw, 70px); }

/* The GRAMMY gramophone is a mark without a name, so it is paired with
   the event set in the same serif wordmark as BravoCon. */
.seen__item--lockup { flex-direction: column; gap: 0.9rem; }
.seen__mark { width: auto; height: clamp(40px, 4.4vw, 54px); }

/* BravoCon and Sin City Swim Week publish no logo anywhere citable, so
   they are set as wordmarks in the display serif — the same treatment the
   partner marquee already gives its text-only names — rather than shipping
   a wrong or invented image alongside the real marks. */
.seen__wordmark {
  font-family: var(--font-display); font-size: clamp(1.05rem, 1.7vw, 1.35rem);
  font-weight: 400; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted); text-align: center; line-height: 1.3;
  text-wrap: balance;
  transition: color 0.35s var(--ease);
}
.seen__item:hover .seen__wordmark { color: var(--strong); }

/* ------------------------------------------------------------------ */
/* Partner profile cards — /about                                      */
/* ------------------------------------------------------------------ */
/* A card apiece rather than a row of marks: the mark on a plate at the
   top, the name under it, two sentences under that. A hairline defines
   the card — no fill, no shadow, nothing rounded — and the whole card is
   the link out. */
.partner-cards {
  list-style: none; margin: clamp(3rem, 6vw, 4.5rem) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.75rem);
}
.partner-cards > li { display: flex; }
/* Our Partners closes /about, and a full --section-y-tight above it left
   the chess spread and the cards reading as two unrelated pages. Both
   halves of that gap have to come down or the reduction is only half
   spent: the section opens on about a third of its facing padding, and
   the block above gives up the same on its way out. :has() pairs the two
   here rather than putting a one-off class on the markup — and if it ever
   fails to match, the gap simply returns to the old rhythm.

   Only the facing padding moves. The hairline over the section and the
   air between the title and the cards are both untouched. */
#partners { padding-top: clamp(2.5rem, 4vw, 3.5rem); }
.section:has(+ #partners) { padding-bottom: clamp(2.5rem, 4vw, 3.5rem); }

/* The padding and the plate height are tokens rather than literals because
   the cover variant below has to bleed back out by exactly the padding it
   sits inside, and has to give back exactly the height it steals. Two
   copies of these clamps would drift the first time one was edited. */
.partner-card {
  --card-pad-x:      clamp(1.5rem, 2.4vw, 2.25rem);
  --card-pad-top:    clamp(2rem, 3vw, 2.75rem);
  --card-pad-bottom: clamp(2.25rem, 3.2vw, 3rem);
  --plate-h:         clamp(88px, 9vw, 112px);

  width: 100%;
  display: flex; flex-direction: column; align-items: center;
  padding: var(--card-pad-top) var(--card-pad-x) var(--card-pad-bottom);
  background: #fafafa;
  border: 1px solid var(--line);
  border-radius: 0;
  text-align: center;
  transition: border-color 0.35s var(--ease), background-color 0.35s var(--ease);
}
.partner-card:hover,
.partner-card:focus-visible { border-color: var(--line-strong); background: #ffffff; }

/* The plate keeps every mark on one baseline whatever shape it is. */
.partner-card__plate {
  display: flex; align-items: center; justify-content: center;
  height: var(--plate-h);
}

/* Noctis carries a photograph behind its mark — the estate rentals the
   biography is about — and Schäs House and Status do the same (below). It fills
   the top of the card outright: stretched to the full width, then bled back
   out through the card's side and top padding so it meets three of the
   card's edges with no margin left showing.

   The height is the plain plate plus the top padding it just cancelled, so
   the bottom edge of the photograph lands exactly where the other two
   plates end — the three names stay on the one baseline the plate exists
   to hold, and the cover costs the card no extra height. */
.partner-card__plate--cover {
  position: relative;
  align-self: stretch;
  margin: calc(-1 * var(--card-pad-top)) calc(-1 * var(--card-pad-x)) 0;
  height: calc(var(--plate-h) + var(--card-pad-top));
  overflow: hidden;
}
.partner-card__cover {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center;
}
/* The photograph is lit unevenly — dusk sky one side, lamplit stone the
   other — so the mark cannot rely on it alone. A scrim weighted downward
   evens it out and keeps the white N off the bright pavilion. */
.partner-card__plate--cover::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(10, 10, 11, 0.2), rgba(10, 10, 11, 0.5));
}
/* The Noctis monogram is black artwork on a square canvas with its own
   margin, so it is shown whole — contained, never cropped — and forced to
   solid black in case the source is ever swapped for a coloured one. */
.partner-card__logo {
  width: auto; height: 100%; max-height: clamp(72px, 7.5vw, 96px);
  object-fit: contain;
  filter: brightness(0);
}
/* Over the photograph the same artwork is whited out instead: brightness(0)
   first flattens it to black whatever colour the source is ever swapped
   for, then invert(1) lifts that to white — the move the credit-line marks
   make. The shadow is what holds it against the lighter passages of the
   scrim; it is on the mark, not on a box behind it, so nothing boxes in a
   layout that has no filled shapes anywhere. */
.partner-card__logo--on-cover {
  position: relative; z-index: 2;
  filter: brightness(0) invert(1) drop-shadow(0 1px 10px rgba(10, 10, 11, 0.55));
}
/* Schäs House and Status publish no mark, so they are set as wordmarks —
   Schäs House in Poppins to match its real logo, the same exception the
   homepage marquee makes. */
.partner-card__wordmark {
  font-family: var(--font-display);
  font-size: clamp(1.45rem, 2.2vw, 1.9rem); font-weight: 400;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--strong); line-height: 1.2; text-wrap: balance;
}
/* Status carries its own photograph too — the brut poured over a heel,
   bright white studio light — on the same cover plate as Noctis. Its
   wordmark is type rather than artwork, so it is set in white directly,
   with the same shadow the Noctis mark uses; the shared scrim takes the
   white ground down far enough to hold it. The crop sits high so the
   bottle and the coupe stay in the band. */
.partner-card__cover--status { object-position: center 28%; }
/* Schäs House gets one too — a living room of theirs, the chandelier over
   the sculpture and console — under the same scrim and white wordmark. The
   picture is portrait, so the crop sits on the chandelier's lower tiers
   and the sculpture, the band where the room reads at plate height. */
.partner-card__cover--schas { object-position: center 38%; }
/* Estetica's is one of their own treatment rooms — a facial under way.
   The crop sits a little high so the practitioner's face stays in the band
   behind the whited-out monogram. */
.partner-card__cover--estetica { object-position: center 30%; }
.partner-card__wordmark--on-cover {
  position: relative; z-index: 2;
  color: #ffffff;
  text-shadow: 0 1px 12px rgba(10, 10, 11, 0.6);
}
.partner-card__wordmark--brand {
  font-family: "Poppins", var(--font-body);
  font-weight: 500; letter-spacing: 0.08em;
}

.partner-card__name {
  margin: clamp(1.4rem, 2.2vw, 1.9rem) 0 0;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 1.9vw, 1.65rem); font-weight: 400;
  line-height: 1.15; letter-spacing: -0.015em;
  color: var(--strong);
}
.partner-card__bio {
  margin: clamp(0.9rem, 1.4vw, 1.2rem) 0 0;
  max-width: 34ch;
  font-size: 0.88rem; font-weight: 300; line-height: 1.85;
  color: var(--muted);
}

/* Four cards since Estetica joined (2026-10-12): four across only where
   each still holds its 34ch biography, two by two below that. */
@media (max-width: 1180px) {
  .partner-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); max-width: 52rem; margin-inline: auto; }
}
@media (max-width: 860px) {
  .partner-cards { grid-template-columns: minmax(0, 1fr); max-width: 30rem; }
  .partner-card__bio { max-width: 42ch; }
}

@media (max-width: 860px) {
  .seen__item { flex-basis: 50%; }
}

/* ------------------------------------------------------------------ */
/* Reviews — the rating as a magazine stat, the quote as a pull quote  */
/* Left-aligned on a two-column grid, split by a column rule.          */
/* ------------------------------------------------------------------ */
.reviews { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); align-items: stretch; }

.rating { padding-right: var(--col-gap); }
/* The two star rows are the one sanctioned exception to the rule that the
   accent never touches type: these are rating markers rather than reading
   copy, the same category as the footer's pulse dot. */
.rating__stars { font-size: 0.95rem; letter-spacing: 0.3em; color: var(--accent); }
.rating__line {
  margin: 1.4rem 0 0; font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500; line-height: 1.6;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
}
/* the score itself is set as a display numeral on its own line */
.rating__line [data-rating-value] {
  display: block;
  margin: 0.6rem 0 1rem;
  font-family: var(--font-display);
  font-size: clamp(4.5rem, 9vw, 7rem);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: -0.03em;
  text-transform: none;
  color: var(--strong);
  font-variant-numeric: lining-nums;
}
.rating__sep { display: none; }
.rating__src {
  margin: 1.1rem 0 0; font-size: 0.62rem; letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--muted);
}
.rating__src[hidden] { display: none; }

.quotes { min-width: 0; padding-left: var(--col-gap); border-left: 1px solid var(--line); }

/* every slide shares one grid cell, so the stage is as tall as the tallest
   quote and nothing shifts as they cycle */
.quotes__stage { display: grid; }
.quote {
  grid-area: 1 / 1;
  margin: 0;
  display: flex; flex-direction: column; align-items: flex-start;
  opacity: 0; visibility: hidden; transform: translateY(10px);
  /* Slides share one grid cell, so the outgoing quote must be gone before the
     next arrives - otherwise two texts sit superimposed mid-fade. Out first,
     in on a delay. */
  transition: opacity 0.32s var(--ease), visibility 0s linear 0.32s, transform 0s linear 0.32s;
}
.quote.is-active {
  opacity: 1; visibility: visible; transform: none;
  transition: opacity 0.55s var(--ease-out) 0.3s, transform 0.55s var(--ease-out) 0.3s;
}
.quote__stars { color: var(--accent); font-size: 0.8rem; letter-spacing: 0.3em; }
.quote blockquote { margin: 1.6rem 0 0; }
.quote blockquote p {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: clamp(1.6rem, 3.1vw, 2.6rem);
  font-weight: 400; line-height: 1.28; letter-spacing: -0.01em;
  color: var(--text); text-wrap: pretty; max-width: 30ch;
}
.quote figcaption {
  margin-top: 2rem; display: flex; flex-direction: column; gap: 0.35rem;
  font-family: var(--font-body);
}
.quote figcaption b { font-size: var(--fs-label); font-weight: 500; letter-spacing: var(--track-label); text-transform: uppercase; color: var(--strong); }
/* The service a reviewer booked, set in the accent so it reads as a tag
   rather than as more caption copy. */
.quote figcaption span { font-size: 0.62rem; letter-spacing: var(--track-label); text-transform: uppercase; color: var(--accent); }

/* A review the client posted with a photo shows it on the left of the
   quote, as tall as the stars, quote and name beside it. The photo is
   size-contained, so it adds nothing to the row's height and is simply
   stretched to the text — the slide is still as tall as its words, and the
   stage still sizes itself to the tallest slide. The text column keeps the
   card's own type and spacing untouched. */
.quote--photo {
  display: grid;
  grid-template-columns: clamp(8.5rem, 13vw, 12.5rem) minmax(0, 1fr);
  column-gap: clamp(1.25rem, 2.6vw, 2.25rem);
  align-items: stretch;
}
.quote__photo {
  grid-column: 1;
  width: 100%; height: 100%; min-height: 12rem;
  contain: size;
  object-fit: cover; object-position: 50% 25%; display: block;
}
.quote__text { display: flex; flex-direction: column; align-items: flex-start; min-width: 0; }

.quotes__dots { display: flex; justify-content: flex-start; gap: 0.5rem; margin-top: clamp(2.4rem, 4vw, 3.2rem); }
.quotes__dots[hidden] { display: none; }
/* a 1px rule to look at, an 18px-tall button to hit */
.quotes__dot {
  position: relative; width: 34px; height: 18px; padding: 0; border: none; cursor: pointer;
  background: transparent;
}
.quotes__dot::before {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px;
  background: var(--line-strong);
  transition: background-color 0.4s var(--ease), transform 0.4s var(--ease-out);
}
.quotes__dot:hover::before { background: var(--muted); }
.quotes__dot.is-active::before { background: var(--strong); transform: scaleY(2); }

/* ------------------------------------------------------------------ */
/* Booking — calendar behind a column rule                             */
/* ------------------------------------------------------------------ */
/* Appointments closes /services directly under the menu, so it carries
   about half the standard section rhythm rather than the full one — the
   two blocks read as a single run. Nothing stands between Booking Policy
   and the calendar any more, so the menu gives up almost all of its foot
   and Appointments most of its head: the calendar opens a short step
   under the policy, the way How it works follows it on the homepage. */
#book.section { padding-block: clamp(3rem, 5vw, 4.5rem); }
#menu:has(+ #book) { padding-bottom: clamp(0.5rem, 1vw, 0.75rem); }
#menu + #book.section { padding-top: clamp(1.5rem, 2.5vw, 2.25rem); }

.booking { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); align-items: stretch; }
.booking__intro { padding-right: var(--col-gap); }
.booking__intro h2 { font-size: var(--fs-h2); margin-top: 1.5rem; }
.booking__intro .lead { margin-top: 1.6rem; }
.booking__contacts { margin-top: 2.4rem; display: flex; flex-wrap: wrap; gap: 0.75rem; }
.booking__contacts .btn { flex: 1 1 14rem; }   /* stack at matching widths rather than ragged */
/* On the page the calendar sits behind a column rule; the copy cloned into
   the modal lives outside .booking and gets none of this. */
.booking > .booking__panel { padding-left: var(--col-gap); border-left: 1px solid var(--line); }

.cal__head {
  display: flex; align-items: center; justify-content: space-between;
  padding-bottom: 1.25rem; margin-bottom: 1.25rem; border-bottom: 1px solid var(--line);
}
.cal__month { font-family: var(--font-display); font-size: 1.5rem; font-weight: 400; letter-spacing: -0.01em; font-variant-numeric: lining-nums; }
.cal__nav { display: inline-flex; gap: 0.5rem; }
.cal__nav button {
  width: 40px; height: 40px; border-radius: 0;
  border: 1px solid var(--line-strong); background: transparent; color: var(--text);
  cursor: pointer; display: grid; place-items: center;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease), opacity 0.3s var(--ease);
}
.cal__nav button svg { width: 15px; height: 15px; }
.cal__nav button:hover:not(:disabled) { border-color: var(--accent); color: var(--strong); }
.cal__nav button:disabled { opacity: 0.3; cursor: not-allowed; }

.cal__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.3rem; }
.cal__weekdays { margin-bottom: 0.5rem; }
.cal__weekdays span {
  text-align: center; padding-block: 0.4rem;
  font-size: 0.6rem; font-weight: 500; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted);
}

/* A fixed height rather than an aspect-ratio: a ratio plus a minimum height
   turns into a minimum width, and seven of those overflow a phone. */
.cal__day {
  height: clamp(2.75rem, 4.4vw, 3.6rem); min-width: 0; border: none; background: transparent; color: var(--text);
  border-radius: 0; font-family: var(--font-body); font-size: 0.88rem; cursor: pointer;
  font-variant-numeric: tabular-nums;
  display: grid; place-items: center;
  transition: background-color 0.25s var(--ease), color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}
.cal__day.is-empty { pointer-events: none; }
.cal__day:hover:not(:disabled):not(.is-selected) { box-shadow: inset 0 0 0 1px var(--accent); }
.cal__day.is-today:not(.is-selected) { box-shadow: inset 0 0 0 1px var(--line-strong); }
.cal__day.is-selected { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
.cal__day:disabled { color: var(--muted-2); opacity: 0.45; cursor: not-allowed; }

.cal__slots { margin-top: 1.6rem; padding-top: 1.5rem; border-top: 1px solid var(--line); display: none; }
.cal__slots.is-visible { display: block; animation: fade-up 0.5s var(--ease-out) both; }
.cal__slots h4 {
  font-family: var(--font-body); font-size: 0.64rem; font-weight: 500; line-height: 1.4;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted); margin: 0 0 1rem;
}
/* Thirteen hourly slots, so they sit on a tight grid of equal cells
   rather than a wrapping line of loose buttons: three short rows in the
   modal instead of a ragged fourth. */
.slot-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(4.6rem, 1fr)); gap: 0.4rem; }
.slot {
  border: 1px solid var(--line-strong); background: transparent; color: var(--text);
  border-radius: 0; padding: 0.55rem 0.4rem; font-family: var(--font-body); white-space: nowrap;
  font-size: 0.74rem; letter-spacing: 0.08em; cursor: pointer;
  transition: border-color 0.25s var(--ease), background-color 0.25s var(--ease), color 0.25s var(--ease);
}
/* Small, tracked and set in black so it cannot be missed under the slots;
   the accent is only the dot in front of it, never the type. */
.cal__note {
  display: flex; align-items: baseline; gap: 0.55rem;
  margin: 0.9rem 0 0;
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; line-height: 1.5;
  color: var(--strong);
}
.cal__note::before {
  content: ""; flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent); transform: translateY(-1px);
}
.slot:hover:not(.is-selected) { border-color: var(--accent); color: var(--strong); }
.slot.is-selected { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 600; }
/* Booked times are not rendered at all; this stands in for the row when
   every time on a day is gone. */
.slot-row__full { grid-column: 1 / -1; margin: 0; font-size: 0.85rem; font-weight: 300; line-height: 1.6; color: var(--muted); }

/* Service first (2026-10-11): each category books from its own calendar,
   so on /services the service is chosen in this list above the calendar,
   and until it is the calendar is locked — days disabled, no times, the
   button saying what is missing. Same box and chevron as the modal's
   Number of People select. */
.cal__service { display: block; margin-bottom: 1.6rem; }
.cal__service-label {
  display: block; margin-bottom: 0.45rem;
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--text);
}
.cal__service select {
  width: 100%; padding: 0.8rem 2.4rem 0.8rem 0.9rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  font-family: var(--font-body); font-size: 0.95rem; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 50%, calc(100% - 0.85rem) 50%;
  background-size: 0.3rem 0.3rem;
  background-repeat: no-repeat;
  transition: border-color 0.3s var(--ease);
}
.cal__service select:focus { outline: none; border-color: var(--accent); }
[data-calendar].is-locked .cal,
[data-calendar].is-locked .cal__slots h4 { opacity: 0.45; }

.booking__confirm { margin-top: 1.8rem; width: 100%; }
.booking__confirm.is-disabled { opacity: 0.45; pointer-events: none; }
@keyframes fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ------------------------------------------------------------------ */
/* Footer — a colophon of ruled columns                                */
/* ------------------------------------------------------------------ */
.footer { border-top: 1px solid var(--line); padding-top: clamp(4.5rem, 9vw, 7rem); padding-bottom: 2.5rem; background: var(--bg-soft); }
.footer__grid { display: grid; grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr)); }
.footer__grid > * { padding-inline: clamp(1.25rem, 2.6vw, 2.5rem); }
.footer__grid > :first-child { padding-left: 0; }
.footer__grid > :last-child { padding-right: 0; }
.footer__grid > * + * { border-left: 1px solid var(--line); }

.footer__brand .brand { margin-bottom: 1.4rem; }
.footer__brand .brand__mark { font-size: 2rem; }
.footer__brand p { margin: 0; color: var(--muted); font-size: 0.92rem; font-weight: 300; line-height: 1.8; max-width: 30ch; }
.footer__socials { display: flex; gap: 0.5rem; margin-top: 1.8rem; }
/* Instagram, Call and Location share one treatment: a white glyph in an
   accent outline that fills with the accent on hover or keyboard focus. */
.footer__socials a {
  width: 42px; height: 42px; display: grid; place-items: center;
  border: 1px solid var(--accent); color: var(--strong);
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease), background-color 0.3s var(--ease);
}
.footer__socials a:hover,
.footer__socials a:focus-visible { color: var(--accent-ink); background-color: var(--accent); }
.footer__socials svg { width: 17px; height: 17px; }

.footer__col h5 {
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500; line-height: 1.4;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--strong); margin: 0 0 1.4rem;
}
.footer__col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.8rem; }
.footer__col a, .footer__col li { color: var(--muted); font-size: 0.9rem; font-weight: 300; line-height: 1.6; transition: color 0.3s var(--ease); }
.footer__col a:hover { color: var(--strong); }
.footer__col .strong { color: var(--text); font-weight: 400; }

.footer__bottom {
  margin-top: clamp(3.5rem, 6vw, 5rem); padding-top: 1.6rem; border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
  color: var(--muted); font-size: 0.62rem; letter-spacing: 0.2em; text-transform: uppercase;
}
.footer__bottom .wip { display: inline-flex; align-items: center; gap: 0.6rem; }
.footer__bottom .wip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: pulse 2.4s var(--ease) infinite; }

/* Entry point to the admin panel, the quietest thing on the page, on the
   footer's bottom rule. Its address is not a secret: knowing it gets a
   visitor only the login form. Access is the password, sign-up being off,
   and the role check (see CLAUDE.md "Admin panel"). */
.admin-link {
  display: inline-flex; align-items: center; gap: 0.45rem;
  font-size: inherit; font-weight: 500; letter-spacing: inherit; text-transform: uppercase;
  color: var(--muted-2);
  transition: color 0.3s var(--ease);
}
.admin-link svg { width: 12px; height: 12px; color: currentColor; }
.admin-link:hover { color: var(--text); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ------------------------------------------------------------------ */
/* Reveal + mobile menu                                                */
/* ------------------------------------------------------------------ */
/* Only opacity and transform move, so these composite without laying the
   page out. will-change is deliberately NOT set: there are dozens of
   .reveal elements on a page and pinning a layer to each of them — kept
   for the life of the document, long after the one-shot transition has
   run — cost more than it bought. */
.reveal {
  opacity: 0; transform: translateY(28px);
  transition: opacity 0.9s var(--ease-out), transform 0.9s var(--ease-out);
  transition-delay: var(--delay, 0ms);
}
.reveal.is-in { opacity: 1; transform: none; }

.mobile-menu {
  position: fixed; inset: 0; z-index: 90;
  display: flex; flex-direction: column; justify-content: center;
  padding: 7rem var(--gutter) 3rem;
  background: var(--bg);
  text-align: left;
  opacity: 0; visibility: hidden; transition: opacity 0.4s var(--ease), visibility 0.4s var(--ease);
}
body.menu-open .mobile-menu { opacity: 1; visibility: visible; }
.mobile-menu a {
  transform: translateY(12px); opacity: 0;
  transition: color 0.3s var(--ease), background-color 0.35s var(--ease), border-color 0.35s var(--ease),
              transform 0.5s var(--ease-out), opacity 0.5s var(--ease-out);
}
body.menu-open .mobile-menu a { transform: none; opacity: 1; }
.mobile-menu a:nth-child(1) { transition-delay: 0.06s; }
.mobile-menu a:nth-child(2) { transition-delay: 0.12s; }
.mobile-menu a:nth-child(3) { transition-delay: 0.18s; }
.mobile-menu a:nth-child(4) { transition-delay: 0.24s; }
.mobile-menu a:nth-child(5) { transition-delay: 0.30s; }
.mobile-menu a:nth-child(6) { transition-delay: 0.36s; }

/* The destinations: display serif on a ruled list. The sheet holds only
   real destinations now — the admin door is in the footer. */
.mobile-menu a:not(.btn) {
  font-family: var(--font-display); font-size: clamp(2rem, 9vw, 2.75rem); font-weight: 400; line-height: 1.15;
  color: var(--muted); padding-block: 1.1rem; border-bottom: 1px solid var(--line);
}
.mobile-menu a:not(.btn):first-child { border-top: 1px solid var(--line); }
.mobile-menu a:not(.btn):hover,
.mobile-menu a[aria-current="page"] { color: var(--strong); }
.mobile-menu .btn { align-self: flex-start; margin-top: 2.5rem; }

/* ------------------------------------------------------------------ */
/* Contact form — a printed coupon rather than a web form              */
/* ------------------------------------------------------------------ */
/* Same two-column geometry as Appointments on /services: copy on the
   left page, column rule, form on the right. It stacks at 960px with the
   rule turning from vertical to horizontal, exactly as .booking does. */
.contact-form { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); align-items: start; }
.contact-form__intro { padding-right: var(--col-gap); }
.contact-form__intro h2 { font-size: var(--fs-h2); margin-top: 1.5rem; }
.contact-form__intro .lead { margin-top: 1.6rem; }

/* Phone, alternate and DM as one stack under the heading, on the left
   page. They are the routes that work this second, so they sit with the
   invitation rather than under the submit — the right column is then the
   form and nothing else. Same ghost button, same single width and same
   left-aligned labels as the submit opposite, so the two columns read as
   one set of actions parted by the rule rather than as two designs. */
.contact-form__direct {
  margin-top: clamp(2rem, 3.4vw, 2.75rem);
  display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;
  max-width: 22rem;
}

/* The area and the hours, closing the left page under the direct routes.
   These replace the ruled .info table that used to sit in its own section
   below the form (deleted further down this file): that one hung a value
   beside its label on a 3fr/9fr grid, which needs the full measure, and
   this column is 5 of 12 — so the label goes above the value instead and
   the rules come off. Held to the same 22rem as the buttons above, so the
   whole left page ends on one edge rather than three. */
.contact-form__details {
  margin: clamp(2.25rem, 3.6vw, 3rem) 0 0;
  max-width: 22rem;
  display: grid; gap: clamp(1.4rem, 2.2vw, 1.85rem);
}
.contact-form__detail dt {
  margin-bottom: 0.55rem;
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
}
/* Smaller than .info dd: that one is a display line across the full page,
   this one is a paragraph in a narrow column, and the hours run long. */
.contact-form__detail dd {
  margin: 0; color: var(--strong);
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.05rem, 1.45vw, 1.2rem); line-height: 1.45;
  font-variant-numeric: lining-nums;
}
/* The right column is the form alone now that the direct stack has moved
   across the rule, and the rule is still drawn once, by the panel — it
   was split between two children here once and left a gap in itself. */
.contact-form__panel { padding-left: var(--col-gap); border-left: 1px solid var(--line); }

/* First and last name share a row; everything else runs the full measure.
   The gap is deliberately wide — the fields are rules, not boxes, so they
   need the air to read as separate entries. */
.field-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: clamp(1.5rem, 3vw, 2.5rem);
  row-gap: clamp(2rem, 3.4vw, 2.75rem);
}
.field { display: block; grid-column: 1 / -1; }
.field--half { grid-column: span 1; }

.field__label {
  display: block; margin-bottom: 0.7rem;
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
}

/* Ruled underneath, nothing else: no box, no fill, no radius. The rule
   thickens to the accent on focus, which is the same move the buttons and
   calendar make. */
.field input,
.field textarea {
  display: block; width: 100%;
  padding: 0 0 0.75rem;
  background: transparent; color: var(--strong);
  border: 0; border-bottom: 1px solid var(--line-strong); border-radius: 0;
  font-family: var(--font-body); font-size: 1rem; font-weight: 400;
  letter-spacing: var(--track-body); line-height: 1.6;
  text-align: left;
  transition: border-color 0.3s var(--ease);
}
.field textarea { resize: vertical; min-height: 5.5rem; }
.field input::placeholder,
.field textarea::placeholder { color: var(--muted-2); font-weight: 300; }
.field input:focus,
.field textarea:focus { outline: none; border-bottom-color: var(--accent); }
/* The rule is the field, so the focus ring would double it. */
.field input:focus-visible,
.field textarea:focus-visible { outline: none; }
.field input.is-invalid,
.field textarea.is-invalid { border-bottom-color: #b23c17; }

.field__error:empty { display: none; }
.field__error {
  display: block; margin-top: 0.45rem;
  font-size: 0.68rem; letter-spacing: 0.04em; color: #b23c17;
}

/* The submit alone, at the foot of the fields and at the same width as
   the direct stack opposite. The three ghost buttons that used to follow
   it here now live under the heading in the left column. */
.contact-form__actions {
  margin-top: clamp(2.5rem, 4.5vw, 3.5rem);
  display: flex; flex-direction: column; align-items: stretch; gap: 0.75rem;
  max-width: 22rem;
}
/* The boxes already share a left edge with the fields above them; the
   labels inside did not, because .btn centres its text, so a centred
   label in a full-width box read as a centred stack on a page that is
   left-aligned everywhere else. Every label now starts on the same line
   as the field labels, in both columns. The left padding drops the nudge
   .btn carries to balance centred tracking — nothing to balance now. */
.contact-form__actions .btn,
.contact-form__direct .btn {
  justify-content: flex-start;
  padding-left: clamp(1.25rem, 2.4vw, 1.9rem);
  padding-right: clamp(1.25rem, 2.4vw, 1.9rem);
}

.contact-form__status:empty { display: none; }
/* The honeypot field: off-screen rather than display:none, which some
   bots skip. */
.contact-form__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.contact-form__status {
  margin: 1.5rem 0 0; max-width: 46ch;
  font-size: 0.88rem; font-weight: 300; line-height: 1.8; color: var(--muted);
}
.contact-form__status b { color: var(--strong); font-weight: 500; }
/* The phone and DM in that message are the live routes, so they have to
   read as links rather than as more sentence. */
.contact-form__status a {
  color: var(--strong);
  text-decoration: underline; text-decoration-color: var(--line-strong);
  text-decoration-thickness: 1px; text-underline-offset: 0.2em;
  transition: text-decoration-color 0.3s var(--ease);
}
.contact-form__status a:hover { text-decoration-color: var(--accent); }

/* ------------------------------------------------------------------ */
/* Responsive                                                          */
/* ------------------------------------------------------------------ */
/* Tracked uppercase labels need more room than the old sentence-case bar:
   the phone number steps out first, then the bar folds into the sheet. */
@media (max-width: 1240px) {
  .nav__phone { display: none; }
}
@media (max-width: 1080px) {
  /* Scoped to .nav__cta so only the bar copy hides — the one inside the
     mobile sheet is exactly where it should be at this width. */
  .nav__links, .nav__cta .btn { display: none; }
  .nav__toggle { display: grid; }

  /* Tablet keeps the print spread but gives the steps the full width: the
     headline and standfirst on the left page, a narrower photograph on the
     right, and the three step columns run across both underneath, where
     they have room to breathe. */
  .spread__grid {
    grid-template-columns: minmax(0, 1fr) clamp(12rem, 30vw, 17rem);
    column-gap: clamp(2rem, 5vw, 3.5rem);
  }
  .spread__figure { grid-row: 2 / 4; }
  .spread__figure img { aspect-ratio: 3 / 4; }
  .spread__steps { grid-column: 1 / -1; margin-top: clamp(2rem, 4vw, 2.75rem); }
}

@media (max-width: 767px) {
  /* Phones get a stacked page instead of a spread: the kicker and headline
     at the top, the photograph full width under them, then the standfirst
     and the steps as one readable column, each under a hairline, with the
     copy at reading size rather than print-caption size. */
  .spread__grid { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
  .spread__title, .spread__standfirst, .spread__figure, .spread__steps { grid-column: 1; grid-row: auto; }
  .spread__title { margin-top: 1.5rem; }
  .spread__figure { order: 1; margin-top: 2rem; }
  .spread__figure img { aspect-ratio: 4 / 5; }
  .spread__standfirst { order: 2; margin-top: 2.25rem; }
  .spread__steps { order: 3; grid-template-columns: minmax(0, 1fr); margin-top: 1.75rem; padding-top: 0; }
  .spread__steps li { padding: 1.5rem 0; }
  .spread__steps li:last-child { padding-bottom: 0; }
  .spread__steps li + li { border-left: 0; border-top: 1px solid var(--line); }
  .spread__steps p {
    max-width: 40ch;
    font-size: 0.94rem; line-height: 1.75; letter-spacing: 0.015em;
  }
}
@media (max-width: 960px) {
  .section-head { grid-template-columns: minmax(0, 1fr); }
  .section-head .lead { grid-column: 1; grid-row: 3; margin-top: 1.6rem; }

  .booking, .contact-form { grid-template-columns: minmax(0, 1fr); }
  .booking__intro, .contact-form__intro { padding-right: 0; padding-bottom: 3.5rem; }
  .booking > .booking__panel,
  .contact-form__panel { padding-left: 0; border-left: 0; padding-top: 3.5rem; border-top: 1px solid var(--line); }

  .reviews { grid-template-columns: minmax(0, 1fr); }
  .rating { padding-right: 0; padding-bottom: 3rem; }
  .quotes { padding-left: 0; border-left: 0; padding-top: 3rem; border-top: 1px solid var(--line); }

  .footer__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 3rem; }
  .footer__brand { grid-column: 1 / -1; padding-right: 0; padding-bottom: 3rem; border-bottom: 1px solid var(--line); }
  .footer__grid > .footer__col:nth-child(2) { border-left: 0; padding-left: 0; }
}
/* Small phones: a price with an alternative ("$150 · w/ blowout $190") is
   wider than the row can spare, so the alternative drops under the price. */
@media (max-width: 520px) {
  /* Two name fields side by side leave about 8 characters each here. */
  .field--half { grid-column: 1 / -1; }
}
@media (max-width: 400px) {
  .svc-grid { grid-template-columns: minmax(0, 1fr); }
  .svc-card__price .alt { display: block; margin-top: 0.2rem; }
}
@media (max-width: 620px) {
  .quote blockquote p { font-size: 1.5rem; }
  .quote--photo { grid-template-columns: 6.5rem minmax(0, 1fr); column-gap: 1rem; }

  .footer__grid { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .footer__grid > * + * { border-left: 0; padding: 2.2rem 0; border-bottom: 1px solid var(--line); }
  .footer__grid > :last-child { border-bottom: 0; padding-bottom: 0; }
}

/* ------------------------------------------------------------------ */
/* Reduced motion                                                      */
/* ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
  .reveal { opacity: 1; transform: none; }
  .marquee__track { animation: none; }
}

/* ------------------------------------------------------------------ */
/* Booking modal                                                       */
/* ------------------------------------------------------------------ */
.modal {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: clamp(1rem, 4vw, 2.5rem);
  opacity: 0; visibility: hidden;
  transition: opacity 0.32s var(--ease), visibility 0.32s var(--ease);
}
.modal.is-open { opacity: 1; visibility: visible; }

.modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(10, 10, 10, 0.82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.modal__panel {
  position: relative;
  width: min(32rem, 100%);
  max-height: min(88svh, 48rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--bg);
  border: 1px solid var(--line-strong);
  padding: clamp(1.75rem, 3.6vw, 2.75rem);
  box-shadow: 0 40px 100px -40px rgba(0, 0, 0, 0.9);
  transform: translateY(14px);
  opacity: 0;
  transition: transform 0.42s var(--ease-out), opacity 0.34s var(--ease-out);
}
.modal.is-open .modal__panel { transform: none; opacity: 1; }

.modal__close {
  position: absolute; top: 1.1rem; right: 1.1rem;
  width: 40px; height: 40px; display: grid; place-items: center;
  border: 1px solid var(--line-strong); border-radius: 0;
  background: transparent; color: var(--muted); cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.modal__close svg { width: 15px; height: 15px; }
.modal__close:hover { color: var(--strong); border-color: var(--accent); }

.modal__title { font-family: var(--font-display); font-size: clamp(1.9rem, 3.6vw, 2.5rem); font-weight: 400; letter-spacing: -0.015em; margin: 1.1rem 0 0; }
.modal__lead { color: var(--muted); font-size: 0.92rem; font-weight: 300; line-height: 1.75; margin: 0.8rem 0 0; max-width: 36ch; }
/* The service a row passed in, carried at the top of the booking panel */
.modal__service {
  display: inline-block; margin: 1.1rem 0 0; padding: 0.55rem 0.9rem;
  border: 1px solid var(--accent); color: var(--strong);
  font-family: var(--font-body); font-size: 0.66rem; font-weight: 500; line-height: 1.3;
  letter-spacing: var(--track-label); text-transform: uppercase;
}
.modal__service[hidden] { display: none; }

.modal__mount { margin-top: 1.8rem; }
/* All three direct routes, stacked one per row at the full width of the
   panel — the same three, in the same order and style, as on /contact. */
.modal__contacts { margin-top: 1.4rem; display: flex; flex-direction: column; align-items: stretch; gap: 0.6rem; }

/* the cloned panel drops its own chrome inside the modal */
.modal .booking__panel { background: transparent; border: none; border-radius: 0; padding: 0; }

body.modal-open { overflow: hidden; }

/* ------------------------------------------------------------------ */
/* Booking modal — the step rail                                       */
/* ------------------------------------------------------------------ */
/* Three panels on one rail. The viewport's height is set from the active
   panel by script.js, so the modal grows and shrinks with the step
   instead of standing permanently at the tallest one. */
/* The rail owns the gap under the header. .modal__mount carried it before
   the rail existed, and since the mount now sits inside step one only,
   steps two and three were left flush against the service chip. */
.modal__steps { overflow: hidden; margin-top: 1.6rem; transition: height 0.45s var(--ease-out); }
.modal__mount { margin-top: 0; }
.modal__steps-track {
  display: flex; align-items: flex-start;
  transition: transform 0.45s var(--ease-out);
}
.modal__step { flex: 0 0 100%; min-width: 100%; }
.modal__step[inert] { pointer-events: none; }

.modal__back {
  display: inline-flex; align-items: center; gap: 0.45rem;
  margin: 0 0 1.1rem; padding: 0;
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-body); font-size: 0.6rem; font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase;
  color: var(--muted);
  transition: color 0.3s var(--ease);
}
/* Without a glyph the tracked uppercase word read as a stray label rather
   than a control. */
.modal__back::before { content: "←"; font-size: 0.85rem; line-height: 1; }
.modal__back:hover { color: var(--text); }
.modal__steplead { margin: 0 0 1.1rem; font-size: 0.9rem; font-weight: 300; color: var(--muted); }

/* The picker carries the service name and its price and nothing else —
   the rinse-time notes on the homepage cards are deliberately absent. */
.svc-pick__cat {
  margin: 1.3rem 0 0.4rem;
  font-family: var(--font-body); font-size: var(--fs-label); font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
}
.svc-pick { list-style: none; margin: 0; padding: 0; }
.svc-pick__row {
  width: 100%; display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: 0.7rem 0; margin: 0;
  background: none; border: 0; border-bottom: 1px solid var(--line);
  font: inherit; letter-spacing: inherit; color: inherit;
  text-align: left; cursor: pointer;
  transition: border-color 0.3s var(--ease);
}
.svc-pick__row:hover,
.svc-pick__row:focus-visible { border-bottom-color: var(--accent); }
.svc-pick__name { font-size: 0.95rem; }
.svc-pick__price { font-family: var(--font-display); font-size: 1.05rem; white-space: nowrap; }

.modal__form { display: grid; gap: 0.9rem; }
.modal__field { display: block; }
.modal__field-label {
  display: block; margin-bottom: 0.35rem;
  font-family: var(--font-body); font-size: 0.58rem; font-weight: 500;
  letter-spacing: var(--track-label); text-transform: uppercase; color: var(--muted);
}
.modal__field input,
.modal__field select {
  width: 100%; padding: 0.72rem 0.85rem;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  font-family: var(--font-body); font-size: 0.92rem;
  transition: border-color 0.3s var(--ease);
}
.modal__field input:focus,
.modal__field select:focus { outline: none; border-color: var(--accent); }
.modal__field input.is-invalid,
.modal__field select.is-invalid { border-color: #b23c17; }
/* Number of People: the same box as the inputs, with the native arrow
   swapped for a thin chevron drawn from two gradients. */
.modal__field select {
  -webkit-appearance: none; appearance: none; cursor: pointer;
  padding-right: 2.4rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 1.15rem) 50%, calc(100% - 0.85rem) 50%;
  background-size: 0.3rem 0.3rem;
  background-repeat: no-repeat;
}
.modal__field-error:empty { display: none; }
.modal__field-error { display: block; margin-top: 0.3rem; font-size: 0.68rem; color: #b23c17; }
/* City and ZIP side by side; ZIP is the narrower of the two. */
.modal__field-row { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 0.9rem; }

/* Street address predictions. They sit in the flow under the field, not
   over it: the step rail is overflow: hidden, so a dropdown hanging out
   of the step would be cut off. Same hairline and square corners as the
   inputs, with the field's teal focus border carried down around them. */
.addr-lookup__list {
  list-style: none; margin: -1px 0 0; padding: 0;
  background: var(--bg); border: 1px solid var(--accent); border-radius: var(--radius);
}
.addr-lookup__list[hidden] { display: none; }
.addr-lookup__item {
  display: flex; align-items: baseline; gap: 0.5rem; min-width: 0;
  padding: 0.6rem 0.85rem; border-bottom: 1px solid var(--line);
  font-size: 0.85rem; cursor: pointer;
  transition: background-color 0.2s var(--ease);
}
.addr-lookup__item:hover,
.addr-lookup__item[aria-selected="true"] { background: var(--surface); }
.addr-lookup__main { flex: 0 0 auto; max-width: 100%; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.addr-lookup__sub { flex: 1 1 auto; min-width: 0; color: var(--muted); font-size: 0.78rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.addr-lookup__credit {
  padding: 0.35rem 0.85rem; text-align: right;
  font-size: 0.6rem; letter-spacing: 0.04em; color: var(--muted);
}

.modal__submit { width: 100%; margin-top: 0.5rem; }

.modal__done-title { font-family: var(--font-display); font-size: 1.6rem; font-weight: 400; margin: 0 0 0.9rem; }
.modal__done-line { margin: 0 0 0.3rem; font-size: 0.95rem; color: var(--text); }
.modal__done-note { margin: 1.1rem 0 0; font-size: 0.85rem; font-weight: 300; line-height: 1.7; color: var(--muted); }
/* "Sending…" and a failed send, under Finish Booking. */
.modal__send-status { margin: 0.9rem 0 0; font-size: 0.85rem; line-height: 1.6; color: var(--text); }
.modal__send-status:empty { display: none; }
.modal__cal-note { margin: 0.75rem 0 0.5rem; color: var(--strong); }

@media (prefers-reduced-motion: reduce) {
  .modal__steps, .modal__steps-track { transition: none; }
}

/* The panel scrolls on short viewports; its scrollbar is drawn from the
   rules palette so it follows whichever theme the panel is sitting in
   rather than being pinned to one of them. */
.modal__panel { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
.modal__panel::-webkit-scrollbar { width: 8px; }
.modal__panel::-webkit-scrollbar-track { background: transparent; }
.modal__panel::-webkit-scrollbar-thumb { background: var(--line); }
.modal__panel::-webkit-scrollbar-thumb:hover { background: var(--line-strong); }

/* ------------------------------------------------------------------ */
/* /services editorial hero                                            */
/* ------------------------------------------------------------------ */
/* The photograph is bright turquoise water, not a dark frame, so the
   scrim is directional: weighted at the left where the type sits and
   clearing almost entirely by the right edge, which keeps the water its
   own colour. A flat wash across the whole frame would have bought
   legibility by killing the only thing the picture has going for it. */
.svc-hero {
  position: relative;
  /* Taller than the other heroes on purpose. It was originally this tall
     so the title sat low enough for the near ankle to cross the end of the
     word; the depth effect is gone, but the height still gives the spread
     its scale, so it is kept. */
  min-height: clamp(560px, 84vh, 940px);
  display: grid;
  align-items: end;
  overflow: hidden;
  isolation: isolate;
}
.svc-hero__bg {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 62% center;
}
.svc-hero__scrim {
  position: absolute; inset: 0; z-index: -1;
  background:
    linear-gradient(100deg, rgba(10, 10, 10, 0.78) 0%, rgba(10, 10, 10, 0.56) 34%,
                    rgba(10, 10, 10, 0.14) 66%, rgba(10, 10, 10, 0) 100%),
    linear-gradient(to bottom, rgba(10, 10, 10, 0.55) 0%, rgba(10, 10, 10, 0) 30%);
}
.svc-hero__inner { padding-block: clamp(3rem, 6vh, 4.5rem); }
/* The kicker is near-black in the light theme; over the scrim it has to
   invert along with the rest of the block. */
.svc-hero .eyebrow { color: #ffffff; }
/* The size and the top margin were originally tuned so the near ankle cut
   across the last letters of the word. That depth effect was reverted on
   2026-09-21 and the title now renders plainly, whole and uncut — but the
   measurements are kept exactly as they were, because they also happen to
   hang the word well on this photograph. They are no longer load-bearing
   for anything, so they are free to change now. */
.svc-hero__title {
  margin: clamp(3rem, 8vh, 6rem) 0 0;
  font-family: var(--font-display);
  font-size: clamp(3.4rem, 10.5vw, 9rem);
  font-weight: 400;
  line-height: 0.92;
  letter-spacing: -0.035em;
  color: #ffffff;
  max-width: 12ch;
}
/* The masked .svc-hero__subject layer that used to lift the legs in front
   of this title — a second copy of the photograph and its scrim, cut out
   by assets/hero/legs-mask.png and switched on only wide and above 3:2 —
   was removed on 2026-09-21. The word is rendered plainly now, whole and
   uncut, so the title needs nothing over it and the hero is three layers
   again: photograph, scrim, type.

   assets/hero/legs-mask.png is left in the repo but is no longer
   referenced by anything. It was generated from this exact photograph
   with macOS Vision, so it is only reusable while that photograph is,
   and it is the one asset to delete if the effect is not coming back. */

.svc-hero__lede {
  margin: clamp(1.4rem, 2.6vw, 2.2rem) 0 0;
  max-width: 38ch;
  font-size: var(--fs-lead);
  font-weight: 300;
  line-height: 1.8;
  color: rgba(255, 255, 255, 0.86);
}

/* ------------------------------------------------------------------ */
/* Standalone pages (/services, /about, /contact)                      */
/* Same tokens as the homepage: white canvas, near-black headings,     */
/* neutral-600 body, accent kept for buttons and active states.        */
/* ------------------------------------------------------------------ */
.page-hero {
  /* clears the fixed 84px nav, then a lot of air on purpose */
  padding-block: clamp(10rem, 18vw, 15rem) clamp(3.5rem, 7vw, 6rem);
  border-bottom: 1px solid var(--line);
}
.page-hero__title {
  margin: 1.6rem 0 0;
  font-family: var(--font-display);
  font-size: clamp(3rem, 8vw, 6.75rem);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: -0.025em;
  color: var(--strong);
  max-width: 14ch;
}
.page-lede {
  margin: 2rem 0 0;
  font-size: var(--fs-lead);
  line-height: 1.8;
  font-weight: 300;
  color: var(--muted);
  max-width: 44ch;
}
/* title left, lede right — the same opener as .section-head */
@media (min-width: 961px) {
  .page-hero .container {
    display: grid;
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    column-gap: var(--col-gap);
    align-items: end;
  }
  .page-hero .eyebrow { grid-column: 1 / -1; justify-self: start; }
  .page-hero__title { grid-column: 1; grid-row: 2; }
  .page-hero .page-lede { grid-column: 2; grid-row: 2; margin: 0; }
}

/* On a phone the gap from the /contact masthead down to "Write to us" ran
   about 150px — the masthead's foot plus the tight section's full head.
   Below 640px both close up to roughly 2rem each; tablet and desktop keep
   the full editorial air. */
@media (max-width: 640px) {
  .page-hero--art { padding-bottom: 1.5rem; }
  .page-hero--art + #write { padding-top: 2rem; }
}

/* A masthead over a photograph (/contact). The picture fills the block
   behind the type; the wash lifts it toward white where the lede sits and
   fades it out completely at the foot, so the block dissolves into the
   canvas instead of ending on its usual hairline. */
.page-hero--art {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-bottom: 0;
}
.page-hero__art {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 30%;
}
.page-hero--art::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0) 45%, var(--bg) 100%),
    linear-gradient(to right, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.55));
}

.page-actions { margin-top: clamp(3.5rem, 7vw, 5rem); display: flex; flex-wrap: wrap; gap: 0.75rem; }
/* The margin above exists to part the actions from copy above them. When
   the actions are the whole section — the closing Book on /contact — there
   is nothing above to part them from, and the margin would just stack on
   top of the section's own padding. */
.page-actions:first-child { margin-top: 0; }

/* The ruled .info table (.info, .info__row, .info dt/dd, .info__link) was
   deleted on 2026-09-21. /contact was its only consumer, and the area and
   the hours now sit in the form's left column as .contact-form__details
   instead. Nothing in the project referenced it afterwards. */

/* About copy set in two print columns with a column rule and a drop cap */
/* /about reads as a chess board: the first block hangs on the left half
   of the measure, the second on the right half, and the empty half of
   each row is the point — the page is as much white as it is type. Both
   blocks keep the display serif for their headings and a drop cap on the
   block's opening paragraph. */
.chess { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--col-gap); }
.chess__block { margin: 0; }
.chess__block--left { grid-column: 1; grid-row: 1; }
.chess__block--right { grid-column: 2; grid-row: 2; margin-top: clamp(4rem, 9vw, 8rem); }

/* A photograph in each row's open half, each one paired with the block
   beside it and exactly as tall as it.

   The height comes from the grid rather than from the picture: the row is
   sized by its text block, the figure stretches to that row, and the image
   is taken out of flow inside it so it cannot push the row taller. That is
   what makes the pair read as one compact block instead of a picture
   running on past its copy. Square corners, no frame. */
.chess__figure {
  align-self: stretch;
  position: relative;
  min-height: 0;
  overflow: hidden;
  margin: 0;
}
.chess__figure img {
  position: absolute; inset: 0;
  display: block; width: 100%; height: 100%;
  object-fit: cover;
  border-radius: 0;
}
/* The Strip sits to the right of "We come to you", pulled a little into
   the gutter so it laps the text column's edge. */
.chess__figure--studio {
  grid-column: 2; grid-row: 1;
  margin-left: calc(-1 * clamp(1rem, 3vw, 3rem));
}
.chess__figure--studio img { object-position: center 62%; }
/* The dunes take the half "Why us?" leaves open, and carry the same step
   down that block does so the two start and finish on the same lines. */
.chess__figure--why {
  grid-column: 1; grid-row: 2;
  margin-top: clamp(4rem, 9vw, 8rem);
  margin-right: calc(-1 * clamp(1rem, 3vw, 3rem));
}
.chess__figure--why img { object-position: center 30%; }
.chess__block h2 {
  margin-top: 1.4rem;
  font-size: clamp(2rem, 3.4vw, 3.1rem);
  font-weight: 400; line-height: 1.04; letter-spacing: -0.025em;
  color: var(--strong);
  text-wrap: balance;
}
.chess__block p {
  margin: 1.6rem 0 0;
  color: var(--muted); font-size: 1.02rem; font-weight: 300; line-height: 1.9;
  max-width: 42ch;
}
.chess__lead::first-letter {
  float: left; margin: 0.06em 0.11em 0 0;
  font-family: var(--font-display); font-size: 3.6em; line-height: 0.82;
  color: var(--strong);
}
.chess__actions {
  grid-column: 1 / -1;
  margin-top: clamp(4rem, 8vw, 7rem);
  display: flex; flex-wrap: wrap; gap: 0.75rem;
}
/* where supported, sink the cap exactly three lines instead of floating it */
@supports (initial-letter: 3) or (-webkit-initial-letter: 3) {
  .chess__lead::first-letter {
    float: none; margin: 0 0.16em 0 0; font-size: inherit; line-height: inherit;
    -webkit-initial-letter: 3; initial-letter: 3;
  }
}

@media (max-width: 960px) {
  .info__row { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }

  /* One column: the board folds to a single run of blocks, and the second
     one loses the step that offset it against the first. */
  .chess { grid-template-columns: minmax(0, 1fr); }
  .chess__block--left, .chess__block--right, .chess__figure { grid-column: 1; grid-row: auto; }
  .chess__block--right { margin-top: clamp(3rem, 8vw, 4.5rem); }
  /* Nothing to pair with on one column: each photograph sits in the run at
     full width in a wider crop, and goes back to sizing itself. */
  .chess__figure {
    margin: clamp(2.5rem, 7vw, 3.5rem) 0 0;
    align-self: start; position: static; overflow: visible;
  }
  .chess__figure img { position: static; height: auto; aspect-ratio: 3 / 2; }
}

/* ------------------------------------------------------------------ */
/* Blog — /blog and /blog/<slug>, rendered by api/blog.js into         */
/* lib/blog-template.html. Same editorial system: hairlines, square    */
/* corners, Playfair for titles, accent only on the small cue.         */
/* ------------------------------------------------------------------ */
.blog-list { padding-block: clamp(3rem, 6vw, 5rem) clamp(5rem, 9vw, 8rem); }
.blog-empty { color: var(--muted); font-size: var(--fs-lead); margin: 0; }

.blog-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
  gap: clamp(2.5rem, 4vw, 3.5rem) var(--col-gap, 2rem);
}
.blog-card { display: flex; flex-direction: column; gap: 0.75rem; color: inherit; text-decoration: none; }
.blog-card__media {
  display: block; aspect-ratio: 3 / 2; overflow: hidden;
  background: var(--surface); border: 1px solid var(--line);
  margin-bottom: 0.5rem;
}
.blog-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.6s var(--ease); }
.blog-card__meta, .post__meta {
  font-size: 0.72rem; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted-2);
}
.blog-card__title {
  font-family: var(--font-display); font-size: clamp(1.4rem, 2vw, 1.7rem); line-height: 1.2; color: var(--strong);
}
.blog-card__teaser { color: var(--muted); line-height: 1.7; }
.blog-card__cue {
  margin-top: 0.25rem; font-size: 0.7rem; font-weight: 500; letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--strong); border-bottom: 1px solid var(--accent); align-self: flex-start; padding-bottom: 0.2rem;
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .blog-card:hover .blog-card__media img { transform: scale(1.03); }
}

.post__inner {
  max-width: 46rem;
  /* clears the fixed 84px nav */
  padding-block: clamp(8rem, 14vw, 11rem) clamp(5rem, 9vw, 8rem);
}
.post__back { display: inline-block; font-size: 0.8rem; color: var(--muted-2); text-decoration: none; margin-bottom: 2.5rem; }
.post__back:hover { color: var(--strong); }
.post__meta { margin: 0; }
.post__title {
  margin: 1rem 0 0;
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(2.4rem, 6vw, 4rem); line-height: 1.08; letter-spacing: -0.02em; color: var(--strong);
}
.post__cover { margin: clamp(2rem, 5vw, 3rem) 0 0; }
.post__cover img { width: 100%; height: auto; display: block; }

.post__body { margin-top: clamp(2rem, 5vw, 3rem); font-size: 1.08rem; line-height: 1.85; color: var(--text); }
/* :where() keeps the reset weaker than the spacing rule after it. */
.post__body :where(p, ul) { margin: 0; }
.post__body > * + * { margin-top: 1.25em; }
.post__body h2 {
  margin: 2.2em 0 0; font-family: var(--font-display); font-weight: 400;
  font-size: clamp(1.7rem, 3vw, 2.2rem); line-height: 1.2; color: var(--strong);
}
.post__body h3 { margin: 1.8em 0 0; font-size: 1.15rem; font-weight: 600; color: var(--strong); }
.post__body h2 + *, .post__body h3 + * { margin-top: 0.75em; }
.post__body strong { font-weight: 600; color: var(--strong); }
.post__body a { color: var(--strong); text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: 0.2em; }
.post__body ul { padding-left: 1.2rem; }
.post__body li + li { margin-top: 0.4em; }
.post__body li::marker { color: var(--accent); }
.post__figure { margin: 2em 0 0; }
.post__figure img { width: 100%; height: auto; display: block; }
.post__figure figcaption { margin-top: 0.6rem; font-size: 0.85rem; color: var(--muted-2); }

.post__foot {
  margin-top: clamp(3rem, 7vw, 4.5rem); padding-top: 2rem; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1.25rem;
}
.post__foot p { margin: 0; color: var(--muted); max-width: 32ch; }
