/* ==========================================================================
   LussoLets — Life's Little Luxuries
   Landing page styles
   ========================================================================== */

/* --------------------------------------------------------------------------
   Fonts
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Cardo';
  src: url('../assets/fonts/cardo-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Cardo';
  src: url('../assets/fonts/cardo-700.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* Variable font — covers the 300–500 range we use */
@font-face {
  font-family: 'Work Sans';
  src: url('../assets/fonts/work-sans-var.woff2') format('woff2');
  font-weight: 300 500;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Brand */
  --champagne: #C2B292;
  --champagne-deep: #A99775;

  /* The same champagne, held at a tint. Large flat fields at the full
     strength above land at 67% lightness — a mid-tone, too dark to read as
     paper and too light to read as a deliberate accent, so a half-screen of
     it has nowhere for the eye to rest. Same hue (40deg), same family, just
     let down for area. Full strength stays on the small stuff, where its
     saturation is an accent rather than a weight. */
  --champagne-tint: #E4DBC9;

  /* The section the page currently ends on. The body's overscroll gradient
     matches this at its foot, so it moves with the last section rather than
     being restated in two places. */
  --page-foot: var(--champagne);

  /* Grounds — warm-biased neutrals, not pure greys */
  --offwhite: #FAF8F4;
  --white: #FFFFFF;

  /* Ink — brown-black, sits in the champagne's family */
  --ink: #2E2A24;
  --ink-italic: #4A4234;
  --rule: rgba(46, 42, 36, 0.18);

  /* Type */
  --font-display: 'Cardo', Georgia, 'Times New Roman', serif;
  --font-body: 'Work Sans', system-ui, -apple-system, sans-serif;

  /* Layout */
  --gutter: clamp(1.5rem, 4vw, 4rem);

  /* Vertical offset the .story section's back-plate uses behind its image —
     shared here since that's the only remaining user of the token. */
  --back-shift-y: 33px;

  /* The sticky bar's two heights: tall while transparent at the top of the
     page, short once scrolled. Both are stated here rather than left to
     emerge from padding, so --bar-h can mirror them exactly. */
  --bar-tall: clamp(8.5rem, 12.6vw, 11.4rem);
  --bar-short: clamp(6rem, 8.6vw, 7.7rem);

  /* Height of the sticky nav bar, mirrored onto the hero's negative margin.
     Switched in CSS alongside the bar itself so the two can't drift; main.js
     only overrides it where the bar's height is genuinely dynamic (the open
     mobile menu). */
  --bar-h: var(--bar-tall);
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

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

html,
body {
  margin: 0;
  padding: 0;
}

/* Rubber-band overscroll pulls the page away from the viewport edge and paints
   whatever is behind it. Containing it here stops the bounce at the document's
   own boundary, so there's nothing to expose above the hero. */
html {
  overscroll-behavior-y: none;
}

/* The masthead is sticky, so an anchor jump parks the target's top edge
   underneath it. Every nav link points at a section, so the offset is stated
   once here rather than per-section — --bar-short because the bar is always in
   its scrolled state by the time a jump has landed. */
[id] {
  scroll-margin-top: calc(var(--bar-short) + clamp(1rem, 3vh, 2rem));
}

body {
  font-family: var(--font-body);
  font-weight: 300;
  color: var(--ink);
  /* Not white: during overscroll the browser paints the body's background past
     the document edge, and a white band there is exactly the flash we're
     chasing. The page opens on the hero's champagne and closes on the tint, so
     each end of this gradient matches the section it sits behind — hard stops
     at the midpoint, since nothing between the two edges is ever visible.
     Fixed to the viewport so the stops don't drift with scroll position. */
  background: linear-gradient(
      to bottom,
      var(--champagne) 0 50%,
      var(--page-foot) 50% 100%
    )
    fixed;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Off-screen but still announced — for text that gives a control the context a
   screen reader needs and the eye already has from what's around it. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link — visible only on keyboard focus */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 1rem 1.5rem;
  background: var(--ink);
  color: var(--champagne);
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
}

.skip-link:focus {
  left: 0;
}

/* --------------------------------------------------------------------------
   Masthead
   -------------------------------------------------------------------------- */

/* Sticky nav. Transparent at the top of the page so the champagne L and the
   white notch read straight through it; once scrolled it takes a solid white
   background so the links stay legible over whatever passes beneath.

   The bar's height is set explicitly rather than emerging from its padding.
   The hero is pulled up beneath it by exactly --bar-h, so a height the CSS
   states outright can be mirrored by that variable; a height that emerges
   from animating padding can only be sampled after the fact, which leaves
   the two out of step for the length of the transition and drops the hero. */
.masthead {
  position: sticky;
  top: 0;
  z-index: 10;
  width: 100%;
  height: var(--bar-tall);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: clamp(1rem, 3vw, 2rem);
  padding: 0 var(--gutter);
  background: transparent;
  transition: background-color 0.35s ease, height 0.35s ease,
    box-shadow 0.35s ease;
}

.masthead[data-scrolled='true'] {
  height: var(--bar-short);
  background-color: var(--white);
  box-shadow: 0 1px 0 var(--rule);
}

/* --bar-h flips with the bar in the same style recalculation, so the hero's
   negative margin and the bar's height are never briefly out of step. */
:root:has(.masthead[data-scrolled='true']) {
  --bar-h: var(--bar-short);
}

/* The logo shrinks slightly once the bar is in its scrolled state */
.masthead[data-scrolled='true'] .logo {
  width: clamp(108px, 10vw, 150px);
}

/* At rest, the bar's right half sits over the hero photograph and the left
   half over the light copy panel — the logo stays ink throughout since the
   copy panel is always light behind it, but the nav crosses onto the photo
   and needs light text with a shadow to hold up there. Once scrolled the
   whole bar has its own white ground, so both drop the special-casing.
   Desktop only — below 901px the nav is the off-canvas panel, which sets
   its own light text without a shadow regardless of scroll state; being
   unscoped here would otherwise outrank that lower-specificity plain
   `.nav a` rule and leave a stray drop-shadow on the panel's links. */
/* Scoped to direct children only — links inside an open .nav-dropdown sit on
   the panel's own solid white ground and need the ordinary ink treatment
   regardless of what the bar behind them is doing. */
@media (min-width: 901px) {
  .masthead:not([data-scrolled='true']) .nav > a,
  .masthead:not([data-scrolled='true']) .nav-item > a,
  .masthead:not([data-scrolled='true']) .nav-item > span {
    color: #fff;
    text-shadow: 0 1px 10px rgba(0, 0, 0, 0.45), 0 1px 2px rgba(0, 0, 0, 0.4);
  }

  .masthead:not([data-scrolled='true']) .nav > a::after,
  .masthead:not([data-scrolled='true']) .nav-item > a::after,
  .masthead:not([data-scrolled='true']) .nav-item > span::after {
    background: #fff;
  }
}

.logo-link {
  display: block;
  line-height: 0;
  flex: none;
}

/* The logo is a transparent-background WebP, recoloured via mask so a single
   file can serve any ink colour. */
.logo {
  display: block;
  width: clamp(132px, 13vw, 196px);
  aspect-ratio: 480 / 262;
  background-color: var(--ink);
  -webkit-mask: url('../assets/img/logo.webp') no-repeat center / contain;
  mask: url('../assets/img/logo.webp') no-repeat center / contain;
}

.nav {
  display: flex;
  gap: clamp(0.9rem, 1.6vw, 1.7rem);
  align-items: center;
}

/* A snug, mostly-transparent chip behind the links, only while the bar sits
   over the photograph — hugs the text itself rather than reading as its own
   shape, just enough lift to keep the words legible against the image.
   Desktop only: below 901px this same selector would otherwise outrank the
   mobile off-canvas panel's own background (lower-specificity plain .nav
   rule), leaking this faint chip styling into the full-height dark panel
   whenever the page hasn't been scrolled yet. */
@media (min-width: 901px) {
  .masthead:not([data-scrolled='true']) .nav {
    padding: 0.5rem 0.3rem;
    background: rgba(20, 17, 13, 0.14);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    border-radius: 2px;
  }
}

.nav a,
.nav-item > span {
  position: relative;
  padding: 0.35rem 0;
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  color: var(--ink);
}

.nav a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.nav a:hover::after,
.nav a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

.nav a:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 6px;
}

/* --------------------------------------------------------------------------
   Nav dropdowns — Services and Locations
   Hover/focus reveals the panel on desktop; on the mobile slide-out the panel
   has no hover to trigger it, so it sits permanently open as an indented
   sub-list instead.
   -------------------------------------------------------------------------- */

.nav-item {
  position: relative;
}

.nav-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  margin: 0;
  padding: 0.6rem 0;
  list-style: none;
  min-width: 11rem;
  background: var(--white);
  border: 1px solid var(--rule);
  box-shadow: 0 12px 28px rgba(46, 42, 36, 0.12);

  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}

/* Desktop only — a mobile .nav-item carrying a :hover style makes iOS
   Safari swallow the first tap as a hover-simulation event instead of
   passing it through as a click, which is exactly the tap the chevron
   button needs to open the panel. */
@media (min-width: 901px) {
  .nav-item:hover .nav-dropdown,
  .nav-item:focus-within .nav-dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }
}

/* Closes the gap between the trigger link and the panel so the pointer
   never leaves a hoverable ancestor while moving down into it. */
.nav-item::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 0.6rem;
}

.nav-dropdown a {
  display: block;
  padding: 0.5rem 1.1rem;
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: none;
  white-space: nowrap;
  color: var(--ink);
}

.nav-dropdown a:hover,
.nav-dropdown a:focus-visible {
  background: var(--champagne-tint);
}

.nav-dropdown a::after {
  display: none;
}

/* The chevron is only a control on the mobile slide-out, where the dropdown
   has no hover to open it. On desktop the trigger link itself still opens
   the panel on hover/focus, so the button is removed from both view and
   tab order rather than just hidden. */
.nav-dropdown-toggle {
  display: none;
}

/* Only shown inside the mobile off-canvas panel, where the bar's own logo
   is out of view. */
.nav-panel-logo {
  display: none;
}

.nav-toggle {
  display: none;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1.1rem;
  background: none;
  border: 1px solid var(--ink);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
}

.nav-toggle:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

.nav-toggle-icon {
  position: relative;
  display: block;
  width: 0.95rem;
  height: 0.95rem;
  flex: none;
}

.nav-toggle-icon svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.nav-toggle-icon-close {
  opacity: 0;
}

.nav[data-open='true'] ~ .nav-toggle .nav-toggle-icon-open {
  opacity: 0;
}

.nav[data-open='true'] ~ .nav-toggle .nav-toggle-icon-close {
  opacity: 1;
}

.nav-backdrop {
  display: none;
}

/* --------------------------------------------------------------------------
   Hero — split panel, image bleeding up behind the sticky masthead
   -------------------------------------------------------------------------- */

/* Fills the viewport below the (overlapping) masthead. The masthead sits in
   flow above this at its tall height, then the hero is pulled up beneath it
   by the same amount — so the image panel's top edge is what actually meets
   the screen's top edge, and the bar reads as floating over the photograph. */
.hero {
  position: relative;
  height: 100svh;
  margin-top: calc(-1 * var(--bar-h));
  transition: margin-top 0.35s ease;
  display: grid;
  grid-template-columns: 40% 60%;
  background: var(--white);
  overflow: hidden;
}

.panel-left {
  position: relative;
  z-index: 2;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(0.75rem, 2vh, 1.4rem);
  padding: calc(var(--bar-h) + clamp(0.75rem, 2vh, 1.5rem)) var(--gutter)
    clamp(4rem, 14vh, 8rem);
  border-right: 1px solid var(--rule);
  overflow: hidden;
}

.hero-divider {
  width: 100%;
  max-width: 6rem;
  margin: 0;
  border: none;
  border-top: 1px solid var(--rule);
  transform-origin: left;
}

.panel-right {
  margin: 0;
  position: relative;
  overflow: hidden;
  min-height: 0;
  background: var(--champagne-tint);
}

.panel-right img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Slight overall wash so the photo reads a touch darker throughout — keeps
   the masthead legible over it wherever the bar happens to sit, not just
   near the very top edge. */
.panel-right::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.16);
  z-index: 1;
}

/* Extra scrim right behind the masthead row, layered on top of the wash. */
.panel-right::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: calc(var(--bar-h) + clamp(1rem, 3vh, 1.5rem));
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.38), rgba(0, 0, 0, 0));
  z-index: 1;
}

.hero h1 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 2.6vw + 1vh, 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.hero h1 em {
  font-style: italic;
  color: var(--ink-italic);
}

/* Each line clips its own inner span, which is what the load animation
   slides. overflow: hidden on a line box crops descenders, so the mask is
   padded and pulled back by the same amount — the letterforms keep their
   full depth. */
.line-mask {
  display: block;
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}

.line-inner {
  display: block;
}

.lede {
  margin: 0;
  max-width: 42ch;
  font-size: clamp(0.92rem, 0.5vw + 0.6rem, 1.06rem);
  line-height: 1.7;
  color: var(--ink);
  opacity: 0.86;
}

.hero-actions {
  display: flex;
  gap: 0.85rem;
  flex-wrap: wrap;
}

.btn {
  display: inline-flex;
  align-items: center;
  padding: 0.85rem 1.6rem;
  font-family: var(--font-body);
  font-size: 0.76rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--ink);
}

.btn-solid {
  background: var(--champagne);
  border-color: var(--champagne);
  color: var(--white);
}

.btn-solid:hover,
.btn-solid:focus-visible {
  background: var(--champagne-deep);
  border-color: var(--champagne-deep);
}

.btn-ghost {
  color: var(--ink);
}

.btn-ghost:hover,
.btn-ghost:focus-visible {
  background: var(--champagne-tint);
}

/* --------------------------------------------------------------------------
   Hero load sequence — curtain rise

   The headline rises out of its mask, the divider draws in, the lede and
   buttons lift up in turn, and the photograph wipes open while settling out
   of a slight over-scale. Everything is gated behind [data-ready='true'],
   set by main.js on the next frame: with JavaScript unavailable the
   attribute never lands and every element renders in its finished position,
   since each animation's `from` state only applies once the attribute (and
   therefore the animation) is present.
   -------------------------------------------------------------------------- */

/* Held in the hidden pre-animation state until main.js flips data-ready on
   the next frame — without JavaScript that attribute never lands and these
   rules never apply, so the fallback below renders everything finished. */
.hero:not([data-ready='true']) .line-inner {
  transform: translateY(105%);
}

.masthead:not([data-ready='true']) .logo,
.masthead:not([data-ready='true']) .nav {
  opacity: 0;
}

.hero:not([data-ready='true']) [data-el='divider'] {
  transform: scaleX(0);
}

.hero:not([data-ready='true']) [data-el='lede'],
.hero:not([data-ready='true']) [data-el='actions'] {
  opacity: 0;
  transform: translateY(10px);
}

.hero:not([data-ready='true']) [data-el='figure'] {
  clip-path: inset(0 0 100% 0);
}

.hero:not([data-ready='true']) [data-el='figure'] img {
  transform: scale(1.15);
}

.hero[data-ready='true'] .line-inner {
  animation: hero-rise 1s cubic-bezier(0.16, 0.84, 0.3, 1) both;
}

.hero[data-ready='true'] .line-mask:nth-of-type(1) .line-inner {
  animation-delay: 0.15s;
}

.hero[data-ready='true'] .line-mask:nth-of-type(2) .line-inner {
  animation-delay: 0.32s;
}

.masthead[data-ready='true'] .logo {
  animation: hero-fade 0.8s ease 0.1s both;
}

.masthead[data-ready='true'] .nav {
  animation: hero-fade 0.8s ease 0.5s both;
}

.hero[data-ready='true'] [data-el='divider'] {
  animation: hero-draw 0.7s cubic-bezier(0.65, 0, 0.35, 1) 0.55s both;
}

.hero[data-ready='true'] [data-el='lede'] {
  animation: hero-lift 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.7s both;
}

.hero[data-ready='true'] [data-el='actions'] {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.88s both;
}

.hero[data-ready='true'] [data-el='figure'] {
  animation: hero-wipe 1.3s cubic-bezier(0.16, 0.84, 0.3, 1) 0.15s both;
}

.hero[data-ready='true'] [data-el='figure'] img {
  animation: hero-drift 1.9s cubic-bezier(0.16, 0.84, 0.3, 1) 0.15s both;
}

@keyframes hero-rise {
  from { transform: translateY(105%); }
  to   { transform: translateY(0); }
}

@keyframes hero-fade {
  from { opacity: 0; }
}

@keyframes hero-draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* The lede rests at 0.86, not 1 — the keyframe has to land there or it would
   brighten by a step at the end of the animation. */
@keyframes hero-lift {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 0.86; transform: none; }
}

@keyframes hero-lift-full {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

@keyframes hero-wipe {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}

@keyframes hero-drift {
  from { transform: scale(1.15); }
  to   { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .hero[data-ready='true'] .line-inner,
  .masthead[data-ready='true'] .logo,
  .masthead[data-ready='true'] .nav,
  .hero[data-ready='true'] [data-el='divider'],
  .hero[data-ready='true'] [data-el='lede'],
  .hero[data-ready='true'] [data-el='actions'],
  .hero[data-ready='true'] [data-el='figure'],
  .hero[data-ready='true'] [data-el='figure'] img {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   Second screen — plain white
   -------------------------------------------------------------------------- */

/* Sits directly beneath the hero — no min-height, so it's only as tall as the
   carousel it holds. */
.screen-white {
  position: relative;
  z-index: 1;
  background: var(--white);
}

/* --------------------------------------------------------------------------
   Locations — full-bleed image with white text over it
   -------------------------------------------------------------------------- */

.locations {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  align-items: center;
  overflow: hidden;
  background: var(--ink);
}

.locations-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The house sits upper-centre in the frame; anchoring there keeps it in
     shot as the section gets wider and shallower. */
  object-position: 50% 38%;
  z-index: 0;
}

/* The lower half of the photograph is pale gravel, so white text needs a
   scrim to stay legible. Darkest on the left where the copy sits. */
.locations::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(
      to right,
      rgba(24, 21, 17, 0.82) 0%,
      rgba(24, 21, 17, 0.62) 38%,
      rgba(24, 21, 17, 0.28) 70%,
      rgba(24, 21, 17, 0.22) 100%
    );
}

/* Everything sits in a single left-hand column over the image */
.locations-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: clamp(3.5rem, 9vh, 6rem) var(--gutter);
  color: var(--white);
}

.locations-copy {
  max-width: 46ch;
}

.locations h2 {
  margin: 0 0 clamp(1.2rem, 2.6vh, 1.8rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 4vw, 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.locations-lede {
  margin: 0;
  max-width: 42ch;
  font-size: clamp(0.94rem, 1.1vw, 1.04rem);
  line-height: 1.75;
  opacity: 0.88;
}

/* Sits under the copy, left-aligned, one place per line */
.locations-list {
  margin: clamp(1.8rem, 4vh, 2.6rem) 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(0.6rem, 1.4vh, 0.95rem);
}

.locations-list a {
  position: relative;
  display: inline-block;
  padding-bottom: 0.25rem;
  color: var(--white);
  text-decoration: none;
  font-size: clamp(0.92rem, 1.35vw, 1.12rem);
  letter-spacing: 0.04em;
  opacity: 0.82;
  transition: opacity 0.35s ease;
}

.locations-list a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--champagne);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}

.locations-list a:hover,
.locations-list a:focus-visible {
  opacity: 1;
}

.locations-list a:hover::after,
.locations-list a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

.locations-list a:focus-visible {
  outline: 1px solid var(--white);
  outline-offset: 6px;
}

/* Entrance — the rule draw
   The copy fades up, then each place's champagne underline sweeps across and
   away as the place itself fades in. The entrance rehearses the hover state:
   the seven places are links, but nothing says so until you're already on one,
   so the animation does the telling. JS adds .is-in once the section is a third
   on screen; without JS the :not(.is-in) guard never applies and everything
   sits at its rest state. */

.locations[data-animate] .locations-copy > * {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.85s ease,
              transform 0.85s cubic-bezier(0.22, 1, 0.36, 1);
}

.locations[data-animate].is-in .locations-copy > * {
  opacity: 1;
  transform: none;
}

.locations[data-animate].is-in .locations-lede { transition-delay: 0.18s; }
.locations[data-animate].is-in .locations-list { transition-delay: 0.3s; }

/* The lede carries a resting opacity of its own, so it can't ride the shared
   `opacity: 1` above without brightening past where it started. */
.locations[data-animate] .locations-lede { opacity: 0; }
.locations[data-animate].is-in .locations-lede { opacity: 0.88; }

/* Links arrive dim and lift to their 0.82 rest opacity as their own rule
   finishes drawing. */
.locations[data-animate] .locations-list a {
  opacity: 0;
  transition: opacity 0.55s ease;
}

.locations[data-animate].is-in .locations-list a { opacity: 0.82; }

/* One continuous gesture: out from the left, then away to the right. Flipping
   transform-origin at the midpoint while the bar is at full width means the
   switch itself is invisible — a plain scaleX would snap. Scoped to
   :not(.rule-settled) so the `animation` declaration is gone entirely once
   main.js confirms the sweep has finished (see the .rule-settled comment
   below) — hover/unhover past that point never touch `animation` again. */
.locations[data-animate].is-in:not(.rule-settled) .locations-list a::after {
  /* The delay has to be folded into this shorthand rather than left to the
     longhand `animation-delay` rule further down: this selector outranks
     that one on specificity (the :not() above), so the shorthand's implicit
     `animation-delay: 0s` would otherwise win and every link's sweep would
     fire at once instead of staggering. */
  animation: locations-rule-sweep 1.05s cubic-bezier(0.4, 0, 0.2, 1)
    var(--enter-delay, 0s) forwards;
}

/* Hover has to win once the sweep has run. Two separate problems: an
   animation beats a transition on the same property whatever the specificity,
   so the rule cancels the animation rather than trying to out-rank it — and
   the .is-in opacity above is more specific than the plain `a:hover` further
   up this file, so the lift to full opacity has to be restated at this weight
   or the link never brightens under the cursor. */
/* The stagger delays below are set on the links themselves, so they'd also
   apply to the hover transition — the underline and the brightening would sit
   still for up to a second before responding. Zero the delay on interaction so
   hover stays immediate. */
.locations[data-animate].is-in .locations-list a:hover,
.locations[data-animate].is-in .locations-list a:focus-visible {
  opacity: 1;
  transition-delay: 0s;
}

.locations[data-animate] .locations-list a:hover::after,
.locations[data-animate] .locations-list a:focus-visible::after {
  animation: none;
  transform: scaleX(1);
  transform-origin: left;
}

/* Once main.js marks the section .rule-settled (the sweep's `animationend`),
   the underline is a plain transition between two rest states instead of a
   keyframe animation. Toggling `animation` on and off on every hover/unhover
   is what caused the double-fire: browsers restart a re-applied keyframe
   animation from 0%, so leaving mid-hover played the whole sweep again on
   the way back out. A transition has no such restart behaviour — it just
   reverses from wherever it currently sits. */
.locations[data-animate].is-in.rule-settled .locations-list a::after {
  transform: scaleX(0);
  transform-origin: right;
}

.locations[data-animate].is-in.rule-settled .locations-list a:hover::after,
.locations[data-animate].is-in.rule-settled .locations-list a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

@keyframes locations-rule-sweep {
  0%   { transform: scaleX(0); transform-origin: left; }
  45%  { transform: scaleX(1); transform-origin: left; }
  55%  { transform: scaleX(1); transform-origin: right; }
  100% { transform: scaleX(0); transform-origin: right; }
}

/* 100ms apart — slow enough to read as seven separate places rather than one
   ripple, quick enough that the last one lands inside two seconds.

   The delay is carried on the li as a custom property rather than set on the
   link directly. Written as `li:nth-child(n) a` it would outrank `a:hover` on
   specificity (the extra type selector) and freeze the hover response for up
   to a second; parked on the parent, the link picks it up through var() at
   whatever weight the winning rule happens to have. */
.locations[data-animate] .locations-list li:nth-child(1) { --enter-delay: 0.40s; }
.locations[data-animate] .locations-list li:nth-child(2) { --enter-delay: 0.50s; }
.locations[data-animate] .locations-list li:nth-child(3) { --enter-delay: 0.60s; }
.locations[data-animate] .locations-list li:nth-child(4) { --enter-delay: 0.70s; }
.locations[data-animate] .locations-list li:nth-child(5) { --enter-delay: 0.80s; }
.locations[data-animate] .locations-list li:nth-child(6) { --enter-delay: 0.90s; }
.locations[data-animate] .locations-list li:nth-child(7) { --enter-delay: 1.00s; }

.locations[data-animate].is-in .locations-list a { transition-delay: var(--enter-delay, 0s); }

/* The global reduced-motion rule at the foot of this file crushes durations to
   0.01ms, which would skip the entrance rather than complete it — the links
   start at opacity 0, so they'd flash. Land them on the rest state instead.
   JS also skips the observer entirely, so .is-in is present from the start. */
@media (prefers-reduced-motion: reduce) {
  .locations[data-animate] .locations-copy > * {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .locations[data-animate] .locations-lede { opacity: 0.88; }

  .locations[data-animate] .locations-list a {
    opacity: 0.82;
    transition: none;
  }

  .locations[data-animate] .locations-list a::after,
  .locations[data-animate].is-in .locations-list a::after {
    animation: none;
  }
}

@media (max-width: 760px) {
  .locations::before {
    background: linear-gradient(
      to bottom,
      rgba(24, 21, 17, 0.55) 0%,
      rgba(24, 21, 17, 0.78) 100%
    );
  }
}

/* --------------------------------------------------------------------------
   What we do — centred title, four icons in a row
   -------------------------------------------------------------------------- */

.offer {
  position: relative;
  z-index: 1;
  /* Pale warm ground — the existing off-white, not a flat grey */
  background: var(--offwhite);
  padding: clamp(4rem, 11vh, 7.5rem) var(--gutter);
  text-align: center;
}

.offer h2 {
  margin: 0 auto clamp(3rem, 7vh, 5rem);
  max-width: 22ch;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.75rem, 3.4vw, 2.9rem);
  line-height: 1.18;
  letter-spacing: -0.012em;
  text-wrap: balance;
  color: var(--ink);
}

/* Forced break only where the line is long enough to want one */
.wrap-md {
  display: none;
}

@media (min-width: 861px) {
  .wrap-md {
    display: inline;
  }

  .offer h2 {
    max-width: none;
  }
}

.offer-list {
  margin: 0 auto;
  padding: 0;
  list-style: none;
  max-width: 1100px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
}

.offer-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 2.4vh, 1.6rem);
}

.offer-icon {
  display: block;
  color: var(--champagne-deep);
}

.offer-icon svg {
  display: block;
  width: clamp(34px, 3.4vw, 44px);
  height: auto;
}

.offer-label {
  font-size: clamp(0.82rem, 1.05vw, 0.95rem);
  letter-spacing: 0.04em;
  line-height: 1.5;
  color: var(--ink);
  opacity: 0.85;
  text-wrap: balance;
}

/* Two-up rather than a cramped single row */
@media (max-width: 640px) {
  .offer-list {
    grid-template-columns: repeat(2, 1fr);
    gap: clamp(2rem, 6vh, 3rem) 1.5rem;
  }
}

/* --------------------------------------------------------------------------
   Featured properties — champagne ground, centred copy
   -------------------------------------------------------------------------- */

/* The same tint the hero's L uses: this is another large flat field, so it
   takes the let-down champagne rather than the full strength.

   Copy sits in a narrow left column; the deck takes the wider remainder. */
.featured {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: grid;
  grid-template-columns: minmax(26ch, 0.8fr) 1.6fr;
  align-items: center;
  gap: clamp(2.5rem, 6vw, 5rem);
  background: var(--champagne-tint);
  padding: clamp(4rem, 11vh, 7.5rem) var(--gutter);
  /* The stacked cards are offset right of the face one and the parked ones sit
     further out again — all invisible, but transforms still count towards
     scroll width, so they'd give the page a sideways scrollbar. `clip` rather
     than `hidden`: `hidden` makes this a scroll container, which would break
     the sticky masthead above it. */
  overflow-x: clip;
}

.featured-copy {
  max-width: 42ch;
}

.featured h2 {
  margin: 0 0 clamp(1.2rem, 2.6vh, 1.8rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.75rem, 3.4vw, 2.9rem);
  line-height: 1.18;
  letter-spacing: -0.012em;
  text-wrap: balance;
  color: var(--ink);
}

.featured-lede {
  margin: 0;
  font-size: clamp(0.96rem, 1.15vw, 1.06rem);
  line-height: 1.75;
  color: var(--ink);
  opacity: 0.86;
}

/* Play/pause sits with the copy rather than floating over the deck: a control
   that only appears on hover can't be reached by keyboard or touch. */
.deck-toggle {
  margin-top: clamp(1.6rem, 3.5vh, 2.2rem);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 1rem 0.55rem 0.85rem;
  background: none;
  border: 1px solid var(--rule);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.66rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 0.35s ease;
}

.deck-toggle:hover {
  border-color: var(--ink);
}

.deck-toggle:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

.deck-toggle svg {
  display: block;
  width: 9px;
  height: 11px;
  fill: currentColor;
}

/* The button reports the state it will move to, so the icon is the inverse of
   what's happening: paused shows play. */
.deck-toggle .icon-play {
  display: none;
}

.deck-toggle[aria-pressed='true'] .icon-play {
  display: block;
}

.deck-toggle[aria-pressed='true'] .icon-pause {
  display: none;
}

/* --------------------------------------------------------------------------
   Featured deck — the hero's plate motif as a stack
   -------------------------------------------------------------------------- */

/* The bordered plate from the hero, repeated: the cards behind the face one
   are the rest of the collection waiting, stepped down and right the way the
   hero's back plate is. */
.deck {
  position: relative;
  height: clamp(340px, 62vh, 580px);
}

.deck-item {
  position: absolute;
  inset: 0;
  border: 1px solid var(--ink);
  background: var(--offwhite);
  overflow: hidden;
  /* Depth is driven by --slot, set per card from script: 0 is the face, 1 and
     2 sit behind it. Keeping the offsets in one transform means the stack
     re-forms correctly wherever the sequence has got to. */
  transform:
    translate(calc(var(--slot, 0) * 26px), calc(var(--slot, 0) * 20px))
    scale(calc(1 - var(--slot, 0) * 0.045));
  opacity: calc(1 - var(--slot, 0) * 0.28);
  z-index: calc(10 - var(--slot, 0));
  transition:
    transform 0.9s cubic-bezier(0.22, 1, 0.36, 1),
    opacity 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Dealt away: up and left, off the top of the stack */
.deck-item[data-state='leaving'] {
  transform: translate(-34px, -28px) scale(1.02);
  opacity: 0;
  z-index: 11;
}

/* Everything past the third card is parked out of sight so the stack reads as
   three plates rather than fanning out into all five. */
.deck-item[data-state='hidden'] {
  transform: translate(78px, 60px) scale(0.865);
  opacity: 0;
  z-index: 1;
}

.deck-item img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Scrim only at the foot, where the caption sits — the photograph itself stays
   clear of it. */
.deck-caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: clamp(1.5rem, 3.5vh, 2.2rem);
  color: var(--white);
  background: linear-gradient(
    to top,
    rgba(24, 21, 17, 0.9) 0%,
    rgba(24, 21, 17, 0.66) 52%,
    rgba(24, 21, 17, 0) 100%
  );
}

.deck-place {
  margin: 0 0 0.5rem;
  font-size: 0.64rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0.82;
}

.deck-name {
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 2.4vw, 2rem);
  line-height: 1.14;
}

.deck-desc {
  margin: 0;
  max-width: 44ch;
  font-size: 0.9rem;
  line-height: 1.7;
  opacity: 0.9;
}

/* Facts, not prose — set apart from the description as small uppercase.

   Separated by space alone, deliberately. A rule between items has to be drawn
   as a pseudo-element on one side or the other, and this list wraps to three
   lines on a narrow screen: whichever side it's hung from, it strands at the
   start or the end of a wrapped line, because CSS has no way to ask whether an
   item begins a line. Wide gaps read as a set without that problem, and the
   quieter treatment suits the page. */
.deck-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem 1.6rem;
  margin: clamp(0.8rem, 1.8vh, 1.1rem) 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  opacity: 0.72;
}

/* The caption steps back as the overlay comes forward — both on screen at once
   would be two competing sets of white text over the same photograph. */
.deck-item:hover .deck-caption,
.deck-item:focus-within .deck-caption {
  opacity: 0;
  transform: translateY(8px);
}

.deck-caption {
  transition: opacity 0.45s ease, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

/* --------------------------------------------------------------------------
   Deck overlay — the name and a way in, on hover
   -------------------------------------------------------------------------- */

/* Covers the whole plate rather than sitting in a corner of it: the card is the
   hover target that pauses the deck, so the thing that appears on hover should
   occupy the same area you're pointing at. */
.deck-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.2rem, 3vh, 1.8rem);
  padding: clamp(1.5rem, 4vw, 2.5rem);
  text-align: center;
  /* Deeper than the caption's scrim and flat across the whole plate, so the
     name reads wherever the photograph happens to be light. */
  background: rgba(24, 21, 17, 0.62);
  opacity: 0;
  /* Deliberately not visibility:hidden or display:none. Either one makes the
     CTA inside unfocusable, so tabbing could never reach it — and since it's
     :focus-within that reveals the overlay, it could never be shown by keyboard
     at all. Kept visible-but-transparent so the link stays in the tab order and
     focusing it brings the overlay up. Buried cards are handled by inert on the
     card itself, which removes them from the tab order properly. */
  transition: opacity 0.45s ease;
}

.deck-item:hover .deck-overlay,
.deck-item:focus-within .deck-overlay {
  opacity: 1;
}

/* Only the face card is interactive: the ones behind it are 28% and 56% faded
   and offset, so a hover there would light up a card nobody can properly see.
   JS marks the face card, and pointer-events keeps the rest inert. */
.deck-item:not([data-face='true']) .deck-overlay {
  pointer-events: none;
}

.deck-overlay-name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  line-height: 1.12;
  color: var(--white);
  text-wrap: balance;
  /* Rises into place, so the overlay resolves rather than simply appearing */
  transform: translateY(10px);
  transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}

.deck-item:hover .deck-overlay-name,
.deck-item:focus-within .deck-overlay-name {
  transform: translateY(0);
}

/* The same hairline box as the other controls on the page, inverted for the
   dark ground it sits on. */
.deck-cta {
  display: inline-block;
  padding: 0.85rem 1.9rem;
  border: 1px solid var(--white);
  color: var(--white);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  transform: translateY(10px);
  transition:
    background-color 0.4s ease,
    color 0.4s ease,
    transform 0.55s cubic-bezier(0.22, 1, 0.36, 1) 0.05s;
}

.deck-item:hover .deck-cta,
.deck-item:focus-within .deck-cta {
  transform: translateY(0);
}

.deck-cta:hover,
.deck-cta:focus-visible {
  background-color: var(--white);
  color: var(--ink);
}

.deck-cta:focus-visible {
  outline: 1px solid var(--white);
  outline-offset: 4px;
}

/* Stacked, copy first — a deck this wide has nowhere to go beside it */
@media (max-width: 900px) {
  .featured {
    grid-template-columns: 1fr;
    min-height: auto;
    gap: clamp(2rem, 5vh, 3rem);
  }

  .featured-copy {
    max-width: 46ch;
  }

  /* Room for the stack's offsets, taken out of the deck's own width rather
     than added as margin — a margin widens the grid track instead. */
  .deck {
    height: clamp(300px, 52vh, 420px);
    padding: 0 26px 20px 0;
  }
}

/* --------------------------------------------------------------------------
   Deck overlay on touch
   -------------------------------------------------------------------------- */

/* A tap fires :hover on most touch browsers, so the overlay would come up and
   then stick — there's no way to stop hovering with a finger, and it covers the
   description and facts to say less than they do. On anything without a real
   pointer the overlay stays down and the CTA sits in the caption instead, where
   it's permanently visible and one tap away. */
@media (hover: none) {
  .deck-overlay {
    position: static;
    display: block;
    padding: 0;
    background: none;
    opacity: 1;
    z-index: auto;
  }

  /* The name is already the caption's heading */
  .deck-overlay-name {
    display: none;
  }

  /* Pulled into the caption's flow, under the facts */
  .deck-cta {
    position: absolute;
    left: clamp(1.5rem, 3.5vh, 2.2rem);
    bottom: clamp(1.5rem, 3.5vh, 2.2rem);
    transform: none;
    padding: 0.7rem 1.4rem;
    font-size: 0.64rem;
  }

  /* The caption has to make room for the button now sitting beneath it, and
     its scrim has to reach the plate's foot so the button has a dark ground to
     sit on rather than trailing off into the photograph. */
  .deck-caption {
    padding-bottom: calc(clamp(1.5rem, 3.5vh, 2.2rem) + 3.4rem);
    background: linear-gradient(
      to top,
      rgba(24, 21, 17, 0.92) 0%,
      rgba(24, 21, 17, 0.88) 40%,
      rgba(24, 21, 17, 0.6) 72%,
      rgba(24, 21, 17, 0) 100%
    );
  }

  /* Nothing recedes, because nothing comes forward */
  .deck-item:hover .deck-caption,
  .deck-item:focus-within .deck-caption {
    opacity: 1;
    transform: none;
  }
}

/* Without the deal animation there's no carousel — the cards would sit in a
   silent stack with four of them permanently hidden. Falling back to a plain
   column shows the whole collection instead, which is the honest static form
   of the same content. */
@media (prefers-reduced-motion: reduce) {
  .deck {
    height: auto;
    display: flex;
    flex-direction: column;
    gap: clamp(1.2rem, 3vh, 2rem);
    margin: 0;
  }

  .deck-item {
    position: relative;
    inset: auto;
    transform: none;
    opacity: 1;
    height: clamp(260px, 42vh, 360px);
  }

  .deck-toggle {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   Who we are — copy left, plate right, on the pale warm ground
   -------------------------------------------------------------------------- */

/* Alternates off the featured section's tint back to the off-white used by
   .offer, so the page reads as bands rather than one continuous field.

   The copy takes the wider column here, the reverse of .featured: this section
   is prose, and the image is there to hold the other half of the screen rather
   than to be read. */
.story {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: grid;
  grid-template-columns: 1.15fr 1fr;
  align-items: center;
  gap: clamp(2.5rem, 6vw, 5.5rem);
  background: var(--offwhite);
  padding: clamp(4rem, 11vh, 7.5rem) var(--gutter);
  /* The back plate is pushed up and right past the figure's own box, and on a
     narrow-ish screen that reaches the viewport edge. `clip` for the same
     reason .featured uses it: `hidden` would make this a scroll container and
     break the sticky masthead. */
  overflow-x: clip;
}

.story-copy {
  max-width: 54ch;
}

.story h2 {
  margin: 0 0 clamp(1.4rem, 3vh, 2rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.85rem, 3.6vw, 3.1rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  text-wrap: balance;
  color: var(--ink);
}

.story h2 em {
  font-style: italic;
  color: var(--ink-italic);
}

.story-lede {
  margin: 0 0 clamp(1rem, 2.4vh, 1.5rem);
  font-size: clamp(0.96rem, 1.15vw, 1.06rem);
  line-height: 1.78;
  color: var(--ink);
  opacity: 0.86;
}

.story-lede:last-of-type {
  margin-bottom: 0;
}

/* Figures set as a definition list: each is a labelled value, not a heading
   with prose under it. The label sits above the number and reads smaller —
   the number is what the eye is meant to land on. */
.story-facts {
  margin: clamp(2.2rem, 5vh, 3.2rem) 0 0;
  padding: clamp(1.8rem, 4vh, 2.4rem) 0 0;
  border-top: 1px solid var(--rule);
  display: grid;
  grid-template-columns: repeat(3, auto);
  grid-template-rows: auto auto;
  justify-content: start;
  gap: 0.45rem clamp(1.5rem, 4vw, 3.5rem);
}

/* The labels are different lengths and some wrap to two lines. Left to
   themselves the numbers would then sit at different heights across the row;
   making each fact a two-row subgrid puts every label in one band and every
   number in the next, so the figures align whatever the labels do. */
.story-fact {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 2;
}

.story-fact dt {
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--ink);
  opacity: 0.62;
}

.story-fact dd {
  /* Spacing comes from the grid's row gap, which the subgrid inherits */
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 3vw, 2.6rem);
  line-height: 1;
  color: var(--ink);
}

/* Same hairline-box treatment as .deck-toggle, so the two controls on the page
   belong to each other. */
.story-cta {
  margin-top: clamp(2.2rem, 5vh, 3rem);
  display: inline-block;
  padding: 0.85rem 1.9rem;
  border: 1px solid var(--ink);
  color: var(--ink);
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color 0.4s ease, color 0.4s ease;
}

.story-cta:hover,
.story-cta:focus-visible {
  background-color: var(--ink);
  color: var(--offwhite);
}

.story-cta:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

/* Held in its own grid cell rather than positioned off the section, which
   keeps the frame's proportions measured from the image instead of the page. */
.story-figure {
  position: relative;
  height: clamp(360px, 64vh, 600px);
}

.story-plate {
  position: absolute;
  inset: 0;
  margin: 0;
  background: var(--white);
  overflow: hidden;
}

.story-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Open corner brackets replace the old staggered outline back-plate: four
   viewfinder-style marks held just outside the photo's own edges (siblings
   of .story-plate, not nested inside its clipped bounds) rather than a full
   traced rectangle, so the frame reads as a light curatorial touch instead
   of a boxed-in photo. */
.story-plate-bracket {
  position: absolute;
  z-index: 3;
  width: clamp(28px, 4vw, 44px);
  height: clamp(28px, 4vw, 44px);
  pointer-events: none;
}

.story-plate-bracket--tl {
  top: -14px;
  left: -14px;
  border-top: 1px solid var(--ink);
  border-left: 1px solid var(--ink);
}

.story-plate-bracket--tr {
  top: -14px;
  right: -14px;
  border-top: 1px solid var(--ink);
  border-right: 1px solid var(--ink);
}

.story-plate-bracket--bl {
  bottom: -14px;
  left: -14px;
  border-bottom: 1px solid var(--ink);
  border-left: 1px solid var(--ink);
}

.story-plate-bracket--br {
  bottom: -14px;
  right: -14px;
  border-bottom: 1px solid var(--ink);
  border-right: 1px solid var(--ink);
}

/* Entrance — curtain open
   The copy slides in from the left, line by line — the reader's eye already
   lands there — while the figure's back plate steps out from behind the photo
   and the photo itself wipes into view left-to-right, as if a curtain were
   drawn back from the room. JS adds .is-in once the section is a third on
   screen; without JS the :not(.is-in) guard never applies and everything
   sits at its rest state. */

.story[data-animate] .story-copy > * {
  opacity: 0;
  transform: translateX(-22px);
  transition: opacity 0.75s ease,
              transform 0.75s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.story[data-animate].is-in .story-copy > * {
  opacity: 1;
  transform: none;
}

/* The lede carries a resting opacity of its own, so it can't ride the shared
   `opacity: 1` above without brightening past where it started. */
.story[data-animate] .story-lede { opacity: 0; }
.story[data-animate].is-in .story-lede { opacity: 0.86; }

.story[data-animate].is-in .story-copy > *:nth-child(1) { transition-delay: 0.05s; }
.story[data-animate].is-in .story-copy > *:nth-child(2) { transition-delay: 0.15s; }
.story[data-animate].is-in .story-copy > *:nth-child(3) { transition-delay: 0.25s; }
.story[data-animate].is-in .story-copy > *:nth-child(4) { transition-delay: 0.35s; }
.story[data-animate].is-in .story-copy > *:nth-child(5) { transition-delay: 0.43s; }

/* The photo wipes open left to right, then the corner brackets fade in just
   behind it, as if settling into place around the now-visible frame. */
.story[data-animate] .story-plate {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.8s cubic-bezier(0.22, 0.61, 0.36, 1);
  transition-delay: 0.1s;
}

.story[data-animate].is-in .story-plate {
  clip-path: inset(0 0 0 0);
}

.story[data-animate] .story-plate-bracket {
  opacity: 0;
  transition: opacity 0.6s ease;
  transition-delay: 0.55s;
}

.story[data-animate].is-in .story-plate-bracket {
  opacity: 1;
}

/* The global reduced-motion rule at the foot of this file crushes durations to
   0.01ms, which would skip the entrance rather than complete it — the copy
   and plate start off-screen/masked, so they'd flash. Land them on the rest
   state instead. JS also skips the observer entirely, so .is-in is present
   from the start. */
@media (prefers-reduced-motion: reduce) {
  .story[data-animate] .story-copy > * {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .story[data-animate] .story-lede { opacity: 0.86; }

  .story[data-animate] .story-plate {
    clip-path: none;
    transition: none;
  }

  .story[data-animate] .story-plate-bracket {
    opacity: 1;
    transition: none;
  }
}

/* Stacked, copy first — matching how .featured collapses at the same width */
@media (max-width: 900px) {
  .story {
    grid-template-columns: 1fr;
    min-height: auto;
    gap: clamp(2.5rem, 6vh, 3.5rem);
  }

  .story-copy {
    max-width: 46ch;
  }

  /* Centred as a block within the now full-width column, rather than
     trailing off to the left the way it sits beside the image on desktop.
     width: fit-content shrinks each box to its own content so the auto
     margins have room to centre it. */
  .story-facts {
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
  }

  .story-cta {
    display: block;
    width: fit-content;
    margin-left: auto;
    margin-right: auto;
  }

  .story-figure {
    height: clamp(300px, 52vh, 420px);
  }
}

/* Three columns of figures don't fit beside each other on a phone. Two would
   leave the third fact orphaned on its own row, so they fold into a single
   column instead: label then number, stacked rather than side by side.
   The grid moves onto .story-facts itself and each .story-fact collapses
   to display: contents, so all three labels share one column width and
   the numbers land in a single conformed column rather than each row
   sizing its own label independently. */
@media (max-width: 520px) {
  .story-facts {
    grid-template-columns: auto 1fr;
    grid-template-rows: none;
    align-items: baseline;
    gap: clamp(0.9rem, 2.5vh, 1.3rem) 0.6rem;
  }

  .story-fact {
    display: contents;
  }
}

/* --------------------------------------------------------------------------
   Reviews — flanked header, moving row of guest reviews
   -------------------------------------------------------------------------- */

.reviews {
  position: relative;
  z-index: 1;
  background: var(--white);
  /* A band of white before the images start, so they read as sitting inside
     this section rather than butting straight onto the one above. The images
     bleed to the page edges, and without this they meet the section above at a
     hard seam with no breathing room. */
  padding-top: clamp(4rem, 12vh, 8rem);
  padding-bottom: clamp(3.5rem, 8vh, 5.5rem);
  /* The duplicated marquee set is translated out past the right-hand edge.
     `clip` rather than `hidden` for the same reason as .featured: `hidden`
     would make this a scroll container and break the sticky masthead. */
  overflow-x: clip;
}

/* The heading holds the middle column; the images either side bleed off the
   page edges and are cropped by the section. */
.reviews-top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr);
  align-items: stretch;
  gap: clamp(1.5rem, 4vw, 3rem);
}

/* Placeholder standing in for photography, to be swapped for real images.
   The label is drawn from the data-label attribute so the markup carries the
   wording rather than the stylesheet. */
.reviews-image {
  position: relative;
  height: clamp(240px, 34vh, 400px);
  background:
    repeating-linear-gradient(
      -45deg,
      rgba(169, 151, 117, 0.13) 0 10px,
      rgba(169, 151, 117, 0.05) 10px 20px
    ),
    var(--champagne-tint);
  display: grid;
  place-items: center;
}

.reviews-image::after {
  content: attr(data-label);
  font-size: 0.58rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.5;
}

/* Filled with a real photograph: the striped wash and label are placeholder
   furniture, so a card carrying an image drops both and lets the photograph
   fill the frame instead. */
.reviews-image--photo {
  background: none;
}

.reviews-image--photo::after {
  content: none;
}

.reviews-image--photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.reviews-head {
  text-align: center;
  padding: clamp(1rem, 3vh, 2rem) var(--gutter);
}

/* The wordmark, recoloured via mask like the masthead logo */
.reviews-mark {
  display: block;
  margin: 0 auto clamp(1.4rem, 3vh, 2rem);
  width: clamp(112px, 11vw, 148px);
  aspect-ratio: 480 / 262;
  background-color: var(--ink);
  -webkit-mask: url('../assets/img/logo.webp') no-repeat center / contain;
  mask: url('../assets/img/logo.webp') no-repeat center / contain;
}

.reviews h2 {
  margin: 0 auto clamp(0.9rem, 2vh, 1.3rem);
  max-width: 18ch;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.6vw, 3rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  text-wrap: balance;
  color: var(--ink);
}

.reviews h2 em {
  font-style: italic;
  color: var(--ink-italic);
}

.reviews-lede {
  margin: 0 auto;
  max-width: 44ch;
  font-size: clamp(0.94rem, 1.1vw, 1.02rem);
  line-height: 1.7;
  color: var(--ink);
  opacity: 0.82;
}

/* The moving row. Masked at both ends so cards enter and leave rather than
   being cut off at a hard edge. */
.marquee {
  margin-top: clamp(3rem, 7vh, 4.5rem);
  overflow: hidden;
  -webkit-mask: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
  mask: linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
}

.marquee-track {
  display: flex;
  align-items: start;
  gap: clamp(1rem, 2vw, 1.6rem);
  width: max-content;
  margin: 0;
  padding: 0 0 0 clamp(1rem, 2vw, 1.6rem);
  list-style: none;
  animation: review-marquee 90s linear infinite;
}

/* Travels exactly half the track — the second set is a duplicate, so the
   restart is invisible. */
@keyframes review-marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/* Pausing lets someone actually read a review instead of chasing it */
.marquee:hover .marquee-track,
.marquee:focus-within .marquee-track {
  animation-play-state: paused;
}

/* Every card is the same height regardless of how long the review runs: a row
   of ragged-bottomed cards reads as broken rather than as varied. The quote
   itself is clamped and the overflow goes behind a control, so a long review
   is still readable in full without setting the height of the whole row. */
.review {
  flex: none;
  width: clamp(280px, 25vw, 340px);
  height: clamp(230px, 30vh, 270px);
  background: var(--white);
  border: 1px solid var(--rule);
  padding: clamp(1.3rem, 3vh, 1.7rem);
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.review-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.review-name {
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink);
}

.stars {
  display: inline-flex;
  gap: 0.1em;
  color: var(--champagne-deep);
  font-size: 0.78rem;
  letter-spacing: 0.05em;
}

/* A four-star rating shows the fifth outline rather than dropping it, so the
   scale stays visible and the score can't be misread as out of four. */
.star-empty {
  opacity: 0.28;
}

.review-quote {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(0.94rem, 1.15vw, 1.02rem);
  font-style: italic;
  line-height: 1.6;
  color: var(--ink);
  /* Clamped to the space the card has. Reviews shorter than this are
     unaffected; longer ones are cut here and continue behind the toggle. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 5;
  line-clamp: 5;
  overflow: hidden;
}

.review-date {
  margin: auto 0 0;
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.55;
}

/* Only present on cards whose review actually overflows — main.js measures
   each one and adds the control where it's needed. */
.review-more {
  align-self: flex-start;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 0.62rem;
  font-weight: 400;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0.7;
  transition: border-color 0.35s ease, opacity 0.35s ease;
}

.review-more:hover,
.review-more:focus-visible {
  border-bottom-color: var(--ink);
  opacity: 1;
}

.review-more:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

/* Expanded, the card leaves the row's fixed height and grows to fit. The row
   is aligned to `start`, so a taller card grows downward and the others stay
   put rather than the whole row jumping. */
.review[data-expanded='true'] {
  height: auto;
}

.review[data-expanded='true'] .review-quote {
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

@media (max-width: 900px) {
  /* The flanking images stack above and below the heading, so the section
     still opens and closes on an image. */
  .reviews-top {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .reviews-image {
    height: clamp(160px, 22vh, 220px);
  }

  /* Sits directly above the wordmark with no gap between them, so the photo
     needs its own clearance rather than relying on .reviews-head's padding. */
  .reviews-image:first-child {
    margin-bottom: clamp(1.5rem, 4vh, 2.5rem);
  }

  .reviews-head {
    padding-top: clamp(2.5rem, 6vh, 3.5rem);
    padding-bottom: clamp(2.5rem, 6vh, 3.5rem);
  }
}

/* Without the scroll there's no seam to hide, so the duplicate set would just
   repeat every review. The cards fall back to a wrapped, self-sizing grid —
   the honest static form of the same content. */
@media (prefers-reduced-motion: reduce) {
  .marquee {
    -webkit-mask: none;
    mask: none;
    padding: 0 var(--gutter);
  }

  .marquee-track {
    animation: none;
    flex-wrap: wrap;
    width: auto;
    justify-content: center;
    padding-left: 0;
  }

  .marquee-track .review[data-duplicate='true'] {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   Social
   -------------------------------------------------------------------------- */

.social {
  background: var(--white);
  padding: 0 var(--gutter) clamp(4rem, 10vh, 6.5rem);
  text-align: center;
}

.social h2 {
  margin: 0 0 clamp(0.6rem, 1.5vh, 0.9rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.85rem, 3.6vw, 3.1rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  text-wrap: balance;
  color: var(--ink);
}

.social h2 em {
  font-style: italic;
  color: var(--ink-italic);
}

.social-handle {
  margin: 0 0 clamp(1.8rem, 4vh, 2.6rem);
  font-size: 0.94rem;
  color: var(--ink);
  opacity: 0.55;
}

.social-list {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(1rem, 2.5vw, 1.6rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Icons are pre-flattened to the champagne palette (their alpha shape
   recoloured to --champagne-deep), so they read as part of the site rather
   than as imported brand marks. */
.social-icon {
  display: grid;
  place-items: center;
  width: clamp(44px, 5vw, 52px);
  height: clamp(44px, 5vw, 52px);
  border-radius: 50%;
  background: var(--champagne-tint);
  transition: transform 0.35s ease;
}

.social-icon img {
  width: 62%;
  height: 62%;
  object-fit: contain;
  display: block;
}

.social-icon:hover,
.social-icon:focus-visible {
  transform: translateY(-2px);
}

.social-icon:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .social-icon {
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   Partner carousel
   -------------------------------------------------------------------------- */

.partners {
  width: 100%;
}

/* Hairline rules top and bottom bracket the strip */
.carousel {
  position: relative;
  overflow: hidden;
  padding: clamp(1.4rem, 3vw, 2.2rem) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}

/* Soften both ends so logos enter and leave rather than being cut off */
.carousel::before,
.carousel::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: clamp(48px, 9%, 140px);
  z-index: 2;
  pointer-events: none;
}

.carousel::before {
  left: 0;
  background: linear-gradient(to right, var(--white), transparent);
}

.carousel::after {
  right: 0;
  background: linear-gradient(to left, var(--white), transparent);
}

.track {
  display: flex;
  align-items: center;
  gap: clamp(2.2rem, 5.5vw, 4.5rem);
  width: max-content;
  margin: 0;
  padding: 0 0 0 clamp(2.2rem, 5.5vw, 4.5rem);
  list-style: none;
  animation: marquee 48s linear infinite;
}

/* Travels exactly half the track — the second set is a duplicate, so the
   restart is invisible */
@keyframes marquee {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/* Pausing lets someone actually reach a logo instead of chasing it */
.carousel:hover .track,
.carousel:focus-within .track {
  animation-play-state: paused;
}

.logo-item {
  flex: none;
  margin: 0;
}

.logo-item a {
  display: block;
}

.logo-item a:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 8px;
}

/* Each mark is flattened to one champagne colour by using its own shape as a
   mask; the full-colour artwork sits underneath and is revealed on hover. This
   treats a dark-tile logo and a thin line drawing with equal weight, which a
   filter chain cannot do. */
.sil {
  position: relative;
  display: block;
  height: clamp(26px, 2.9vw, 36px);
}

.sil .shape {
  position: absolute;
  inset: 0;
  background-color: var(--champagne-deep);
  -webkit-mask: var(--m) no-repeat center / contain;
  mask: var(--m) no-repeat center / contain;
  transition: opacity 0.4s ease;
}

.sil .real {
  display: block;
  height: 100%;
  width: auto;
  opacity: 0;
  transition: opacity 0.4s ease, transform 0.45s ease;
}

.logo-item a:hover .shape,
.logo-item a:focus-visible .shape {
  opacity: 0;
}

.logo-item a:hover .real,
.logo-item a:focus-visible .real {
  opacity: 1;
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .track {
    animation: none;
    flex-wrap: wrap;
    width: auto;
    justify-content: center;
    padding-left: 0;
    gap: clamp(1.5rem, 4vw, 3rem);
  }

  /* Without the scroll there's no seam to hide, so the duplicate set would
     just repeat every logo */
  .track .logo-item[aria-hidden='true'] {
    display: none;
  }

  .carousel {
    padding-left: var(--gutter);
    padding-right: var(--gutter);
  }
}

/* --------------------------------------------------------------------------
   Responsive — single column, image drops below the copy
   -------------------------------------------------------------------------- */

@media (max-width: 900px) {
  .hero {
    grid-template-columns: 1fr;
    height: auto;
    overflow: visible;
  }

  .panel-left {
    justify-content: flex-start;
    padding-top: calc(var(--bar-h) + clamp(1.5rem, 4vh, 2.5rem));
    padding-bottom: 2.5rem;
    border-right: none;
    overflow: visible;
  }

  .hero h1 {
    font-size: clamp(1.8rem, 6.4vw, 2.8rem);
  }

  .panel-right {
    min-height: clamp(280px, 52vh, 420px);
  }

  /* The bar itself no longer grows to hold the menu — the menu is a fixed
     off-canvas panel over the page, not part of the bar's own flow — so the
     toggle stays put on the right at a constant bar height whether the menu
     is open or not. */
  .masthead[data-scrolled='true'],
  .masthead {
    flex-wrap: nowrap;
  }

  /* Panel slides in from the left edge of the viewport, full height, above
     everything else on the page. Closed, it sits just off-screen rather
     than display: none, so the transform transition has something to
     animate between. A dark, frosted ground (rather than solid white) so
     the white logo and nav text sit legibly on it. */
  .nav {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 40;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1.1rem;
    width: min(80vw, 22rem);
    height: 100%;
    margin: 0;
    padding: calc(var(--bar-short) + 1rem) 1.75rem 2rem calc(1.75rem + 10px);
    background: rgba(20, 17, 13, 0.82);
    backdrop-filter: blur(18px) saturate(140%);
    -webkit-backdrop-filter: blur(18px) saturate(140%);
    box-shadow: 8px 0 32px rgba(46, 42, 36, 0.28);
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
  }

  .nav[data-open='true'] {
    transform: translateX(0);
  }

  /* The panel sits on its own dark ground regardless of scroll state or
     which page it's on, so its links always take light text rather than
     the ink treatment the closed bar uses once scrolled or on inner pages. */
  .nav a {
    color: var(--white);
    text-shadow: none;
    padding: 0.3rem 0;
    font-size: 0.78rem;
  }

  .nav a::after {
    background: var(--white);
  }

  .nav-panel-logo {
    display: block;
    width: clamp(120px, 32vw, 160px);
    aspect-ratio: 480 / 262;
    margin-bottom: 0.5rem;
    background-color: var(--white);
    -webkit-mask: url('../assets/img/logo.webp') no-repeat center / contain;
    mask: url('../assets/img/logo.webp') no-repeat center / contain;
    flex: none;
  }

  /* Dims and blocks the page behind the open panel; clicking it closes the
     menu same as Escape does. Kept in the DOM at all times (not
     display: none) so the opacity transition has a starting frame. */
  .nav-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgba(20, 17, 13, 0.4);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.35s ease, visibility 0.35s;
  }

  .nav-backdrop:not([hidden]) {
    opacity: 1;
    visibility: visible;
  }

  .nav-toggle {
    display: inline-flex;
    position: relative;
    z-index: 41;
  }

  /* Right side sits 5px past --gutter so the menu button isn't flush with
     the screen edge at phone widths; left side gets a further 5px on top
     of that (10px total) for the logo. */
  .masthead {
    padding-left: calc(var(--gutter) + 10px);
    padding-right: calc(var(--gutter) + 5px);
  }

  /* No hover on touch, so the dropdown can't be a reveal-on-hover panel —
     the chevron button gates it open/closed as an indented sub-list under
     its trigger instead. */
  .nav-item {
    width: 100%;
  }

  .nav-item > a,
  .nav-item > span {
    display: inline-block;
  }

  .nav-dropdown-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    margin-left: 0.25rem;
    padding: 0;
    background: none;
    border: none;
    color: var(--white);
    cursor: pointer;
    vertical-align: middle;
  }

  .nav-dropdown-toggle svg {
    width: 1rem;
    height: 1rem;
    transition: transform 0.25s ease;
  }

  .nav-dropdown-toggle[aria-expanded='true'] svg {
    transform: rotate(180deg);
  }

  .nav-dropdown {
    position: static;
    z-index: auto;
    min-width: 0;
    opacity: 1;
    visibility: visible;
    transform: none;
    border: none;
    box-shadow: none;
    background: none;
    max-height: 0;
    padding: 0 0 0 1.25rem;
    overflow: hidden;
    transition: max-height 0.3s ease;
  }

  .nav-dropdown-toggle[aria-expanded='true'] + .nav-dropdown {
    max-height: 20rem;
    padding-top: 0.4rem;
    padding-bottom: 0.3rem;
  }

  .nav-item::after {
    display: none;
  }

  .nav-dropdown a {
    padding: 0.4rem 0;
  }
}

/* --------------------------------------------------------------------------
   Inner pages — masthead without a hero behind it
   The homepage's masthead opens transparent because it sits over the hero
   photograph; an inner page has no photograph there, so it needs the solid,
   scrolled-looking bar from the very first frame. Flagged from the body
   rather than duplicating the masthead rules, and layered after the
   data-scrolled rule below it in the cascade so main.js's own toggle (which
   inner pages never leave at 'false' anyway) can't undo it.
   -------------------------------------------------------------------------- */

/* Inner pages don't share the homepage's champagne-to-tint story, so the
   overscroll gradient is flattened to the one ground every section here
   actually sits on. */
body.inner-page {
  background: var(--offwhite) fixed;
}

body.inner-page .masthead {
  background-color: var(--white);
  box-shadow: 0 1px 0 var(--rule);
}

/* The logo/nav fade-in above is gated behind the hero's own load sequence,
   which only runs on the homepage. An inner page has no hero to set
   data-ready, so without this the bar would stay invisible forever. */
body.inner-page .masthead .logo,
body.inner-page .masthead .nav {
  opacity: 1;
}

/* Only above the mobile breakpoint — below it the off-canvas panel keeps
   its own dark ground and white text on every page, inner or not. */
@media (min-width: 901px) {
  body.inner-page .masthead .nav a,
  body.inner-page .masthead .nav-item > span {
    color: var(--ink);
    text-shadow: none;
  }

  body.inner-page .masthead .nav a::after,
  body.inner-page .masthead .nav-item > span::after {
    background: var(--ink);
  }

  body.inner-page .masthead .nav {
    padding: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* --------------------------------------------------------------------------
   Stay With Us — hero
   -------------------------------------------------------------------------- */

/* Off-white ground carries the headline alone - no photo, so the copy has
   to hold the screen on its own. Same ground as .offer further down, so the
   page opens and resolves on one warm neutral. Filled to the viewport, less the sticky
   masthead, so the page opens on this screen and nothing peeks in below
   it. Inner pages never reach --bar-short (that only fires once the
   homepage hero has scrolled past), so --bar-tall is the one to clear. */
.stay-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 1.5rem;
  min-height: calc(100svh - var(--bar-tall));
  padding: clamp(2rem, 6vw, 4rem) var(--gutter);
  background: var(--offwhite);
}

.stay-hero-kicker {
  margin: 0;
  font-size: 0.75rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--champagne-deep);
}

.stay-hero h1 {
  margin: 0;
  max-width: 34ch;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.6rem, 7vw, 5.4rem);
  line-height: 1.02;
  text-wrap: balance;
}

.stay-hero h1 em {
  font-style: italic;
  color: var(--ink-italic);
}

.stay-hero-lede {
  margin: 0;
  max-width: 30rem;
  font-size: 1.05rem;
  line-height: 1.65;
  color: var(--ink-italic);
}

.stay-hero-actions {
  display: flex;
  gap: 0.85rem;
  flex-wrap: wrap;
  justify-content: center;
}

/* --------------------------------------------------------------------------
   About page
   -------------------------------------------------------------------------- */

/* "The Interior" — full-bleed photograph with a frosted glass panel of
   copy floating over it, as if reading it while standing in the room. */
.about-interior {
  position: relative;
  min-height: 640px;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.about-interior-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* Entrance — rise + Ken Burns. The photo opens slightly zoomed in and
   settles to rest over 1.6s while the panel fades and rises in shortly
   after — a slow, cinematic open that suits sitting first on the page.
   JS adds .is-in once the section is a third on screen (see main.js); the
   :not(.is-in) guard is meaningless without it, so both states collapse to
   the finished look with JS or the observer unavailable. */
.about-interior[data-animate] .about-interior-bg {
  transform: scale(1.08);
  transition: transform 1.6s cubic-bezier(0.16, 0.6, 0.3, 1);
}

.about-interior[data-animate].is-in .about-interior-bg {
  transform: scale(1);
}

.about-interior[data-animate] .about-interior-panel {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s cubic-bezier(0.16, 0.6, 0.3, 1),
              transform 0.9s cubic-bezier(0.16, 0.6, 0.3, 1);
  transition-delay: 0.15s;
}

.about-interior[data-animate].is-in .about-interior-panel {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .about-interior[data-animate] .about-interior-bg {
    transform: none;
    transition: none;
  }

  .about-interior[data-animate] .about-interior-panel {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.about-interior::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(115deg,
    rgba(20, 17, 12, 0.62) 0%,
    rgba(20, 17, 12, 0.34) 42%,
    rgba(20, 17, 12, 0.08) 62%,
    transparent 78%);
}

.about-interior-panel {
  position: relative;
  z-index: 2;
  margin: clamp(1.5rem, 4vw, 4rem) var(--gutter);
  padding: clamp(2.2rem, 4vw, 3.4rem);
  max-width: 34rem;
  background: rgba(250, 248, 244, 0.14);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  border: 1px solid rgba(255, 255, 255, 0.22);
}

.about-interior-kicker {
  margin: 0 0 1.2rem;
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--champagne);
}

.about-interior-panel h2 {
  margin: 0 0 1.2rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 2.7rem);
  line-height: 1.16;
  color: var(--white);
  text-wrap: balance;
}

.about-interior-panel h2 em {
  font-style: italic;
  color: var(--champagne-tint);
}

.about-interior-panel p {
  margin: 0 0 1.1rem;
  max-width: 44ch;
  font-size: 0.98rem;
  line-height: 1.8;
  color: rgba(255, 255, 255, 0.88);
}

.about-interior-panel p:last-of-type {
  margin-bottom: 0;
}

.about-interior-stats {
  display: flex;
  gap: clamp(1.6rem, 4vw, 2.6rem);
  margin: 1.8rem 0 0;
  padding-top: 1.6rem;
  border-top: 1px solid rgba(255, 255, 255, 0.28);
}

.about-interior-stat dt {
  margin: 0 0 0.3rem;
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.66);
}

.about-interior-stat dd {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.4vw, 1.9rem);
  color: var(--white);
}

@media (max-width: 700px) {
  .about-interior {
    min-height: 560px;
  }

  .about-interior-panel {
    margin: 1rem;
    padding: 1.6rem;
  }
}

/* Values grid — three short pillars, each photo framed with the same open
   corner brackets as .story-plate, staggered per card on scroll-in since
   this is a plain grid rather than a single hero-weight figure. */
.about-values {
  padding: clamp(3.5rem, 8vh, 6rem) var(--gutter);
  background: var(--white);
}

.about-values-inner {
  max-width: 1200px;
  margin: 0 auto;
}

/* Heading / copy / figure share a row across all three columns via subgrid,
   so a shorter blurb in one column can't float its photo out of line with
   the other two — every .about-value-figure lands on the same baseline
   regardless of how many lines its paragraph wraps to. The copy row is
   pinned to a fixed height (three lines, the tallest blurb) and the
   paragraph bottom-aligns within it, so the gap before the photo stays the
   same small size whether a blurb wraps to two lines or three. */
.about-values-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto calc(0.92rem * 1.6 * 3) auto;
  gap: clamp(1.6rem, 3vw, 2.2rem);
}

.about-value {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
  justify-items: center;
  text-align: center;
}

.about-value h3 {
  margin: 0 0 0.5rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 2vw, 1.7rem);
  letter-spacing: -0.01em;
  color: var(--ink);
}

.about-value p {
  align-self: end;
  margin: 0 0 1rem;
  max-width: 32ch;
  font-size: 0.92rem;
  line-height: 1.6;
  color: var(--ink);
  opacity: 0.86;
}

.about-value-figure {
  position: relative;
  align-self: end;
  width: 100%;
  aspect-ratio: 4 / 3;
}

.about-value-plate {
  position: absolute;
  inset: 0;
  margin: 0;
  background: var(--white);
  overflow: hidden;
}

/* Open corner brackets replace the old staggered outline back-plate:
   four viewfinder-style marks held just outside the photo's own edges
   (siblings of .about-value-plate, not nested inside its clipped bounds)
   rather than a full traced rectangle, so the frame reads as a light
   curatorial touch instead of a boxed-in photo. */
.about-value-plate-bracket {
  position: absolute;
  z-index: 3;
  width: 26px;
  height: 26px;
  pointer-events: none;
}

.about-value-plate-bracket--tl {
  top: -10px;
  left: -10px;
  border-top: 1px solid var(--ink);
  border-left: 1px solid var(--ink);
}

.about-value-plate-bracket--tr {
  top: -10px;
  right: -10px;
  border-top: 1px solid var(--ink);
  border-right: 1px solid var(--ink);
}

.about-value-plate-bracket--bl {
  bottom: -10px;
  left: -10px;
  border-bottom: 1px solid var(--ink);
  border-left: 1px solid var(--ink);
}

.about-value-plate-bracket--br {
  bottom: -10px;
  right: -10px;
  border-bottom: 1px solid var(--ink);
  border-right: 1px solid var(--ink);
}

.about-value-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Entrance — photo wipes open, then the corner brackets fade in just behind
   it, staggered per card left to right so the grid arrives as a sequence
   rather than at once. JS adds .is-in via the shared observer in main.js. */
.about-values[data-animate] .about-value-plate {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}

.about-values[data-animate] .about-value-plate-bracket {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.about-values[data-animate] .about-value h3,
.about-values[data-animate] .about-value p {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.about-values[data-animate].is-in .about-value:nth-child(1) .about-value-plate-bracket { transition-delay: 0.4s; }
.about-values[data-animate].is-in .about-value:nth-child(2) .about-value-plate-bracket { transition-delay: 0.57s; }
.about-values[data-animate].is-in .about-value:nth-child(3) .about-value-plate-bracket { transition-delay: 0.74s; }

.about-values[data-animate].is-in .about-value:nth-child(1) .about-value-plate { transition-delay: 0.15s; }
.about-values[data-animate].is-in .about-value:nth-child(2) .about-value-plate { transition-delay: 0.32s; }
.about-values[data-animate].is-in .about-value:nth-child(3) .about-value-plate { transition-delay: 0.49s; }

.about-values[data-animate].is-in .about-value:nth-child(1) h3,
.about-values[data-animate].is-in .about-value:nth-child(1) p { transition-delay: 0.02s; }
.about-values[data-animate].is-in .about-value:nth-child(2) h3,
.about-values[data-animate].is-in .about-value:nth-child(2) p { transition-delay: 0.18s; }
.about-values[data-animate].is-in .about-value:nth-child(3) h3,
.about-values[data-animate].is-in .about-value:nth-child(3) p { transition-delay: 0.34s; }

.about-values[data-animate].is-in .about-value-plate {
  clip-path: inset(0 0 0 0);
}

.about-values[data-animate].is-in .about-value-plate-bracket {
  opacity: 1;
}

.about-values[data-animate].is-in .about-value h3,
.about-values[data-animate].is-in .about-value p {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .about-values[data-animate] .about-value-plate {
    clip-path: none;
    transition: none;
  }

  .about-values[data-animate] .about-value-plate-bracket {
    opacity: 1;
    transition: none;
  }

  .about-values[data-animate] .about-value h3,
  .about-values[data-animate] .about-value p {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Vision — "The Plaque": restrained and centred, a thin rule above and
   below with a small diamond mark, like a dedication plaque rather than a
   web content block. One statement, no eyebrow label — it carries itself. */
.about-vision {
  padding: clamp(4.5rem, 10vh, 7.5rem) var(--gutter);
  background: var(--offwhite);
  display: flex;
  justify-content: center;
}

.about-vision-card {
  position: relative;
  max-width: 40rem;
  padding: clamp(2.8rem, 5vw, 4rem) clamp(1.5rem, 4vw, 3rem);
  border-top: 1px solid var(--ink);
  border-bottom: 1px solid var(--ink);
  text-align: center;
}

.about-vision-card::before,
.about-vision-card::after {
  content: '';
  position: absolute;
  left: 50%;
  width: 0.35rem;
  height: 0.35rem;
  background: var(--champagne-deep);
  transform: translateX(-50%) rotate(45deg);
}

.about-vision-card::before {
  top: -0.2rem;
}

.about-vision-card::after {
  bottom: -0.2rem;
}

.about-vision-quote {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.35rem, 2.2vw, 1.75rem);
  line-height: 1.55;
  color: var(--ink);
  opacity: 0.92;
  text-wrap: balance;
}

.about-vision-quote em {
  font-style: italic;
  color: var(--ink-italic);
}

/* Entrance — marks pop, rules extend, then text. The card's own top/bottom
   border is swapped for two independent rule elements so they can animate
   separately from the diamond marks; both land before the quote fades up.
   JS adds .is-in via the shared observer in main.js. */
.about-vision[data-animate] .about-vision-card {
  border-color: transparent;
}

.about-vision[data-animate] .about-vision-rule-top,
.about-vision[data-animate] .about-vision-rule-bottom {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--ink);
  transform: scaleX(0);
  transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.3, 1);
  transition-delay: 0.35s;
}

.about-vision[data-animate] .about-vision-rule-top {
  top: 0;
}

.about-vision[data-animate] .about-vision-rule-bottom {
  bottom: 0;
}

.about-vision[data-animate].is-in .about-vision-rule-top,
.about-vision[data-animate].is-in .about-vision-rule-bottom {
  transform: scaleX(1);
}

.about-vision[data-animate] .about-vision-card::before,
.about-vision[data-animate] .about-vision-card::after {
  transform: translateX(-50%) rotate(45deg) scale(0);
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.about-vision[data-animate].is-in .about-vision-card::before,
.about-vision[data-animate].is-in .about-vision-card::after {
  transform: translateX(-50%) rotate(45deg) scale(1);
}

.about-vision[data-animate] .about-vision-quote {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s ease, transform 0.6s ease;
  transition-delay: 0.15s;
}

.about-vision[data-animate].is-in .about-vision-quote {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .about-vision[data-animate] .about-vision-card {
    border-color: var(--ink);
  }

  .about-vision[data-animate] .about-vision-rule-top,
  .about-vision[data-animate] .about-vision-rule-bottom {
    display: none;
  }

  .about-vision[data-animate] .about-vision-card::before,
  .about-vision[data-animate] .about-vision-card::after {
    transform: translateX(-50%) rotate(45deg) scale(1);
    transition: none;
  }

  .about-vision[data-animate] .about-vision-quote {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.about-cta {
  padding: clamp(3rem, 7vh, 4.5rem) var(--gutter);
  background: var(--champagne-tint);
  text-align: center;
}

.about-cta h2 {
  margin: 0 0 0.8rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 2.8vw, 2.2rem);
  color: var(--ink);
}

.about-cta-lede {
  margin: 0 auto clamp(1.8rem, 4vh, 2.4rem);
  max-width: 46ch;
  font-size: 0.96rem;
  line-height: 1.7;
  color: var(--ink);
  opacity: 0.86;
}

.about-cta .story-cta {
  margin-top: 0;
}

/* Entrance — rise, then the button pops in with a slight overshoot. JS
   adds .is-in via the shared observer in main.js. */
.about-cta[data-animate] h2,
.about-cta[data-animate] .about-cta-lede {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.about-cta[data-animate].is-in h2 {
  opacity: 1;
  transform: none;
  transition-delay: 0.05s;
}

.about-cta[data-animate].is-in .about-cta-lede {
  opacity: 1;
  transform: none;
  transition-delay: 0.18s;
}

.about-cta[data-animate] .story-cta {
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 0.5s cubic-bezier(0.34, 1.56, 0.64, 1),
              transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  transition-delay: 0.4s;
}

.about-cta[data-animate].is-in .story-cta {
  opacity: 1;
  transform: scale(1);
}

@media (prefers-reduced-motion: reduce) {
  .about-cta[data-animate] h2,
  .about-cta[data-animate] .about-cta-lede,
  .about-cta[data-animate] .story-cta {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

@media (max-width: 760px) {
  .about-values-list {
    grid-template-columns: 1fr;
    gap: clamp(2rem, 6vh, 3rem);
  }
}

/* --------------------------------------------------------------------------
   Stay With Us page
   -------------------------------------------------------------------------- */

/* Copy and a single photo in the same offset back-plate as .story-plate,
   held to card scale rather than the hero-weight figure on the homepage. */
.stay-feature {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(2.5rem, 6vw, 5rem);
  max-width: 1200px;
  margin: 0 auto;
  padding: clamp(4rem, 9vh, 6.5rem) var(--gutter);
}

.stay-feature-copy h2 {
  margin: 0 0 1rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.2vw, 2.6rem);
  line-height: 1.16;
  letter-spacing: -0.012em;
  color: var(--ink);
}

.stay-feature-copy h2 em {
  font-style: italic;
  color: var(--ink-italic);
}

.stay-feature-lede {
  margin: 0 0 1.6rem;
  max-width: 42ch;
  font-size: 0.96rem;
  line-height: 1.7;
  color: var(--ink);
  opacity: 0.86;
}

.stay-feature-figure {
  position: relative;
  aspect-ratio: 4 / 3;
  --feature-back-shift-x: 20px;
  --feature-back-shift-y: 20px;
}

.stay-feature-plate-back {
  position: absolute;
  z-index: 1;
  inset: calc(-1 * var(--feature-back-shift-y)) calc(-1 * var(--feature-back-shift-x))
    var(--feature-back-shift-y) var(--feature-back-shift-x);
  border: 1px solid var(--ink);
}

.stay-feature-plate {
  position: absolute;
  z-index: 2;
  inset: 0;
  margin: 0;
  border: 1px solid var(--ink);
  background: var(--white);
  overflow: hidden;
}

.stay-feature-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media (max-width: 860px) {
  .stay-feature {
    grid-template-columns: 1fr;
  }

  .stay-feature-copy {
    order: 1;
    text-align: center;
  }

  .stay-feature-lede {
    margin-left: auto;
    margin-right: auto;
  }

  .stay-feature-figure {
    order: 0;
    margin: var(--feature-back-shift-y) var(--feature-back-shift-x) 0 0;
  }
}

/* One review at a time, plain text rather than a card — just the quote and
   who said it. The track holds every slide stacked in the same grid cell;
   JS cross-fades between them on a timer, with dots below as the only
   manual control. Without JS every slide sits in a plain stacked column, so
   the content is still there to read. */
.stay-reviews {
  padding: clamp(3.5rem, 8vh, 6rem) var(--gutter);
  background: var(--white);
  text-align: center;
}

.stay-reviews h2 {
  margin: 0 auto 0.6rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 3.2vw, 2.5rem);
  color: var(--ink);
}

.stay-reviews-lede {
  margin: 0 auto clamp(2.2rem, 5vh, 3rem);
  max-width: 40ch;
  font-size: 0.94rem;
  color: var(--ink);
  opacity: 0.8;
}

.stay-carousel {
  max-width: 640px;
  min-height: clamp(180px, 24vh, 220px);
  margin: 0 auto;
}

.stay-carousel-track {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Stacked by default so every review is present and readable without
   script. JS adds .is-active, which lands every slide in the same grid
   cell so they can cross-fade over one another. */
.stay-carousel.is-active .stay-carousel-track {
  display: block;
  position: relative;
}

.stay-carousel-slide {
  padding: 0 0 1.5rem;
}

.stay-carousel.is-active .stay-carousel-slide {
  grid-area: 1 / 1;
  position: absolute;
  inset: 0;
  padding: 0;
  opacity: 0;
  transition: opacity 0.9s ease;
  pointer-events: none;
}

.stay-carousel.is-active .stay-carousel-slide[data-active='true'] {
  opacity: 1;
  pointer-events: auto;
}

.stay-quote {
  margin: 0;
}

.stay-quote p {
  margin: 0 auto 1rem;
  max-width: 56ch;
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.8vw, 1.3rem);
  font-style: italic;
  line-height: 1.6;
  color: var(--ink);
}

.stay-quote cite {
  display: block;
  font-style: normal;
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.6;
}

.stay-carousel-dots {
  display: none;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  margin-top: clamp(1.6rem, 4vh, 2.2rem);
}

.stay-carousel.is-active ~ .stay-carousel-dots {
  display: flex;
}

/* Idle dots are small round blobs; the active one elongates into a pill
   (see [aria-selected='true'] below) so it has room to show a countdown -
   .stay-carousel-dot-fill fills that pill left-to-right, timed to match
   the carousel's own interval. The track gets its own visible outline
   (rather than just a tinted background) since champagne-on-tint is too
   close in value to read as a bar at this size otherwise. */
.stay-carousel-dot {
  position: relative;
  width: 0.55rem;
  height: 0.5rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--rule);
  cursor: pointer;
  overflow: hidden;
  transition: width 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}

.stay-carousel-dot[aria-selected='true'] {
  width: 3.6rem;
  background: var(--white);
  border-color: var(--champagne-deep);
}

.stay-carousel-dot-fill {
  position: absolute;
  inset: 1px;
  border-radius: inherit;
  transform-origin: left center;
  transform: scaleX(0);
  background: var(--champagne-deep);
}

.stay-carousel-dot.is-filling .stay-carousel-dot-fill {
  animation: stay-dot-countdown 4s linear;
}

@keyframes stay-dot-countdown {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@media (prefers-reduced-motion: reduce) {
  .stay-carousel-dot.is-filling .stay-carousel-dot-fill {
    animation: none;
    transform: scaleX(1);
  }
}

.stay-carousel-dot:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------------- */

.site-footer {
  background: var(--champagne);
  color: var(--ink);
}

.footer-inner {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: space-between;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
  padding: clamp(3rem, 7vh, 4.5rem) var(--gutter) clamp(2.5rem, 6vh, 3.5rem);
}

.footer-brand .logo-link {
  display: block;
}

/* Same masked logo as the header, recoloured to sit on champagne */
.footer-logo {
  background-color: var(--ink);
}

.footer-contact {
  text-align: left;
}

.footer-heading {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 400;
}

.footer-contact p {
  margin: 0 0 0.4rem;
  font-size: 0.85rem;
  line-height: 1.5;
}

.footer-contact a {
  color: var(--ink);
  text-decoration: none;
}

.footer-contact a:hover,
.footer-contact a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Same champagne-tinted discs as the Social section, scaled down to sit
   under the contact lines without outweighing them. The tint is lifted to
   white here because the footer itself is already champagne. */
.footer-social {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.9rem 0 0;
  padding: 0;
  list-style: none;
}

.footer-social-icon {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--white);
  transition: transform 0.35s ease;
}

.footer-social-icon img {
  width: 58%;
  height: 58%;
  object-fit: contain;
  display: block;
}

.footer-social-icon:hover,
.footer-social-icon:focus-visible {
  transform: translateY(-2px);
}

.footer-social-icon:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .footer-social-icon {
    transition: none;
  }
}

/* Three-column grid: studio credit left, copyright centred, legal links right.
   The outer columns are 1fr each so they stay equal, which keeps the copyright
   optically centred on the row no matter how wide the credit or links get. */
.footer-bottom {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 0.75rem;
  padding: 1.25rem var(--gutter);
  border-top: 1px solid rgba(46, 42, 36, 0.18);
}

.footer-copyright {
  margin: 0;
  font-size: 0.72rem;
  text-align: center;
  opacity: 0.7;
}

.footer-legal {
  display: flex;
  justify-content: flex-end;
  gap: 1.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-legal a {
  font-size: 0.72rem;
  color: var(--ink);
  text-decoration: none;
  opacity: 0.7;
}

.footer-legal a:hover,
.footer-legal a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
  opacity: 1;
}

/* Studio credit — Rizer's own footer tag, supplied as a self-contained block
   (rizer-studio-tag.html). Its styles live here rather than inline on every
   page: 50 pages carry this footer, so the shared sheet is the only place it
   belongs. Every class is namespaced .rz*, so nothing here collides with the
   site's own rules, and the vendor block below is kept as delivered — only the
   footer-grid placement is ours.

   The mark runs three beats on hover: [RIZER-STUDIO] -> [WANT SOMETHING
   SIMILAR?] -> [CLICK HERE ->]. js/rizer-tag.js measures each line and drives
   the timing. */
.rz {
  --rz-orange: #E2571F;
  --rz-cream: #F2EFE9;
  --rz-ink: #161513;
  --rz-swap: 0.34s;

  display: inline-flex;
  align-items: baseline;
  gap: 0.45em;

  font-family: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  font-weight: 500;
  font-size: 0.8125rem;
  line-height: 1.4;
  letter-spacing: 0.02em;

  /* inherits the host site's text colour until hovered */
  color: currentColor;
  opacity: 0.7;

  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;

  /* stable hitbox — grows the target without shifting the text */
  padding: 0.4em 0.55em;
  margin: -0.4em -0.55em;
  transition: opacity 0.28s ease;
}

.rz:hover,
.rz:focus-visible {
  opacity: 1;
  outline: none;
}

.rz:focus-visible .rz__mark {
  outline: 2px solid var(--rz-orange);
  outline-offset: 3px;
}

.rz__mark {
  display: inline-flex;
  align-items: baseline;
  font-weight: 700;
  letter-spacing: 0.055em;
}

.rz__br {
  display: inline-block;
  color: inherit;
  transition: color var(--rz-swap) ease;
}

.rz:hover .rz__br,
.rz:focus-visible .rz__br {
  color: var(--rz-orange);
}

/* tuck the closer so the gap stays even at any line length */
.rz__br--r {
  margin-left: -0.055em;
}

/* the slot resizes to whichever line is lit */
.rz__slot {
  position: relative;
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  width: var(--rz-w-1, auto);
  transition: width var(--rz-swap) cubic-bezier(0.65, 0, 0.35, 1);
}

.rz__line {
  display: block;
  transition: transform var(--rz-swap) cubic-bezier(0.65, 0, 0.35, 1),
              opacity calc(var(--rz-swap) * 0.6) ease;
}

.rz__line--2,
.rz__line--3 {
  position: absolute;
  inset: 0 auto 0 0;
  width: max-content;
}

.rz__line--1 { transform: translateY(0); opacity: 1; }
.rz__line--2 { transform: translateY(-105%); opacity: 0; color: var(--rz-cream); }
.rz__line--3 { transform: translateY(-105%); opacity: 0; color: var(--rz-cream); }

.rz[data-beat="2"] .rz__slot { width: var(--rz-w-2, auto); }
.rz[data-beat="2"] .rz__line--1 { transform: translateY(105%); opacity: 0; }
.rz[data-beat="2"] .rz__line--2 { transform: translateY(0); opacity: 1; }

.rz[data-beat="3"] .rz__slot { width: var(--rz-w-3, auto); }
.rz[data-beat="3"] .rz__line--1 { transform: translateY(105%); opacity: 0; }
.rz[data-beat="3"] .rz__line--2 { transform: translateY(105%); opacity: 0; }
.rz[data-beat="3"] .rz__line--3 { transform: translateY(0); opacity: 1; }

.rz__arrow {
  display: inline-block;
  margin-left: 0.3em;
  color: var(--rz-orange);
  transform: translateX(-0.22em);
  opacity: 0;
  transition: transform 0.3s cubic-bezier(0.2, 0.9, 0.25, 1) 0.06s,
              opacity 0.3s ease 0.06s;
}

.rz[data-beat="3"] .rz__arrow {
  transform: translateX(0);
  opacity: 1;
}

/* light backgrounds */
.rz--light .rz__line--2,
.rz--light .rz__line--3 {
  color: var(--rz-ink);
}

@media (prefers-reduced-motion: reduce) {
  .rz,
  .rz__slot,
  .rz__line,
  .rz__arrow {
    transition-duration: 0.01ms;
  }
}

/* Ours: the tag is the first cell of the bottom row's grid, at the left edge.
   The champagne footer is a light plate, so it takes rz--light in the markup
   and the revealed lines resolve to ink rather than cream. */
.footer-credit {
  justify-self: start;
  color: var(--ink);
}

@media (max-width: 720px) {
  .footer-inner {
    grid-template-columns: 1fr;
    gap: 2rem;
  }

  /* Three columns don't fit: collapse to one centred stack. */
  .footer-bottom {
    grid-template-columns: 1fr;
    justify-items: center;
  }

  /* Stacked, the credit reads last — after the legal links. */
  .footer-credit {
    order: 1;
    justify-self: center;
  }

  .footer-legal {
    justify-content: center;
  }
}

/* --------------------------------------------------------------------------
   Let With Us page
   -------------------------------------------------------------------------- */

/* Parallax opener — background-attachment: fixed rather than the
   .about-interior glass-panel-on-the-left treatment, so this page doesn't
   read as a re-skin of /stay-with-us/. Centred title and copy over an even
   scrim instead of a card. */
.let-hero {
  position: relative;
  min-height: 82svh;
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: url('../assets/img/stay-with-us.avif');
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

/* Fixed backgrounds are treated as their own compositing context on iOS
   Safari and effectively ignore background-attachment, silently falling
   back to normal scroll — harmless, since the section still reads fine
   without the effect there. */
.let-hero::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(20, 17, 12, 0.58);
}

.let-hero-inner {
  position: relative;
  z-index: 1;
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(3rem, 8vh, 5rem) var(--gutter);
  text-align: center;
}

.let-hero h1 {
  margin: 0 0 1.2rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 4.4vw, 3.2rem);
  line-height: 1.16;
  color: var(--white);
  text-wrap: balance;
}

.let-hero h1 em {
  font-style: italic;
  color: var(--champagne-tint);
}

.let-hero p {
  margin: 0 auto;
  max-width: 48ch;
  font-size: 0.98rem;
  line-height: 1.8;
  color: rgba(255, 255, 255, 0.88);
}

.let-hero p + p {
  margin-top: 1.1rem;
}

.let-hero-stats {
  display: flex;
  justify-content: center;
  gap: clamp(1.6rem, 4vw, 2.6rem);
  margin: 1.8rem 0 0;
  padding-top: 1.6rem;
  border-top: 1px solid rgba(255, 255, 255, 0.28);
}

.let-hero-stat dt {
  margin: 0 0 0.3rem;
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.66);
}

.let-hero-stat dd {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.4vw, 1.9rem);
  color: var(--white);
}

@media (max-width: 640px) {
  .let-hero {
    min-height: 72svh;
  }

  .let-hero-stats {
    gap: 1.5rem;
  }
}

/* Entrance — the headline wipes in left to right like a line being drawn
   under a term, then the lede and stat row fade up after. JS adds .is-in
   via the shared observer in main.js. */
.let-hero[data-animate] h1 {
  opacity: 0;
  clip-path: inset(0 100% 0 0);
  transition: opacity 1ms linear, clip-path 0.9s cubic-bezier(0.65, 0, 0.35, 1);
}

.let-hero[data-animate].is-in h1 {
  opacity: 1;
  clip-path: inset(0 0 0 0);
}

.let-hero[data-animate] p,
.let-hero[data-animate] .let-hero-stats {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.6s ease, transform 0.6s ease;
  transition-delay: 0.75s;
}

.let-hero[data-animate] .let-hero-stats {
  transition-delay: 0.85s;
}

.let-hero[data-animate].is-in p,
.let-hero[data-animate].is-in .let-hero-stats {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .let-hero[data-animate] h1 {
    opacity: 1;
    clip-path: inset(0 0 0 0);
    transition: none;
  }

  .let-hero[data-animate] p,
  .let-hero[data-animate] .let-hero-stats {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .let-hero,
  .let-statement {
    background-attachment: scroll;
  }
}

/* Dark, full-bleed statement — the same register as .locations, so a
   landlord reads it as a deliberate anchor rather than another pale
   section. No icons: three considerations set as large serif numerals,
   the kind of thing you'd find on a term sheet rather than a brochure. */
.let-terms {
  position: relative;
  z-index: 1;
  background: var(--ink);
  padding: clamp(4.5rem, 11vh, 8rem) var(--gutter);
}

.let-terms-head {
  max-width: 620px;
  margin: 0 auto clamp(3rem, 8vh, 5rem);
  text-align: center;
}

.let-terms-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 3.4vw, 2.7rem);
  line-height: 1.16;
  letter-spacing: -0.012em;
  color: var(--white);
  text-wrap: balance;
}

.let-terms-list {
  margin: 0 auto;
  padding: 0;
  max-width: 1080px;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2.5rem, 5vw, 3.5rem);
}

.let-term {
  position: relative;
  padding-top: clamp(1.6rem, 3.5vh, 2.2rem);
  border-top: 1px solid rgba(250, 248, 244, 0.22);
}

.let-term-index {
  display: block;
  margin: 0 0 clamp(1rem, 2.5vh, 1.5rem);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.2rem, 4vw, 3rem);
  line-height: 1;
  color: var(--champagne);
}

.let-term h3 {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.2rem, 1.7vw, 1.4rem);
  color: var(--white);
}

.let-term p {
  margin: 0;
  max-width: 30ch;
  font-size: 0.9rem;
  line-height: 1.7;
  color: var(--white);
  opacity: 0.72;
}

/* Entrance — each card's top rule draws left to right in turn, then its
   numeral, heading and copy fade up together. JS adds .is-in via the shared
   observer in main.js. The rest-state rule is swapped for a scaling ::before
   so it can animate independently of the card's own border. */
.let-terms[data-animate] .let-term {
  border-top-color: transparent;
}

.let-terms[data-animate] .let-term::before {
  content: '';
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--champagne);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.3, 1);
}

.let-terms[data-animate].is-in .let-term::before {
  transform: scaleX(1);
}

.let-terms[data-animate] .let-term-index,
.let-terms[data-animate] .let-term h3,
.let-terms[data-animate] .let-term p {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.5s ease, transform 0.5s ease;
  transition-delay: 0.3s;
}

.let-terms[data-animate].is-in .let-term-index,
.let-terms[data-animate].is-in .let-term h3,
.let-terms[data-animate].is-in .let-term p {
  opacity: 1;
  transform: none;
}

.let-terms[data-animate] .let-term:nth-child(2)::before {
  transition-delay: 0.18s;
}

.let-terms[data-animate] .let-term:nth-child(3)::before {
  transition-delay: 0.36s;
}

.let-terms[data-animate].is-in .let-term:nth-child(2) .let-term-index,
.let-terms[data-animate].is-in .let-term:nth-child(2) h3,
.let-terms[data-animate].is-in .let-term:nth-child(2) p {
  transition-delay: 0.48s;
}

.let-terms[data-animate].is-in .let-term:nth-child(3) .let-term-index,
.let-terms[data-animate].is-in .let-term:nth-child(3) h3,
.let-terms[data-animate].is-in .let-term:nth-child(3) p {
  transition-delay: 0.66s;
}

@media (prefers-reduced-motion: reduce) {
  .let-terms[data-animate] .let-term {
    border-top-color: rgba(250, 248, 244, 0.22);
  }

  .let-terms[data-animate] .let-term::before {
    display: none;
  }

  .let-terms[data-animate] .let-term-index,
  .let-terms[data-animate] .let-term h3,
  .let-terms[data-animate] .let-term p {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

@media (max-width: 760px) {
  .let-terms-list {
    grid-template-columns: 1fr;
    max-width: 420px;
    gap: 2rem;
  }
}

/* Single large statement over a full-bleed photo — deliberately not the
   plate-and-copy split used elsewhere on the site, so this page doesn't
   read as the same two components re-skinned. */
.let-statement {
  position: relative;
  min-height: 76svh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background-image: url('../assets/img/grange-table.avif');
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

.let-statement::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to top,
    rgba(24, 21, 17, 0.92) 0%,
    rgba(24, 21, 17, 0.72) 40%,
    rgba(24, 21, 17, 0.34) 68%,
    rgba(24, 21, 17, 0.12) 88%
  );
}

.let-statement-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  padding: clamp(3rem, 8vh, 5rem) var(--gutter);
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: end;
  max-width: 1280px;
  margin: 0 auto;
}

.let-statement h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.2vw, 3.4rem);
  line-height: 1.14;
  letter-spacing: -0.015em;
  color: var(--white);
  text-wrap: balance;
  text-shadow: 0 2px 18px rgba(15, 13, 10, 0.4);
}

.let-statement h2 em {
  font-style: italic;
  color: var(--champagne);
}

.let-statement-copy {
  margin: 0 0 clamp(1.4rem, 3vh, 1.8rem);
  max-width: 42ch;
  font-size: 0.96rem;
  line-height: 1.75;
  color: var(--white);
  opacity: 0.92;
  text-shadow: 0 1px 12px rgba(15, 13, 10, 0.5);
}

.let-statement-copy:last-of-type {
  margin-bottom: 0;
}

/* .story-cta assumes ink-on-light; this section sits over a photo, so the
   button needs to be legible in white instead. */
.let-statement .story-cta {
  border-color: var(--white);
  color: var(--white);
}

.let-statement .story-cta:hover,
.let-statement .story-cta:focus-visible {
  background-color: var(--white);
  color: var(--ink);
}

.let-statement .story-cta:focus-visible {
  outline-color: var(--white);
}

/* Entrance — heading, copy and CTA rise from below in sequence, as though
   surfacing with the fixed background. JS adds .is-in via the shared
   observer in main.js. */
.let-statement[data-animate] h2 {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s cubic-bezier(0.22, 0.68, 0.35, 1), transform 0.7s cubic-bezier(0.22, 0.68, 0.35, 1);
}

.let-statement[data-animate].is-in h2 {
  opacity: 1;
  transform: none;
}

.let-statement[data-animate] .let-statement-copy {
  transform: translateY(22px);
  transition: opacity 0.7s cubic-bezier(0.22, 0.68, 0.35, 1), transform 0.7s cubic-bezier(0.22, 0.68, 0.35, 1);
  transition-delay: 0.12s;
}

.let-statement[data-animate] .let-statement-copy {
  opacity: 0;
}

.let-statement[data-animate].is-in .let-statement-copy {
  opacity: 0.92;
  transform: none;
}

.let-statement[data-animate] .story-cta {
  opacity: 0;
  transform: translateY(22px);
  transition: opacity 0.7s cubic-bezier(0.22, 0.68, 0.35, 1), transform 0.7s cubic-bezier(0.22, 0.68, 0.35, 1);
  transition-delay: 0.24s;
}

.let-statement[data-animate].is-in .story-cta {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .let-statement[data-animate] h2,
  .let-statement[data-animate] .story-cta {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .let-statement[data-animate] .let-statement-copy {
    opacity: 0.92;
    transform: none;
    transition: none;
  }
}

@media (max-width: 860px) {
  .let-statement-inner {
    grid-template-columns: 1fr;
  }
}

/* Ledger — three lines read like a statement, not a card grid. Rule lines
   stand in for the photography used everywhere else on the site, which
   keeps this page from feeling like the same triptych re-run a third time. */
.let-ledger {
  padding: clamp(4rem, 9vh, 6.5rem) var(--gutter);
  background: var(--ink);
}

.let-ledger-inner {
  max-width: 880px;
  margin: 0 auto;
}

.let-ledger-head {
  max-width: 52ch;
  margin: 0 0 clamp(2.5rem, 6vh, 3.5rem);
}

.let-ledger-head h2 {
  margin: 0 0 0.9rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.8rem, 3vw, 2.4rem);
  line-height: 1.18;
  letter-spacing: -0.012em;
  color: var(--white);
}

.let-ledger-lede {
  margin: 0;
  font-size: 0.96rem;
  line-height: 1.7;
  color: var(--white);
  opacity: 0.75;
}

.let-ledger-list {
  margin: 0;
  padding: clamp(1.6rem, 4vh, 2.2rem) 0 0;
  border-top: 1px solid rgba(250, 248, 244, 0.22);
  list-style: none;
}

.let-ledger-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: clamp(1rem, 4vw, 3rem);
  padding: clamp(1.6rem, 4vh, 2.2rem) 0;
  border-bottom: 1px solid rgba(250, 248, 244, 0.22);
}

.let-ledger-row:first-child {
  padding-top: 0;
}

.let-ledger-row h3 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.3rem, 1.9vw, 1.6rem);
  color: var(--champagne);
}

.let-ledger-row p {
  margin: 0;
  max-width: 52ch;
  font-size: 0.92rem;
  line-height: 1.75;
  color: var(--white);
  opacity: 0.78;
}

/* Entrance — each row fades up in turn, top to bottom, matching reading
   order. JS adds .is-in via the shared observer in main.js. */
.let-ledger[data-animate] .let-ledger-row {
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.let-ledger[data-animate] .let-ledger-row h3 {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.let-ledger[data-animate] .let-ledger-row p {
  opacity: 0;
  transition: opacity 0.5s ease;
}

.let-ledger[data-animate].is-in .let-ledger-row {
  transform: none;
}

.let-ledger[data-animate].is-in .let-ledger-row h3 {
  opacity: 1;
}

.let-ledger[data-animate].is-in .let-ledger-row p {
  opacity: 0.78;
}

.let-ledger[data-animate] .let-ledger-row:nth-child(1) { transition-delay: 0s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(2) { transition-delay: 0.1s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(3) { transition-delay: 0.2s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(4) { transition-delay: 0.3s; }

.let-ledger[data-animate] .let-ledger-row:nth-child(1) h3,
.let-ledger[data-animate] .let-ledger-row:nth-child(1) p { transition-delay: 0s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(2) h3,
.let-ledger[data-animate] .let-ledger-row:nth-child(2) p { transition-delay: 0.1s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(3) h3,
.let-ledger[data-animate] .let-ledger-row:nth-child(3) p { transition-delay: 0.2s; }
.let-ledger[data-animate] .let-ledger-row:nth-child(4) h3,
.let-ledger[data-animate] .let-ledger-row:nth-child(4) p { transition-delay: 0.3s; }

@media (prefers-reduced-motion: reduce) {
  .let-ledger[data-animate] .let-ledger-row {
    transform: none;
    transition: none;
  }

  .let-ledger[data-animate] .let-ledger-row h3 {
    opacity: 1;
    transition: none;
  }

  .let-ledger[data-animate] .let-ledger-row p {
    opacity: 0.78;
    transition: none;
  }
}

@media (max-width: 640px) {
  .let-ledger-row {
    grid-template-columns: 1fr;
    gap: 0.5rem;
  }
}

/* Dark variant of .about-cta, scoped to this page so the shared class stays
   untouched for /about/. */
.let-cta {
  position: relative;
  padding: clamp(3rem, 7vh, 4.5rem) var(--gutter);
  background: var(--ink);
  text-align: center;
}

.let-cta h2 {
  margin: 0 0 0.8rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 2.8vw, 2.2rem);
  color: var(--white);
}

.let-cta-lede {
  margin: 0 auto clamp(1.8rem, 4vh, 2.4rem);
  max-width: 46ch;
  font-size: 0.96rem;
  line-height: 1.7;
  color: var(--white);
  opacity: 0.78;
}

.let-cta .story-cta {
  margin-top: 0;
  border-color: var(--white);
  color: var(--white);
}

.let-cta .story-cta:hover,
.let-cta .story-cta:focus-visible {
  background-color: var(--white);
  color: var(--ink);
}

.let-cta .story-cta:focus-visible {
  outline-color: var(--white);
}

/* Entrance — a champagne rule expands outward from the centre, like a
   curtain opening down the middle, then the heading, lede and button fade
   in after it settles. JS adds .is-in via the shared observer in main.js.
   Sits in normal flow between the heading and lede (rather than absolutely
   centred on the section) so it can't drift into wrapped lede text when
   the copy runs to a second line. */
.let-cta-divider {
  display: block;
  width: 0;
  height: 1px;
  margin: 1.1rem auto;
  background: var(--champagne);
  transition: width 0.5s ease;
}

.let-cta[data-animate].is-in .let-cta-divider {
  width: min(70%, 220px);
}

.let-cta[data-animate] h2,
.let-cta[data-animate] .let-cta-lede,
.let-cta[data-animate] .story-cta {
  opacity: 0;
  transition: opacity 0.4s ease;
}

.let-cta[data-animate] h2 { transition-delay: 0.35s; }
.let-cta[data-animate] .let-cta-lede { transition-delay: 0.48s; }
.let-cta[data-animate] .story-cta { transition-delay: 0.6s; }

.let-cta[data-animate].is-in h2 {
  opacity: 1;
}

.let-cta[data-animate].is-in .let-cta-lede {
  opacity: 0.78;
}

.let-cta[data-animate].is-in .story-cta {
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .let-cta-divider {
    display: none;
  }

  .let-cta[data-animate] h2,
  .let-cta[data-animate] .story-cta {
    opacity: 1;
    transition: none;
  }

  .let-cta[data-animate] .let-cta-lede {
    opacity: 0.78;
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   Contact page
   -------------------------------------------------------------------------- */

.contact-hero {
  padding: clamp(4.5rem, 11vh, 7rem) var(--gutter) clamp(2.5rem, 6vh, 3.5rem);
  background: var(--offwhite);
  text-align: center;
}

.contact-hero-inner {
  max-width: 640px;
  margin: 0 auto;
}

.contact-hero-kicker {
  margin: 0 0 0.8rem;
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--champagne-deep);
}

.contact-hero h1 {
  margin: 0 0 1.2rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 4.4vw, 3.2rem);
  line-height: 1.16;
  color: var(--ink);
  text-wrap: balance;
}

.contact-hero h1 em {
  font-style: italic;
  color: var(--champagne-deep);
}

.contact-hero p:not(.contact-hero-kicker) {
  margin: 0 auto;
  max-width: 52ch;
  font-size: 0.98rem;
  line-height: 1.8;
  color: var(--ink-italic);
  opacity: 0.85;
}

/* Two columns: the left is left empty for the contact form plugin to
   mount into, and the right holds the details a guest or landlord would
   otherwise dig through the footer for — surfaced properly on the one
   page built for reaching out. */
.contact-layout {
  display: grid;
  grid-template-columns: 1.15fr 0.85fr;
  gap: clamp(2rem, 5vw, 4rem);
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 var(--gutter) clamp(4.5rem, 11vh, 7rem);
}

/* Mount point for the contact form plugin. */
.contact-form-panel {
  min-height: 100%;
}

.contact-details-panel {
  padding: clamp(2rem, 4vw, 2.75rem);
  background: var(--ink);
  border-radius: 4px;
  color: var(--white);
}

.contact-details-panel h2 {
  margin: 0 0 1.6rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.4rem;
  color: var(--white);
}

.contact-details-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.contact-detail {
  display: flex;
  align-items: flex-start;
  gap: 0.9rem;
}

.contact-detail-icon {
  flex: none;
  width: 1.4rem;
  height: 1.4rem;
  color: var(--champagne);
}

.contact-detail-icon svg {
  width: 100%;
  height: 100%;
}

.contact-detail-label {
  margin: 0 0 0.2rem;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}

.contact-detail-value {
  margin: 0;
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--white);
}

.contact-detail-value a {
  color: var(--white);
  text-decoration: none;
}

.contact-detail-value a:hover,
.contact-detail-value a:focus-visible {
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

.contact-social {
  margin-top: clamp(2rem, 4vh, 2.5rem);
  padding-top: clamp(1.5rem, 3vh, 2rem);
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}

.contact-social-list {
  margin: 0.6rem 0 0;
  padding: 0;
  justify-content: flex-start;
  gap: 0.9rem;
}

@media (max-width: 860px) {
  .contact-layout {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   Property listing pages — /properties/ and the seven region pages

   The card below is a deliberate exception to this stylesheet's usual
   register (hairline borders, square buttons, quiet uppercase labels): it's
   a faithful rebuild of the card already live on lussolets.com today, kept
   pixel-for-pixel so the two sites read as the same product mid-migration.
   Tokens are still pulled from :root (--champagne, --font-display, etc.)
   rather than hand-rolled hex, so it stays in step if the palette moves.
   -------------------------------------------------------------------------- */

/* Full-screen intro, above the grid -- same convention as the homepage
   .hero: 100svh with a negative top margin equal to the sticky bar's
   height, so the transparent masthead overlaps the top of the section
   instead of pushing it down. Two variants share this base:
     --photo     full-bleed hero image, centered copy over a scrim.
                 Used once, on properties.html, where a single strong
                 image can stand for the whole collection.
     --masthead  no image -- a left-aligned editorial title between two
                 hairline rules, with the region name and listing count
                 set like a print masthead. Used on the seven region
                 pages, where a single photo would misrepresent pages
                 that only hold one or two listings (Birmingham,
                 Stratford). */
.property-hero {
  position: relative;
  height: 100svh;
  margin-top: calc(-1 * var(--bar-h));
}

/* ---- --photo ---- */
.property-hero--photo {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--ink);
}

/* The real photo, once chosen -- full-bleed behind the scrim and copy. */
.property-hero-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* No photo chosen yet -- same placeholder convention as
   .property-card-img-placeholder. data-placeholder-for carries the
   final asset path; swap this <div> for a .property-hero-media <img>
   using that path and the hero needs no other change. */
.property-hero-media-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    repeating-linear-gradient(
      135deg,
      rgba(244, 239, 227, 0.04) 0 10px,
      rgba(244, 239, 227, 0.08) 10px 20px
    ),
    var(--ink);
  border: 1.5px dashed var(--champagne);
}

.property-hero-media-placeholder span {
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--champagne);
  opacity: 0.85;
}

/* A radial wash centred where the copy actually sits (the section is
   centre-aligned), on top of the same top/bottom gradient as before --
   the previous version faded almost to nothing across the middle band,
   which is exactly where the title and lede land, so contrast depended
   entirely on how light the photo happened to be there. */
.property-hero--photo .property-hero-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      ellipse at center,
      rgba(15, 13, 9, 0.55) 0%,
      rgba(15, 13, 9, 0.32) 45%,
      transparent 75%
    ),
    linear-gradient(
      180deg,
      rgba(20, 17, 12, 0.42) 0%,
      rgba(20, 17, 12, 0.2) 38%,
      rgba(20, 17, 12, 0.5) 100%
    );
}

.property-hero--photo .property-hero-inner {
  position: relative;
  z-index: 1;
  max-width: 60ch;
  text-align: center;
  padding: var(--gutter);
  color: var(--white);
}

.property-hero--photo .property-listing-kicker {
  color: var(--white);
  opacity: 0.85;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);
}

.property-hero--photo .property-hero-inner h1 {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.25rem, 5vw, 3.75rem);
  line-height: 1.1;
  letter-spacing: -0.01em;
  color: var(--white);
  text-wrap: balance;
  text-shadow: 0 2px 20px rgba(0, 0, 0, 0.4);
}

.property-hero--photo .property-hero-divider {
  width: 2.75rem;
  margin: 0 auto 1.25rem;
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.55);
}

.property-hero--photo .property-hero-inner p {
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.35);
}

.property-hero--photo .property-hero-scroll-cue {
  position: absolute;
  z-index: 1;
  left: 50%;
  bottom: clamp(1.5rem, 4vh, 2.75rem);
  transform: translateX(-50%);
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.8);
}

.property-hero-scroll-line {
  width: 1px;
  height: 1.5rem;
  background: rgba(255, 255, 255, 0.6);
}

/* Curtain-rise load sequence, on properties.html's photo hero only --
   same keyframes as the homepage .hero (hero-rise/hero-draw/hero-lift/
   hero-drift), gated behind its own [data-ready] rather than reusing
   .hero's, since this is a different element entirely. main.js sets the
   attribute a frame after load, mirroring the homepage's hero exactly;
   see "Property hero -- load sequence" in main.js. */
.property-hero--photo:not([data-ready='true']) .property-hero-media-placeholder,
.property-hero--photo:not([data-ready='true']) .property-hero-media {
  transform: scale(1.15);
}

.property-hero--photo:not([data-ready='true']) .property-listing-kicker,
.property-hero--photo:not([data-ready='true']) .property-hero-inner h1 {
  opacity: 0;
  transform: translateY(14px);
}

.property-hero--photo:not([data-ready='true']) .property-hero-divider {
  transform: scaleX(0);
}

.property-hero--photo:not([data-ready='true']) .property-hero-inner p {
  opacity: 0;
  transform: translateY(10px);
}

/* Horizontally centred via left:50% + translateX(-50%) in the base rule,
   so the lift-in here can't just animate to transform:none like the other
   elements do -- that would silently drop the translateX and leave the
   cue snapped to its left edge instead of centred. Kept as its own
   keyframe rather than reusing hero-lift-full for that reason. */
.property-hero--photo:not([data-ready='true']) .property-hero-scroll-cue {
  opacity: 0;
  transform: translateX(-50%) translateY(10px);
}

.property-hero--photo[data-ready='true'] .property-hero-media-placeholder,
.property-hero--photo[data-ready='true'] .property-hero-media {
  animation: hero-drift 1.9s cubic-bezier(0.16, 0.84, 0.3, 1) 0.15s both;
}

.property-hero--photo[data-ready='true'] .property-listing-kicker {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.15s both;
}

.property-hero--photo[data-ready='true'] .property-hero-inner h1 {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.32s both;
}

.property-hero--photo[data-ready='true'] .property-hero-divider {
  animation: hero-draw 0.7s cubic-bezier(0.65, 0, 0.35, 1) 0.55s both;
}

.property-hero--photo[data-ready='true'] .property-hero-inner p {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.7s both;
}

.property-hero--photo[data-ready='true'] .property-hero-scroll-cue {
  animation: scroll-cue-lift 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.88s both;
}

/* Same translateY lift as hero-lift-full, but keeps the translateX(-50%)
   this element needs to stay centred (see the base rule) -- hero-lift-full
   ends on transform:none, which would silently drop that and leave the
   cue snapped to its left edge. */
@keyframes scroll-cue-lift {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%); }
}

@media (prefers-reduced-motion: reduce) {
  .property-hero--photo:not([data-ready='true']) .property-hero-media-placeholder,
  .property-hero--photo:not([data-ready='true']) .property-hero-media,
  .property-hero--photo:not([data-ready='true']) .property-listing-kicker,
  .property-hero--photo:not([data-ready='true']) .property-hero-inner h1,
  .property-hero--photo:not([data-ready='true']) .property-hero-divider,
  .property-hero--photo:not([data-ready='true']) .property-hero-inner p {
    opacity: 1;
    transform: none;
  }

  .property-hero--photo:not([data-ready='true']) .property-hero-scroll-cue {
    opacity: 1;
    transform: translateX(-50%);
  }
}

/* ---- --masthead ---- */
.property-hero--masthead {
  display: flex;
  align-items: flex-end;
  background: var(--white);
  padding: 0 var(--gutter) clamp(2.5rem, 6vh, 4rem);
}

.property-hero--masthead::before {
  content: '';
  position: absolute;
  top: calc(var(--bar-h) + clamp(0.75rem, 2vh, 1.25rem));
  left: var(--gutter);
  right: var(--gutter);
  border-top: 1px solid var(--rule);
}

.property-hero-kicker {
  position: absolute;
  top: calc(var(--bar-h) + clamp(1.1rem, 2.6vh, 1.7rem));
  left: var(--gutter);
  margin: 0;
  font-size: 0.68rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--champagne-deep);
}

.property-hero-count {
  position: absolute;
  top: calc(var(--bar-h) + clamp(1.1rem, 2.6vh, 1.7rem));
  right: var(--gutter);
  margin: 0;
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--ink-italic);
  font-variant-numeric: tabular-nums;
}

/* Wide enough that the longest real heading -- "Stays in Broadway & the
   Cotswolds." at 34 characters -- sits on one line rather than wrapping
   a single region name onto its own line, where it could be wider than
   the box at this font size and run past the edge (seen on Worcestershire
   and Herefordshire) rather than breaking cleanly. */
.property-hero--masthead .property-hero-inner {
  max-width: 50ch;
}

.property-hero--masthead .property-hero-inner h1 {
  margin: 0 0 0.9rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.5rem, 5.4vw, 4rem);
  line-height: 1.05;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-wrap: balance;
}

.property-hero--masthead .property-hero-inner p {
  margin: 0;
  max-width: 40ch;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--ink-italic);
}

.property-hero--masthead .property-hero-scroll-cue {
  margin: 1.75rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.65rem;
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-italic);
}

.property-hero--masthead .property-hero-scroll-line {
  width: 1.5rem;
  height: 1px;
  background: var(--rule);
}

/* Type-in load sequence, on the seven region pages' masthead hero.
   Kicker/count slide in from the outer edges toward the rule, the rule
   itself fades in, then the headline reveals left-to-right via clip-path
   (a wipe, not a rise -- reads as a masthead being set rather than
   echoing the collection page's curtain-rise), then lede and scroll cue
   fade in last. Gated behind its own [data-ready], set by main.js the
   same way as .hero and .property-hero--photo -- see "Region hero -- the
   load sequence" in main.js. */
.property-hero--masthead:not([data-ready='true'])::before {
  opacity: 0;
}

.property-hero--masthead:not([data-ready='true']) .property-hero-kicker {
  opacity: 0;
  transform: translateX(-6px);
}

.property-hero--masthead:not([data-ready='true']) .property-hero-count {
  opacity: 0;
  transform: translateX(6px);
}

.property-hero--masthead:not([data-ready='true']) .property-hero-inner h1 {
  clip-path: inset(0 100% 0 0);
}

.property-hero--masthead:not([data-ready='true']) .property-hero-inner p,
.property-hero--masthead:not([data-ready='true']) .property-hero-scroll-cue {
  opacity: 0;
}

.property-hero--masthead[data-ready='true']::before {
  animation: hero-fade 0.5s ease both;
}

.property-hero--masthead[data-ready='true'] .property-hero-kicker {
  animation: masthead-slide-in-left 0.5s ease 0.15s both;
}

.property-hero--masthead[data-ready='true'] .property-hero-count {
  animation: masthead-slide-in-right 0.5s ease 0.15s both;
}

.property-hero--masthead[data-ready='true'] .property-hero-inner h1 {
  animation: masthead-type-in 0.9s cubic-bezier(0.65, 0, 0.35, 1) 0.3s both;
}

.property-hero--masthead[data-ready='true'] .property-hero-inner p {
  animation: hero-fade 0.6s ease 0.85s both;
}

.property-hero--masthead[data-ready='true'] .property-hero-scroll-cue {
  animation: hero-fade 0.6s ease 1s both;
}

@keyframes masthead-slide-in-left {
  from { opacity: 0; transform: translateX(-6px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes masthead-slide-in-right {
  from { opacity: 0; transform: translateX(6px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes masthead-type-in {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

@media (prefers-reduced-motion: reduce) {
  .property-hero--masthead:not([data-ready='true'])::before,
  .property-hero--masthead:not([data-ready='true']) .property-hero-kicker,
  .property-hero--masthead:not([data-ready='true']) .property-hero-count,
  .property-hero--masthead:not([data-ready='true']) .property-hero-inner p,
  .property-hero--masthead:not([data-ready='true']) .property-hero-scroll-cue {
    opacity: 1;
    transform: none;
  }

  .property-hero--masthead:not([data-ready='true']) .property-hero-inner h1 {
    clip-path: none;
  }
}

/* --------------------------------------------------------------------------
   Region hero, contents-page variant -- shropshire.html only for now

   Sits on plain white ground, like the masthead-hero pages, rather than
   bleeding a photo under the nav -- so it needs none of the transparent-bar
   machinery the earlier photo-bleed version required.
   -------------------------------------------------------------------------- */

.region-hero {
  position: relative;
  height: 100svh;
  margin-top: calc(-1 * var(--bar-h));
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  align-items: end;
  gap: clamp(2rem, 5vw, 4rem);
  background: var(--white);
  padding: 0 var(--gutter) clamp(2.5rem, 6vh, 4rem);
  overflow-x: clip;
}

/* Same positioning as .property-hero--masthead::before: measured down from
   the section's own top edge, which the negative margin above has already
   pulled up to the true viewport top, so this lands just under the bar
   regardless of its tall/short state. */
.region-hero::before {
  content: '';
  position: absolute;
  top: calc(var(--bar-h) + clamp(0.75rem, 2vh, 1.25rem));
  left: var(--gutter);
  right: var(--gutter);
  border-top: 1px solid var(--rule);
}

.region-hero-text {
  position: relative;
}

.region-hero-text .property-hero-kicker {
  position: static;
  margin: 0 0 clamp(1.4rem, 3vh, 2rem);
}

.region-hero-text .property-hero-inner {
  max-width: none;
  margin: 0 0 clamp(1.5rem, 3.5vh, 2.2rem);
}

/* Same type scale as .hero h1 -- both sit in a similarly proportioned
   column, so the homepage's own tuned clamp is the right reference rather
   than the full-width masthead hero's larger one. */
.region-hero-text .property-hero-inner h1 {
  margin: 0 0 0.9rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 2.6vw + 1vh, 3.4rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  color: var(--ink);
  text-wrap: balance;
}

.region-hero-text .property-hero-inner p {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--ink);
  opacity: 0.86;
  max-width: 38ch;
}

.region-hero-text .property-hero-scroll-cue {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-italic);
}

/* The shared .property-hero-scroll-line is built for the photo hero's
   vertical, white "scroll down" cue (1px wide, 1.5rem tall) -- wrong shape
   and colour for this horizontal, on-white layout. Same override the
   masthead-hero pages already use for their own horizontal scroll-cue. */
.region-hero-text .property-hero-scroll-line {
  width: 1.5rem;
  height: 1px;
  background: var(--rule);
}

/* The figure's paper-mount treatment -- photo inset in a thick off-white
   card with a captioned margin underneath, like a print in an album,
   rather than the site's usual stacked-hairline backplate. */
.region-hero-figure {
  position: relative;
  margin: 0;
  height: clamp(360px, 62vh, 610px);
  display: flex;
  flex-direction: column;
  background: var(--white);
  border: 1px solid var(--rule);
  box-shadow: 0 14px 32px -14px rgba(46, 42, 36, 0.28);
  padding: 14px 14px 0;
}

.region-hero-figure img {
  flex: 1;
  min-height: 0;
  width: 100%;
  object-fit: cover;
  display: block;
}

.region-hero-figure-caption {
  flex-shrink: 0;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 0.9rem;
  color: var(--ink-italic);
}

/* Photo not chosen yet for this region -- same placeholder convention as
   .property-card-img-placeholder. data-placeholder-for carries the final
   asset path; swap this <div> for a .region-hero-figure img using that
   path and the frame needs no other change. */
.region-hero-figure-placeholder {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px dashed var(--champagne-deep);
  background:
    repeating-linear-gradient(
      135deg,
      rgba(46, 42, 36, 0.05) 0 10px,
      rgba(46, 42, 36, 0.09) 10px 20px
    ),
    var(--offwhite);
}

.region-hero-figure-placeholder span {
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--champagne-deep);
  opacity: 0.85;
  text-align: center;
  padding: 0 1rem;
}

@media (max-width: 900px) {
  /* No min-height here -- with the figure hidden below, forcing 100svh
     left a large empty gap between the text and the property grid. */
  .region-hero {
    grid-template-columns: 1fr;
    height: auto;
    padding-top: calc(var(--bar-h) + clamp(1.5rem, 4vh, 2.5rem));
    padding-bottom: clamp(2rem, 6vh, 3rem);
  }

  /* Text-only on mobile -- the image stays hidden, just given more room
     to breathe below the masthead than the desktop layout needs. */
  .region-hero-text {
    padding-top: clamp(1.5rem, 4vh, 2.5rem);
  }

  .region-hero-figure {
    display: none;
  }
}

.property-listing {
  padding: clamp(3rem, 7vh, 5rem) var(--gutter) clamp(4rem, 9vh, 6rem);
  background: var(--offwhite);
}

.property-grid {
  max-width: 1332px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(1.5rem, 3vw, 2.25rem);
}

@media (max-width: 980px) {
  .property-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 640px) {
  .property-grid {
    grid-template-columns: 1fr;
  }
}

/* Staggered rise, on scroll into view. Targets the <li> -- the whole card
   slot, not .property-card itself -- so this keeps working unchanged if
   the card design inside it is ever swapped out. Trigger: .property-listing
   (the section wrapping the grid) carries [data-animate], picked up by its
   own IntersectionObserver in main.js ("Property grid entrance") that adds
   .is-in as soon as a sliver of the section is visible -- a much lower
   threshold than the generic About-page observer, since this section's
   height scales with card count (1 card on birmingham.html, 28 on
   properties.html) and a fixed 33%-of-target threshold could sit unmet for
   an entire tall grid. Delay caps at the 9th card so a 28-card grid doesn't
   leave the last row waiting several seconds -- everything from the 9th
   card on rises together. */
.property-listing[data-animate] .property-grid > li {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s cubic-bezier(0.16, 0.84, 0.3, 1),
              transform 0.6s cubic-bezier(0.16, 0.84, 0.3, 1);
}

.property-listing[data-animate].is-in .property-grid > li {
  opacity: 1;
  transform: none;
}

.property-listing[data-animate] .property-grid > li:nth-child(1) { transition-delay: 0.05s; }
.property-listing[data-animate] .property-grid > li:nth-child(2) { transition-delay: 0.12s; }
.property-listing[data-animate] .property-grid > li:nth-child(3) { transition-delay: 0.19s; }
.property-listing[data-animate] .property-grid > li:nth-child(4) { transition-delay: 0.26s; }
.property-listing[data-animate] .property-grid > li:nth-child(5) { transition-delay: 0.33s; }
.property-listing[data-animate] .property-grid > li:nth-child(6) { transition-delay: 0.4s; }
.property-listing[data-animate] .property-grid > li:nth-child(7) { transition-delay: 0.47s; }
.property-listing[data-animate] .property-grid > li:nth-child(8) { transition-delay: 0.54s; }
.property-listing[data-animate] .property-grid > li:nth-child(n+9) { transition-delay: 0.6s; }

@media (prefers-reduced-motion: reduce) {
  .property-listing[data-animate] .property-grid > li {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.property-pagination {
  display: flex;
  justify-content: center;
  gap: 0.6rem;
  max-width: 1160px;
  margin: clamp(2.5rem, 5vh, 3.5rem) auto 0;
}

.property-pagination-btn {
  width: 2.6rem;
  height: 2.6rem;
  border: 1px solid var(--rule);
  background: var(--white);
  color: var(--ink);
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.property-pagination-btn:hover,
.property-pagination-btn:focus-visible {
  border-color: var(--champagne-deep);
  background: var(--champagne-tint);
}

.property-pagination-btn:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

.property-pagination-btn[aria-current='true'] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--champagne);
}

/* App-tile card: 4:3 photo (~60% of card height), title + region line, a
   two-slide carousel (arrows either side of the description panel) that
   swaps the description for a 3x2 feature-icon grid, then a price + CTA
   footer (~40% of card height). Region pages reuse the exact same card and
   grid; this note is here so an agent asked to add a new listing knows
   where it lives and how it's found. Every card carries data-lusso=
   "property" plus a stable data-property slug and a data-region slug
   matching its page. Query [data-lusso="property"] to find every card on a
   page; match data-property values to find the same property's other copy
   (e.g. on /properties/ vs. its region page) and keep the two in sync by
   hand — there's no shared data source behind these yet. */
/* No overflow:hidden here on purpose: the expanded description overlay has to
   be able to reach past the bottom of the card. The rounded corners are
   clipped by the media and footer instead, which are the only children that
   actually touch the tile's edges. */
.property-tile {
  border-radius: 16px;
  box-shadow: 0 1px 2px rgba(28, 26, 23, 0.06), 0 8px 24px -12px rgba(28, 26, 23, 0.18);
  background: var(--champagne-tint);
  border: 1px solid var(--champagne-deep);
}

.property-tile-media {
  position: relative;
  border-radius: 15px 15px 0 0;
  overflow: hidden;
}

.property-tile-media img {
  width: 100%;
  height: auto;
  aspect-ratio: 6 / 5;
  object-fit: cover;
  display: block;
}

.property-tile-body {
  padding: 16px 23px 21px;
}

.property-tile-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 25px;
  margin: 0 0 3px;
  color: var(--ink);
}

.property-tile-loc {
  font-size: 15px;
  margin: 0 0 14px;
  padding-bottom: 11px;
  color: var(--ink-italic);
  border-bottom: 1px solid var(--champagne-deep);
}

.property-tile-carousel {
  margin: 0 0 14px;
}

.property-tile-feats {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-template-rows: repeat(2, 1fr);
  gap: 7px;
}

.property-tile-feat {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  min-width: 0;
  text-align: center;
  border-radius: 8px;
  padding: 7px 3px;
  background: var(--champagne-tint);
  color: var(--ink);
}

.property-tile-feat svg {
  width: 39px;
  height: 39px;
}

.property-tile-feat span {
  font-size: 8.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.2;
  overflow-wrap: break-word;
}

.property-tile-arrow:hover:not(:disabled) {
  opacity: 1;
}

.property-tile-arrow:disabled {
  opacity: 0.35;
  cursor: default;
}

.property-tile-arrow:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

.property-tile-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 16px;
  border-top: 1px solid var(--champagne-deep);
}

.property-tile-price {
  font-size: 22px;
  font-weight: 700;
  font-family: var(--font-body);
  color: var(--ink);
}

.property-tile-price span {
  font-size: 13px;
  font-weight: 400;
  color: var(--ink-italic);
}

.property-tile-btn {
  font-size: 14px;
  font-weight: 700;
  padding: 10px 21px;
  border-radius: 999px;
  text-decoration: none;
  white-space: nowrap;
  background: var(--ink);
  color: var(--champagne-tint);
}

.property-tile-btn:hover,
.property-tile-btn:focus-visible {
  background: var(--ink-italic);
}

/* --------------------------------------------------------------------------
   FAQ accordion
   Two grounds sharing one component: dark (.faq-section) for stay-with-us
   and let-with-us, where the FAQ is the last objection before the page's
   CTA; light (.faq-section--light) for the region pages, so seven of them
   in a row don't repeat the same dark block down the site. Markup is a
   details-free custom disclosure (button + grid-rows trick) because native
   <details> can't animate its height smoothly across browsers.
   -------------------------------------------------------------------------- */

.faq-section {
  position: relative;
  overflow: hidden;
  padding: clamp(3.5rem, 9vw, 6rem) var(--gutter);
  background: var(--ink);
}

.faq-section::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 60% 50% at 15% 0%, rgba(194, 178, 146, 0.14), transparent 60%),
    radial-gradient(ellipse 50% 40% at 100% 100%, rgba(194, 178, 146, 0.08), transparent 60%);
  pointer-events: none;
}

/* Parallax ground — let-with-us only. Same fixed-background technique as
   .let-hero/.let-statement above, with a dark scrim so the champagne/white
   text keeps its contrast over the photo. */
.faq-section--parallax {
  background-image: url('../assets/img/vinegar-loft-landing.avif');
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

.faq-section--parallax::before {
  background:
    linear-gradient(rgba(24, 21, 17, 0.86), rgba(24, 21, 17, 0.86)),
    radial-gradient(ellipse 60% 50% at 15% 0%, rgba(194, 178, 146, 0.14), transparent 60%),
    radial-gradient(ellipse 50% 40% at 100% 100%, rgba(194, 178, 146, 0.08), transparent 60%);
}

@media (prefers-reduced-motion: reduce) {
  .faq-section--parallax {
    background-attachment: scroll;
  }
}

.faq-head {
  position: relative;
  text-align: center;
  margin: 0 auto clamp(2rem, 5vw, 3rem);
  max-width: 640px;
}

.faq-kicker {
  margin: 0 0 0.9rem;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--champagne);
  opacity: 0.85;
}

.faq-head h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.6rem, 4vw, 2.3rem);
  line-height: 1.2;
  color: var(--white);
  text-wrap: balance;
}

.faq-head h2 em {
  font-style: italic;
  color: var(--champagne);
}

.faq-list {
  position: relative;
  max-width: 680px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

.faq-item {
  border-bottom: 1px solid rgba(250, 248, 244, 0.14);
}

.faq-item:first-child {
  border-top: 1px solid rgba(250, 248, 244, 0.14);
}

.faq-q {
  width: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  margin: 0;
  padding: 1.35rem 0.2rem;
  background: none;
  border: none;
  font-family: var(--font-display);
  font-size: clamp(1rem, 2vw, 1.18rem);
  line-height: 1.4;
  color: var(--white);
  text-align: left;
  cursor: pointer;
  transition: color 0.3s ease;
}

.faq-q:hover,
.faq-q:focus-visible {
  color: var(--champagne);
}

.faq-q:focus-visible {
  outline: 1px solid var(--champagne);
  outline-offset: 4px;
}

.faq-index {
  flex: none;
  margin-right: 0.9rem;
  padding-top: 0.3em;
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--champagne-deep);
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}

.faq-q-text {
  flex: 1;
}

.faq-icon {
  position: relative;
  flex: none;
  width: 1.4rem;
  height: 1.4rem;
  margin-top: 0.3em;
}

.faq-icon::before,
.faq-icon::after {
  content: '';
  position: absolute;
  background: var(--champagne);
  border-radius: 1px;
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.3s ease;
}

.faq-icon::before {
  left: 0;
  top: 50%;
  width: 100%;
  height: 1.5px;
  transform: translateY(-50%);
}

.faq-icon::after {
  top: 0;
  left: 50%;
  width: 1.5px;
  height: 100%;
  transform: translateX(-50%);
}

.faq-item.is-open .faq-icon::before {
  transform: translateY(-50%) rotate(180deg);
}

.faq-item.is-open .faq-icon::after {
  transform: translateX(-50%) rotate(90deg);
  opacity: 0;
}

.faq-a-wrap {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.55s cubic-bezier(0.65, 0, 0.35, 1);
}

.faq-item.is-open .faq-a-wrap {
  grid-template-rows: 1fr;
}

.faq-a-inner {
  overflow: hidden;
}

.faq-a {
  margin: 0;
  padding: 0 2.6rem 1.5rem 0.2rem;
  font-family: var(--font-body);
  font-size: 0.92rem;
  line-height: 1.75;
  color: var(--champagne-tint);
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.4s ease 0.08s, transform 0.4s ease 0.08s;
}

.faq-item.is-open .faq-a {
  opacity: 0.92;
  transform: translateY(0);
}

.faq-foot {
  position: relative;
  text-align: center;
  margin-top: clamp(2rem, 5vw, 2.75rem);
}

.faq-foot a {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--champagne);
  text-decoration: none;
  padding-bottom: 0.2rem;
  border-bottom: 1px solid rgba(194, 178, 146, 0.4);
  transition: border-color 0.3s ease, opacity 0.3s ease;
}

.faq-foot a:hover,
.faq-foot a:focus-visible {
  border-color: var(--champagne);
  opacity: 0.85;
}

/* Light ground for the region pages — same component, quieter so seven of
   these down the site don't repeat the same dark block. */
.faq-section--light {
  background: var(--offwhite);
}

/* stay-with-us sits between two warmer sections, so its FAQ takes pure
   white rather than the offwhite the region pages use, to read as a clean
   break rather than blending into the champagne tones either side. */
.faq-section--white {
  background: var(--white);
}

.faq-section--light::before {
  display: none;
}

.faq-section--light .faq-kicker,
.faq-section--light .faq-index {
  color: var(--champagne-deep);
}

.faq-section--light .faq-head h2,
.faq-section--light .faq-q {
  color: var(--ink);
}

.faq-section--light .faq-head h2 em {
  color: var(--champagne-deep);
}

.faq-section--light .faq-q:hover,
.faq-section--light .faq-q:focus-visible {
  color: var(--champagne-deep);
}

.faq-section--light .faq-item,
.faq-section--light .faq-item:first-child {
  border-color: var(--rule);
}

.faq-section--light .faq-icon::before,
.faq-section--light .faq-icon::after {
  background: var(--champagne-deep);
}

.faq-section--light .faq-a {
  color: var(--ink-italic);
}

/* --------------------------------------------------------------------------
   Stay With Us — entrances

   Four pieces: the hero's line rise on load, the feature's curtain and the
   offer icons' stagger on scroll, and a rule that sweeps under every section
   heading as it arrives. The reviews carousel is deliberately untouched — it
   has motion of its own in stay-carousel.js.

   The scroll-triggered pieces ride the generic [data-animate] observer in
   main.js, which adds .is-in once a third of the section is on screen. The
   hero can't use that (it's on screen at load), so it borrows the homepage's
   data-ready attribute instead — set in main.js after two frames.
   -------------------------------------------------------------------------- */

/* --- 01. Hero line rise ---------------------------------------------------

   .line-mask / .line-inner already carry the clipping and the descender
   padding from the homepage hero, so only the timing belongs here.

   Held in the opening state behind :not([data-ready='true']) rather than in
   the keyframe alone: with JavaScript unavailable the attribute never lands
   and the hero renders finished, exactly as the homepage behaves. */
.stay-hero:not([data-ready='true']) .line-inner {
  transform: translateY(105%);
}

.stay-hero:not([data-ready='true']) .stay-hero-lede,
.stay-hero:not([data-ready='true']) .stay-hero-actions {
  opacity: 0;
}

.stay-hero[data-ready='true'] .line-inner {
  animation: hero-rise 1s cubic-bezier(0.16, 0.84, 0.3, 1) both;
}

.stay-hero[data-ready='true'] .line-mask:nth-of-type(1) .line-inner {
  animation-delay: 0.15s;
}

.stay-hero[data-ready='true'] .line-mask:nth-of-type(2) .line-inner {
  animation-delay: 0.32s;
}

/* The lede rests at full opacity here (unlike the homepage's 0.86), so it
   takes hero-lift-full rather than hero-lift. */
.stay-hero[data-ready='true'] .stay-hero-lede {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.7s both;
}

.stay-hero[data-ready='true'] .stay-hero-actions {
  animation: hero-lift-full 0.8s cubic-bezier(0.16, 0.84, 0.3, 1) 0.88s both;
}

/* --- 03. Feature curtain --------------------------------------------------

   Copy in from the left, the plate uncovered by a wipe, and the outlined
   back-plate settling in behind once the photo is most of the way open. */
.stay-feature[data-animate] .stay-feature-copy {
  opacity: 0;
  transform: translateX(-24px);
  transition: opacity 0.85s ease 0.1s,
              transform 0.9s cubic-bezier(0.16, 0.84, 0.3, 1) 0.1s;
}

.stay-feature[data-animate].is-in .stay-feature-copy {
  opacity: 1;
  transform: none;
}

.stay-feature[data-animate] .stay-feature-plate {
  clip-path: inset(0 0 100% 0);
}

.stay-feature[data-animate].is-in .stay-feature-plate {
  animation: hero-wipe 1.1s cubic-bezier(0.16, 0.84, 0.3, 1) 0.25s both;
}

/* Offset by its own shift tokens so it arrives from behind the photo rather
   than sliding in from nowhere. */
.stay-feature[data-animate] .stay-feature-plate-back {
  opacity: 0;
  transform: translate(
    calc(-1 * var(--feature-back-shift-x)),
    var(--feature-back-shift-y)
  );
  transition: opacity 0.8s ease 0.55s,
              transform 0.9s cubic-bezier(0.16, 0.84, 0.3, 1) 0.55s;
}

.stay-feature[data-animate].is-in .stay-feature-plate-back {
  opacity: 1;
  transform: none;
}

/* --- 04. Offer icons, staggered ------------------------------------------

   90ms apart, left to right. The delay sits on the li as a custom property
   for the same reason as the locations list further up: written as
   `li:nth-child(n) .offer-item` it would outrank later rules on specificity. */
.offer[data-animate] .offer-item {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.7s ease var(--enter-delay, 0s),
              transform 0.7s cubic-bezier(0.16, 0.84, 0.3, 1) var(--enter-delay, 0s);
}

.offer[data-animate].is-in .offer-item {
  opacity: 1;
  transform: none;
}

.offer[data-animate] .offer-item:nth-child(1) { --enter-delay: 0.15s; }
.offer[data-animate] .offer-item:nth-child(2) { --enter-delay: 0.24s; }
.offer[data-animate] .offer-item:nth-child(3) { --enter-delay: 0.33s; }
.offer[data-animate] .offer-item:nth-child(4) { --enter-delay: 0.42s; }

/* --- 05. Section rule sweep ----------------------------------------------

   A hairline drawn out beneath each heading as its section arrives. Unlike
   the locations sweep (which draws then retracts, being a hover affordance),
   this one is an arrival mark and stays put — so it uses hero-draw.

   scaleX on a 1px pseudo-element rather than an animated width: transform
   stays off the layout path, so the sweep can't reflow the heading. */
/* The rule needs room of its own beneath the heading, or it draws straight
   through the lede that follows. The margin opens that gap; the pseudo-element
   then sits inside it rather than overlapping the next element. */
.rule-sweep {
  position: relative;
  padding-bottom: 0.75em;
}

/* Short and centred by default — three of the four headings are centred, and
   a rule held to a fixed width reads as a mark under the title rather than a
   full-width divider cutting the section in half. */
.rule-sweep::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 5.5rem;
  height: 1px;
  background: var(--champagne-deep);
  transform: translateX(-50%) scaleX(0);
  transform-origin: center;
}

[data-animate].is-in .rule-sweep::after {
  animation: rule-sweep-draw 1.05s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}

@keyframes rule-sweep-draw {
  from { transform: translateX(-50%) scaleX(0); }
  to   { transform: translateX(-50%) scaleX(1); }
}

/* The feature heading is the one left-aligned heading of the four, so its
   rule starts at the text's own left edge and draws outward. */
.stay-feature .rule-sweep::after {
  left: 0;
  transform: scaleX(0);
  transform-origin: left;
}

.stay-feature[data-animate].is-in .rule-sweep::after {
  animation: hero-draw 1.05s cubic-bezier(0.4, 0, 0.2, 1) 0.2s both;
}

/* The global rule below crushes durations to 0.01ms, which would skip these
   entrances rather than complete them — every one starts from opacity 0 or a
   clipped state, so they would flash or vanish. Land them on their rest
   values instead. main.js also skips the observer under reduced motion, so
   .is-in is present from the start and data-ready never arrives. */
@media (prefers-reduced-motion: reduce) {
  .stay-hero:not([data-ready='true']) .line-inner,
  .stay-hero .line-inner {
    transform: none;
    animation: none;
  }

  .stay-hero:not([data-ready='true']) .stay-hero-lede,
  .stay-hero:not([data-ready='true']) .stay-hero-actions {
    opacity: 1;
  }

  .stay-feature[data-animate] .stay-feature-copy,
  .stay-feature[data-animate] .stay-feature-plate-back,
  .offer[data-animate] .offer-item {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .stay-feature[data-animate] .stay-feature-plate,
  .stay-feature[data-animate].is-in .stay-feature-plate {
    clip-path: inset(0 0 0 0);
    animation: none;
  }

  .rule-sweep::after,
  [data-animate].is-in .rule-sweep::after {
    animation: none;
    transform: translateX(-50%) scaleX(1);
  }

  .stay-feature .rule-sweep::after,
  .stay-feature[data-animate].is-in .rule-sweep::after {
    animation: none;
    transform: scaleX(1);
  }
}

/* --------------------------------------------------------------------------
   404
   -------------------------------------------------------------------------- */

/* No masthead and no footer. A guest here has hit a dead end, and the one
   thing they want is the way out — a bar full of alternatives buries it, so
   the page is a single centred column with the logo standing in as the mark
   of where they are, and the way back as the only real weight on the screen.

   The logo is the link home, which covers the one thing the nav would have
   been for. */
body.error-page {
  /* No hero, no champagne story to close — the one warm neutral the inner
     pages resolve on, so an overscroll bounce has somewhere to land. */
  background: var(--offwhite) fixed;
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
}

.error {
  flex: 1;
  display: grid;
  place-items: center;
  padding: clamp(3rem, 10vh, 6rem) var(--gutter);
  text-align: center;
}

.error-inner {
  /* Holds the lede to a comfortable measure; everything else is shorter than
     this and centres inside it. */
  max-width: 34rem;
}

.error-logo-link {
  /* .logo-link is `display: block`, which would stretch the hit area the full
     width of the column — inline-block keeps the target on the mark itself. */
  display: inline-block;
}

.error-logo {
  width: clamp(150px, 17vw, 210px);
}

/* Hairline between the mark and the message — the same device the footer uses
   to separate the brand from the detail beneath it. */
.error-rule {
  display: block;
  width: 3rem;
  height: 1px;
  margin: clamp(2rem, 5vh, 2.8rem) auto clamp(1.6rem, 4vh, 2.2rem);
  background: var(--rule);
}

/* The code is the small print here, not the headline. A guest who has hit a
   dead end needs the sentence, not the status number, so it sits above the
   title at caption weight and lets the words carry the screen. */
.error-code {
  margin: 0 0 0.9rem;
  font-size: 0.68rem;
  font-weight: 400;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.55;
}

.error-title {
  /* Held narrower than .error-inner, and balanced: left to the full column
     the line breaks after "up" and drops "yet." on a line of its own, which
     reads as a mistake rather than a phrase. */
  margin: 0 auto;
  max-width: 22rem;
  text-wrap: balance;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.9rem, 4.4vw, 3rem);
  line-height: 1.15;
  color: var(--ink);
}

.error-title em {
  font-style: italic;
  color: var(--ink-italic);
}

.error-lede {
  margin: clamp(1rem, 2.5vh, 1.4rem) auto 0;
  max-width: 27rem;
  font-size: clamp(0.92rem, 1.6vw, 1.02rem);
  line-height: 1.65;
  color: var(--ink);
  opacity: 0.78;
}

.error-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 2.4vh, 1.3rem);
}

/* .story-cta carries its own top margin for use mid-section; here the flex
   column owns the spacing, so it's restated once rather than fought. */
.error-cta {
  margin-top: clamp(2rem, 5vh, 2.6rem);
}

/* Deliberately quieter than the button beside it — an escape hatch for
   someone who didn't want the page they came from, not a second CTA. */
.error-link {
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: var(--ink);
  opacity: 0.62;
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: opacity 0.3s ease, border-color 0.3s ease;
}

.error-link:hover,
.error-link:focus-visible {
  opacity: 1;
  border-bottom-color: var(--rule);
}

.error-link:focus-visible {
  outline: 1px solid var(--ink);
  outline-offset: 4px;
  border-bottom-color: transparent;
}

/* The column settles in on load. There's no scrolling on this page, so it
   isn't gated behind an observer the way the sections are — it just runs. */
@media (prefers-reduced-motion: no-preference) {
  .error-inner {
    animation: error-settle 0.7s ease both;
  }
}

@keyframes error-settle {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   Legal pages — privacy policy, terms
   -------------------------------------------------------------------------- */

/* Shares the contact hero's centred, photo-less opening: on a page that is
   entirely prose, the headline has to hold the screen by itself. */
.legal-hero {
  padding: clamp(4.5rem, 11vh, 7rem) var(--gutter) clamp(2rem, 5vh, 3rem);
  background: var(--offwhite);
  text-align: center;
}

.legal-hero-inner {
  max-width: 640px;
  margin: 0 auto;
}

.legal-hero h1 {
  margin: 0 0 1.2rem;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.1rem, 4.4vw, 3.2rem);
  line-height: 1.16;
  color: var(--ink);
  text-wrap: balance;
}

.legal-hero h1 em {
  font-style: italic;
  color: var(--champagne-deep);
}

.legal-hero-lede {
  margin: 0 auto;
  max-width: 54ch;
  font-size: 0.98rem;
  line-height: 1.8;
  color: var(--ink-italic);
  opacity: 0.85;
}

/* The date a reader needs to know this version is current. Set below the
   lede as small caps so it registers as metadata, not as more prose. */
.legal-hero-meta {
  margin: 1.6rem 0 0;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-italic);
  opacity: 0.6;
}

/* Index alongside the text rather than stacked above it. Twelve numbered
   sections is enough that a reader arriving for one of them — usually
   their rights, or how to complain — shouldn't have to scroll to find it. */
.legal-layout {
  display: grid;
  grid-template-columns: 260px 1fr;
  gap: clamp(2.5rem, 6vw, 5rem);
  max-width: 1120px;
  margin: 0 auto;
  padding: clamp(2rem, 5vh, 3rem) var(--gutter) clamp(4.5rem, 11vh, 7rem);
}

.legal-index {
  position: sticky;
  top: calc(var(--bar-tall) + 1.5rem);
  align-self: start;
  padding: 1.75rem 1.5rem;
  border-radius: 4px;
  background: var(--champagne-tint);
}

.legal-index h2 {
  margin: 0 0 1rem;
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.65;
}

.legal-index ol {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: legal-index;
}

.legal-index li {
  counter-increment: legal-index;
}

.legal-index a {
  display: grid;
  grid-template-columns: 1.6rem 1fr;
  gap: 0.2rem;
  padding: 0.42rem 0;
  font-size: 0.88rem;
  line-height: 1.45;
  color: var(--ink);
  text-decoration: none;
  opacity: 0.8;
  transition: opacity 0.2s ease, color 0.2s ease;
}

/* The number comes from the counter, so the markup carries only the label
   and the two orderings can't drift apart. */
.legal-index a::before {
  content: counter(legal-index) '.';
  font-variant-numeric: tabular-nums;
  opacity: 0.55;
}

.legal-index a:hover,
.legal-index a:focus-visible {
  opacity: 1;
  color: var(--champagne-deep);
}

.legal-index a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

.legal-body {
  max-width: 68ch;
}

.legal-section + .legal-section {
  margin-top: clamp(2.5rem, 5vh, 3.5rem);
}

/* Hairline above each heading, so the twelve sections read as a sequence of
   discrete clauses rather than one continuous column of text. */
.legal-section > h2 {
  display: flex;
  align-items: baseline;
  gap: 0.85rem;
  margin: 0 0 1rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.35rem, 2.4vw, 1.7rem);
  line-height: 1.25;
  color: var(--ink);
}

.legal-section > h2 .legal-section-number {
  font-size: 0.72rem;
  font-weight: 500;
  font-family: var(--font-body);
  letter-spacing: 0.1em;
  color: var(--champagne-deep);
  font-variant-numeric: tabular-nums;
}

.legal-section h3 {
  margin: 1.75rem 0 0.75rem;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-italic);
}

.legal-section p {
  margin: 0 0 1rem;
  font-size: 0.96rem;
  line-height: 1.8;
  color: var(--ink-italic);
}

.legal-section p:last-child {
  margin-bottom: 0;
}

.legal-section a {
  color: var(--champagne-deep);
  text-decoration: underline;
  text-underline-offset: 0.18em;
  text-decoration-thickness: 1px;
}

.legal-section a:hover,
.legal-section a:focus-visible {
  color: var(--ink);
}

/* Champagne markers instead of bullets — the one place on these pages the
   brand colour appears at full strength, and it stays small. */
.legal-list {
  margin: 0 0 1rem;
  padding: 0;
  list-style: none;
}

.legal-list li {
  position: relative;
  padding-left: 1.4rem;
  margin-bottom: 0.6rem;
  font-size: 0.96rem;
  line-height: 1.75;
  color: var(--ink-italic);
}

.legal-list li::before {
  content: '';
  position: absolute;
  left: 0.15rem;
  top: 0.72em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--champagne);
}

.legal-list li:last-child {
  margin-bottom: 0;
}

/* Definition-style variant, for the lawful bases where each item is a term
   and its explanation rather than a bare phrase. */
.legal-list-defined li strong {
  font-weight: 500;
  color: var(--ink);
}

/* The four contact rows a reader lands on if they want to exercise a right.
   Boxed so it reads as the page's endpoint rather than a thirteenth clause. */
.legal-contact-card {
  margin-top: 1.25rem;
  padding: clamp(1.5rem, 3vw, 2rem);
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--white);
}

.legal-contact-card dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.7rem 1.5rem;
  margin: 0;
}

.legal-contact-card dt {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  opacity: 0.55;
  padding-top: 0.15em;
}

.legal-contact-card dd {
  margin: 0;
  font-size: 0.96rem;
  line-height: 1.7;
  color: var(--ink-italic);
}

@media (max-width: 900px) {
  /* Sticky costs more than it gives once the index is full-width above the
     text — it would hold a third of the screen for the whole scroll. */
  .legal-layout {
    grid-template-columns: 1fr;
  }

  .legal-index {
    position: static;
  }

  .legal-index ol {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 0 1.25rem;
  }
}

@media (max-width: 600px) {
  .legal-contact-card dl {
    grid-template-columns: 1fr;
    gap: 0.2rem;
  }

  .legal-contact-card dd + dt {
    margin-top: 0.9rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* --------------------------------------------------------------------------
   Cookie consent bar

   Sits above the masthead's z-index: 100 and the mobile nav's 41, since it
   has to stay reachable while the menu is open. Fixed to the foot of the
   viewport rather than the document so it holds position on long pages.
   -------------------------------------------------------------------------- */

.consent-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 200;
  background: var(--white);
  border-top: 1px solid var(--rule);
  box-shadow: 0 -14px 34px -26px rgba(46, 42, 36, 0.5);

  /* Off-screen until .is-open. Starting from the resting position and
     animating out would flash the bar at its final place on first paint. */
  transform: translateY(100%);
  transition: transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}

.consent-bar.is-open {
  transform: translateY(0);
}

.consent-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1.5rem;
  padding: 1.05rem var(--gutter);
  max-width: 90rem;
  margin: 0 auto;
}

.consent-text {
  flex: 1 1 22rem;
  margin: 0;
  max-width: 62ch;
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 350;
  line-height: 1.6;
  color: var(--ink-italic);
}

.consent-link {
  color: var(--ink);
  text-underline-offset: 3px;
  text-decoration-color: var(--rule);
}

.consent-link:hover,
.consent-link:focus-visible {
  text-decoration-color: currentColor;
}

.consent-actions {
  display: flex;
  gap: 0.6rem;
  flex-shrink: 0;
}

/* .btn is an <a> elsewhere on the site; as a <button> it needs the type
   normalisation the anchor never required. */
.consent-btn {
  font: inherit;
  font-size: 0.68rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  padding: 0.72rem 1.35rem;
  cursor: pointer;
  transition: background-color 200ms ease, border-color 200ms ease;
}

@media (max-width: 600px) {
  .consent-inner {
    align-items: stretch;
    gap: 0.9rem;
    padding-block: 0.95rem;
  }

  /* Full width and equal share below the text, so neither choice is the
     easier target — refusing has to be as easy as accepting. The extra
     block padding clears WCAG's 44px target size; the desktop bar gets it
     from the pointer instead, where 38px is comfortable. */
  .consent-actions .consent-btn {
    flex: 1;
    justify-content: center;
    padding-block: 0.95rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .consent-bar {
    transition-duration: 0.01ms;
  }
}
