/* ==================================================================
   PINK BY GEO : styles.css (revision 2)
   1  Reset & base           7  Custom GelX act
   2  Brand tokens           8  Nail Repair act (berry 1)
   3  Type roles             9  Lookbook act (three modes)
   4  Layout & utilities    10  About act
   5  Header / nav / menu   11  Booking / footer (berry 2)
   6  Opening act           12  Motion & reveal states
                            13  Responsive adjustments
                            14  Reduced motion & accessibility
   ================================================================== */

/* -- 1 · Reset & base ---------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; }
body {
  font-family: "DM Sans", system-ui, sans-serif;
  font-weight: 400;
  font-size: clamp(1.0625rem, 0.98rem + 0.25vw, 1.2rem);
  line-height: 1.6;
  color: var(--charcoal);
  background: var(--porcelain);
  overflow-x: hidden;  /* Safari <16 fallback; clip below wins elsewhere */
  overflow-x: clip;
}
img, svg, picture { max-width: 100%; display: block; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; }

/* -- 2 · Brand tokens ---------------------------------------------- */
:root {
  --porcelain: #FDF8F6;
  --blush:     #FADCE3;
  --petal:     #F7C9D3;
  --rose:      #E89AAE;  /* rules and decoration only, never small text */
  --berry:     #7C2D45;
  --berry-deep:#5E2136;
  --neutral:   #F5E9E2;
  --vanilla:   #F7EED9;  /* Custom GelX field only (revision 8) */
  --charcoal:  #3A3236;
  --mute:      #A8919A;  /* hairlines and decoration only */
  --mute-ink:  #755663;  /* readable muted text: AA on porcelain, blush, neutral */

  --page-gutter: clamp(1.25rem, 4vw, 4rem);
  --content-max: 92rem;
  --reading-max: 62ch;
  --header-h: 4.5rem;

  --hairline: 1px solid rgba(232, 154, 174, .5);

  --ease-editorial: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.4, 0, .2, 1);
  --duration-fast: 180ms;
  --duration-medium: 450ms;
  --duration-slow: 550ms;

  --radius-0: 0;
  --radius-1: 2px;
}

/* -- 3 · Type roles (nothing readable below 1rem / 16px) ----------- */
.display-hero {
  font-family: "Instrument Serif", serif;
  font-weight: 400;
  font-size: clamp(2.6rem, 7vw, 5.5rem);
  line-height: 1.04;
  letter-spacing: -.01em;
  text-wrap: balance;
}
.display-hero em, .display-title em { font-style: italic; font-weight: 400; }
.display-title {
  font-family: "Instrument Serif", serif;
  font-weight: 400;
  font-size: clamp(2rem, 4vw, 3.4rem);
  line-height: 1.08;
}
.display-title--light { color: var(--porcelain); }
/* .display-small was removed 2026-08-13: it was a type role no markup
   ever used. --neutral above is the same situation but is kept on
   purpose, documented at section 10. */
.price-display {
  font-family: "Instrument Serif", serif;
  font-weight: 400;
  font-size: clamp(2.9rem, 5.5vw, 4.4rem);
  line-height: 1;
  margin-block: 1.4rem .6rem;
}
.price-display sup {
  font-size: .45em;
  vertical-align: .65em;
  font-weight: 400;
  margin-right: .06em;
}
.price-display--light { color: var(--petal); }
.body-copy { max-width: var(--reading-max); }
.body-copy--light { color: var(--porcelain); }
.micro-label {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--mute-ink);
}
.micro-label span { color: var(--berry); margin-right: .5em; }
.micro-label--onberry { color: var(--petal); }
.caption { font-size: 1rem; color: var(--mute-ink); line-height: 1.55; }
.caption--dark { color: var(--mute-ink); }

/* -- 4 · Layout & utilities ---------------------------------------- */
section[id] { scroll-margin-top: calc(var(--header-h) + 1rem); }

/* Screen-reader-only text (the prices' accessible names). The standard
   clip pattern: rendered nowhere visually, still in the a11y tree. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: var(--page-gutter);
  z-index: 300;
  background: var(--berry);
  color: var(--porcelain);
  padding: .8rem 1.4rem;
  text-decoration: none;
  font-weight: 600;
}
.skip-link:focus-visible { top: .75rem; }

.action {
  display: inline-block;
  background: var(--berry);
  color: var(--porcelain);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  padding: .9rem 1.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius-1);
  min-height: 44px;
  transition: background var(--duration-fast) var(--ease-soft),
              color var(--duration-fast) var(--ease-soft);
}
.action:hover { background: var(--berry-deep); }
.action--light { background: var(--porcelain); color: var(--berry); }
.action--light:hover { background: var(--petal); color: var(--berry); }
.action--small { padding: .55rem 1rem; }

.quiet-link {
  background: none;
  border: 0;
  padding: .7rem 0;   /* inline: paints to a 44px tap box, no reflow */
  font-size: 1rem;
  font-weight: 500;
  color: var(--charcoal);
  text-decoration: underline;
  text-underline-offset: .35em;
  text-decoration-color: var(--rose);
  transition: color var(--duration-fast) var(--ease-soft);
}
.quiet-link:hover { color: var(--berry); }

.photo {
  --focal-x: 50%;
  --focal-y: 50%;
  width: 100%;
  height: auto;
  object-fit: cover;
  object-position: var(--focal-x) var(--focal-y);
}

/* Berry fields enter through a petal-derived asymmetric curve */
.berry-field {
  background: var(--berry);
  color: var(--porcelain);
  border-radius: 44% 56% 0 0 / 64px 40px 0 0;
}

/* -- 5 · Header, nav, mobile menu ---------------------------------- */
/* Without JS the header cannot adapt its colors over berry sections,
   so it scrolls away with the page instead of staying fixed. */
.no-js .site-header { position: absolute; }
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: clamp(1rem, 2.5vw, 2.25rem);
  height: var(--header-h);          /* constant: never changes on scroll */
  padding: 0 var(--page-gutter);
  transition: background var(--duration-medium) var(--ease-soft),
              box-shadow var(--duration-medium) var(--ease-soft);
}
.site-header.is-scrolled {
  background: rgba(253, 248, 246, .97);
  box-shadow: 0 1px 0 rgba(232, 154, 174, .4);
}
.site-header.on-berry {
  background: var(--berry);
  box-shadow: 0 1px 0 rgba(253, 248, 246, .18);
  color: var(--porcelain);
}
.brand {
  display: flex;
  align-items: center;
  gap: .7rem;
  text-decoration: none;
  margin-right: auto;
  /* Lifts the 34px content box past the 44px tap floor. The header is a
     72px flex row aligning centre, so the extra box height changes
     nothing visually. */
  padding-block: .4rem;
}
.brand-word {
  font-family: "Instrument Serif", serif;
  font-weight: 400;
  font-size: 1.3rem;
  letter-spacing: .01em;
}
.site-nav { display: flex; gap: clamp(1rem, 2vw, 1.8rem); }
.site-nav a {
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  text-decoration: none;
  padding: .6rem 0;   /* 44px+ tap box; the 72px header centres it, no shift */
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-fast) var(--ease-soft);
}
.site-nav a:hover,
.site-nav a[aria-current="true"] { border-bottom-color: var(--rose); }
.on-berry .site-nav a:hover,
.on-berry .site-nav a[aria-current="true"] { border-bottom-color: var(--petal); }

.menu-toggle {
  display: none;
  align-items: center;
  gap: .6rem;
  background: none;
  border: 0;
  padding: .6rem;
  min-height: 44px;
  min-width: 44px;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: inherit;
}
.menu-toggle-lines { display: grid; gap: 5px; width: 22px; }
.menu-toggle-lines span {
  height: 1.5px;
  background: currentColor;
  transition: transform var(--duration-fast) var(--ease-soft);
}
body.menu-open .menu-toggle-lines span:first-child { transform: translateY(3.25px) rotate(45deg); }
body.menu-open .menu-toggle-lines span:last-child { transform: translateY(-3.25px) rotate(-45deg); }

.menu-panel {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: var(--berry);
  color: var(--porcelain);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: calc(var(--header-h) + 2rem) var(--page-gutter) 2.5rem;
  overflow-y: auto;
}
.menu-panel[hidden] { display: none; }
.menu-panel-nav { display: grid; gap: .4rem; }
.menu-panel-nav a {
  font-family: "Instrument Serif", serif;
  font-size: clamp(2.1rem, 9vw, 3rem);
  font-weight: 400;
  text-decoration: none;
  padding: .35rem 0;
  border-bottom: 1px solid rgba(253, 248, 246, .14);
}
.menu-index {
  font-family: "DM Sans", sans-serif;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .12em;
  color: var(--petal);
  vertical-align: .75em;
  margin-right: 1rem;
}
.menu-panel-foot { display: grid; gap: 1.25rem; }
.menu-panel-foot p { color: var(--blush); font-size: 1.0625rem; line-height: 1.8; }
.menu-panel-foot a { color: var(--porcelain); text-underline-offset: .3em; padding-block: .45rem; }
/* The rule above must not recolour the porcelain CTA: porcelain text
   on the porcelain fill rendered the label invisible. */
.menu-panel-foot .action--light { color: var(--berry); }

/* -- 6 · Opening act ----------------------------------------------- */
.opening {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  background: var(--blush);
  /* Taller masthead (revision 6). The floor is 10.5rem, not the
     briefed 7rem: the 96px wordmark + sub + separator + margins stack
     to ~159px, and anything smaller overflows the fixed box on
     shorter desktop viewports (same failure mode as revision 5). */
  --masthead-h: calc(var(--header-h) + clamp(12.5rem, 17vh, 14rem));
}
.opening-media {
  position: absolute;
  /* Contained figure rather than a full-bleed panel: breathing room on
     every side so the photograph reads as part of the composition
     instead of dominating it (revision 5.1). */
  inset: calc(var(--masthead-h) + 5rem) 8% 10% 66%;
  overflow: hidden;
}
.opening-media picture, .opening-media img { width: 100%; height: 100%; }
.opening-copy {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;   /* content starts under the separator */
  gap: 1.15rem;
  min-height: calc(100vh - var(--masthead-h) - 4rem);
  min-height: calc(100svh - var(--masthead-h) - 4rem);
  padding: 5rem var(--page-gutter) 6rem clamp(3.5rem, 16vw, 16rem);
  max-width: 62rem;
}
.opening-copy .display-hero { width: max-content; max-width: 88vw; }

/* The cycling "you": one look per person, the visual echo of custom.

   The word is PAINTED, never laid out: the outer span is a fixed slot
   (height 1em of the headline's own size, min-width set once by
   initHeroYou to the widest face) and the ink span rides absolutely
   inside it, baseline-anchored by a JS-computed translate. A face
   swap therefore changes pixels only. The first build set the face
   fonts on the in-flow span itself, and every swap re-metricised the
   line box: the headline breathed 13px and the lede bounced with it,
   which is exactly the layout-shift jitter this rule set exists to
   prevent.

   Gating: absolute positioning without the sizing work renders the
   word collapsed to a point over "for". So these rules require
   .is-live, which initHeroYou adds only AFTER the faces have loaded
   and the slot is measured. Not .wired: a stale cached main.js
   carries the wired stamp too, and pairing yesterday's script with
   today's CSS shipped exactly that collapsed render once. CSS may
   only key on states the current script has demonstrably reached,
   never on the assumption that the script serving this page is the
   one written alongside it. Every other audience (no JS, failed JS,
   failed fonts, stale script, reduced motion) gets the plain inline
   serif word, which is the designed fallback, not a degradation. */
@media (prefers-reduced-motion: no-preference) {
  .hero-you.is-live {
    position: relative;
    display: inline-block;
    height: 1em;
  }
  .hero-you.is-live .hero-you-ink {
    position: absolute;
    /* Left-anchored, not centred: the slot is as wide as the WIDEST
       face, and a narrower face centred in it floated away from
       "for", which read as the word sitting offset from its own
       sentence. Anchored left, every face hugs the sentence and the
       slack trails invisibly at the line's end. */
    left: 0;
    bottom: 0;
    transform: translate(0, var(--ink-shift, 0em));
    line-height: 1;
    white-space: nowrap;
    transition: opacity 160ms var(--ease-soft);
  }
  .hero-you.is-live.is-dip .hero-you-ink { opacity: 0; }
  /* Faces live on the INK, so the slot keeps the headline's metrics.
     Expressive range, owner's call: script, didone, handwritten. Kept
     in step with FACES in initHeroYou, which measures each face's
     REAL rendered baseline seat (script font metrics lie; computed
     shifts sat visibly high). Per-face em sizes level the optical
     size: the script runs small at the serif's size, the didone runs
     large. */
  .hero-you.is-live.yf-1 .hero-you-ink { font-family: "Gwendolyn", cursive; font-weight: 700; font-size: 1.18em; }
  .hero-you.is-live.yf-2 .hero-you-ink { font-family: "DM Serif Display", serif; font-size: .95em; }
  .hero-you.is-live.yf-3 .hero-you-ink { font-family: "Caveat", cursive; font-weight: 600; font-size: 1.05em; }
}
.opening-lede { max-width: 30ch; }
.opening-actions {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  margin-top: .75rem;
}
.opening-vertical {
  position: absolute;
  left: clamp(.4rem, 1.2vw, 1.4rem);
  top: 50%;
  transform: translateY(-50%) rotate(180deg);
  writing-mode: vertical-rl;
}
.opening-exit {
  position: absolute;
  bottom: 1.75rem;
  left: clamp(3.5rem, 16vw, 16rem);   /* stays aligned with the copy */
  display: flex;
  align-items: center;
  gap: 1rem;
}
.opening-exit::after {
  content: "";
  width: clamp(3rem, 8vw, 7rem);
  height: 1px;
  background: var(--rose);
}

/* Falling petals: desktop only, max 3, pausable (see main.js) */
.petals { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.petal {
  position: absolute;
  top: -6%;
  width: 17px;
  height: 17px;
  opacity: 0;
  animation: petal-fall var(--pf-dur, 17s) linear var(--pf-delay, 0s) infinite;
}
.petals[data-paused] .petal { animation-play-state: paused; }
@keyframes petal-fall {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 0; }
  8%   { opacity: .4; }
  85%  { opacity: .35; }
  100% { transform: translate3d(var(--pf-drift, -4rem), 112vh, 0) rotate(290deg); opacity: 0; }
}

/* -- 7 · Custom GelX act ------------------------------------------- */
.service-custom {
  position: relative;
  --sc-pad-top: clamp(4.5rem, 9vw, 8.5rem);
  --sc-pad-bottom: clamp(4rem, 8vw, 7rem);
  /* Depth of the blush-to-porcelain bridge that carries act 1's field
     over the section boundary. See section 15d. */
  --sc-bridge: clamp(11rem, 26vh, 17rem);
  padding-block: var(--sc-pad-top) var(--sc-pad-bottom);
  /* The section opens on the same blush the hero closes on and releases
     into the field across --sc-bridge. The stops are a smoothstep, not
     a straight ramp: a linear fade holds a constant slope and then
     stops dead, and the eye reads that slope break as a faint line
     (Mach band) exactly where the fade ends. Easing both ends to zero
     slope removes it, and the slow start also keeps the pink alive a
     little longer under the seam. Each stop is the literal blush/
     vanilla mix at 3p^2-2p^3, so these seven values are recomputed if
     either endpoint token ever changes. */
  background:
    linear-gradient(to bottom,
      var(--blush)  0,
      #FADDE2      15%,
      #F9E0E1      30%,
      #F9E4DF      45%,
      #F8E8DD      60%,
      #F7EBDB      75%,
      #F7EDD9      90%,
      var(--vanilla) 100%) top / 100% var(--sc-bridge) no-repeat,
    var(--vanilla);
  /* Wrap the field around the berry curve below. .berry-field enters
     through a 64px/40px asymmetric radius, so without this the notches
     either side of the curve expose the body's porcelain as a pale
     wedge between this field and the berry. Pulling the next act up by
     the deeper of the two radii makes the curve carve into this
     section instead, and vanilla (right) and the blush band (left) run
     right down into it. Same device as .booking's margin-top:-64px
     over the About field; expressed as a bottom margin here so the
     Nail Repair rules stay untouched. */
  margin-bottom: -64px;
}

/* The blush field is a grid item spanning the same tracks as .sc-text,
   not a viewport percentage, so its right edge and the text column can
   never drift apart at any width. It bleeds left past the viewport and
   vertically over the section padding to read as a full-height band. */
.sc-band {
  grid-column: 1 / 5;
  grid-row: 1;
  margin-left: -100vw;
  margin-block: calc(-1 * var(--sc-pad-top)) calc(-1 * var(--sc-pad-bottom));
  background: var(--blush);
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.sc-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2.75rem);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}
.sc-text {
  grid-column: 1 / 5;
  grid-row: 1;              /* share row 1 with .sc-band */
  align-self: start;
  position: sticky;
  top: calc(var(--header-h) + 1.5rem);
  padding-right: clamp(0rem, 2vw, 2rem);
  display: grid;
  gap: .8rem;
  justify-items: start;
}
/* Two photographs, evenly stacked and centred in the media column
   (revision 8; the unequal three-image family and its cross-boundary
   bleed were retired). Both source files are 1448x1086, so equal width
   plus .photo's height:auto gives equal height for free: nothing here
   may set aspect-ratio or a fixed height, or they would crop. */
.sc-media {
  grid-column: 5 / 13;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 2vw, 1.5rem);
}
/* Border on the image, not the figure, so both frames stay identical
   regardless of what else a figure ever carries. */
.sc-fig { margin: 0; overflow: hidden; align-self: start; }
.sc-fig .photo { border: var(--hairline); }
/* Tunables: the share of the media column each photograph takes, and
   the ceiling that stops them ballooning past ~1600px viewports. */
.sc-fig--dominant,
.sc-fig--detail {
  align-self: center;
  width: 68%;
  max-width: 38rem;
}

/* -- 8 · Nail Repair act: berry anchor 1 --------------------------- */
.service-repair {
  position: relative;
  padding-block: clamp(6rem, 11vw, 10rem) clamp(5rem, 9vw, 8rem);
}
.sr-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2.75rem);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}
.sr-brand {
  grid-column: 1 / 6;
  align-self: start;
  position: sticky;
  top: calc(var(--header-h) + 3rem);
  padding-right: clamp(0rem, 2vw, 2rem);
}
.lockup { width: 100%; height: auto; }
.sr-content {
  grid-column: 7 / 13;
  display: grid;
  gap: .9rem;
  justify-items: start;
}
/* The comeback line: the owner's attention-catcher for a visitor
   scrolling past, and the one deliberate departure from body sizing in
   this section. Owner's call: suddenly BOLD, no italics. Instrument
   Serif has no bold cut (weight 400 only; asking for one synthesizes
   a smeared fake), so bold here means the interface voice, DM Sans at
   600, its heaviest loaded weight. No fixed line break: the sentences
   are short enough to rag cleanly at any width. The clamp floors at
   1.2rem, safely over the 16px rule, and stays above the 17-19px body
   copy below it. */
.sr-hook {
  font-family: "DM Sans", sans-serif;
  font-weight: 600;
  font-size: clamp(1.2rem, .95rem + .8vw, 1.5rem);
  line-height: 1.4;
  letter-spacing: .01em;
  color: var(--porcelain);
}
.sr-fig {
  margin-block: 1.25rem .25rem;
  background: var(--porcelain);
  padding: clamp(.8rem, 1.4vw, 1.25rem);
  max-width: 34rem;
}
.sr-fig .caption { padding-top: .7rem; }

/* -- 9 · Lookbook act ---------------------------------------------- */
.lookbook {
  padding-block: clamp(5rem, 9vw, 8rem) clamp(3.5rem, 6vw, 5.5rem);
  background: var(--blush);
}
.lookbook-head {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vw, 2.75rem);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
  margin-bottom: clamp(2rem, 4vw, 3.5rem);
}
.lookbook-head .micro-label { grid-column: 1 / 3; padding-top: .9rem; }
.lookbook-head .display-title { grid-column: 3 / 10; }

/* Revision 9: the captions and the per-spread editorial collages were
   retired; every frame is now the same 4:3 box, and each spread is a
   centred row of them. The frame's height comes from its aspect ratio,
   so the img's height:100% + .photo's object-fit:cover crops the three
   portrait shots (gallery-02/06/10) to the band the frame shows; tune
   with --focal-x/--focal-y on the img if one crops badly. */
.lb-fig {
  margin: 0;
  overflow: hidden;
  border: var(--hairline);
  background: var(--porcelain);
  aspect-ratio: 4 / 3;
}
.lb-fig picture, .lb-fig img { width: 100%; height: 100%; }

.lb-btn {
  min-width: 44px;
  min-height: 44px;
  padding: .55rem 1.1rem;
  background: none;
  border: 1px solid var(--rose);
  border-radius: var(--radius-1);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--charcoal);
  transition: background var(--duration-fast) var(--ease-soft),
              color var(--duration-fast) var(--ease-soft);
}
.lb-btn:hover { background: var(--berry); border-color: var(--berry); color: var(--porcelain); }
.lb-scenes { display: flex; gap: .25rem; }
.lb-scene {
  min-width: 44px;
  min-height: 44px;
  background: none;
  border: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--mute-ink);
  border-bottom: 2px solid transparent;
}
.lb-scene[aria-current="true"] { color: var(--berry); border-bottom-color: var(--berry); }
.lb-progress { font-size: 1rem; color: var(--mute-ink); min-width: 4.5rem; text-align: center; }
.lb-controls {
  display: flex;
  align-items: center;
  gap: clamp(.5rem, 2vw, 1.25rem);
  flex-wrap: wrap;
}

/* Base layout (also the no-JS fallback): all five spreads visible in a
   compact stacked composition. JS + media queries layer the stage and
   rail modes on top of this. */
.lb-scroll {
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}
.lb-spreads { display: grid; gap: clamp(2rem, 4vw, 3.5rem); }
.lb-spread { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem; }
.lb-spread .lb-fig { width: min(100%, 30rem); }
.no-js .lb-controls, .no-js .lb-triggers { display: none; }
.lb-triggers { display: none; }

/* Rail mode: phones and tablet portrait (JS active, below 1024px) */
@media (max-width: 1023.98px) {
  .js.wired .lb-scroll { padding-inline: 0; }
  .js.wired .lb-spreads {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    gap: clamp(.9rem, 2vw, 1.5rem);
    padding-inline: var(--page-gutter);
    padding-bottom: .75rem;
    scroll-padding-inline: var(--page-gutter);
  }
  /* Each card is a centred row (phone: a stack) of identical frames.
     The frame width is fixed per breakpoint rather than flexed, so a
     2-photo card shows the same size frames as a 3-photo card. */
  .js.wired .lb-spread {
    flex: 0 0 86%;
    scroll-snap-align: center;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: .9rem;
    align-content: start;
  }
  .js.wired .lb-spread .lb-fig { width: 100%; }
  .js.wired .lb-controls { margin-top: 1rem; padding-inline: var(--page-gutter); }
}
@media (min-width: 700px) and (max-width: 1023.98px) {
  .js.wired .lb-spread { flex-basis: 84%; }
  /* Same two-tier sizing as the desktop stage: pairs split the card,
     triples go three across. */
  .js.wired .lb-spread .lb-fig { width: calc((100% - .9rem) / 2); }
  .js.wired .lb-spread-2 .lb-fig,
  .js.wired .lb-spread-4 .lb-fig { width: calc((100% - 1.8rem) / 3); }
}

/* Stage mode: desktop (JS active, 1024px and wider). The wrapper is
   330vh tall; five invisible triggers advance the sticky stage.
   Lowered from 1100 to 1024 in the tablet-landscape pass: the owner
   asked that 1024 read as the desktop layout, mechanics included, so
   the desktop boundary moved rather than a third variant being made.
   Every frame width here is a percentage of the stage, so the
   composition scales to the narrower column on its own. */
@media (min-width: 1024px) {
  .js.wired .lb-scroll { position: relative; height: 330vh; max-width: none; padding-inline: 0; }
  .js.wired .lb-stage {
    position: sticky;
    top: var(--header-h);
    height: calc(100vh - var(--header-h));
    height: calc(100svh - var(--header-h));
    max-width: var(--content-max);
    margin-inline: auto;
    padding: clamp(1rem, 2.5vh, 2rem) var(--page-gutter) 0;
    display: flex;
    flex-direction: column;
  }
  .js.wired .lb-spreads {
    display: block;
    position: relative;
    flex: 1;
    min-height: 0;
  }
  .js.wired .lb-spread {
    position: absolute;
    inset: 0;
    opacity: 0;
    transform: translateY(12px);
    transition: opacity var(--duration-medium) var(--ease-editorial),
                transform var(--duration-medium) var(--ease-editorial);
    pointer-events: none;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: clamp(1.25rem, 2.5vw, 3rem);
  }
  .js.wired .lb-spread.is-active { opacity: 1; transform: none; pointer-events: auto; }

  /* Two frame sizes (owner's call, revision 9.1): 2-photo spreads run
     larger, 3-photo spreads (spreads 2 and 4 -- keep these selectors in
     step with the markup if photos move) drop to what still seats three
     across: 3 x 31% + two 2.5vw gaps = ~98.4% at the tightest. The
     second min() term keeps the 4:3 frame's height inside the sticky
     stage on short viewports: stage inner height is 100svh less the
     header, stage padding, and controls row, ~12rem all told, and
     width may be at most 4/3 of that. */
  .js.wired .lb-spread .lb-fig {
    width: min(42%, calc((100vh - 12rem) * 1.3333)); width: min(42%, calc((100svh - 12rem) * 1.3333));
  }
  .js.wired .lb-spread-2 .lb-fig,
  .js.wired .lb-spread-4 .lb-fig {
    width: min(31%, calc((100vh - 12rem) * 1.3333)); width: min(31%, calc((100svh - 12rem) * 1.3333));
  }

  .js.wired .lb-controls {
    flex: none;
    height: 4.75rem;
    border-top: var(--hairline);
    margin-top: .75rem;
    flex-wrap: nowrap;
  }
  .js.wired .lb-progress { margin-left: auto; }
  .js.wired .lb-triggers {
    display: block;
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
  }
  .js.wired .lb-triggers div { height: 20%; }
}

/* -- 10 · About act ------------------------------------------------ */
.about {
  position: relative;
  isolation: isolate;
  /* Revision 9.2: the neutral field and its sakura tile were retired;
     About now runs the same blush-to-vanilla combo as Custom GelX. The
     lookbook above closes on flat blush, so the section opens on blush
     and releases into vanilla through the same smoothstep bridge as
     section 15d (same stops, same reasoning: eased ends kill the Mach
     band at the fade's terminus). The berry curve below already carves
     in via .booking's margin-top:-64px, so the vanilla wraps into the
     rounded seam exactly as GelX does. --neutral is now unreferenced
     but kept as a token. */
  background:
    linear-gradient(to bottom,
      var(--blush)  0,
      #FADDE2      15%,
      #F9E0E1      30%,
      #F9E4DF      45%,
      #F8E8DD      60%,
      #F7EBDB      75%,
      #F7EDD9      90%,
      var(--vanilla) 100%) top / 100% clamp(11rem, 26vh, 17rem) no-repeat,
    var(--vanilla);
  padding-block: clamp(4.5rem, 8vw, 7rem);
  overflow: hidden;
  /* No content-visibility here: this section is small, so skipping its
     render saves nothing measurable, while an inexact intrinsic size
     made scrollHeight (and the scrollbar) shift on first scroll past. */
}
.about-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3rem);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
  align-items: center;
  position: relative;
  z-index: 1;
}
.about-fig {
  grid-column: 1 / 7;
  margin: 0;
  overflow: hidden;
  border: var(--hairline);
  height: min(66vh, 31rem);
}
.about-fig picture, .about-fig img { width: 100%; height: 100%; }
.about-text { grid-column: 8 / 13; display: grid; gap: 1.1rem; justify-items: start; }
.about-mark {
  position: absolute;
  right: -2.5rem;
  bottom: -3rem;
  width: clamp(11rem, 16vw, 15rem);
  height: auto;
  opacity: .16;
  pointer-events: none;
}

/* -- 11 · Booking act + footer: berry anchor 2 --------------------- */
.booking { padding-top: clamp(6rem, 11vw, 10rem); position: relative; isolation: isolate; }
.booking-grid {
  display: grid;
  position: relative;
  z-index: 1;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(1.5rem, 3vw, 3.5rem);
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}
/* Two explicit rows in the left column, lockup over fallback, with the
   booking block spanning both on the right. The DOM order is
   lockup / block / fallback so the mobile stack reads correctly; these
   rows put the fallback back where it belongs on desktop. The column
   is no longer sticky: with the shorter block the section only scrolls
   about a screen, so there was nothing left to stick to. */
.booking-brand {
  grid-column: 1 / 6;
  grid-row: 1;
  align-self: start;
  display: grid;
  gap: 2rem;
}
.booking-direct {
  grid-column: 1 / 6;
  grid-row: 2;
  align-self: start;
  display: grid;
  gap: .5rem;
  margin-top: 2.5rem;
}
.direct-line {
  font-family: "Instrument Serif", serif;
  font-size: clamp(1.25rem, 2vw, 1.6rem);
}
.direct-line a {
  color: var(--porcelain);
  text-decoration-color: var(--petal);
  text-underline-offset: .3em;
  padding-block: .6rem;    /* inline: paints, never reflows (see .footer-contact a) */
}
.direct-line a:hover { color: var(--petal); }
.direct-note { color: var(--blush); font-size: 1.0625rem; max-width: 38ch; }

.booking-form-col {
  grid-column: 7 / 13;
  grid-row: 1 / span 2;
  display: grid;
  gap: 1.1rem;
  align-content: start;
}
/* The six-field request form that used to live here was replaced by
   the chip form in section 17 (revision 10); its rules are gone with
   it. Only the embed placeholder remains. */
.scheduling-slot[hidden] { display: none; }

.site-footer {
  /* Tightened in revision 11: the chip form is far shorter than the
     six-field form this margin was set against, and the old 8rem read
     as a void rather than breathing room. */
  margin-top: clamp(3rem, 5vw, 5rem);
  border-top: 1px solid rgba(253, 248, 246, .18);
  max-width: var(--content-max);
  margin-inline: auto;
  padding: 2.25rem var(--page-gutter) calc(2.25rem + env(safe-area-inset-bottom, 0px));
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem 3rem;
}
.footer-brand { display: flex; align-items: center; gap: .9rem; margin-right: auto; }
.footer-word { font-family: "Instrument Serif", serif; font-size: 1.25rem; }
.footer-word span {
  display: block;
  font-family: "DM Sans", sans-serif;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--petal);
}
.footer-contact { font-size: 1.0625rem; color: var(--blush); }
/* padding-block on an INLINE element paints outside the line box without
   moving any line: the tap target grows from ~22px to 44+ while the
   layout stays byte-identical. Same device on .direct-line a and the
   menu foot links. Do not switch these to inline-block, which would
   turn the padding into real layout. */
.footer-contact a { color: var(--porcelain); text-underline-offset: .3em; padding-block: .7rem; }
.footer-note { font-size: 1rem; color: var(--blush); flex-basis: 100%; }

/* Persistent booking bar, small screens */
.mobile-book-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 80;
  display: none;
  background: var(--berry);
  color: var(--porcelain);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  padding: .9rem 1rem calc(.9rem + env(safe-area-inset-bottom, 0px));
  box-shadow: 0 -1px 0 rgba(247, 201, 211, .45);
  transform: translateY(100%);
  transition: transform var(--duration-medium) var(--ease-editorial);
}
.mobile-book-bar.is-shown { transform: translateY(0); }

/* -- 12 · Motion & reveal states ----------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
  .js .reveal {
    opacity: 0;
    transform: translateY(12px);
    transition: opacity 500ms var(--ease-editorial),
                transform 500ms var(--ease-editorial);
  }
  .js .reveal.is-in { opacity: 1; transform: none; }

  /* Failsafe: if main.js never runs (blocked by an extension or proxy,
     failed request, parse error), the inline head script has already
     committed the page to .js, and every .reveal above would hold
     opacity:0 forever: the content present but invisible. main.js
     stamps .wired on the root as the first act of its DOMContentLoaded
     handler; when that stamp is missing, this animation forces every
     reveal visible 5 seconds in. An animation outranks the normal
     declarations above in the cascade, so it overrides the hide
     without touching the working path: the moment .wired lands the
     selector stops matching and the designed reveal takes over.

     The same stamp gates every layout that NEEDS the driver: all
     .js.wired .lb-* rules (the lookbook stage hides every spread at
     opacity 0 until JS drives .is-active, which without main.js is a
     permanently blank gallery) and the header brand-word hide (which
     waits for an .is-scrolled class only main.js adds). Without the
     stamp those fall back to the designed no-JS layouts instead. */
  .js:not(.wired) .reveal { animation: reveal-failsafe 0s 5s both; }
  @keyframes reveal-failsafe { to { opacity: 1; transform: none; } }
}

@media (hover: hover) {
  .sc-fig img, .about-fig img { transition: transform var(--duration-medium) var(--ease-editorial); }
  .sc-fig:hover img, .about-fig:hover img { transform: scale(1.02); }
}

/* -- 13 · Responsive adjustments ----------------------------------- */
@media (max-width: 1023.98px) {
  .sc-grid, .sr-grid, .booking-grid, .about-grid { display: block; }
  .sc-text, .sr-brand, .booking-brand { position: static; }

  /* Below the grid breakpoint .sc-text carries its own blush field,
     so the bleeding band is not needed. */
  .sc-band { display: none; }
  /* Flat field here: the desktop rule's --sc-bridge gradient belongs to
     the band layout, and .sc-text supplies the blush at the top of this
     one. Keeping --vanilla in step with the desktop rule matters, or
     the two layouts disagree either side of 1024px. */
  .service-custom { background: var(--vanilla); padding-top: 0; }
  .sc-text {
    background: var(--blush);
    padding: clamp(2.5rem, 6vw, 4rem) var(--page-gutter);
    margin-inline: calc(var(--page-gutter) * -1);
    margin-bottom: 2.5rem;
  }
  /* The stack is already the desktop layout; below the grid breakpoint
     it only widens to the full column. */
  .sc-fig--dominant,
  .sc-fig--detail { width: 100%; max-width: none; }

  .sr-brand { max-width: 30rem; margin-bottom: 2.5rem; }
  .sr-fig { max-width: 100%; }

  .about-fig { height: min(48vh, 24rem); height: min(48svh, 24rem); margin-bottom: 2rem; }
  .about-text { display: grid; }
  .about-mark { right: -2rem; bottom: -2.5rem; width: 10rem; }

  .booking-brand { max-width: 30rem; margin-bottom: 2.5rem; }
  .booking-form-col { display: grid; }
  /* The fallback now follows the booking block, so it needs a rule to
     read as a separate offer rather than a stray tail of the form. */
  .booking-direct {
    max-width: 30rem;
    margin-top: 3rem;
    padding-top: 2.5rem;
    border-top: 1px solid rgba(253, 248, 246, .18);
  }
}

/* Menu threshold: full nav appears only when it genuinely fits.
   Lowered from 1100 to 1024 with the rest of the desktop boundary;
   verified the brand, five links and the CTA still seat on one row at
   1024 with room to spare (verify-t1024.mjs). */
@media (max-width: 1023.98px) {
  .site-nav, .header-cta { display: none; }
  .menu-toggle { display: flex; }
  /* No-JS fallback: the menu button cannot work, so show the links */
  .no-js .menu-toggle { display: none; }
  .no-js .site-nav { display: flex; flex-wrap: wrap; gap: .75rem; }
}
@media (max-width: 899.98px) {
  .mobile-book-bar { display: block; }

  .opening { min-height: auto; display: flex; flex-direction: column; }
  .opening-media { position: static; order: 2; height: min(52vh, 28rem); height: min(52svh, 28rem); }
  .opening-copy {
    order: 1;
    min-height: 0;
    max-width: none;
    padding: calc(var(--header-h) + 3rem) var(--page-gutter) 3rem;
  }
  .opening-copy .display-hero { width: auto; max-width: none; }
  .opening-vertical { display: none; }
  /* Seam duty on mobile (section 15d). The hero's wash layer is
     anchored to the top with a fixed height here, on purpose, to keep
     font-load reflow out of CLS, so it cannot be masked a safe
     distance from a bottom edge that moves. .opening-exit does the job
     instead: it is the last row of the flex column and the only blush
     the eye sees between the photo and the next section, and in-flow
     content paints above a z-index:-2 layer, so a fade in its own
     background settles the hero's last band on flat --blush however
     tall the opening reflows. Padding-top is up from 1.5rem to give
     the fade enough run. */
  .opening-exit {
    position: static;
    order: 3;
    padding: 3.25rem var(--page-gutter) 2rem;
    background: linear-gradient(to bottom, rgba(250, 220, 227, 0), var(--blush) 88%);
  }
}

@media (max-width: 767.98px) {
  .site-footer { padding-bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px)); }
  .lookbook-head { display: block; }
  .lookbook-head .micro-label { margin-bottom: .8rem; }
}

/* Short landscape viewports */
@media (max-height: 620px) {
  .opening, .opening-copy { min-height: 0; }
  .opening-copy { padding-block: calc(var(--header-h) + 2.5rem) 3rem; }
}

/* -- 14 · Reduced motion & accessibility overrides ----------------- */
:focus-visible { outline: 2px solid var(--berry); outline-offset: 3px; }
.berry-field :focus-visible,
.menu-panel :focus-visible,
.on-berry :focus-visible,
.mobile-book-bar:focus-visible { outline-color: var(--petal); }

@media (prefers-reduced-motion: reduce) {
  .petals { display: none; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}

/* -- 15 · Living backgrounds (appended, revision 3) ------------------
   Animated layers only. Every layer is a pseudo-element at z-index:-1
   inside an isolated parent, animates transform/opacity only, and ends
   its cycle pixel-identical to its start (so the global reduced-motion
   override freezes it into a clean static background). */

/* 15a · Watercolour wash, shared by the hero and the Custom GelX
   blush field so the two acts read as one continuous pink (rev 4).
   GelX: on .sc-band on desktop; ≤1023.98px .sc-band is display:none
   and the wash moves to .sc-text (sticky on desktop, so never washed
   there; see the media block below, where `content` is set). The
   visual rules are shared; a pseudo-element without `content`
   generates no box.
   Hero: since revision 5 the hero wash lives on .opening::after alone
   (all three blooms in one layer at z-index:-2), because ::before now
   carries the masthead bottle pattern (section 16) at z-index:-1,
   which must paint above the wash. */
.sc-band::before, .sc-band::after,
.sc-text::before, .sc-text::after {
  position: absolute;
  inset: -8%;              /* oversized so drift + scale never shows an edge */
  z-index: -1;
  pointer-events: none;
  will-change: transform;
}
.sc-band::before, .sc-text::before {
  background-image:
    radial-gradient(ellipse 62% 55% at 22% 20%,
      rgba(239, 168, 188, .80), rgba(244, 190, 205, .34) 55%, rgba(247, 201, 211, 0) 78%),
    radial-gradient(ellipse 58% 52% at 76% 74%,
      rgba(247, 201, 211, .92), rgba(250, 220, 227, 0) 72%);
  animation: gelx-bloom-a 34s ease-in-out infinite;
}
.sc-band::after, .sc-text::after {
  background-image:
    radial-gradient(ellipse 52% 46% at 55% 6%,
      rgba(224, 143, 166, .46), rgba(239, 182, 198, 0) 70%);
  animation: gelx-bloom-b 46s ease-in-out infinite;
}
.sc-band::before, .sc-band::after { content: ""; }

/* The hero is already position:relative; isolate it so the z-index:-1
   layers stay inside, and clip so the oversized wash cannot spill
   over the header area or into the porcelain of the next act. */
.opening {
  isolation: isolate;
  overflow: hidden;  /* Safari <16 fallback */
  overflow: clip;
}
.opening::after {
  content: "";
  position: absolute;
  inset: -8%;
  z-index: -2;             /* beneath the masthead pattern layer */
  pointer-events: none;
  will-change: transform;
  background-image:
    radial-gradient(ellipse 62% 55% at 22% 20%,
      rgba(239, 168, 188, .80), rgba(244, 190, 205, .34) 55%, rgba(247, 201, 211, 0) 78%),
    radial-gradient(ellipse 58% 52% at 76% 74%,
      rgba(247, 201, 211, .92), rgba(250, 220, 227, 0) 72%),
    radial-gradient(ellipse 52% 46% at 55% 6%,
      rgba(224, 143, 166, .46), rgba(239, 182, 198, 0) 70%);
  animation: gelx-bloom-a 34s ease-in-out infinite;
}

/* Below 900px the hero is auto-height and reflows when the webfonts
   arrive; a percentage-inset layer would move with that reflow and
   count as layout shift. Anchor the wash to the top with a fixed,
   generous height instead; overflow:clip trims the excess. */
@media (max-width: 899.98px) {
  .opening::after {
    inset: auto -8% auto -8%;
    top: -4rem;
    height: 190vh;
    height: 190svh;
  }
}

@media (max-width: 1023.98px) {
  /* position:relative (not the sticky the desktop rule sets) so the
     pseudo-elements anchor to this box, not to .service-custom;
     top:auto because the desktop rule's sticky top offset would
     otherwise shift a relative box down 96px; overflow:clip so the
     oversized wash cannot paint past the blush block onto the
     porcelain around it. */
  .sc-text {
    position: relative;
    top: auto;
    isolation: isolate;
    overflow: hidden;  /* Safari <16 fallback */
    overflow: clip;
  }
  /* Same seam treatment as .sc-band (section 15d). Below the grid
     breakpoint .sc-text is the blush block and it starts flush against
     the hero, so at full strength its wash met the hero's edge with a
     40/255 step, pink against a deeper pink. Fading it in over the top
     third lands both sides within ~6/255 of --blush. */
  .sc-text::before, .sc-text::after {
    content: "";
    -webkit-mask-image: linear-gradient(to bottom, transparent 18%, #000 38%);
            mask-image: linear-gradient(to bottom, transparent 18%, #000 38%);
  }
}

@keyframes gelx-bloom-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(3%, -2.5%, 0) scale(1.12); }
}
@keyframes gelx-bloom-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(-3.5%, 2%, 0) scale(1.09); }
}

/* 15d · The act 1 -> act 2 seam.
   Two things used to break the flow where the opening meets Custom
   GelX. First, the section's porcelain met the hero's blush edge to
   edge across everything right of .sc-band: a ~36/26 step in G/B, the
   hard line. That is handled by the --sc-bridge gradient on
   .service-custom above.

   Second, and the reason a flat colour match alone cannot work: the
   hero's bottom edge is not one colour. The wash's lower-right bloom
   still carries tint there, so the edge ran from #FADCE3 on the left
   to #F8D0D9 on the right, and because the layer is animated that
   figure breathes on a 34s cycle. No static gradient can meet a moving
   target. So both washes are pulled back from the join instead: the
   hero's fades out before its own bottom edge, the band's fades in
   below its top edge, and the two acts meet on flat --blush at every
   x and every phase of the animation.

   The mask travels with the transformed layer it masks (the same trap
   documented in section 16), so each fade must finish clear of the
   worst-case travel: bloom-a reaches scale(1.12) translateY(-2.5%),
   bloom-b scale(1.09) translateY(2%). The stops below leave roughly a
   4% margin at both ends, verified by sampling the seam at eight
   points through the cycle. Do not tighten them toward the edges. */
@media (min-width: 900px) {
  .opening::after {
    -webkit-mask-image: linear-gradient(to bottom, #000 58%, transparent 80%);
            mask-image: linear-gradient(to bottom, #000 58%, transparent 80%);
  }
}
.sc-band::before, .sc-band::after {
  -webkit-mask-image: linear-gradient(to bottom, transparent 18%, #000 34%);
          mask-image: linear-gradient(to bottom, transparent 18%, #000 34%);
}

/* 15b · About: the sakura tile was removed in revision 9.2; the field
   now lives on .about itself (section 10). */

/* -- 16 · Opening masthead (revision 5) ------------------------------
   A centred wordmark banner at the top of the hero, backed by a
   drifting nail-polish-bottle tile that dissolves downward into the
   blush field. Tunables: --masthead-h on .opening (height),
   opacity .7 on .opening::before (tile strength), the mask stops
   (34% / 96%, fade band), and the drift durations (40s / 46s). */

.opening-masthead {
  position: relative;
  z-index: 2;                       /* above the pattern layer */
  height: var(--masthead-h);
  padding-top: var(--header-h);     /* clearance under the fixed header */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}
.masthead-word {
  font-family: "Instrument Serif", serif;
  font-weight: 400;
  font-size: clamp(2.75rem, 7.5vw, 6rem);
  line-height: 1;
  color: var(--charcoal);           /* display headings inherit this */
  margin-top: auto;                 /* pairs with the rule's auto margin:
                                       centres word+sub above the rule */
}
.masthead-sub {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--mute-ink);
  margin-top: .7rem;
  line-height: 1;          /* keeps the stack inside the fixed height */
}

/* Separator: full-width hairline broken by the blossom mark, pinned
   to the bottom edge of the masthead block. */
.masthead-rule {
  display: flex;
  align-items: center;
  gap: clamp(.85rem, 2vw, 1.4rem);
  width: 100%;
  padding-inline: var(--page-gutter);
  position: relative;
  z-index: 2;
  margin-top: auto;        /* pins the rule to the masthead's bottom */
  margin-bottom: 1.1rem;
}
.masthead-rule span {
  flex: 1;
  height: 2px;
  background: var(--rose);
  opacity: 1;
}
.masthead-rule img {
  flex: none;
  display: block;
  width: 44px;
  height: 44px;
  /* The shared mark is petal pink on a blush field, which all but
     disappears. Deepen it here only; the header, menu, and footer
     blossoms keep the original asset colour. */
  filter: brightness(.82) saturate(1.45);
}

/* Wide screens only: pull the photograph left, toward the copy, and
   pull its right edge in by the same amount so the size is unchanged.
   Below 1280px the copy column would run into it, so the base inset
   (anchored to the right gutter) stands there. */
@media (min-width: 1280px) {
  .opening-media { inset: calc(var(--masthead-h) + 5rem) 20% 14% 55%; }
}

/* Hero type, scoped to the opening only; .display-hero elsewhere on
   the page must not change. */
.opening-copy .display-hero {
  font-size: clamp(2.1rem, 5.2vw, 4.1rem);
}
.opening-lede {
  font-size: clamp(1rem, 0.92rem + 0.23vw, 1.1rem);  /* ~8% down, 16px floor */
}

/* The pattern layer. Note on the drift direction: the brief asked for
   a diagonal translate3d(-170px,-170px) on this masked layer, but a
   mask travels with the element it masks, so any vertical motion
   makes the fade edge creep upward through the cycle and snap back at
   the loop point; there is no seamless diagonal loop for a masked
   layer in CSS. A vertical fade gradient is invariant under purely
   horizontal translation, so the drift is horizontal: exactly one
   tile per cycle, pixel-identical at the wrap, fade edge rock-still.
   The box is oversized sideways so the moving edges stay offscreen;
   .opening's overflow:clip contains it. */
.opening::before {
  content: "";
  position: absolute;
  inset: 0 -190px auto -190px;
  /* Pattern is confined to the masthead: it ends at the separator line
     (masthead height, less the rule's bottom margin and half the
     34px blossom row) and never enters the hero field below it. */
  height: calc(var(--masthead-h) - 1.1rem - 22px);
  z-index: -1;
  pointer-events: none;
  opacity: .7;
  background-repeat: repeat;
  background-size: 170px 170px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cdefs%3E%3Cg id='b'%3E%3Crect x='-6' y='-26' width='12' height='11' rx='2.5' fill='%23FDF8F6' fill-opacity='.8'/%3E%3Crect x='-4' y='-17' width='8' height='5' fill='%23FDF8F6' fill-opacity='.6'/%3E%3Crect x='-13' y='-13' width='26' height='33' rx='7' fill='%23EFB6C6'/%3E%3Crect x='-8.5' y='-8' width='5' height='17' rx='2.5' fill='%23FDF8F6' fill-opacity='.38'/%3E%3C/g%3E%3C/defs%3E%3Cuse href='%23b' transform='translate(40,44) rotate(-20) scale(0.78)'/%3E%3Cuse href='%23b' transform='translate(126,58) rotate(28) scale(0.64)'/%3E%3Cuse href='%23b' transform='translate(58,130) rotate(14) scale(0.7)'/%3E%3Cuse href='%23b' transform='translate(138,138) rotate(-34) scale(0.6)'/%3E%3C/svg%3E");
  /* Fades out just as it reaches the separator, so the pattern meets
     the line without a hard edge. */
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
  animation: masthead-drift 40s linear infinite;
  will-change: transform;
}
@keyframes masthead-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-170px, 0, 0); }
}

/* Header wordmark: the masthead already says the name, so the header
   word stays silent until the masthead scrolls away. Opacity only, so
   the name never leaves the accessibility tree and the .brand link
   never changes size. Scoped to .js: without JS is-scrolled never
   fires and the word must stay visible. */
.brand-word {
  transition: opacity var(--duration-medium) var(--ease-soft);
}
.js.wired .site-header:not(.is-scrolled) .brand-word { opacity: 0; }

@media (max-width: 899.98px) {
  /* Floor 6rem, not the briefed 4.5rem: the 44px word, sub, separator,
     and margins stack to ~93px and 11vh undershoots that on an
     iPhone SE. Sub margin tightened to match. */
  .opening { --masthead-h: calc(var(--header-h) + clamp(7rem, 13vh, 7.5rem)); }
  .opening-masthead { order: 0; }
  .masthead-word { font-size: 2.75rem; }  /* masthead, not splash screen */
  .masthead-sub { margin-top: .45rem; }
  .masthead-rule { margin-bottom: .9rem; }
  .masthead-rule img { width: 30px; height: 30px; }
  .opening::before { height: calc(var(--masthead-h) - .9rem - 15px); }

  /* The masthead now provides the header clearance, so the copy keeps
     only its own breathing room (overrides section 13's
     calc(var(--header-h) + 3rem)). */
  .opening-copy { padding-top: 2.5rem; }

  /* Smaller tile so the pattern reads as a pattern on narrow screens.
     Own keyframes: the travel must equal this tile size exactly or
     the loop jumps. Slightly slower so the shorter distance does not
     read faster. */
  .opening::before {
    inset: 0 -140px auto -140px;
    background-size: 120px 120px;
    animation: masthead-drift-m 46s linear infinite;
  }
}
@keyframes masthead-drift-m {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-120px, 0, 0); }
}

/* Short landscape viewports: section 13 re-adds header clearance to
   the copy padding there; the masthead already provides it. */
@media (max-height: 620px) {
  .opening-copy { padding-block: 1.5rem 3rem; }
}

/* 15c · Booking + footer: plain berry (the revision-3 bottle tile was
   removed in revision 4 at the owner's request). position/isolation on
   .booking and the z-index safeguard on .booking-grid stay: they keep
   the section painting above the About field, which matters for the
   curve overlap below.

   The berry-field radius (64px / 40px vertical) leaves body porcelain
   showing inside the box above the curve. Pulling the section up by
   the deeper radius makes the curve carve into the About field, so the
   neutral sakura background fills the notches edge to edge. */
.booking { margin-top: -64px; }

/* -- 17 · Booking chip form (revision 10) ----------------------------
   Tap-first replacement for the six-field request form. Every selector
   here is bk- prefixed and lives inside .booking-form-col, so nothing
   above this line is redefined and nothing outside the booking column
   can match. Mobile-first; the single breakpoint is the same 768px
   step where section 13 already collapses this column.

   Colour notes: text is porcelain or blush on berry, both well clear
   of AA. Petal is used for focus rings and the error line only (6.2:1
   on berry), never for body text. Motion is opacity and transform
   only, so the global reduced-motion block in section 14 freezes it. */

.bk-intro {
  color: var(--blush);
  font-size: 1.0625rem;
  line-height: 1.7;
  max-width: 44ch;
}

/* Fieldset carries a border, margin and padding in every browser and
   legend sits inside the border box. Zero all four, then make the
   legend behave as an ordinary label line. */
.bk-group {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.bk-legend {
  display: block;
  float: none;
  margin: 0 0 .75rem;
  padding: 0;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--porcelain);
}

/* The radio itself stays in the focus order. display:none or
   visibility:hidden would remove it and take keyboard selection and
   arrow-key traversal with it. position:relative on the row keeps the
   collapsed inputs anchored inside the group. */
.bk-chips {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}
.bk-chip-input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.bk-chip {
  display: inline-block;
  border: 1px solid rgba(253, 248, 246, .4);
  border-radius: 999px;
  /* 11px, not 10: 20px line + 22 padding + 2 border = 44px, the tap
     minimum. At 10px the pills measured 42px. */
  padding: 11px 18px;
  /* 16px floor: the owner's no-text-below-16px rule covers chips. */
  font-size: 1rem;
  line-height: 1.25;
  color: var(--porcelain);
  cursor: pointer;
  transition: opacity var(--duration-fast) var(--ease-soft);
}
.bk-chip-input:checked + .bk-chip {
  background: var(--porcelain);
  border-color: var(--porcelain);
  color: var(--berry-deep);
  font-weight: 500;
}
/* The only keyboard affordance these pills have, so it has to read
   clearly against both the berry field and the porcelain fill. */
.bk-chip-input:focus-visible + .bk-chip {
  outline: 2px solid var(--petal);
  outline-offset: 3px;
}

.bk-fields {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
}
.bk-field { display: grid; gap: .45rem; }
.bk-label {
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--porcelain);
}
.bk-input {
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  width: 100%;
  /* Anything under 16px makes iOS Safari zoom the viewport on focus. */
  font-size: 1rem;
  font-family: inherit;
  background: rgba(253, 248, 246, .07);
  border: 1px solid rgba(253, 248, 246, .4);
  border-radius: var(--radius-1);
  padding: .8rem .95rem;
  color: var(--porcelain);
}
.bk-input::placeholder { color: var(--blush); opacity: 1; }
.bk-input:focus-visible {
  outline: 2px solid var(--petal);
  outline-offset: 2px;
}

.bk-send {
  justify-self: start;
  border: 0;
  border-radius: 999px;
  background: var(--porcelain);
  color: var(--berry);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  min-height: 48px;
  padding: 13px 26px;
  cursor: pointer;
  transition: opacity var(--duration-fast) var(--ease-soft),
              transform var(--duration-fast) var(--ease-soft);
}
.bk-send:focus-visible {
  outline: 2px solid var(--petal);
  outline-offset: 3px;
}
.bk-send:active { transform: translateY(1px); }

@media (hover: hover) {
  .bk-chip:hover { opacity: .78; }
  .bk-send:hover { opacity: .88; }
}

/* min-height reserves the line, so revealing the error moves nothing
   below it. main.js unhides this empty on load: a polite live region
   that is absent from the accessibility tree until its text changes is
   announced unreliably. */
.bk-error {
  min-height: 1.6em;
  /* Negative block margins claw back part of the column's 1.1rem gap
     so the button, the error line and the note read as one cluster
     rather than three stranded rows. */
  margin-block: -.55rem;
  color: var(--petal);
  font-size: 1rem;
  line-height: 1.6;
}
.bk-error[hidden] { display: none; }
.bk-note {
  color: var(--blush);
  font-size: 1rem;
  line-height: 1.6;
  max-width: 48ch;
}

/* Web-compose fallback, revealed by main.js after a desktop send
   attempt. Sits on the berry field, so it is drawn as an outline rather
   than a filled card: a porcelain panel here would compete with the
   SEND EMAIL button beside it for the eye. */
.bk-fallback {
  display: grid;
  gap: .85rem;
  /* Matches .bk-error's negative margin so the button, note and this
     panel stay one cluster instead of three stranded rows. */
  margin-block: -.35rem;
  padding: 1.15rem 1.25rem;
  max-width: 48ch;
  border: 1px solid rgba(247, 201, 211, .38);
  border-radius: 14px;
}
/* display:grid above would otherwise defeat the hidden attribute. */
.bk-fallback[hidden] { display: none; }

.bk-fallback-lede {
  color: var(--porcelain);
  font-size: 1rem;
  font-weight: 500;
  line-height: 1.5;
}
.bk-fallback-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
}
/* inline-flex + min-height keeps these at the 44px touch floor the rest
   of the site was brought up to, without a fixed height that would clip
   a wrapped label at large text sizes. */
.bk-fallback-btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 11px 20px;
  border: 1px solid var(--petal);
  border-radius: 999px;
  color: var(--porcelain);
  font-size: 1rem;
  line-height: 1.2;
  text-decoration: none;
  transition: opacity var(--duration-fast) var(--ease-soft);
}
.bk-fallback-btn:focus-visible {
  outline: 2px solid var(--petal);
  outline-offset: 3px;
}
.bk-fallback-alt {
  color: var(--blush);
  font-size: 1rem;
  line-height: 1.6;
}
.bk-fallback-alt a {
  color: var(--porcelain);
  /* Padding on an inline element paints outside the line box, so the
     touch target grows without moving the line. Same technique used for
     the footer and menu links. */
  padding: 11px 2px;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.bk-fallback-alt a:focus-visible {
  outline: 2px solid var(--petal);
  outline-offset: 2px;
}

@media (hover: hover) {
  .bk-fallback-btn:hover { opacity: .8; }
}

@media (min-width: 768px) {
  /* minmax(0, 1fr), not 1fr: the default min-width:auto lets a long
     value push a column past its track. */
  .bk-fields { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

/* -- 18 · Mobile revamp (phones only, max-width 767.98px) -----------
   Appended after every other section ON PURPOSE: these blocks win all
   cascade ties against the older responsive blocks in sections 9, 13,
   15 and 16. Everything in here is scoped to 767.98px and below, so
   tablet (768 to 1023) and desktop (1024 up) render exactly as before.
   18a is layout and rhythm; 18b is the pinned lookbook stage and must
   always describe the same audience as isStage() in main.js:
   width under 768, motion allowed, JS on. Change them together. */

/* 18a · Layout, rhythm, and the first screen */
@media (max-width: 767.98px) {
  :root {
    --m-space-1: 1rem;      /* inside a block */
    --m-space-2: 1.75rem;   /* block to block */
    --m-space-4: 4.25rem;   /* section padding */
  }

  /* First screen: the masthead has just said the name, so the copy's
     micro-label repeats it; drop it and tighten the stack so the top
     of the hero photograph rises above the fold. */
  .opening-copy .micro-label { display: none; }
  .opening-copy {
    padding: 1.75rem var(--page-gutter) 2.25rem;
    gap: var(--m-space-1);
    align-items: center;
    text-align: center;
  }
  .opening-lede { max-width: 34ch; margin-inline: auto; }
  .opening-actions {
    margin-top: .5rem;
    justify-content: center;
    flex-wrap: wrap;          /* base is nowrap; 390px can overflow */
    gap: .9rem 1.75rem;
  }
  /* One call to action on the phone. The menu and the section-02 dots
     both reach the work, so the second link only diluted the first. */
  .opening-copy .quiet-link { display: none; }

  /* Hero photograph: a contained figure inside the gutters instead of
     a full-bleed band, so the centred type stays the loudest thing on
     the first screen. This height is the only dial for its size. */
  .opening-media {
    height: min(34vh, 16rem); height: min(34svh, 16rem);
    margin-inline: var(--page-gutter);
  }
  .opening-media .photo { border: var(--hairline); }

  /* Photographs sit inside the gutters in a consistent band height, so
     text keeps the rhythm instead of every image running full 4:3.
     The height cap layers over the section 7 no-fixed-height rule
     without touching it: that rule guards the desktop equal-height
     pairing, which does not exist in this stacked layout. */
  .sc-fig { height: min(62vw, 17rem); }
  .sc-fig picture { display: block; height: 100%; }
  .sc-fig .photo { height: 100%; }
  .sr-fig picture { display: block; }
  .sr-fig .photo { height: min(62vw, 17rem); }
  .about-fig { order: 2; height: min(62vw, 17rem); margin-bottom: 0; }

  /* The stacked lockup at 30rem dominates the berry sections on a
     phone; cap it to a brand mark, not a billboard, and centre it over
     the copy it introduces. */
  .sr-brand, .booking-brand {
    max-width: 17rem;
    margin-inline: auto;
    margin-bottom: var(--m-space-2);
  }

  /* Centred columns. The desktop hangs its type off a left edge that
     only exists because a photograph sits beside it; with everything
     stacked, the same copy reads better down the middle of the phone.
     These are all grid containers, so justify-items centres the
     buttons and figures while text-align centres the type. */
  .sc-text, .sr-content, .about-text {
    justify-items: center;
    text-align: center;
  }
  .sc-text .body-copy,
  .sr-content .body-copy,
  .about-text .body-copy { max-width: 36ch; }
  .sr-fig { width: 100%; }
  .booking-form-col > .display-title,
  .booking-form-col > .bk-intro { text-align: center; }
  .bk-intro { max-width: none; }

  /* The numbered act markers stay hung on the left edge: they separate
     the page rather than belong to a heading. 01 and 02 already sit
     left (.opening-exit and .lookbook-head keep their base
     alignment); 03 is the only one inside a centred grid. The
     unnumbered eyebrows in Services and Nail Repair stay centred with
     the headings they introduce. */
  .about-text .micro-label {
    justify-self: start;
    text-align: left;
    margin-bottom: .75rem;   /* clears the heading below it */
  }

  /* The direct-contact block repeats the footer, which carries the
     same number and address a few hundred pixels further down. One
     berry column is enough on a phone. */
  .booking-direct { display: none; }

  /* The booking form's own snap point was REMOVED on 2026-08-13 (owner:
     "it needs to be slightly more free scrolling like every other
     website"). It used to pull the viewport onto the form whenever a
     flick ended near it, which is the behaviour that made the page feel
     like it was fighting the thumb. The compaction below stays: it is
     what makes title-through-Send fit a 390x797 viewport, and it earns
     its keep without anything grabbing the scroll.
     The page's only remaining snap targets are the lookbook's, in
     section 22, and section 22 has since removed those too: the site
     now has no scroll snapping at all. */
  .booking-form-col {
    gap: .7rem;
  }
  .booking-form-col .display-title { font-size: 1.9rem; }
  .bk-intro { font-size: 1rem; }
  .bk-legend { margin-bottom: .5rem; }
  .bk-fields { gap: .75rem; }
  .bk-input { padding: .65rem .9rem; }

  /* Tap ergonomics: manipulation removes the double-tap-zoom delay on
     every control, and the inputs already hold 1rem type, which is
     what stops iOS zooming the page when a field gains focus. */
  .bk-chip, .bk-input, .bk-send, .action, .quiet-link,
  .mobile-book-bar, .lb-scene, .menu-toggle { touch-action: manipulation; }

  /* Phones draw transient overlay scrollbars, but a desktop engine at
     phone width reserves a permanent track down the right edge and it
     reads as part of the design. Hidden below the tablet boundary
     only, so the desktop scrollbar is untouched. */
  html { scrollbar-width: none; }
  html::-webkit-scrollbar { display: none; }

  /* The compact menu card moved to section 19x when tablet portrait
     adopted it. It renders identically here; only its audience grew. */

  /* The photograph closes About instead of opening it: the story
     reads first, the image is the last note before the contact card.
     Flex order only, so the desktop grid is untouched. Stretch is
     explicit because the base rule centres items, and a centred flex
     item would size itself to the photograph's intrinsic width. */
  .about-grid {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--m-space-2);
  }

  /* Section rhythm: the desktop clamps floor too high for a 390px
     column. 4.5rem top padding on .booking still clears the 64px
     berry-curve carve. */
  .service-repair { padding-block: 4.5rem 4rem; }
  /* Tight head on the lookbook: the stage below opens with the first
     photograph, so anything generous here reads as a dead field. */
  .lookbook { padding-block: 2.5rem 3rem; }
  .lookbook-head { margin-bottom: 1.25rem; }
  /* Deeper foot on About: the berry contact card curves up into this
     section, and the centred copy needs room before it does. */
  .about { padding-block: var(--m-space-4) 6.5rem; }
  .booking { padding-top: 4.5rem; }
  .sc-text { margin-bottom: var(--m-space-2); }
  .price-display { font-size: 2.75rem; }
  .site-footer { gap: var(--m-space-1) 2rem; }
}

/* 18b · The lookbook stage: the compact analogue of the desktop stage
   (section 9). The section pins for 360svh of travel and each spread
   takes the screen with a cross-fade, its photographs as equal 4:3
   frames stacked and centred, sized off 100svh. Reduced motion or no
   JS falls back to the section 9 rail / stack, which is the designed
   fallback rather than a degradation.

   Its audience grew to 1023.98 when tablet portrait asked for the same
   animated stage, and the stacked geometry suits a tall portrait
   tablet better than the desktop row would. The band 1024 to 1099.98
   still gets the rail, since the desktop stage starts at 1100; that is
   for the 1024 pass to rule on.

   COUPLING: this query must always describe the same audience as
   isStage() in main.js. Change them together. */
@media (max-width: 1023.98px) and (prefers-reduced-motion: no-preference) {
  .js.wired .lb-scroll { position: relative; height: 360vh; height: 360svh; max-width: none; padding-inline: 0; }
  /* Sticks below the header rather than behind it, the same geometry
     the desktop stage uses. Padding under a top:0 stage pushed the
     first photograph a full header-height further down the screen,
     which is what made the entry read as a dead field. */
  .js.wired .lb-stage {
    position: sticky;
    top: var(--header-h);
    height: calc(100vh - var(--header-h)); height: calc(100svh - var(--header-h));
    padding-top: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;          /* Safari <16 fallback; harmless here: the
                                  stage is ITSELF the sticky element, and
                                  overflow only endangers sticky DESCENDANTS */
    overflow: clip;            /* clip, not hidden: this is the project sticky rule */
    background: var(--blush);  /* fills the strip the iOS URL bar reveals */
  }
  .js.wired .lb-spreads {
    display: block;
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
    overflow: visible;
    padding: 0;
    gap: 0;
    scroll-snap-type: none;
  }
  .js.wired .lb-spread {
    position: absolute;
    inset: 0;
    flex: none;
    display: flex;
    flex-direction: column;
    /* nowrap and align-content matter: the rail block above leaves
       flex-wrap:wrap and align-content:start on this element, and a
       wrapped column packs its single line to the left instead of
       centring it. */
    flex-wrap: nowrap;
    align-content: center;
    align-items: center;
    justify-content: center;
    gap: .75rem;
    padding-inline: 0;   /* the photographs carry the section, edge to edge */
    opacity: 0;
    transform: translateY(14px);
    transition: opacity var(--duration-slow) var(--ease-editorial),
                transform var(--duration-slow) var(--ease-editorial);
    pointer-events: none;
    /* Not cosmetic. The rail block above leaves scroll-snap-align on
       these, and 18a makes the page itself a snap container for the
       booking form, so without this reset every spread becomes a snap
       target for the whole page and a relayout can throw the visitor
       into the middle of the gallery. */
    scroll-snap-align: none;
  }
  .js.wired .lb-spread.is-active { opacity: 1; transform: none; pointer-events: auto; }

  /* Equal frames, same as the desktop stage: every photograph in a
     spread gets the identical 4:3 box, stacked and centred. The width
     is whichever is smaller, the column or what the frame's height may
     be for the stage to seat two (or three) of them: stage inner
     height is 100svh less the header, the controls row and slack,
     about 10rem, and width may be at most 4/3 of each frame's share.
     Keep the spread-2 / spread-4 selectors in step with the markup if
     photos move (same note as section 9). */
  .js.wired .lb-spread .lb-fig {
    flex: none;
    width: min(100%, calc((100vh - 9rem - .75rem - env(safe-area-inset-bottom, 0px)) / 2 * 4 / 3)); width: min(100%, calc((100svh - 9rem - .75rem - env(safe-area-inset-bottom, 0px)) / 2 * 4 / 3));
    aspect-ratio: 4 / 3;
    border: var(--hairline);
    margin: 0;
    overflow: hidden;
  }
  /* The safe-area term is not decoration: the controls row carries the
     home-indicator inset on a real handset, and the three-across
     spreads clear the stage by only a few pixels without it. */
  .js.wired .lb-spread-2 .lb-fig,
  .js.wired .lb-spread-4 .lb-fig {
    width: min(100%, calc((100vh - 9rem - 1.5rem - env(safe-area-inset-bottom, 0px)) / 3 * 4 / 3)); width: min(100%, calc((100svh - 9rem - 1.5rem - env(safe-area-inset-bottom, 0px)) / 3 * 4 / 3));
  }
  .js.wired .lb-spread .lb-fig picture,
  .js.wired .lb-spread .lb-fig img { display: block; width: 100%; height: 100%; }

  /* Slow settle on the active spread, staggered a beat per frame.
     Ends at identity, so the global reduced-motion freeze (section 14)
     lands clean. */
  .js.wired .lb-spread.is-active .lb-fig img {
    animation: lbm-kenburns 6.5s var(--ease-soft) both;
  }
  .js.wired .lb-spread.is-active .lb-fig-b img { animation-delay: .12s; }
  .js.wired .lb-spread.is-active .lb-fig-c img { animation-delay: .24s; }

  /* Dots and progress as a slim pinned bar; buttons yield to swipe. */
  .js.wired .lb-controls {
    flex: none;
    margin: 0;
    padding: .35rem var(--page-gutter) calc(.35rem + env(safe-area-inset-bottom, 0px));
    justify-content: center;
    flex-wrap: nowrap;
    background: var(--blush);
  }
  .js.wired .lb-btn { display: none; }
  .js.wired .lb-progress { min-width: 0; margin-left: .75rem; }

  .js.wired .lb-triggers {
    display: block;
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
  }
  .js.wired .lb-triggers div { height: 20%; }

  @keyframes lbm-kenburns {
    from { transform: scale(1.07) translateY(-1.2%); }
    to   { transform: scale(1) translateY(0); }
  }
}

/* -- 19 · Tablet portrait (768 to 1023.98) --------------------------
   The tablet range used to fall straight through to the pre-revamp
   rules in section 13, which collapse every grid to one stacked
   column. At 768 that left the body copy running an ~85 character
   measure and stacked full-width photographs back to back, so Custom
   GelX alone scrolled about 1,700px of image.

   This section restores a two-column composition wherever the width
   genuinely supports one and caps the measure where it does not.
   Scoped 768 and up so the locked phone layout (section 18) is
   untouched, and 1023.98 and down so desktop is untouched. Landscape
   tablets at 1024 take the desktop rules and are a separate pass. */

/* 19x · The compact menu card, moved verbatim out of 18a because
   tablet portrait now shares it. The panel element itself stays
   full-viewport, repurposed as the dim scrim and the tap-outside hit
   area (main.js closes on a scrim tap; the header, above it at
   z-index 100, keeps Close live). The foot's CTA and contact lines
   are gone here: Book is a link right in the card and the footer
   carries the contact.

   COUPLING: this query is mirrored by COMPACT in main.js, which gates
   both the scrim-tap close and the exit animation. Change them together.
   At 1024 and above the desktop nav takes over and .menu-toggle is
   hidden (section 13), so nothing above this breakpoint renders a menu
   panel at all. */
@media (max-width: 1023.98px) {
  .menu-panel {
    background: rgba(58, 50, 54, .38);
    justify-content: flex-start;
    align-items: flex-end;
    padding: calc(var(--header-h) + .4rem) 1rem 0;
    overflow: visible;
    animation: menu-scrim-in var(--duration-medium) var(--ease-soft) both;
  }
  .menu-panel-nav {
    min-width: 13.5rem;
    background: var(--berry);
    padding: .4rem 1.4rem;
    border-radius: 18px;
    box-shadow: 0 18px 44px rgba(58, 50, 54, .35);
    transform-origin: top right;
    animation: menu-drop var(--duration-medium) var(--ease-editorial) both;
  }
  /* The card speaks the interface voice, not the display voice: the
     serif at menu size reads like an unstyled browser default. Same
     face and case as the desktop nav links. */
  .menu-panel-nav a {
    font-family: "DM Sans", sans-serif;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    padding: .95rem 0;
  }
  .menu-panel-nav a:last-child { border-bottom: 0; }
  /* The raised-index alignment was tuned for the big serif; on one
     sans line the number sits on the baseline like a list marker. */
  .menu-panel-nav .menu-index { vertical-align: baseline; margin-right: .9rem; }
  .menu-panel-foot { display: none; }

  /* Closing: main.js holds the panel open under .is-closing until the
     card's animationend, then hides it. pointer-events off so a
     mid-animation tap cannot re-enter the card. */
  .menu-panel.is-closing { animation: menu-scrim-out var(--duration-fast) var(--ease-soft) both; pointer-events: none; }
  .menu-panel.is-closing .menu-panel-nav { animation: menu-drop-out var(--duration-fast) var(--ease-soft) both; }

  /* Every pair ends where its counterpart starts, so the reduced-motion
     freeze lands clean at either end. */
  @keyframes menu-drop {
    from { opacity: 0; transform: scale(.88) translateY(-.6rem); }
    to   { opacity: 1; transform: scale(1) translateY(0); }
  }
  @keyframes menu-drop-out {
    from { opacity: 1; transform: scale(1) translateY(0); }
    to   { opacity: 0; transform: scale(.88) translateY(-.6rem); }
  }
  @keyframes menu-scrim-in { from { background-color: rgba(58, 50, 54, 0); } }
  @keyframes menu-scrim-out { to { background-color: rgba(58, 50, 54, 0); } }
}

/* 19a · Tablet portrait layout */
@media (min-width: 768px) and (max-width: 1023.98px) {
  /* Opening: the photograph arrived here as a full-bleed slab under the
     copy, and a full-width box over a 4:3 source is a 2:1 crop that
     takes the tips off the nails. Give it the source's own ratio so
     nothing is cut, and size it as a plate rather than a banner. This
     width is the only dial for it. */
  .opening-media {
    width: min(62%, 23rem);
    height: auto;
    aspect-ratio: 4 / 3;
    margin-inline: auto;
  }
  .opening-media .photo { border: var(--hairline); }

  /* Opening copy reads down the middle, matching the masthead above it
     and the Custom GelX block below. .opening-copy is a flex column,
     so align-items does the centring that justify-items does in the
     grid blocks. "See the work" goes: the menu and the section-02 dots
     both reach the gallery, so the second link only diluted the first,
     the same call the phone layout made. */
  .opening-copy {
    align-items: center;
    text-align: center;
  }
  .opening-lede { max-width: 42ch; margin-inline: auto; }
  .opening-actions { justify-content: center; }
  .opening-copy .quiet-link { display: none; }

  /* Custom GelX: the pair side by side. Both sources are 1448x1086,
     so equal columns give equal heights for free, which is why the
     section 7 rule against setting a height or aspect-ratio here still
     holds. */
  .sc-media {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.25rem;
    align-items: start;
  }
  .sc-fig--dominant,
  .sc-fig--detail { align-self: start; }
  /* Careful with ch here: DM Sans has a wide zero, about 12px at this
     size, so the usual 58ch reads as 698px and caps nothing at 768.
     42ch is the value that actually lands near a 48 character line. */
  /* Owner's call: the whole Custom GelX block reads down the middle,
     label and heading and price included. .sc-text is a grid, so
     justify-items centres the price and the button while text-align
     centres the type. */
  .sc-text {
    justify-items: center;
    text-align: center;
  }
  .sc-text .body-copy { max-width: 42ch; }

  /* Pairing the photographs made the media column short enough that
     the berry curve below reached it: .service-custom's 64px bottom
     padding is exactly cancelled by the -64px margin that carves the
     curve into this field, so the frames butted straight into it.
     Deepen the padding so the cancellation leaves real space. The
     band this token also sizes is display:none below 1024. */
  .service-custom { --sc-pad-bottom: 7rem; }

  /* Nail Repair: copy left, photograph right. The figure is a child of
     .sr-content rather than a sibling of it, so the split happens
     inside that grid; the rows are explicit because the figure spans
     all of them, and justify-self has to be stated because the base
     rule sets justify-items:start, which would shrink the frame to its
     caption. */
  /* The lockup heads the section from the middle, over the two columns
     it introduces, rather than sitting in the left one. */
  .sr-brand { max-width: 20rem; margin-inline: auto; }
  .sr-content {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: clamp(1.5rem, 3vw, 2.5rem);
    align-content: start;
  }
  .sr-content > .micro-label   { grid-area: 1 / 1; }
  .sr-content > .display-title { grid-area: 2 / 1; }
  .sr-content > .price-display { grid-area: 3 / 1; }
  .sr-content > .sr-hook       { grid-area: 4 / 1; }
  .sr-content > .body-copy     { grid-area: 5 / 1; }
  .sr-content > .action        { grid-area: 6 / 1; }
  .sr-fig {
    grid-area: 1 / 2 / 7 / 3;
    justify-self: stretch;
    align-self: start;
    margin-block: 0;
    max-width: none;
  }

  /* About: back to the desktop pairing, photograph left. The desktop
     column spans (1/7 and 8/13) must be cleared or this two-column
     grid grows implicit tracks and pushes the text off the page. */
  .about-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(1.5rem, 3vw, 2.5rem);
    align-items: center;
  }
  .about-fig {
    grid-column: auto;
    height: min(52vh, 26rem); height: min(52svh, 26rem);
    margin-bottom: 0;
  }
  .about-text { grid-column: auto; }
  /* The berry contact card carves 64px up into this section, so About's
     own bottom padding is most of what separates the two: at the
     desktop clamp's 72px floor only 8px survived the carve and the
     curve landed almost on the last line of copy. */
  .about { padding-bottom: 8.5rem; }

  /* Tablets draw transient overlay scrollbars, but a desktop engine at
     tablet width reserves a permanent track down the right edge and it
     reads as part of the design. Same rule the phones carry in 18a,
     extended to the top of this range.

     Note the boundary this creates: hiding the track gives ~15px of
     viewport back, so a window whose width sits within 15px of 1024
     could in principle qualify for these rules with the bar shown and
     not without it. Verified stable across 1015-1045 (see
     verify-t768-scrollbar.mjs): the layout settles once and does not
     flip-flop. */
  html { scrollbar-width: none; }
  html::-webkit-scrollbar { display: none; }

  /* The lookbook eyebrow sits in 2 of 12 columns, which is 101px here
     and broke "02 The work" across two lines against the one-line
     title beside it. Four columns give it 222px and a single line. */
  .lookbook-head .micro-label { grid-column: 1 / 5; }
  .lookbook-head .display-title { grid-column: 5 / 13; }

  /* Booking stays a single column: the service and timing chips need
     the full width or they wrap into three ragged rows. Owner's call:
     only the lockup is centred, heading the section the way the Nail
     Repair one does. The form itself keeps its left edge, so the
     legends, labels and chips all hang off the same line. */
  .booking-brand { max-width: 20rem; margin-inline: auto; }
  .bk-intro { max-width: 44ch; }

  /* THE TABLET BOOKING SNAP WAS REMOVED on 2026-08-13, and it was a real
     bug, not just a preference. It made .booking-brand a proximity snap
     target on a page whose remaining scroll below that point is shorter
     than the catch zone, so a scroll to the very bottom was dragged
     back and THE FOOTER COULD NEVER BE REACHED. Measured before the
     removal: iPad Air 820x1180 dragged back 229px with the footer 52px
     off the bottom of the screen, iPad Pro 834x1194 dragged back 216px
     -- in both cases the footer's phone number and email, the only
     contact details on a tablet, were unreachable. 768x1024 escaped it
     only because its proportions differ.

     Do not reintroduce a snap target near the end of the document. If a
     one-screen booking effect is ever wanted back here, it has to be
     paired with enough trailing scroll for the footer to clear the
     catch zone, and verified by scrolling to the bottom at 820 and 834
     wide specifically. */
}

/* -- 20 · Tablet landscape (1024 to 1099.98) ------------------------
   Pass 2. At the owner's direction 1024 takes the DESKTOP layout and
   every desktop mechanic, so the desktop boundary moved from 1100 down
   to 1024 (sections 9 and 13) rather than a third variant being built.
   This section only tunes that desktop composition for the shorter,
   narrower screen: nothing here changes the layout's structure.

   Scoped to stop at 1099.98 so the locked desktop rendering at 1100 and
   above is untouched, and to start at 1024 so locked tablet portrait is
   untouched. */
@media (min-width: 1024px) and (max-width: 1099.98px) {
  /* Tablets draw transient overlay scrollbars; a desktop engine here
     reserves a permanent track that reads as part of the design. Same
     rule the phones and tablet portrait carry, stopping short of the
     locked desktop range. Verified not to oscillate at the 1100 seam:
     Chrome evaluates the query against innerWidth, which does not
     change when the track is hidden, so there is no feedback loop. */
  html { scrollbar-width: none; }
  html::-webkit-scrollbar { display: none; }

  /* Opening. "See the work" goes, matching every other breakpoint: the
     nav and the section-02 dots both reach the gallery. The photograph
     grows from ~26% of the width to ~39% so the nails are actually
     legible, and the copy column gets an explicit right padding to
     clear it. That padding is load-bearing: the base inset is anchored
     to the right gutter precisely because below 1280 the copy runs
     into a photograph pulled any further left (section 6). */
  .opening-copy .quiet-link { display: none; }
  .opening-media { inset: calc(var(--masthead-h) + 3.5rem) var(--page-gutter) 12% 57%; }
  .opening-copy { padding-right: 46%; }

  /* Owner's call: the act marker sits out at the page edge here rather
     than staying aligned with the copy's deep left indent. */
  .opening-exit { left: var(--page-gutter); }

  /* Owner's call: the Custom GelX column stops travelling with the
     scroll and simply heads its section, locked at the top and reading
     down. relative, NOT static: .sc-band shares grid row 1, sits
     earlier in the DOM, and is a positioned element, so a static text
     block paints UNDERNEATH the blush panel and every word vanishes.
     That shipped for one round. Sticky text was positioned, which is
     the only reason the desktop stack ever worked; staying positioned
     is load-bearing. top must be reset too: section 6's sticky offset
     would shift a relative box 96px down the column. */
  .sc-text { position: relative; top: auto; }

  /* Lookbook. The eyebrow wrapped to two lines in 2 of 12 columns
     (~150px here), and the stage centres its spread in a 696px box, so
     the head sat a long way above the photographs. Widen the eyebrow
     to one line, tighten the head, and let the frames run larger so
     they take up the slack rather than leaving a blank field. */
  .lookbook { padding-block: clamp(2.5rem, 5vw, 4rem) clamp(2rem, 4vw, 3rem); }
  .lookbook-head { margin-bottom: 1.25rem; }
  .lookbook-head .micro-label { grid-column: 1 / 4; }
  .lookbook-head .display-title { grid-column: 4 / 11; }
  /* The stage is a fixed 100svh box. Centring the spread alone put
     144px of empty blush above the photographs with the pager pinned a
     screen below; top-aligning everything jammed the photographs
     against the header with the emptiness moved underneath. What
     actually closes the composition is sizing the spread box to the
     photographs (the flex-basis below) so the pager sits directly
     under them, then centring that whole block in the stage. */
  .js.wired .lb-stage { padding-top: 0; justify-content: center; }
  .js.wired .lb-spread { align-items: flex-start; }
  /* Size the spread box to the tallest frame so the controls close the
     composition instead of being pinned a screen below it. Derived,
     not guessed: the gutter is 4vw across this whole range (the clamp
     floor 1.25rem and ceiling 4rem both sit outside it), so the stage
     is 100vw - 8vw = 92vw wide, the two-photo frame is 47% of that =
     43.24vw, and a 4:3 frame is three quarters as tall = 32.43vw. The
     blush below the controls is the section's own field, so the stage
     ending early is invisible. */
  .js.wired .lb-spreads { flex: 0 0 32.5vw; }
  .js.wired .lb-spread .lb-fig { width: min(47%, calc((100vh - 9rem) * 1.3333)); width: min(47%, calc((100svh - 9rem) * 1.3333)); }
  /* 31% is not a round number, it is the ceiling: three frames plus the
     two 2.5vw gaps come to ~927 of the 942px stage. 32% wrapped the
     three-photo spreads onto a second row. */
  .js.wired .lb-spread-2 .lb-fig,
  .js.wired .lb-spread-4 .lb-fig { width: min(31%, calc((100vh - 9rem) * 1.3333)); width: min(31%, calc((100svh - 9rem) * 1.3333)); }

  /* The contact-block snap that used to live here was REMOVED on
     2026-08-13 along with the tablet-portrait one (see 19a), for the
     same two reasons: a snap target this close to the end of the
     document can drag a bottom-bound scroll back off the footer, and
     the owner asked for ordinary free scrolling everywhere except the
     lookbook. Nothing in this range is a snap target now. */
}

/* -- 21 · Share section (mobile only, additive) ----------------------
   A share card and one button, sitting in the existing berry field
   between the booking block and the footer. Every selector is sh-
   prefixed and scoped inside .sh-share, so nothing here can cascade
   into an existing component; no existing rule was edited to add it.

   THE GATE IS NOT A MEDIA QUERY, ON PURPOSE. The section is
   display:none until main.js adds .sh-enabled, which it does only for
   a phone-width viewport that genuinely supports navigator.share. A
   desktop window narrowed to phone width must never reveal a control
   whose only action is a native share sheet it does not have, and a
   CSS breakpoint alone could not tell those two cases apart.

   No animation, transition, transform, mask or filter is used for
   layout or state here. The one filter present is on the blossom
   image, and it is a static colour correction, not motion: it is the
   same treatment the masthead already applies to the same shared
   asset, which is petal pink and would all but vanish on cream. */
:root {
  --sh-card-rule: #E0CFA8;   /* hairline inside the cream card only */
}

.sh-share { display: none; }
.sh-share.sh-enabled {
  display: block;
  max-width: var(--content-max);
  margin-inline: auto;
  /* Matches the footer's own separator value: there is no reusable
     hairline class in the berry field, the footer declares this
     inline on .site-footer and .booking-direct repeats it. */
  border-top: 1px solid rgba(253, 248, 246, .18);
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
  padding: clamp(2rem, 7vw, 2.75rem) var(--page-gutter) 0;
}

/* The card. Cream ground, centred, and capped so it stays a card
   rather than stretching edge to edge on a wide phone. */
.sh-share .sh-card {
  background: var(--vanilla);
  border-radius: 12px;
  padding: 26px 20px 22px;
  text-align: center;
  max-width: 22rem;
  margin-inline: auto;
}
.sh-share .sh-mark {
  display: block;
  width: 44px;
  height: 44px;
  margin: 0 auto .9rem;
  filter: brightness(.82) saturate(1.45);
}
.sh-share .sh-card-name {
  font-family: "Instrument Serif", serif;
  font-weight: 400;          /* the serif has no other weight */
  font-size: 1.75rem;
  line-height: 1.1;
  color: var(--berry);
}
.sh-share .sh-card-services {
  margin-top: .45rem;
  font-family: "DM Sans", sans-serif;
  font-size: 1rem;           /* 16px floor applies to every visible string */
  font-weight: 500;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--mute-ink);
}
.sh-share .sh-card-rule {
  display: block;
  width: 46px;
  height: 1px;
  margin: 1rem auto;
  background: var(--sh-card-rule);
}
.sh-share .sh-card-line {
  font-family: "Instrument Serif", serif;
  font-style: italic;
  font-weight: 400;
  font-size: 1.125rem;
  line-height: 1.35;
  color: var(--berry);
}
.sh-share .sh-card-url {
  margin-top: .55rem;
  font-family: "DM Sans", sans-serif;
  font-size: 1rem;
  letter-spacing: .08em;
  color: var(--mute-ink);
}

/* Visually the SEND TEXT button, written standalone: .bk-send is left
   completely untouched so the booking form cannot be affected by
   anything in this section. Full width here, unlike the form's
   justify-self:start, because it is the section's only action. */
.sh-share .sh-button {
  display: block;
  width: 100%;
  margin-top: 1.25rem;
  border: 0;
  border-radius: 999px;
  background: var(--porcelain);
  color: var(--berry);
  font-family: "DM Sans", sans-serif;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  min-height: 48px;
  padding: 13px 26px;
  cursor: pointer;
  touch-action: manipulation;
}
.sh-share .sh-button:focus-visible {
  outline: 2px solid var(--petal);
  outline-offset: 3px;
}

/* -- 23 · Print (revision 12) -----------------------------------------
   Screens are unaffected by anything in this block; @media print cannot
   apply anywhere else.

   Printing was genuinely broken. Browsers drop background colours by
   default, so both berry sections printed porcelain text on white paper:
   the entire Nail Repair section AND the footer's phone number and email
   came out invisible. A printed page of this site could not tell you how
   to contact Geo. On top of that the lookbook's scroll runway is three
   screens of empty sticky stage, which printed as blank sheets.

   The approach is to stop relying on background colour for contrast
   anywhere, collapse the mechanics that only make sense on screen, and
   let the page fall into ordinary document flow. */
@media print {
  /* Anything that is only an on-screen affordance. */
  .site-header, .menu-panel, .mobile-book-bar, .petals,
  .lb-controls, .lb-triggers, .sh-share, .opening-vertical,
  .opening-exit, .about-mark { display: none !important; }

  /* Berry and blush fields become plain paper; every string that relied
     on a dark ground for contrast is restated as dark ink. */
  body, .opening, .service-custom, .service-repair, .lookbook,
  .about, .booking, .berry-field, .site-footer, .sc-text, .sc-band {
    background: none !important;
    color: #000 !important;
    box-shadow: none !important;
  }
  .display-title--light, .body-copy--light, .price-display--light,
  .micro-label--onberry, .sr-hook, .bk-intro, .bk-legend, .bk-label,
  .bk-chip, .direct-line, .direct-line a, .direct-note, .footer-word,
  .footer-word span, .footer-contact, .footer-contact a, .footer-note,
  .micro-label, .caption { color: #000 !important; }
  /* The lockups are light artwork meant for a berry ground; on white
     paper they disappear entirely. The wordmark is set in live text
     elsewhere on the page, so nothing is lost by dropping them. */
  .lockup { display: none !important; }

  /* .action is porcelain type on a berry fill, so with the fill dropped
     it printed white-on-white. The buttons cannot be pressed on paper
     but they still say what the page offers, so outline them instead of
     hiding them. */
  .action, .action--light, .quiet-link {
    background: none !important;
    color: #000 !important;
    border: 1px solid #000 !important;
    text-decoration: none !important;
  }

  /* The berry curve is a screen device; on paper it prints as a stray
     rounded edge with nothing behind it. */
  .berry-field { border-radius: 0 !important; }
  .service-custom, .booking { margin: 0 !important; }

  /* Collapse the sticky lookbook: release the runway, un-pin the stage,
     and show all five spreads as an ordinary stacked gallery instead of
     three screens of blank sheet with one photograph on top. */
  .js.wired .lb-scroll { height: auto !important; padding-inline: 0 !important; }
  .js.wired .lb-stage {
    position: static !important;
    height: auto !important;
    overflow: visible !important;
    background: none !important;
  }
  .js.wired .lb-spreads {
    display: block !important;
    position: static !important;
    overflow: visible !important;
  }
  /* Paper is narrower than 1024, so the phone/tablet rules (18b) apply
     during printing and would stack every photograph one per row: five
     spreads became four pages of gallery. Force the row back. */
  .js.wired .lb-spread {
    position: static !important;
    opacity: 1 !important;
    transform: none !important;
    display: flex !important;
    flex-direction: row !important;
    flex-wrap: wrap !important;
    justify-content: center !important;
    align-items: flex-start !important;
    gap: .4rem !important;
    page-break-inside: avoid;
    margin-bottom: .6rem;
  }
  .js.wired .lb-spread .lb-fig {
    width: 30% !important;
    aspect-ratio: 4 / 3 !important;
  }
  .js.wired .lb-spread.is-active .lb-fig img { animation: none !important; }

  /* Hero art is decorative and eats a whole sheet of ink. */
  .opening-media { display: none !important; }
  .opening { min-height: 0 !important; }
  .opening-copy { min-height: 0 !important; padding: 0 !important; }

  /* The chips are the only place the prices appear in this section, so
     they have to stay legible once their fill is gone. */
  .bk-chip { border: 1px solid #000 !important; background: none !important; }
  .bk-chip-input:checked + .bk-chip { font-weight: 600 !important; }

  /* The form cannot be used on paper, but the contact details must
     survive, so the block is kept and the fallback is forced visible
     even at phone widths where 18a hides it. */
  .bk-send, .bk-note, .bk-error, .bk-fallback, .scheduling-slot { display: none !important; }
  .booking-direct { display: block !important; }

  a[href^="tel:"]::after, a[href^="sms:"]::after,
  a[href^="mailto:"]::after { content: ""; }
  section, figure, img { page-break-inside: avoid; }
}

/* -- 22 · Phone lookbook pacing (revision 12) ------------------------
   NOTHING SNAPS THE PAGE SCROLL ANY MORE, at any width. Read the history
   below before adding any back, because it has now been tried twice and
   removed twice. (The one surviving snap in the file is the horizontal
   rail's own `x proximity` in section 9, which is an ordinary carousel
   inside the gallery element, never the document, and is switched off
   entirely in stage mode by 18b.)

   The original problem was real. Measured on a 390x844 handset, viewport
   664: the runway ran 2390px, 3.6 screens and ~30% of the whole page, at
   478px per spread. A modest thumb-flick travels ~900px, so one gesture
   crossed 1.88 spreads and a hard flick nearly four, and spread 1 held
   for only 136px after the stage pinned, replacing itself almost
   immediately.

   What FIXES it is the geometry in this block, and it does so without
   any snapping. The numbers are derived, not picked. Write V for the
   viewport and H for the header; the stage is S = V - H tall and stays
   pinned while the scroll runs from (lbTop - H) to (lbTop + R - V), so
   the point the observer actually reads -- the middle of the viewport --
   sweeps the runway from (V/2 - H) to (R - V/2). Only that sweep is
   visible dwell; anything a trigger occupies outside it is invisible.
   Dividing the sweep into five equal shares of 0.4V each gives:

     runway  R  = 300svh - H       trigger 1 = 90svh - H   (0.4V dwell)
     triggers 2,3,4 = 40svh each   trigger 5 = 90svh
     sum: (90svh - H) + 3(40svh) + 90svh = 300svh - H       exact

   TRIGGER 1 IS THE ODD ONE ON PURPOSE. The stage pins with the reading
   point already (V/2 - H) deep into the runway, so a trigger 1 sized
   like the others is nearly used up before the stage even sticks; it has
   to absorb that offset on top of its own share. With five equal slices
   spread 1 held for 40px against 320-480px for the others, i.e. the
   first photograph replaced itself the instant the stage pinned. Trigger
   5 is likewise oversized: its share is the same 0.4V, and the rest is
   the tail the sticky stage needs to stay pinned to the end and then
   release cleanly.

   These are svh and never percentages: a percentage trigger scales with
   the RUNWAY, while every condition above scales with the VIEWPORT, and
   the two only agree at a single screen size.

   What did NOT work was scroll snapping, tried twice:
     - `scroll-snap-stop: always` arrested every gesture at every spread.
       Five separate swipes to pass the gallery read as the page refusing
       to scroll ("the site falls flat when scrolling").
     - Loosening to `normal` and keeping the snap points was still wrong.
       Snapping the root element makes the ENTIRE PAGE a snap container,
       and the owner reported scrolling still felt locked on a real
       handset even well away from any snap point. That is a WebKit
       behaviour this project cannot reproduce in a desktop engine, so it
       cannot be tuned here with any confidence.
   Both are gone. Ordinary momentum scrolling everywhere, which is what
   was asked for and what every other site does.

   The stage's actual mechanics never depended on snapping and are
   untouched: sticky stage, scroll-driven cross-fade through the five
   triggers, numbered dots, and the "N of 5" readout, all driven by the
   IntersectionObserver in initLookbook. The only thing snapping added
   was settling exactly on a spread; without it a hard flick may cross
   more than one, which is the ordinary behaviour of a scrolling page.

   Phone only, on purpose. 18b is shared with tablet portrait, so this
   block is scoped to 767.98 rather than editing 18b. */
@media (max-width: 767.98px) and (prefers-reduced-motion: no-preference) {
  .js.wired .lb-scroll { height: calc(300vh - var(--header-h)); height: calc(300svh - var(--header-h)); }
  .js.wired .lb-triggers div { height: 40vh; height: 40svh; }
  /* Absorbs the (V/2 - H) the reading point is already carrying when the
     stage pins, so spread 1 gets the same 0.4V of dwell as the rest. */
  .js.wired .lb-triggers div:first-child { height: calc(90vh - var(--header-h)); height: calc(90svh - var(--header-h)); }
  /* Its 0.4V share plus the release tail. */
  .js.wired .lb-triggers div:last-child { height: 90vh; height: 90svh; }
}
