/* ============================================================================
   Homepage — section styles
   ============================================================================
   All values measured off live production via getComputedStyle, recorded in
   tests/visual/data/styles-index-*.json. Where a number looks oddly specific
   (letter-spacing: 1.04px, not 1px) it's because that IS the live value —
   Framer computes letter-spacing from an em ratio, and rounding it to a
   "nicer" number would shift text width and fail the pixel-diff gate.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   GRAIN OVERLAY — fixed, full viewport, 6% opacity, above all content.
   Values measured off live: position:fixed, opacity 0.06, z-index 9999.
   pointer-events:none is essential or it would swallow every click on the page.
   --------------------------------------------------------------------------- */
.grain {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  pointer-events: none;
  z-index: 9999;
  opacity: 0.06;
  display: block;
  image-rendering: pixelated;
}

/* ---------------------------------------------------------------------------
   NAV — 72px tall, sticky, near-opaque page ground.
   Symmetric 32px gutters (logo x=32, last link ends x=1408 at 1440 wide).
   --------------------------------------------------------------------------- */
.nav {
  position: sticky;
  top: 0;
  z-index: 100;
  height: 72px;
  background: rgba(10, 10, 10, 0.95);
  backdrop-filter: saturate(140%) blur(6px);
}

.nav-inner {
  height: 72px;
  padding-inline: 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
@media (min-width: 810px) { .nav-inner { padding-inline: 32px; } }

.nav-mark {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: 0.36px;
  line-height: 1;
  color: var(--color-text-primary);
  white-space: nowrap;
  /* Full-height hit area inside the 72px bar. */
  display: inline-flex;
  align-items: center;
  align-self: stretch;
}

.nav-links {
  display: none;
  align-items: stretch;
  /* Stretch the list itself to the 72px bar — without this the flex container
     shrink-wraps to 17px and its links inherit that height, leaving a
     text-sized click target. */
  align-self: stretch;
  gap: 41px; /* measured: 'Home' ends x=972, 'Music' starts x=1013 */
}
@media (min-width: 1200px) { .nav-links { display: flex; } }

.nav-links a {
  /* Stretch to the full 72px nav height so the tappable/clickable region is
     comfortably large, while the text itself stays at its measured 13px. */
  display: inline-flex;
  align-items: center;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 1.04px;
  /* Production is responsive here: 13px/13px, 1.04px, rgba(255,255,255,.6)
     at desktop and 11px/11px, 1.1px, .5 on mobile (see the media query below).
     A single flat rule flips the diff between viewports. */
  line-height: 13px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
  transition: color 0.2s ease;
}

.nav-links a:hover { color: var(--color-text-primary); }

/* Hamburger — two bars, matching the live mobile header. */
.nav-burger {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-end;
  gap: 5px;
  /* 44px touch target — the two 28px-wide bars stay visually identical, but
     the tappable box meets the minimum comfortable size on phones. */
  width: 44px;
  height: 44px;
  margin-right: -8px;
}
.nav-burger span { width: 28px; }
@media (min-width: 1200px) { .nav-burger { display: none; } }
.nav-burger span {
  display: block;
  height: 1px;
  width: 22px;
  background: var(--color-text-primary);
  transition: transform 0.25s ease, opacity 0.2s ease;
}
.nav-burger[aria-expanded="true"] span:first-child { transform: translateY(3px) rotate(45deg); }
.nav-burger[aria-expanded="true"] span:last-child { transform: translateY(-3px) rotate(-45deg); }

.nav-mobile {
  position: fixed;
  inset: 72px 0 auto 0;
  background: rgba(10, 10, 10, 0.98);
  display: grid;
  gap: 4px;
  padding: 8px 24px 28px;
  transform: translateY(-12px);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease;
  z-index: 99;
}
.nav-mobile.is-open { opacity: 1; transform: none; pointer-events: auto; }
.nav-mobile a {
  padding: 14px 0;
  font-size: 13px;
  letter-spacing: 1.04px;
  text-transform: uppercase;
  color: var(--color-text-nav);
  border-bottom: 1px solid var(--color-hairline);
}
.nav-mobile a:last-child { border-bottom: 0; }
@media (min-width: 1200px) { .nav-mobile { display: none; } }

/* ---------------------------------------------------------------------------
   HERO — video bed, 900px tall at desktop, two gradient overlays, vinyl.
   --------------------------------------------------------------------------- */
.hero {
  position: relative;
  /* Full viewport height: the fixed nav overlays the hero. */
  height: 100svh;
  min-height: 560px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
@media (min-width: 1200px) { .hero { height: 900px; } }

.hero-media { position: absolute; inset: 0; }

.hero-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
}

/* Measured verbatim off live. */
.hero-overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.85) 100%);
}
/* Production stacks THREE layers over the video, not two: a flat 50%-opacity
   #0a0a0a darkener under the gradient. Without it the footage reads noticeably
   lighter than the live site. */
.hero-darken {
  position: absolute;
  inset: 0;
  background: rgb(10, 10, 10);
  opacity: 0.5;
}
.hero-fade {
  position: absolute;
  inset: 0;
  background: linear-gradient(rgba(0, 0, 0, 0) 80%, rgb(10, 10, 10) 100%);
}

/* Sound bars — 13 bars, 3px wide, 8px pitch, container 15% opacity.
   Decorative equaliser under the hero title. */
.sound-bars {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 60px;
  height: 120px;
  display: flex;
  align-items: flex-end;
  gap: 5px;
  opacity: 0.15;
  z-index: 2;
  pointer-events: none;
}
.sound-bars span {
  display: block;
  width: 3px;
  height: var(--h, 40px);
  background: #fff;
}
@media (prefers-reduced-motion: no-preference) {
  .sound-bars span { animation: sp-eq 1.4s ease-in-out infinite alternate; }
  .sound-bars span:nth-child(odd)  { animation-duration: 1.9s; }
  .sound-bars span:nth-child(3n)   { animation-duration: 2.3s; }
  .sound-bars span:nth-child(4n)   { animation-delay: -0.7s; }
  .sound-bars span:nth-child(5n)   { animation-delay: -1.1s; }
  @keyframes sp-eq {
    from { height: calc(var(--h, 40px) * 0.45); }
    to   { height: var(--h, 40px); }
  }
}

/* Vinyl: 481px, absolute right, slowly rotating. Framer rotates it with a
   matrix; a plain infinite rotate reproduces the same motion. */
.hero-vinyl {
  position: absolute;
  top: 130px;
  right: 9px;
  width: 481px;
  height: 481px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%,
      #1a1a1a 0 18%,
      #0d0d0d 18% 19%,
      #141414 19% 100%);
  box-shadow: 0 0 0 1px rgba(201, 168, 76, 0.12), 0 40px 120px rgba(0, 0, 0, 0.6);
  display: grid;
  place-items: center;
  animation: vinyl-spin 24s linear infinite;
  opacity: 0.55;
}
.hero-vinyl::after {
  /* groove rings */
  content: "";
  position: absolute;
  inset: 6%;
  border-radius: 50%;
  background: repeating-radial-gradient(circle at 50% 50%,
    rgba(255, 255, 255, 0.035) 0 1px,
    rgba(255, 255, 255, 0) 1px 5px);
}
.hero-vinyl-mark {
  position: relative;
  z-index: 1;
  font-family: var(--font-display);
  /* Measured on production: 16px italic with 0.8px tracking — not a large
     upright monogram. */
  font-size: 16px;
  font-style: italic;
  font-weight: 700;
  letter-spacing: 0.8px;
  color: var(--color-gold);
  /* Counter-rotate so the monogram stays upright while the disc spins.
     Production's vinyl carries a fixed -133.7deg transform and its SP still
     reads level; letting the spin carry the text made it render upside-down
     for most of the loop. */
  animation: vinyl-spin-counter 24s linear infinite;
}
@keyframes vinyl-spin { to { transform: rotate(360deg); } }
@keyframes vinyl-spin-counter { to { transform: rotate(-360deg); } }
@media (prefers-reduced-motion: reduce) {
  .hero-vinyl, .hero-vinyl-mark { animation: none; }
}
@media (max-width: 1199.98px) { .hero-vinyl { display: none; } }

.hero-content {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 1000px;
  padding-inline: 24px;
  text-align: center;
}

/* 40x1px gold rule above the eyebrow — measured at x=700,y=442 on a 1440
   viewport, i.e. horizontally centred. */
.hero-rule {
  display: block;
  width: 40px;
  height: 1px;
  margin: 0 auto 20px;
  background: var(--color-gold);
}

.hero-eyebrow, .hero-eyebrow span {
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 2.2px;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 2.2px;
  line-height: 1;
  text-transform: uppercase;
  color: var(--color-gold);
  margin-bottom: 20px;
}

.hero-title {
  font-family: var(--font-display);
  font-size: 108px;
  font-weight: 700;
  letter-spacing: -3.24px;   /* live value: -0.03em of 108px */
  line-height: 1;
  color: var(--color-text-primary);
}
@media (max-width: 809.98px) {
  /* Wraps to two lines on phones — same as live, where the 108px size is
     kept and the text simply wraps rather than scaling down. */
  .hero-title { font-size: 108px; line-height: 1; }
}

/* ---------------------------------------------------------------------------
   MARQUEE — 66px band, two tracks scrolling left continuously.
   --------------------------------------------------------------------------- */
.marquee {
  position: relative;
  height: 66px;
  background: var(--color-bg-page);
  border-block: 1px solid var(--color-hairline);
  overflow: hidden;
  display: flex;
  align-items: center;
}
.marquee-track {
  display: flex;
  width: max-content;
  animation: marquee-scroll 38s linear infinite;
}
.marquee-track p {
  /* Playfair Display ITALIC. Both parts matter: the family AND the style.
     Matching only the family still rendered the upright face, which is why the
     sliding text kept looking wrong after the first fix. */
  font-family: var(--font-display);
  font-style: italic;
  font-size: 20px;
  font-weight: 400;
  letter-spacing: 2.4px;
  line-height: 64px;
  white-space: pre;
  color: rgba(255, 255, 255, 0.85);
}
@keyframes marquee-scroll { to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .marquee-track { animation: none; } }

/* ---------------------------------------------------------------------------
   ABOUT — the site's single light section (#f5f5f0).
   Desktop layout measured off live at 1440:
     portrait  x=80   y=1193  420x560
     caption   x=80   y=1726  11px
     text col  x=580  width 780
     eyebrow   y=1230 | h2 y=1258 52px | body y=1391 16px | stats y=1583
   Section is 823px tall starting at y=1038.
   --------------------------------------------------------------------------- */
.about {
  background: var(--color-bg-light);
  color: var(--color-text-on-light);
  /* Measured: section spans y=1038..1861 (823px). Portrait starts y=1193, i.e.
     155px below the section top; the text column's eyebrow starts y=1230
     (192px down) because the text column is vertically offset from the image
     rather than top-aligned with it. */
  padding-block: 155px 71px;
}

.about-inner {
  max-width: 1440px;
  margin-inline: auto;
  padding-inline: 24px;
  display: grid;
  gap: 48px;
}
@media (min-width: 810px) { .about-inner { padding-inline: 40px; } }
@media (min-width: 1200px) {
  .about-inner {
    /* Measured on live at 1440: portrait x=80 w=420, text col x=580 w=780.
       So gap = 580 - (80+420) = 80, and the right edge lands at 1360 with an
       80px gutter. Using a fixed 1440 max-width with 80px side padding
       reproduces that exactly; letting the grid centre itself instead pushed
       the portrait to x=69. */
    max-width: 1440px;
    grid-template-columns: 420px 780px;
    column-gap: 80px;
    padding-inline: 80px;
    justify-content: start;
  }
}

/* PARALLAX WINDOW.
   The FIGURE is a fixed-height window that owns the layout height; the IMG
   inside is 12% taller so it has room to slide without ever exposing an edge.
   site.js translates only the img. Because the window height never changes,
   the section height is untouched and the photo keeps its full scale — it
   cannot shrink, which is the failure this structure exists to prevent.

   Heights are production's measured values: 416 @360, 456 @390, 560 @768,
   480 @1024, 560 @1440. */
.about-figure {
  margin: 0;
  overflow: hidden;
  height: 416px;
  /* The window must establish a positioning context so the oversized image can
     be centred inside it. */
  position: relative;
}
.about-figure img {
  /* Centre the oversized image in its window so the slack is split evenly
     above and below — that symmetry is what lets the clamp in site.js keep an
     edge from ever showing, in either scroll direction. */
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  width: 100%;
  /* Framer's own config: fit "fill", positionY "top", i.e. object-position
     50% 0%, and the image is exactly the figure's height (560px measured on
     production). My 112% oversize + centre 42% was a workaround I invented to
     give the parallax somewhere to travel; it changed the visible crop of
     Sonya's portrait. The parallax now rides a translate INSIDE the
     overflow:hidden window instead, which is what production does. */
  height: 112%;
  object-fit: cover;
  object-position: 50% 0%;
  will-change: transform;
  /* Fade in on decode. Measured on Fast 3G, this portrait arrived ~4s after
     its container had finished animating and appeared in a single frame — the
     flicker Moe reported. ONLY opacity is transitioned: site.js drives
     transform for the parallax clamp, and transitioning that would fight the
     per-frame writes and expose an edge. */
  opacity: 0;
  transition: opacity 0.5s ease;
}
.about-figure img.is-loaded,
.about-figure img[data-loaded] { opacity: 1; }
/* Without JS the portrait must still be visible. */
@media (scripting: none) { .about-figure img { opacity: 1; } }
@media (min-width: 380px)  { .about-figure { height: 456px; } }
@media (min-width: 700px)  { .about-figure { height: 560px; } }
@media (min-width: 1000px) { .about-figure { height: 480px; } }
@media (min-width: 1200px) { .about-figure { height: 560px; } }
.about-figure figcaption {
  margin-top: 18px;
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: 1.1px;
  color: var(--color-text-on-light);
  opacity: 0.55;
}

.about-body {
  /* Text column offset so the eyebrow lands at y=1230 while the portrait
     starts at y=1193. Solved against the rendered box (see
     design-explorations/solve-about2.mjs) rather than derived by hand — the
     paragraphs' wrap height makes hand arithmetic unreliable here. */
  padding-top: 37px;
}

.about-title {
  font-family: var(--font-display);
  font-size: 36px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--color-text-on-light);
  /* h2 renders 109px tall (2 lines @ 54.6px), same as live. */
  margin-block: 17px 24px;
}
@media (min-width: 1200px) {
  .about-title { font-size: 52px; letter-spacing: -1.04px; line-height: 54.6px; }
}

.about-copy {
  font-size: 16px;
  font-weight: 300;
  line-height: 28px;
  color: rgb(85, 85, 80);
}
.about-copy + .about-copy { margin-top: 24px; }

.about-stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 24px;
  /* Live: stats dt top lands at y=1583. Solved against the rendered box
     rather than computed by hand, because the two body paragraphs' exact
     wrap height determines where the block ends. */
  margin: 28px 0 0;
}
.about-stats dt {
  font-family: var(--font-display);
  font-size: 36px;
  font-weight: 700;
  letter-spacing: -0.72px;
  line-height: 1;
  color: var(--color-text-on-light);
}
.about-stats dd {
  margin: 12px 0 0;
  font-size: 11px;
  letter-spacing: 1.1px;
  line-height: 1;
  color: rgb(136, 136, 128);
  text-transform: uppercase;
}

/* ---------------------------------------------------------------------------
   WATCH — dark section (#111), y=2701 h=975. Eyebrow x=80 y=2838,
   h2 56px y=2868, iframe 772x434 centred at x=334 y=2999.
   --------------------------------------------------------------------------- */
.watch {
  background: var(--color-bg-section);
  /* Section measured 975px tall on live (y=2701..3676) at 1440.
     Tablet widths need much less: 737 @768, 855 @1024. */
  padding-block: 92px 92px;
}
@media (min-width: 700px)  { .watch { padding-block: 60px 60px; } }
@media (min-width: 1000px) { .watch { padding-block: 90px 90px; } }
@media (min-width: 1200px) { .watch { padding-block: 137px 243px; } }
.watch-inner {
  max-width: 1440px;
  margin-inline: auto;
  padding-inline: 24px;
}
@media (min-width: 810px) { .watch-inner { padding-inline: 80px; } }

.watch-title {
  font-family: var(--font-display);
  font-size: 36px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.2;
  color: var(--color-text-primary);
  margin-block: 19px 0;
}
@media (min-width: 1200px) { .watch-title { font-size: 56px; line-height: 67.2px; } }

.watch-frame {
  margin: 64px auto 0;
  max-width: 772px;
  aspect-ratio: 772 / 434;
  background: #000;
  border-radius: 4px;
  overflow: hidden;
}
.watch-frame iframe { width: 100%; height: 100%; border: 0; display: block; }

/* ---------------------------------------------------------------------------
   PIANO KEYS — decorative. y=4344 h=220.
   White keys 50x160 radius 0 0 4 4; black 32x100 radius 0 0 3 3, overlaid.
   --------------------------------------------------------------------------- */
.piano {
  background: var(--color-bg-page);
  height: 220px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  /* 1px, not 21px. Measured by scanning painted pixels: production's light band
     runs right up to the keyboard container (ends at -1), while a 21px pad left
     a 20px dark strip above the keys that production does not have. The DOM
     boxes matched exactly either way — only the rendered pixels showed it. */
  padding-top: 1px;
  overflow: hidden;
}
/* White keys sit flush against each other — production shows a continuous
   keyboard, not spaced tiles. The 4px gap made the dark background show
   through between keys, which read as extra black keys. */
.key-white:last-child { border-right: 0; }
.piano-caption {
  /* 20px below the keyboard on production, measured from the container's
     bottom edge. */
  margin-top: 20px;
  font-weight: 300;
  font-size: 10px;
  letter-spacing: 2px;
  line-height: 1;
  text-transform: uppercase;
  color: var(--color-gold-50);
  text-align: center;
}
@media (max-width: 809.98px) {
  /* Production KEEPS the keyboard on phones — measured "Keyboard h=180" at both
     360px and 390px. It overflows the viewport by design and is clipped by the
     section's overflow:hidden, giving the impression of a keyboard running off
     both edges. Scaling the keys down instead would not match. */
  .piano { height: 220px; }
      .piano-caption { margin-top: 20px; }
}

/* ---------------------------------------------------------------------------
   BOOKING — y=4564 h=1363. Centred column, 800px title, 580px sub.
   --------------------------------------------------------------------------- */
.booking {
  background: var(--color-bg-page);
  /* Section measured 1363px tall on live (y=4564..5927). */
  padding-block: 111px 91px;
}
.booking-inner {
  max-width: 800px;
  margin-inline: auto;
  padding-inline: 24px;
  text-align: center;
}
.booking-eyebrow { text-align: center; }
.booking-title {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--color-text-primary);
  margin-block: 20px 0;
}
@media (min-width: 1200px) { .booking-title { font-size: 56px; line-height: 58.8px; } }

.booking-sub {
  max-width: 580px;
  margin: 20px auto 0;
  font-size: 16px;
  font-weight: 300;
  line-height: 27.2px;
  color: rgb(136, 136, 128);
}

.booking-form {
  margin-top: 56px;
  display: grid;
  gap: 16px;
  text-align: left;
}
@media (min-width: 810px) { .booking-form { gap: 24px; } }
/* Two across at EVERY width — measured on live at 360px: paired inputs sit
   116px wide side by side, not stacked. Stacking them added ~540px of height
   to the booking section on phones. */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (min-width: 810px) { .field-row { gap: 24px; } }

.field { display: grid; gap: 10px; }
/* Production renders this one label in Inter 16px sentence case while every
   other field label is a 10px uppercase micro-label. That is an inconsistency
   in Framer's own build, not a system — reproduced here deliberately so the
   page matches. */
.field > span.field-label-alt {
  font-family: Inter, var(--font-body);
  font-size: 16px;
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  color: rgba(255, 255, 255, 0.72);
}

.field > span {
  font-size: 10px;
  /* Production leaves this at `normal` at every width. */
  line-height: normal;
  text-align: start;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}
.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 14px 16px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 14px;
  transition: border-color 0.2s ease, background 0.2s ease;
  /* iOS Safari gives form controls — especially input[type=date] — an
     INTRINSIC minimum width from their native picker UI, which wins over
     width:100% and pushes the field past the grid column. Moe's iPhone
     screenshot shows exactly that: EVENT DATE running off the right edge while
     EVENT TYPE beside it ends short. min-width:0 lets the grid item shrink,
     and max-width caps it regardless of intrinsic sizing. Chrome never showed
     this, which is why the responsive sweep read clean. */
  min-width: 0;
  max-width: 100%;
}
/* Safari also ignores width on the date input unless it is told not to use the
   native control's own sizing. */
/* The grid ITEM must also be allowed to shrink: a grid/flex item defaults to
   min-width:auto, which is its content's intrinsic width. Without this the
   track itself widens and no rule on the input can save it. */
.field {
  min-width: 0;
  /* Final containment: whatever intrinsic width the engine gives a native
     control, it cannot paint outside its own column. This is what actually
     guarantees the iPhone date field stays inside the grid — min-width:0 alone
     is beaten by an engine-imposed intrinsic minimum. */
  overflow: hidden;
}
.field input[type="date"] {
  -webkit-appearance: none;
  appearance: none;
  min-width: 0;
  /* width:100% + border-box makes the control track its column exactly; a
     stray intrinsic width cannot widen it. */
  width: 100%;
  max-width: 100%;
  /* Keep the text baseline consistent with the sibling text inputs: Safari's
     date control renders its value slightly differently without this. */
  min-height: 47px;
}
.field input[type="date"]::-webkit-date-and-time-value { text-align: left; }
.field input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1);
  opacity: 0.45;
}
.field textarea { resize: vertical; min-height: 110px; }
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--color-gold);
  background: rgba(255, 255, 255, 0.09);
}
.field input::placeholder,
.field textarea::placeholder { color: rgba(255, 255, 255, 0.35); }
.field select { appearance: none; cursor: pointer; }
.field select option { background: var(--color-bg-section); }

/* Honeypots: off-screen rather than display:none — some bots skip hidden
   fields, but they happily fill a field that's merely positioned away. */
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Turnstile. The slot collapses to nothing until the widget renders, so a
   missing site key leaves no empty gap above the submit button.

   The widget Cloudflare injects has its own fixed width, so a full-bleed grid
   cell would leave it parked against the left edge. Centre it the same way the
   submit button below is centred — applies at every width, no media query. */
.cf-turnstile-slot:empty { display: none; }
.cf-turnstile-slot {
  margin: 4px 0 18px;
  justify-self: center;
  display: flex;
  justify-content: center;
  max-width: 100%;
}

/* When Turnstile cannot render (domain not allowed, Cloudflare outage, a
   blocking extension) it injects a bare "Troubleshoot" link, which reads as a
   broken blue link on this dark form. The booking still goes through — the
   server stores it and flags it unverified — so hide the link rather than
   show the visitor an error they cannot act on. */
.cf-turnstile-slot a[href*="troubleshoot"],
.cf-turnstile-slot a[href*="cloudflare.com/turnstile"] { display: none; }

.booking-submit {
  justify-self: center;
  margin-top: 8px;
  padding: 15px 40px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 2px;
  background: transparent;
  color: rgba(255, 255, 255, 0.9);
  font-size: 11px;
  letter-spacing: 1.65px;
  text-transform: uppercase;
  transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
}
.booking-submit:hover { background: var(--color-gold); border-color: var(--color-gold); color: var(--color-bg-page); }
.booking-submit[disabled] { opacity: 0.55; cursor: default; }

.form-status { text-align: center; font-size: 13px; min-height: 20px; }
.form-status.is-error { color: #ff8f8f; }
.form-status.is-ok { color: var(--color-gold); }

/* ---------------------------------------------------------------------------
   FOOTER — y=5927 h=329.
   --------------------------------------------------------------------------- */
.footer {
  background: var(--color-bg-page);
  border-top: 1px solid var(--color-hairline);
  /* Section measured 329px tall on live (y=5927..6256). */
  padding-block: 62px 47px;
  text-align: center;
}
.footer-mark {
  font-family: var(--font-display);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: 0.56px;
  line-height: 1;
  color: var(--color-text-primary);
}
.footer-social {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 20px;
  color: rgba(255, 255, 255, 0.6);
}
.footer-social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  transition: color 0.2s ease;
}
.footer-social a:hover { color: var(--color-gold); }

.footer-nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 28px;
  margin-top: 20px;
}
.footer-nav a {
  /* Padded hit area so the tappable region clears 44px on touch, even though
     the text box itself is only ~13px tall. */
  display: inline-block;
  padding-block: 14px;
  /* Production: 11px on an 11px line box, 1.1px tracking. */
  font-size: 11px;
  line-height: 11px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
  transition: color 0.2s ease;
}
.footer-nav a:hover { color: var(--color-text-primary); }

.footer-copy {
  margin-top: 40px;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.6px;
  color: rgba(255, 255, 255, 0.3);
}

/* Watch caption — video title + description under the embed.
   Production centres a 900px column beneath the 772px frame: title at y=3473
   (30px tall), description at y=3507 (17px). */
.watch-title-line {
  font-family: var(--font-display);
  font-size: 20px;
  font-weight: 400;
  line-height: 30px;
  color: var(--color-text-primary);
}
.watch-desc {
  margin-top: 4px;
  font-size: 13px;
  line-height: 17px;
  color: rgba(255, 255, 255, 0.55);
}

/* ---------------------------------------------------------------------------
   WATCH CAROUSEL — 7 videos, prev/next arrows, dot navigation.
   Measured on production: arrows are 44px circles with a 1px
   rgba(255,255,255,.15) border sitting outside the 772px frame (x=270 and
   x=1126); dots are 6px circles at rgba(255,255,255,.3), the active one
   slightly larger; the caption is 11px DM Sans uppercase in gold.
   --------------------------------------------------------------------------- */
.watch-carousel {
  position: relative;
  margin-top: 64px;
  display: grid;
  /* One row holds [prev][frame][next] so the arrows are vertically centred on
     the video, exactly as production has them (arrow y=493 vs frame y=297,
     h=434 — dead centre). Stacking them in separate rows put the arrows above
     the video. */
  grid-template-columns: 44px minmax(0, 1fr) 44px;
  grid-template-rows: auto auto auto;
  align-items: center;
  gap: 20px;
  max-width: 900px;
  margin-inline: auto;
}
.watch-carousel .watch-frame { grid-row: 1; }
.watch-controls { display: contents; }
.watch-prev { grid-row: 1; grid-column: 1; }
.watch-next { grid-row: 1; grid-column: 3; }
.watch-caption { grid-row: 2; }
.watch-dots { grid-row: 3; }
.watch-carousel .watch-frame { grid-column: 2; margin: 0; }

/* The arrows live in a wrapper so mobile can move them below the video as a
   pair. On desktop the wrapper dissolves and the grid places each arrow in its
   own column, flanking the frame exactly as production does. */
.watch-controls { display: contents; }

.watch-arrow {
  width: 44px;
  height: 44px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 50%;
  background: transparent;
  color: rgba(255, 255, 255, 0.75);
  display: grid;
  place-items: center;
  transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease;
}
.watch-arrow:hover {
  border-color: var(--color-gold);
  color: var(--color-gold);
}
.watch-arrow:focus-visible {
  outline: 2px solid var(--color-gold);
  outline-offset: 3px;
}
.watch-prev { grid-column: 1; }
.watch-next { grid-column: 3; }


.watch-dots {
  grid-column: 1 / -1;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  /* Production puts 41px between the description baseline and the dots. The
     grid's 20px row gap covers most of it. */
  margin-top: 2px;
}
.watch-dot {
  position: relative;
  width: 6px;
  height: 6px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  padding: 0;
  transition: background 0.2s ease, transform 0.2s ease;
}
.watch-dot::after {
  /* Transparent 24px hit area, centred on the 6px visual dot. Does not affect
     layout — the dots still sit exactly where production's do. */
  content: "";
  position: absolute;
  inset: -9px;
  border-radius: 50%;
}

.watch-dot.is-active {
  background: rgba(255, 255, 255, 0.85);
  transform: scale(1.35);
}
.watch-dot:focus-visible {
  outline: 2px solid var(--color-gold);
  outline-offset: 4px;
}

@media (max-width: 809.98px) {
  .watch-carousel { grid-template-columns: 36px minmax(0, 1fr) 36px; gap: 10px; }
  .watch-arrow { width: 36px; height: 36px; }
}

/* ---------------------------------------------------------------------------
   WATCH CAPTION — measured on production at 1440 and 390.

   Desktop: the caption block starts at the frame's LEFT edge (x=270, the same
   column as the arrows) and is 900px wide, left aligned. Three lines:
     +752  11px DM Sans uppercase gold        category
     +772  22px Playfair Display white        video title
     +806  13px DM Sans rgba(255,255,255,.5)  description
   Mobile: the same three lines, centred, at the container's full width.
   --------------------------------------------------------------------------- */
.watch-caption {
  grid-column: 1 / -1;
  /* The grid's 20px row gap already provides production's spacing below the
     frame (frame bottom 732 -> caption 752). An extra margin here pushed the
     caption 24px low. */
  margin-top: 0;
  text-align: left;
  max-width: 900px;
  margin-inline: auto;
  width: 100%;
}
.watch-category {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.88px;
  line-height: 13.2px;
  text-transform: uppercase;
  color: var(--color-gold);
}
.watch-video-title {
  margin-top: 7px;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 700;
  line-height: 30px;
  color: var(--color-text-primary);
}
.watch-video-desc {
  /* Production: title 772 -> description 806 = 34px. With a 30px title line
     box that is a 4px margin, not 8px. */
  margin-top: 4px;
  font-size: 13px;
  font-weight: 400;
  line-height: 20px;
  color: rgba(255, 255, 255, 0.5);
}

@media (max-width: 809.98px) {
  /* Production stacks the arrows BELOW the video on phones, side by side and
     centred, rather than flanking a narrow frame. */
  .watch-carousel {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .watch-carousel .watch-frame { grid-column: 1; }
  .watch-arrow { grid-column: 1; }
  .watch-controls {
    grid-column: 1;
    display: flex;
    justify-content: center;
    gap: 16px;
    margin-top: 26px;
  }
  .watch-caption { text-align: center; margin-top: 22px; }
}

/* ---------------------------------------------------------------------------
   KEYBOARD — measured on production at 1440px.

   14 white keys, 50x160, 52px pitch (2px gap), container 726px wide and 180
   tall. 10 black keys, 32x100, absolutely positioned, z-index 2, #111.
   A black key sits over the boundary after white key N, offset by 38px from
   that key's left edge.

   Production's keys have NO entry animation — no fade, no stagger, no
   transform. They are simply there. The staggered fade-in this previously had
   was what read as jittery.
   --------------------------------------------------------------------------- */
.piano-keys {
  position: relative;
  width: 726px;
  /* Framer XML <Keyboard> is height:180px, overflow:visible, gap:2px,
     stackAlignment:end. Its parent <PianoKeys> is height:220px with
     padding 20px 0 28px 0 and centred content — that padding is applied on
     .piano (the section), not here. */
  height: 180px;
  overflow: visible;
  margin-inline: auto;
  display: flex;
  gap: 2px;
  align-items: flex-start;
}

.key-white {
  width: 50px;
  height: 160px;
  flex: 0 0 50px;
  /* Framer XML: Keyboard is height:180px with stackAlignment "end" and keys are
     50x160, so a white key RESTS at top = 180-160 = 20px.
     My previous 100px was production's ANIMATING position (rest 20 + the
     enter offset of 80) measured mid-animation and mistaken for geometry. */
  margin-top: 20px;
  background: rgb(245, 245, 240);
  border-radius: 0 0 4px 4px;
  /* Entry animation — extracted from Framer's own bundle on main
     (site/_/m/F_UyGD...mjs). Each key has __framer__enter {opacity:0, y:80}
     with animateOnce and threshold .1. JS drives the reveal; these are the
     resting values. */
  will-change: transform, opacity;
}

.key-black {
  position: absolute;
  /* Framer XML: position absolute, top 0px. (My previous 60px was the black
     key's enter offset, not its resting position.) */
  top: 0;
  /* Measured offsets from production, in px from the container's left edge:
     38, 92, 198, 252, 306, 413, 467, 573, 627, 681. Deriving these from the
     white-key pitch drifted 19px by the last key, because production's black
     keys step 54px within a group, not 52px. */
  left: calc(var(--key-x) * 1px);
  width: 32px;
  height: 100px;
  z-index: 2;
  background: rgb(17, 17, 17);
  border-radius: 0 0 3px 3px;
  /* Black keys enter from y:60 over 0.6s (whites: y:80 over 0.7s). */
  will-change: transform, opacity;
}

@media (max-width: 809.98px) {
  /* Production does NOT scale the keyboard on phones: it stays 726x180 at full
     size and the section's overflow:hidden crops it, so the keys run off both
     edges at life size. Scaling to 0.46 produced a shrunken toy keyboard. */
  .piano-keys {
    transform: none;
    margin-bottom: 0;
  }
}


/* --------------------------------------------------------------------------
   PIANO KEY ENTRY
   Values taken from Framer's compiled bundle, not guessed:
     white keys  enter {opacity:0, y:80}  duration .7s  delay 0 + .04s * index
     black keys  enter {opacity:0, y:60}  duration .6s  delay .10 + .04s * index
     ease cubic-bezier(.44, 0, .56, 1), animateOnce, threshold .1
   The keys only get the hidden state once JS is running, so with JS disabled
   the keyboard is simply visible.
   -------------------------------------------------------------------------- */
.piano-keys.js-anim .key-white,
.piano-keys.js-anim .key-black {
  opacity: 0;
}
.piano-keys.js-anim .key-white { transform: translateY(80px); }
.piano-keys.js-anim .key-black { transform: translateY(60px); }

.piano-keys.js-anim.is-in .key-white,
.piano-keys.js-anim.is-in .key-black {
  opacity: 1;
  transform: translateY(0);
  transition: opacity var(--key-dur) cubic-bezier(.44, 0, .56, 1) var(--key-delay),
              transform var(--key-dur) cubic-bezier(.44, 0, .56, 1) var(--key-delay);
}

@media (prefers-reduced-motion: reduce) {
  .piano-keys.js-anim .key-white,
  .piano-keys.js-anim .key-black { opacity: 1; transform: none; transition: none; }
}

/* ---------------------------------------------------------------------------
   TESTIMONIAL — Kyle D. (2026). Sits between the piano transition and the
   booking form.

   Every value is taken from the site's own scale rather than invented:
     eyebrow  .eyebrow (site.css) — 11px / 0.2em / gold
     title    matches .booking-title — Playfair 30px / 700 / -0.02em / 1.05
     body     matches .booking-sub  — 16px / 27.2px / rgb(136,136,128)
   The pull quote is the one new type size: Playfair italic, sized to sit
   between the section title and body without competing with the h2.

   Background alternates to --color-bg-section so the run of page-coloured
   sections (piano, booking) is broken up, the same way .watch does.
   --------------------------------------------------------------------------- */
.testimonial {
  background: var(--color-bg-section);
  padding-block: 96px 96px;
  padding-inline: 24px;
}

.testimonial-inner {
  max-width: 860px;
  margin: 0 auto;
  text-align: center;
}

.testimonial-title {
  font-family: var(--font-display);
  font-size: 30px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.05;
  color: var(--color-text-primary);
  margin-block: 20px 0;
}

.testimonial-card {
  margin: 44px 0 0;
}

/* His strongest line, promoted so the eye has somewhere to land before the
   full review. Deliberately NOT repeated in the body copy below.

   Set on ONE line at every width. Two things were needed:
     - `max-width: 20ch` is gone. It capped the line near 320px, so 32px text
       needing ~580px wrapped even on a 1440px screen. Width was never the
       constraint on desktop; that rule was.
     - The font-size now tracks the viewport. Measured: this string occupies
       17.9x its font-size in Playfair Display italic, and the container is
       (100vw - 48px) until it caps at 860px. calc(5.2vw - 2.5px) stays ~7%
       under that limit at every width, and 32px keeps it from growing past
       its original size on wide screens.

   `nowrap` makes the single line a guarantee rather than a coincidence. It is
   only safe because the size calculation leaves headroom — verified at
   320-1920px with no horizontal overflow. Re-measure if the quote text
   changes: a longer string needs a smaller coefficient. */
.testimonial-pull {
  margin: 0 auto 32px;
  font-family: var(--font-display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(12px, calc(5.2vw - 2.5px), 32px);
  white-space: nowrap;
  line-height: 1.4;
  letter-spacing: -0.01em;
  color: var(--color-text-primary);
}

.testimonial-body {
  margin: 0 auto;
  max-width: 640px;
  font-size: 16px;
  font-weight: 300;
  line-height: 27.2px;
  color: rgb(136, 136, 128);
}
.testimonial-body p { margin: 0 0 15px; }
.testimonial-body p:last-child { margin-bottom: 0; }

.testimonial-credit {
  display: block;
  margin-top: 34px;
}

.testimonial-rule {
  display: block;
  width: 48px;
  height: 1px;
  margin: 0 auto 20px;
  background: var(--color-gold);
  opacity: 0.6;
}

.testimonial-name {
  display: block;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-primary);
}

.testimonial-role {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.45);
}

@media (max-width: 809px) {
  .testimonial { padding-block: 72px 72px; }
  /* font-size and max-width are deliberately NOT set here: the base rule's
     clamp() already scales this to one line at every width, and re-declaring
     either would reintroduce the wrap this media query used to cause. */
  .testimonial-pull { margin-bottom: 26px; }
  .testimonial-body { font-size: 15px; line-height: 26px; }
  .testimonial-card { margin-top: 36px; }
}
