/* ══════════════════════════════════════════════════════════════════════════
   Casa Diáspora
   Bright, airy, white-dominant, with the photography carrying the colour.
   The palette is drawn from the building itself — salmon-pink stucco, a
   terracotta roof, rust velvet, sage walls and green-grey marble — rather
   than from a generic resort-beige. One accent colour, reserved entirely for
   the booking call to action, so nothing on the page competes with it.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  --shell:      #FDFBF9;   /* page base, warm white          */
  --clay:       #F4E7E0;   /* alternate band — the façade,
                              one step desaturated            */
  --ink:        #2A2724;   /* primary text                   */
  --muted:      #6E6862;   /* secondary text                 */
  --terracotta: #C2703D;   /* CTA — the roof and the sofas   */
  --terracotta-dark: #A85F33;
  --sage:       #3A3F39;   /* contrast band — interior walls
                              and the green-grey marble       */
  --sage-soft:  #6E7268;   /* muted text on a sage ground    */
  --brass:      #B8862F;   /* hairlines, flourishes          */
  --line:       #E8DCD4;

  --serif: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  /* Karla over a system stack: it has enough character to sit beside Fraunces
     without the flatness of the usual UI grotesques. */
  --sans:  'Karla', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;

  --wrap: 1240px;
  --gutter: clamp(20px, 5vw, 64px);

  /* Rounded, not sharp. Every interactive element is a pill or a soft panel —
     the single most consistent trait across the reference sites, and the one
     that reads as hospitality rather than gallery. */
  --r-pill:  999px;
  --r-panel: 20px;
  --r-input: 12px;

  /* Long and calm. No bounce anywhere. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --slow: 900ms;
  --med:  600ms;
  --fade: 1200ms;          /* hero crossfade */

  --header-h: 76px;
}

/* ─── Reset ─────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--shell);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 17px;
  font-weight: 300;
  line-height: 1.75;
  overflow-x: hidden;          /* belt and braces against a stray wide child */
  -webkit-font-smoothing: antialiased;
}
img, video { max-width: 100%; display: block; }
h1, h2, h3 { font-family: var(--serif); font-weight: 300; line-height: 1.1; margin: 0; }
p { margin: 0 0 1.1em; }
a { color: inherit; }
button { font: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--terracotta);
  outline-offset: 3px;
  border-radius: var(--r-input);
}
/* Over photography the terracotta ring loses contrast against the roof tiles
   and the red earth, so it gains a light outer ring to sit against. */
.hero :focus-visible,
.shots :focus-visible {
  outline-color: #fff;
  box-shadow: 0 0 0 5px rgba(42, 39, 36, .55);
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 200;
  background: var(--ink); color: #fff; padding: 12px 20px; text-decoration: none;
}
.skip:focus { left: 0; }

/* ─── Layout helpers ────────────────────────────────────────────────────── */
.wrap    { max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); }
.measure { max-width: 720px; margin-inline: auto; padding-inline: var(--gutter); text-align: center; }
.centre  { text-align: center; }

/* Sections sit closer than they did. 148px of air at the top and bottom of
   every band meant nearly 300px of nothing between one section and the next,
   which reads as the page having stopped rather than as breathing room. */
.band { padding: clamp(56px, 7vw, 96px) 0; }
.band--clay     { background: var(--clay); }
.band--sage { background: var(--sage); color: #F2EBE0; }
.band--sage .eyebrow { color: var(--brass); }
.band--flush    { padding-bottom: clamp(44px, 5.5vw, 72px); }

.section-head { max-width: 640px; margin: 0 auto clamp(40px, 6vw, 72px); text-align: center; }

/* ─── Type ──────────────────────────────────────────────────────────────── */
.eyebrow {
  font-family: var(--sans);
  font-size: 12px; font-weight: 500;
  letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 18px;
}
.display   { font-size: clamp(30px, 4.6vw, 54px); letter-spacing: -0.01em; margin-bottom: 24px; }
.lede      { font-size: clamp(17px, 1.6vw, 20px); }
.muted     { color: var(--muted); }
.rule      { display: block; width: 40px; height: 1px; background: var(--brass); margin-bottom: 20px; }

/* ─── Buttons ───────────────────────────────────────────────────────────── */
/* Pills. Every reference site rounds its interactive elements fully, and it is
   the detail that separates a hospitality page from a gallery one. */
.btn {
  display: inline-block;
  padding: 13px 30px;
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-size: 13px; font-weight: 500;
  letter-spacing: 0.12em; text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--med) var(--ease), color var(--med) var(--ease),
              border-color var(--med) var(--ease);
}
.btn--primary { background: var(--terracotta); color: #fff; }
.btn--primary:hover { background: var(--terracotta-dark); }
.btn--quiet   { background: transparent; color: var(--ink); border-color: var(--line); }
.btn--quiet:hover { border-color: var(--ink); }
.band--sage .btn--quiet { color: #F2EBE0; border-color: rgba(242, 235, 224, .32); }
.band--sage .btn--quiet:hover { border-color: #F2EBE0; }
.btn--lg    { padding: 16px 36px; }
.btn--block { display: block; width: 100%; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }

.linky {
  /* inline-flex, not the default inline: min-height has no effect on an inline
     box, so `<a class="linky">` kept a 27px target while the buttons got 44. */
  display: inline-flex; align-items: center;
  background: none; border: 0; padding: 6px 0; min-height: 44px; cursor: pointer;
  font-size: 13px; color: var(--muted); text-decoration: underline;
  text-underline-offset: 3px;
}
.linky:hover { color: var(--ink); }

/* ─── Header ────────────────────────────────────────────────────────────── */
.site-header {
  position: fixed; inset: 0 0 auto; z-index: 100;
  display: flex; align-items: center; gap: 32px;
  height: var(--header-h);
  padding-inline: var(--gutter);
  background: rgba(252, 250, 247, 0);
  transition: background var(--med) var(--ease), box-shadow var(--med) var(--ease);
}
.site-header.is-stuck {
  background: rgba(253, 251, 249, .96);
  backdrop-filter: saturate(140%) blur(8px);
  box-shadow: 0 1px 0 var(--line);
}

/* Over the hero photograph the header inverts. `.over-hero` is set by main.js
   only on pages that actually have a hero, so gallery.html and booking.html —
   which open on a pale background — keep dark chrome. */
.site-header.over-hero:not(.is-stuck) { color: #fff; }
.site-header.over-hero:not(.is-stuck) .wordmark__place,
.site-header.over-hero:not(.is-stuck) .site-nav a,
.site-header.over-hero:not(.is-stuck) .lang button { color: rgba(255, 255, 255, .82); }
.site-header.over-hero:not(.is-stuck) .site-nav a:hover,
.site-header.over-hero:not(.is-stuck) .lang button:hover,
.site-header.over-hero:not(.is-stuck) .lang button[aria-pressed="true"] { color: #fff; }
.site-header.over-hero:not(.is-stuck) .burger span { background: #fff; }
/* The one element that must not invert: the reserve button is the page's
   single accent and stays terracotta on every background. */
.wordmark { text-decoration: none; line-height: 1.15;
            display: flex; flex-direction: column; justify-content: center;
            min-height: 44px; }
.wordmark__name  { display: block; font-family: var(--serif); font-size: 21px;
                   letter-spacing: .01em; white-space: nowrap; }
.wordmark__place { display: block; font-size: 9.5px; letter-spacing: .3em;
                   text-transform: uppercase; color: var(--muted); white-space: nowrap; }

.site-nav { display: flex; gap: 30px; margin-left: auto; }
.site-nav a {
  font-size: 13px; letter-spacing: .07em; text-decoration: none; color: var(--ink);
  padding-bottom: 3px; border-bottom: 1px solid transparent;
  transition: border-color var(--med) var(--ease);
}
.site-nav a:hover { border-color: var(--brass); }

.header-actions { display: flex; align-items: center; gap: 18px; }

.lang { display: flex; gap: 2px; }
.lang button {
  background: none; border: 0; cursor: pointer; border-radius: var(--r-pill);
  font-size: 12px; letter-spacing: .1em; padding: 5px 9px; color: var(--muted);
  /* A comfortable target without a 44px-wide box crowding the header: the
     button is tall enough to hit and the row stays compact. */
  min-height: 44px;
  transition: color var(--med) var(--ease);
}
.lang button[aria-pressed="true"] { color: var(--ink); text-decoration: underline;
                                    text-underline-offset: 4px; }
.lang button:hover { color: var(--ink); }
.lang--footer { flex-direction: column; align-items: flex-start; gap: 4px; }
.lang--footer button { padding: 2px 0; font-size: 14px; letter-spacing: .02em; text-transform: none; }

.burger { display: none; background: none; border: 0; cursor: pointer;
          width: 44px; height: 44px; padding: 6px 7px; }
.burger span { display: block; height: 1px; background: var(--ink); margin: 6px 0;
               transition: transform var(--med) var(--ease), opacity var(--med) var(--ease); }
.burger[aria-expanded="true"] span:first-child { transform: translateY(3.5px) rotate(45deg); }
.burger[aria-expanded="true"] span:last-child  { transform: translateY(-3.5px) rotate(-45deg); }

/* ─── Hero ──────────────────────────────────────────────────────────────── */
/* Full-bleed photography as the page's ground, with every piece of chrome
   floating on top of it. Slides are the wide façade shots only: the interiors
   are portrait 3:4, and covering a full-height desktop viewport with one shows
   a narrow horizontal band of the room instead of the room. */
.hero {
  position: relative;
  min-height: 100svh;               /* svh, so the mobile URL bar cannot jog it */
  display: grid;
  grid-template-rows: 1fr auto auto;
  padding: calc(var(--header-h) + 24px) var(--gutter) clamp(20px, 3vw, 34px);
  color: #fff;
  background: var(--sage);          /* seen only before the first slide paints */
}

/* Explicit layers, all positive: photograph → scrim → text. Negative z-index
   would put the stage behind the hero's own background in some engines, and
   the difference is the entire photograph. */
.hero__inner, .dock { position: relative; z-index: 2; }
/* Taller than the hero on both edges, so it can drift with the scroll without
   ever exposing a strip of the band colour behind it. The hero clips. */
.hero { overflow: hidden; }
.hero__stage { position: absolute; inset: -9% 0; overflow: hidden; z-index: 0; }
/* The outgoing slide stays fully opaque underneath while the incoming one
   fades in above it. Cross-fading both at once does not sum to 1 in the
   middle, so the hero would visibly dip to the background colour on every
   transition. */
.hero__slide {
  position: absolute; inset: 0;
  opacity: 0; z-index: 0;
  transition: opacity var(--fade) var(--ease);
}
.hero__slide.is-prev   { opacity: 1; z-index: 1; transition: none; }
.hero__slide.is-active { opacity: 1; z-index: 2; }
.hero__slide img,
.hero__slide video,
.hero__slide picture { width: 100%; height: 100%; display: block; }
.hero__slide video { object-fit: cover; }
.hero__slide img { object-fit: cover; object-position: var(--focal, 50% 50%); }

/* A very slow push-in, restarted per slide by the carousel toggling the class.
   Long enough that it reads as the photograph breathing rather than as motion. */
.hero__slide.is-active img { animation: drift 11s var(--ease) forwards; }
@keyframes drift { from { transform: scale(1); } to { transform: scale(1.08); } }

/* Three gradients rather than one flat wash: a flat overlay would grey out the
   whole photograph, whereas these darken only where text actually sits — the
   header strip, the bottom edge, and the left column under the headline.
   The façade is bright pink in full sun, so the left layer is doing real
   contrast work, not decoration. */
/* These exact stops are measured, not eyeballed. White text over the four
   façade photographs is composited offline and sampled for its worst
   (brightest) pixel; an early pass failed WCAG AA at 2.0 against the 3.0
   large-text threshold, because the text sat mid-left over bright sky.
   As it now stands — the wordmark at the bottom left — the worst slide gives
   wordmark 8.5 and dashes 4.7, while darkening the photograph as little as
   possible. Re-measure whenever the text moves or a hero photograph is added:
   `node scripts/check-hero-contrast.mjs` in api/ does exactly that. */
.hero__scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(26, 22, 20, .55), rgba(26, 22, 20, 0) 26%),
    linear-gradient(to right,  rgba(26, 22, 20, .80), rgba(26, 22, 20, .45) 45%,
                               rgba(26, 22, 20, 0) 80%),
    linear-gradient(to top,    rgba(26, 22, 20, .80), rgba(26, 22, 20, .34) 45%,
                               rgba(26, 22, 20, 0) 75%);
}

.hero__inner {
  grid-row: 2;
  display: flex; flex-wrap: wrap; align-items: flex-end;
  justify-content: space-between; gap: 28px;
  max-width: var(--wrap); width: 100%; margin-inline: auto;
  padding-bottom: clamp(24px, 3.5vw, 44px);
}
/* The name, bottom-left, at the largest size the viewport will carry — the
   photograph says everything the paragraph used to.
   Sized in vw rather than ch: `ch` resolves against the body font size, not the
   display size set here, and would collapse the line to one word.
   `min-content` lets it break to two lines on a phone rather than run off the
   side; the tight leading is what makes a two-line break read as intentional. */
.hero__wordmark {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(40px, 7.5vw, 112px);
  line-height: 0.92;
  letter-spacing: -0.03em;
  margin: 0;
  /* No max-width. `min-content` here forced a break between the two words even
     with 900px of room to spare; left alone the name sits on one line wherever
     it fits and stacks only where it genuinely cannot. The tight leading is
     what makes that stacked break read as intentional rather than as overflow. */
  text-shadow: 0 2px 34px rgba(20, 16, 14, .42);
}

/* Dash pagination, bottom right. Thin rules rather than dots — the reference
   detail that makes the carousel read as editorial rather than as a widget. */
.hero__dots { display: flex; gap: 10px; align-items: center; }
.hero__dots button {
  /* The visible dash is the ::after; the button itself stays a 44px target. */
  position: relative; width: 46px; height: 44px;
  background: none; border: 0; padding: 0; cursor: pointer;
}
.hero__dots button::after {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: 2px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, .42);
  /* The dashes sit bottom-right, where the scrim has faded out — over a
     sunlit wall a plain white rule disappears, so each carries its own shadow. */
  box-shadow: 0 1px 4px rgba(20, 16, 14, .75);
  transition: background var(--med) var(--ease), height var(--med) var(--ease);
}
.hero__dots button:hover::after { background: rgba(255, 255, 255, .75); }
.hero__dots button[aria-current="true"]::after { background: #fff; height: 3px; }

/* ─── Docked booking bar ────────────────────────────────────────────────── */
/* Floats over the base of the hero. Frosted rather than solid so the
   photograph stays continuous behind it, with a solid fallback for browsers
   that do not support backdrop-filter. */
.dock {
  grid-row: 3;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
  gap: 10px; align-items: end;
  max-width: var(--wrap); width: 100%; margin-inline: auto;
  padding: 16px 18px;
  border-radius: var(--r-panel);
  background: rgba(253, 251, 249, .92);
  border: 1px solid rgba(255, 255, 255, .5);
  box-shadow: 0 30px 60px -30px rgba(20, 16, 14, .6);
  color: var(--ink);
}
@supports (backdrop-filter: blur(1px)) {
  .dock { background: rgba(253, 251, 249, .74); backdrop-filter: saturate(150%) blur(18px); }
}
.dock label {
  margin-bottom: 5px; font-size: 10px; letter-spacing: .16em; color: var(--muted);
}
.dock input, .dock select {
  padding: 10px 12px; min-height: 44px;
  /* 16px exactly: below it, iOS zooms the whole page when the field is
     focused and leaves the visitor scrolled off to one side. */
  font-size: 16px;
  border-color: transparent; background: rgba(255, 255, 255, .72);
}
.dock input:hover, .dock select:hover { border-color: var(--line); }
.dock .field { margin: 0; }
.dock__go { white-space: nowrap; min-height: 44px; padding-inline: 26px; }

/* One pill instead of a five-column form. A date picker squeezed into a
   phone-width bar is worse than a button that takes you to a real one. */
.dock__compact { display: none; }

/* Tablet: the fields still fit, two to a row, with the button across. */
@media (max-width: 1000px) and (min-width: 701px) {
  .dock { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dock__go { grid-column: 1 / -1; width: 100%; }
}

/* Phone: one pill instead of a five-column form. A date picker squeezed into a
   phone-width bar is worse than a button that opens a real one. */
@media (max-width: 700px) {
  .hero__inner { padding-bottom: 20px; }
  .dock { grid-template-columns: 1fr; padding: 0; background: none; border: 0;
          box-shadow: none; backdrop-filter: none; }
  @supports (backdrop-filter: blur(1px)) { .dock { backdrop-filter: none; background: none; } }
  .dock .field, .dock__go { display: none; }
  .dock__compact { display: block; width: 100%; }
}

/* ─── Hero bar date picker ─────────────────────────────────────────────── */
/* Replaces the native <input type="date"> popup, whose month and weekday
   names Chrome renders in the browser's UI language rather than the one
   chosen on the site. The grid reuses .cal__month / .cal__day from the
   #reserve calendar so the two read as the same component. */
.dock .field { position: relative; }

.dock__date {
  width: 100%; min-height: 44px;
  padding: 10px 12px;
  font: inherit; font-size: 16px; text-align: left;
  color: var(--ink);
  background: rgba(255, 255, 255, .72);
  border: 1px solid transparent; border-radius: var(--r-input);
  cursor: pointer;
  transition: border-color var(--med) var(--ease);
}
.dock__date:hover { border-color: var(--line); }
.dock__date.is-empty { color: var(--muted); }

.dockcal {
  position: absolute; left: 0; z-index: 40;
  width: min(340px, calc(100vw - 24px));
  overflow: auto;
  padding: 12px 12px 6px;
  background: var(--shell);
  border: 1px solid var(--line); border-radius: var(--r-panel);
  box-shadow: 0 24px 50px -24px rgba(20, 16, 14, .55);
}
@media (prefers-reduced-motion: no-preference) {
  .dockcal[data-place="above"] { animation: dockcal-up 150ms var(--ease) both; }
  .dockcal[data-place="below"] { animation: dockcal-down 150ms var(--ease) both; }
}
@keyframes dockcal-up   { from { opacity: 0; transform: translateY(6px); } }
@keyframes dockcal-down  { from { opacity: 0; transform: translateY(-6px); } }

.dockcal__head {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
.dockcal__title { font-family: var(--serif); font-size: 16px; }
/* Intl gives "setembro de 2026"; lift only the first letter, not every word. */
.dockcal__title::first-letter { text-transform: uppercase; }
.dockcal__nav {
  width: 36px; height: 36px; flex: none;
  background: none; border: 1px solid var(--line); border-radius: var(--r-pill);
  font-size: 15px; line-height: 1; color: var(--ink); cursor: pointer;
  transition: border-color var(--med) var(--ease);
}
.dockcal__nav:hover:not(:disabled) { border-color: var(--ink); }
.dockcal__nav:disabled { opacity: .3; cursor: not-allowed; }

.dockcal .cal__month { table-layout: fixed; }
.dockcal .cal__month td { padding: 0; }
.dockcal .cal__month caption { display: none; }

.dockcal__foot { text-align: right; }
.dockcal__clear {
  background: none; border: 0; cursor: pointer;
  font-size: 12px; letter-spacing: .04em; color: var(--muted);
  padding: 8px 4px;
  transition: color var(--med) var(--ease);
}
.dockcal__clear:hover { color: var(--ink); }

/* The hero gives descendants a white focus ring to sit on dark photography;
   inside this pale popover that would vanish, so restore the standard ring. */
.dockcal :focus-visible {
  outline: 2px solid var(--terracotta); outline-offset: 2px;
  box-shadow: none;
}

/* ─── Split section ─────────────────────────────────────────────────────── */
.split {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: clamp(32px, 6vw, 80px); align-items: center;
  max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter);
}
.split__media { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
.split__media .ph--tall { grid-row: span 2; }

.facts { margin: 26px 0; padding-left: 0; }
.facts dt { font-size: 12px; letter-spacing: .18em; text-transform: uppercase;
            color: var(--muted); margin-bottom: 10px; }
.facts dd { margin: 0 0 6px; padding-left: 18px; position: relative; }
.facts dd::before { content: ''; position: absolute; left: 0; top: .7em;
                    width: 7px; height: 1px; background: var(--brass);
                    /* Drawn rather than simply present: each rule extends from
                       the left as its line arrives, which gives the list a
                       reading order instead of appearing all at once. */
                    transform: scaleX(0); transform-origin: left;
                    transition: transform 700ms var(--motion-ease);
                    transition-delay: calc(var(--i, 0) * 70ms + 260ms); }
.js [data-motion~="stagger"].is-in .facts dd::before,
.js .facts[data-motion].is-in dd::before { transform: scaleX(1); }

/* ─── Location ──────────────────────────────────────────────────────────── */
.location__actions { display: flex; flex-wrap: wrap; gap: 12px; margin: 26px 0 0; }
.location__coords {
  margin: 16px 0 0; font-size: 13px; color: var(--muted);
  display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
}
.location__coords span { letter-spacing: .14em; text-transform: uppercase; font-size: 11px; }
.location__coords a { font-variant-numeric: tabular-nums; text-underline-offset: 3px; }
/* The map, not a photograph: shown whole rather than cropped, because a map
   cropped to a decorative aspect ratio loses the two places it is about.
   It is also the widest thing in the section — the two labelled points and the
   route between them have to stay readable, and at a small size they do not. */
.location__map { display: block; margin: 0; }
.location__map a {
  display: block; border-radius: var(--r-panel); overflow: hidden;
  border: 1px solid var(--line);
  box-shadow: 0 18px 40px -30px rgba(20, 16, 14, .45);
  transition: border-color var(--med) var(--ease), box-shadow var(--med) var(--ease);
}
.location__map a:hover { border-color: var(--brass); box-shadow: 0 22px 46px -28px rgba(20, 16, 14, .55); }
.location__map img { width: 100%; height: auto; display: block; }

/* Desktop and tablet: the map takes the larger half. */
@media (min-width: 761px) {
  #location .split { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); }
}
/* Phone: full-bleed to the gutters — a map is only useful if you can read the
   labels on it, and at half a phone width you cannot. */
@media (max-width: 760px) {
  .location__map {
    width: calc(100% + var(--gutter) * 2);
    margin-inline: calc(var(--gutter) * -1);
  }
  .location__map a { border-radius: 0; border-left: 0; border-right: 0; }
  .location__map figcaption { padding-inline: var(--gutter); }
}
.location__map figcaption {
  margin-top: 12px; font-size: 13px; line-height: 1.55; color: var(--muted);
}

/* ─── Triptych ──────────────────────────────────────────────────────────── */
.triptych {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(28px, 4vw, 56px);
  max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter);
}
.triptych h3 { font-size: 24px; margin-bottom: 12px; }
.triptych p  { color: var(--muted); margin: 0; }

/* ─── Placeholders ──────────────────────────────────────────────────────── */
/* Photographs are uploaded through the admin panel. Until then these read as
   a deliberate part of the design rather than as broken images. */
.ph {
  position: relative;
  background:
    linear-gradient(135deg, rgba(255,255,255,.5), rgba(255,255,255,0) 60%),
    linear-gradient(150deg, #EFE5D6, #D9C7AC 55%, #BFA684);
  display: grid; place-items: center;
}
.ph--tall { aspect-ratio: 3 / 4; }
.ph--wide { aspect-ratio: 4 / 3; }
.ph::after {
  content: attr(data-slot) ' · ' attr(data-ratio);
  font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: rgba(42, 42, 40, .42);
  border: 1px solid rgba(42, 42, 40, .18); padding: 5px 11px;
}

/* ─── Interiors carousel ────────────────────────────────────────────────── */
/* The rooms, at their native 3:4. CSS scroll-snap does the work — no library,
   and no JavaScript intercepting the scroll, so a trackpad, a touchscreen and
   the arrow buttons all drive the same thing. */
.shots__head {
  display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between;
  gap: 20px; max-width: var(--wrap); margin: 0 auto clamp(26px, 4vw, 44px);
  padding-inline: var(--gutter);
}
.shots__head .section-head { margin: 0; text-align: left; max-width: 46ch; }

/* Unit filter. Rendered only once photographs have actually been assigned to a
   unit in the admin panel — until then an "Casa 1 / Casa 2" tab strip would be
   filtering nothing, so gallery.js leaves it out. */
.shots__controls { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
.shots__tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.shots__tabs button {
  padding: 10px 18px; min-height: 44px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: none; cursor: pointer;
  font-size: 12px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
  transition: background var(--med) var(--ease), color var(--med) var(--ease),
              border-color var(--med) var(--ease);
}
.shots__tabs button:hover { border-color: var(--ink); color: var(--ink); }
.shots__tabs button[aria-pressed="true"] {
  background: var(--ink); border-color: var(--ink); color: var(--shell);
}

.shots__track {
  display: flex; gap: clamp(10px, 1.2vw, 18px);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--gutter);
  padding: 4px var(--gutter) 8px;
  scrollbar-width: none;
}
.shots__track::-webkit-scrollbar { display: none; }

.shots__track a {
  flex: 0 0 auto;
  width: 78%;                        /* ~1.15 tiles visible on a phone */
  scroll-snap-align: start;
  border-radius: var(--r-panel);
  overflow: hidden;
  aspect-ratio: 3 / 4;               /* the source ratio — nothing is cropped */
  background: linear-gradient(150deg, #F0E3DA, #D3B9AC);
  text-decoration: none;
  position: relative;
}
.shots__track img {
  width: 100%; height: 100%; object-fit: cover;
  transition: transform 1s ease-in-out;
}
.shots__track a:hover img,
.shots__track a:focus-visible img { transform: scale(1.06); }

/* Tiles arrive in sequence rather than as one block. The container carries the
   state so a filter change can replay it by toggling one class. */
.js .shots__track > a {
  opacity: 0; transform: translateY(28px);
  transition: opacity 900ms var(--motion-ease), transform 900ms var(--motion-ease);
  transition-delay: calc(var(--i, 0) * 70ms);
}
.js .shots__track.is-in > a {
  opacity: var(--depth-fade, 1);
  transform: scale(var(--depth-scale, 1));
}
/* Two phases, because the arrival and the depth pass want opposite timings.
   The arrival is long and staggered; the depth pass runs on every scroll frame
   and has to be short, with no delay — a tile coming in from the side must not
   wait its original turn in the queue. `is-settled` is added once the last
   tile has landed. */
.js .shots__track.is-settled > a {
  transition: opacity 260ms var(--ease), transform 260ms var(--ease);
  transition-delay: 0s;
}
.shots__track.is-dragging { cursor: grabbing; scroll-snap-type: none; }
.shots__track.is-dragging a { pointer-events: none; }
.shots__track:not(.is-dragging) { cursor: grab; }

/* Fills as the strip moves. The hero's dash language rather than a scrollbar. */
.shots__progress {
  position: relative;
  height: 2px; margin-top: clamp(20px, 3vw, 32px);
  background: var(--line);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.shots__progress::after {
  content: ''; position: absolute; inset: 0;
  background: var(--ink);
  transform-origin: left;
  transform: scaleX(calc(0.08 + 0.92 * var(--progress, 0)));
  transition: transform 220ms var(--ease);
}

@media (min-width: 700px)  { .shots__track a { width: 38%; } }
@media (min-width: 1100px) { .shots__track a { width: 27%; } }

.shots__arrows { display: flex; gap: 8px; }
.shots__arrows button {
  width: 44px; height: 44px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--shell); cursor: pointer;
  font-size: 17px; line-height: 1; color: var(--ink);
  transition: opacity var(--med) var(--ease), border-color var(--med) var(--ease);
}
.shots__arrows button:hover { border-color: var(--ink); }
/* Hidden rather than disabled at the ends: a control that cannot do anything
   should not hold a tab stop. */
.shots__arrows button[hidden] { display: none; }


/* ─── Social links ──────────────────────────────────────────────────────── */
/* Rendered by social.js only for accounts that have an address and are
   published, so this is often empty — hence no fixed width or margin that
   would leave a gap when nothing is shown. */
.social { display: flex; align-items: center; gap: 4px; }
.social:empty { display: none; }
.social__link {
  display: grid; place-items: center;
  width: 44px; height: 44px; border-radius: var(--r-pill);
  color: inherit; text-decoration: none;
  transition: background var(--med) var(--ease), color var(--med) var(--ease);
}
.social__link:hover { background: rgba(42, 39, 36, .08); color: var(--terracotta); }
.social--header { margin-right: 4px; }
.site-header.over-hero:not(.is-stuck) .social__link { color: rgba(255, 255, 255, .88); }
.site-header.over-hero:not(.is-stuck) .social__link:hover {
  background: rgba(255, 255, 255, .16); color: #fff;
}
.social--footer { margin-top: 14px; }
.site-footer .social__link { color: #E9E2D9; }
.site-footer .social__link:hover { background: rgba(233, 226, 217, .14); color: #fff; }
@media (max-width: 900px) { .social--header { display: none; } }

/* ─── Admin preview banner ──────────────────────────────────────────────── */
/* Shown on the public gallery when a signed-in admin is previewing, so it is
   never ambiguous whether what is on screen is what a guest would see. */
.preview-bar {
  position: sticky; top: var(--header-h); z-index: 80;
  background: var(--sage); color: #F2EBE0;
  padding: 10px var(--gutter);
  font-size: 13px; letter-spacing: .04em;
  display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap;
}
.preview-bar a { color: #fff; }
/* A video tile sits in the Inside track alongside the photographs; it just is
   not clickable through to the lightbox, which only knows how to show a still. */
.shots__track a.is-video { cursor: default; }
.shots__track a.is-video video {
  width: 100%; height: 100%; object-fit: cover; display: block; background: #000;
}

/* ─── Lightbox ──────────────────────────────────────────────────────────── */
.lightbox {
  position: fixed; inset: 0; z-index: 300;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20, 22, 24, .94);
  padding: clamp(20px, 5vw, 60px);
}
.lightbox[hidden] { display: none; }
.lightbox__figure { margin: 0; max-width: 100%; max-height: 100%; }
.lightbox__figure img { max-height: 84vh; width: auto; margin-inline: auto; }
.lightbox__close, .lightbox__nav {
  position: absolute; background: none; border: 0; color: #fff; cursor: pointer;
  font-size: 34px; line-height: 1; padding: 14px;
  /* The glyphs are narrow — a bare "×" made the close button 43px wide, just
     under the thumb-sized minimum on the one device where it matters most. */
  min-width: 44px; min-height: 44px;
}
.lightbox__close { top: 12px; right: 16px; font-size: 30px; }
.lightbox__nav--prev { left: 6px; top: 50%; transform: translateY(-50%); font-size: 46px; }
.lightbox__nav--next { right: 6px; top: 50%; transform: translateY(-50%); font-size: 46px; }
.lightbox__count { position: absolute; bottom: 20px; left: 0; right: 0; text-align: center;
                   color: rgba(255,255,255,.7); font-size: 12px; letter-spacing: .14em; }

/* ─── Booking ───────────────────────────────────────────────────────────── */
.booking {
  display: grid; grid-template-columns: 1.15fr 1fr;
  gap: clamp(28px, 4vw, 56px);
  align-items: start;
  background: var(--shell);
  padding: clamp(24px, 3.5vw, 48px);
  border: 1px solid var(--line);
  border-radius: var(--r-panel);
}
.booking__done { grid-column: 1 / -1; text-align: center; padding: 40px 0; }

/* Which home */
/* The house and room choices decide everything below them, so they are set out
   as a headed, centred block rather than as a quiet row of pills above the
   calendar — which is where guests were missing them. */
.pick { text-align: center; margin-bottom: 26px; }
.pick__label {
  margin: 0 0 12px;
  font-family: var(--serif);
  font-size: clamp(19px, 2.1vw, 24px);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.pick__hint {
  margin: 12px auto 0; max-width: 46ch;
  font-size: 13px; color: var(--muted);
}

.unit-pick, .room-pick {
  display: flex; gap: 10px; flex-wrap: wrap; justify-content: center;
}
.room-pick { margin-bottom: 0; }
.room-pick button {
  padding: 9px 16px; min-height: 44px;
  border: 1px solid var(--line); border-radius: var(--r-pill);
  background: none; cursor: pointer;
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted);
  transition: background var(--med) var(--ease), color var(--med) var(--ease),
              border-color var(--med) var(--ease);
}
.room-pick button:hover { border-color: var(--ink); color: var(--ink); }
.room-pick button[aria-pressed="true"] {
  background: var(--ink); border-color: var(--ink); color: var(--shell);
}

.unit-pick { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 0; }
.unit-pick button {
  /* Bigger and bolder than the room pills below: this is the first decision,
     and it was previously the same weight as everything around it. */
  padding: 13px 30px; min-height: 48px;
  border: 1.5px solid var(--line); border-radius: var(--r-pill);
  background: none; cursor: pointer;
  font-size: 14px; font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink);
  transition: background var(--med) var(--ease), color var(--med) var(--ease),
              border-color var(--med) var(--ease);
}
.unit-pick button:hover { border-color: var(--ink); color: var(--ink); }
.unit-pick button[aria-pressed="true"] {
  background: var(--terracotta); border-color: var(--terracotta); color: #fff;
}

/* Calendar */
.cal { display: grid; grid-template-columns: 1fr; gap: 26px; }
@media (min-width: 900px) { .cal { grid-template-columns: 1fr 1fr; } }

.cal__month caption {
  font-family: var(--serif); font-size: 19px; padding-bottom: 14px;
  text-align: center; caption-side: top;
}
.cal__month { width: 100%; border-collapse: collapse; }
.cal__month th {
  font-size: 10px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); padding-bottom: 8px;
}
.cal__month td { padding: 1px; text-align: center; }

.cal__day {
  /* No aspect-ratio: combined with a 44px min-height it would force 44px-wide
     cells, and seven of those plus padding overflow a 320px screen. Height is
     what makes the target tappable; width can follow the column. */
  width: 100%; min-height: 44px;
  background: none; border: 1px solid transparent; border-radius: var(--r-pill);
  cursor: pointer;
  font-size: 14px; color: var(--ink);
  transition: background var(--med) var(--ease), color var(--med) var(--ease);
}
.cal__day:hover:not(:disabled) { background: var(--clay); }
.cal__day { transition: background 200ms var(--ease), color 200ms var(--ease),
                        border-color 200ms var(--ease); }

/* The new month arrives from the side of the arrow that was pressed. One
   animation on the container, not thirty on the cells: a wave across a grid of
   dates reads as a loading state, which is the wrong message for a calendar
   that has already loaded. */
@keyframes cal-in-right { from { opacity: 0; transform: translateX(14px); } }
@keyframes cal-in-left  { from { opacity: 0; transform: translateX(-14px); } }
.cal--in-right .cal__month { animation: cal-in-right 420ms var(--motion-ease) both; }
.cal--in-left  .cal__month { animation: cal-in-left  420ms var(--motion-ease) both; }
.cal__day:disabled { color: var(--line); cursor: not-allowed; text-decoration: line-through; }
.cal__day.is-start, .cal__day.is-end { background: var(--terracotta); color: #fff; }
.cal__day.is-between { background: rgba(194, 112, 61, .14); border-radius: 0; }
.cal__day.is-start { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.cal__day.is-end   { border-top-left-radius: 0;  border-bottom-left-radius: 0; }
.cal__day.is-today { border-color: var(--brass); }

.cal__nav { display: flex; justify-content: space-between; align-items: center;
            grid-column: 1 / -1; margin-bottom: -6px; }
.cal__nav button {
  background: none; border: 1px solid var(--line); border-radius: var(--r-pill);
  cursor: pointer;
  width: 44px; height: 44px; font-size: 16px; line-height: 1;
}
.cal__nav button:hover:not(:disabled) { border-color: var(--ink); }
.cal__nav button:disabled { opacity: .3; cursor: not-allowed; }
.cal__hint { font-size: 13px; color: var(--muted); margin: 16px 0 0; text-align: center; }

/* Quote */
.booking__form input, .booking__form textarea, .booking__form select {
  transition: border-color 300ms var(--ease), box-shadow 300ms var(--ease);
}
.booking__form input:focus, .booking__form textarea:focus, .booking__form select:focus {
  border-color: var(--terracotta);
  box-shadow: 0 0 0 3px rgba(194, 112, 61, .14);
}

/* Underlines that grow from the left rather than switching on. background-size
   animates on the compositor; text-decoration does not. */
.site-footer a:not(.social__link) {
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size 320ms var(--ease);
}
.site-footer a:not(.social__link):hover,
.site-footer a:not(.social__link):focus-visible { background-size: 100% 1px; }
.social__link { transition: transform 260ms var(--ease), background 260ms var(--ease), color 260ms var(--ease); }
.social__link:hover { transform: translateY(-2px); }

.quote { border: 1px solid var(--line); border-radius: var(--r-input);
         padding: 18px 20px; margin-bottom: 24px; background: #fff; }
.quote__dates { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px;
                font-size: 14px; margin-bottom: 12px; }
.quote__total { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.quote__label  { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.quote__total strong { font-family: var(--serif); font-size: 27px; font-weight: 400; }
.quote__nights { font-size: 13px; color: var(--muted); }
.quote__note   { font-size: 12px; color: var(--muted); margin: 10px 0 0; }
.quote.is-error { border-color: #B4534F; }
.quote__lines { list-style: none; margin: 0 0 14px; padding: 0 0 12px;
               border-bottom: 1px solid var(--line); }
.quote__line { display: grid; grid-template-columns: 1fr auto; column-gap: 12px;
               padding: 8px 0; font-size: 14px; }
.quote__line + .quote__line { border-top: 1px dashed var(--line); }
.quote__when  { color: var(--ink); }
.quote__count { color: var(--muted); }
.quote__sum   { font-variant-numeric: tabular-nums; text-align: right; }
.quote__how   { grid-column: 1 / -1; font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.quote__tag   { display: inline-block; font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
                color: var(--terracotta-dark); border: 1px solid #EBCDBB; border-radius: var(--r-pill);
                padding: 1px 8px; margin-right: 6px; vertical-align: 1px; }

/* Fields */
.field { margin-bottom: 18px; }
.field--narrow input { max-width: 110px; }
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
label { display: block; font-size: 11px; font-weight: 500; letter-spacing: .14em;
        text-transform: uppercase; color: var(--muted); margin-bottom: 7px; }
label em { font-style: normal; text-transform: none; letter-spacing: 0; opacity: .8; }
input, textarea, select {
  width: 100%; font: inherit; font-size: 16px;   /* 16px stops iOS zooming on focus */
  padding: 11px 14px; border: 1px solid var(--line); border-radius: var(--r-input);
  background: #fff; color: var(--ink);
  transition: border-color var(--med) var(--ease);
}
select { cursor: pointer; }
input:hover, textarea:hover, select:hover { border-color: #D8C7BC; }
input:focus, textarea:focus, select:focus { border-color: var(--terracotta); outline: none; }
input[aria-invalid="true"], textarea[aria-invalid="true"] { border-color: #B4534F; }
textarea { resize: vertical; }

.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form-error { color: #8C3438; font-size: 14px; margin-bottom: 16px; }

/* ─── Footer ────────────────────────────────────────────────────────────── */
.site-footer { background: var(--sage); color: #E9E2D9; padding: clamp(56px, 7vw, 88px) 0 32px; }
.site-footer .eyebrow { color: var(--brass); }
.footer-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 40px; }
.site-footer .wordmark__name { font-family: var(--serif); font-size: 24px; margin: 0 0 6px; }
.site-footer .muted { color: #B3B5AC; }
.site-footer .lang button { color: #B3B5AC; }
.site-footer .lang button[aria-pressed="true"] { color: #fff; }
.contact { list-style: none; margin: 0; padding: 0; }
.contact li { margin-bottom: 7px; }
.contact a { color: #E9E2D9; text-decoration: none; border-bottom: 1px solid rgba(233,226,217,.28); }
.contact a:hover { border-color: var(--brass); }
.footer-base { margin-top: 48px; padding-top: 22px;
               border-top: 1px solid rgba(233,226,217,.18); color: #9DA096; }

/* ─── Scroll reveal ─────────────────────────────────────────────────────── */
/* Hiding is gated behind the `js` class, which an inline script in <head> adds
   before first paint. If scripting is off, blocked, or the module fails to
   load, every section simply renders — a broken script must never leave the
   page blank below the hero. */
/* ─── Motion vocabulary ─────────────────────────────────────────────────── */
/* Five behaviours, set from the markup with data-motion and driven by the one
   observer in motion.js. All of it is transform and opacity, so none of it can
   force a layout mid-scroll.

   The pacing is the point. A luxury feel comes from long, decelerating moves
   that finish where the eye already was — not from more animation. Hence 1.1s
   arrivals on an ease with a very long tail, rather than the 300ms most UI
   guidance suggests: that advice is about interface feedback, where a delay is
   a cost. This is editorial, where the movement is the content settling. */
:root {
  --motion-arrive: 1100ms;
  --motion-ease: cubic-bezier(0.16, 1, 0.3, 1);   /* strong start, long glide */
  --motion-step: 90ms;
}

.js [data-motion] { will-change: opacity, transform; }
/* Once it has arrived it will never move again — holding a compositor layer for
   every section for the rest of the visit is a real cost on a phone. */
.js [data-motion].is-in { will-change: auto; }

/* rise — the default arrival. */
.js [data-motion~="rise"] {
  opacity: 0; transform: translateY(32px);
  transition: opacity var(--motion-arrive) var(--motion-ease),
              transform var(--motion-arrive) var(--motion-ease);
}
.js [data-motion~="rise"].is-in { opacity: 1; transform: none; }

/* stagger — children arrive in sequence. motion.js writes --i on each child;
   the container itself does not move, so a stagger can sit on a rise. */
.js [data-motion~="stagger"] > * {
  opacity: 0; transform: translateY(24px);
  transition: opacity var(--motion-arrive) var(--motion-ease),
              transform var(--motion-arrive) var(--motion-ease);
  transition-delay: calc(var(--i, 0) * var(--motion-step));
}
.js [data-motion~="stagger"].is-in > * { opacity: 1; transform: none; }
/* A container that is both rise and stagger would move its children twice. */
.js [data-motion~="stagger"][data-motion~="rise"] { opacity: 1; transform: none; transition: none; }

/* mask — the line rises into view from behind its own top edge. Kept for
   blocks that are not headings; headings use `words` below, which is the same
   idea one word at a time and far harder to miss. The resting inset is negative
   at the bottom because Fraunces' descenders sit below the baseline and a flush
   clip would shear the tail off every 'p' and 'g'. */
.js [data-motion~="mask"] {
  opacity: 0;
  transform: translateY(0.42em);
  clip-path: inset(0 0 105% 0);
  transition: opacity var(--motion-arrive) var(--motion-ease),
              transform var(--motion-arrive) var(--motion-ease),
              clip-path var(--motion-arrive) var(--motion-ease);
}
.js [data-motion~="mask"].is-in {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 -25% 0);
}

/* words — each word climbs out from behind its own line, 55ms after the one
   before. This is the single most visible thing on the page after the hero: a
   heading fading up as one block is easy to scroll past without registering,
   whereas a line assembling itself is not.

   The outer span clips and the inner one moves, which is what makes it read as
   emerging from behind the line rather than merely sliding. */
.js [data-motion~="words"] .w {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* Room for descenders, pulled back so it does not change the line box. */
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
.js [data-motion~="words"] .w__i {
  display: inline-block;
  transform: translateY(115%);
  transition: transform 1000ms var(--motion-ease);
  transition-delay: calc(var(--i, 0) * 55ms);
}
.js [data-motion~="words"].is-in .w__i { transform: none; }

/* settle — a photograph relaxes to its true size instead of appearing at it. */
.js [data-motion~="settle"] {
  opacity: 0;
  transition: opacity 1400ms var(--motion-ease);
}
.js [data-motion~="settle"].is-in { opacity: 1; }

/* Scroll coupling — the frame clips, the child drifts and grows inside it for
   the whole time it is on screen. motion.js writes the transform every frame,
   so there is deliberately no transition here: a transition would lag the
   scroll and turn a direct response into a rubber band. */
/* Only a frame that is explicitly a media frame clips. Sections are frames too
   — they are what a text column measures its progress against — and clipping a
   whole section would cut off anything that legitimately overflows it. */
.split__media[data-parallax-frame],
.place__stage[data-parallax-frame],
.hero[data-parallax-frame] { overflow: hidden; }
[data-parallax], [data-scroll-scale] { will-change: transform; }

/* ─── Responsive ────────────────────────────────────────────────────────── */
/* Tablet keeps two columns; only phone widths stack everything. 768px is
   iPad portrait, so the breakpoint sits below it rather than above. */
@media (max-width: 760px) {
  .booking { grid-template-columns: 1fr; }
  .split { grid-template-columns: 1fr; }
  .footer-grid { grid-template-columns: 1fr; }
}
@media (max-width: 700px) {
  .triptych { grid-template-columns: 1fr; }
}

/* Seven day-columns inside a padded card leave very little per day on a phone,
   so the card gives its padding back below 420px. */
@media (max-width: 420px) {
  .booking { padding: 18px 12px; }
  .cal__month td { padding: 0; }
  .quote { padding: 14px 14px; }
}
@media (max-width: 900px) {
  .shots__head { flex-direction: column; align-items: flex-start; }
  .shots__arrows { display: none; }   /* swipe is the interaction here */
}

@media (max-width: 900px) {
  .burger { display: block; }
  .site-nav {
    position: fixed; inset: var(--header-h) 0 auto; flex-direction: column; gap: 0;
    background: var(--shell); padding: 8px var(--gutter) 24px;
    box-shadow: 0 12px 30px -18px rgba(0,0,0,.4);
    transform: translateY(-8px); opacity: 0; pointer-events: none;
    transition: opacity var(--med) var(--ease), transform var(--med) var(--ease);
  }
  .site-nav.is-open { transform: none; opacity: 1; pointer-events: auto; }
  .site-nav a { padding: 14px 0; border-bottom: 1px solid var(--line); }
  .header-cta { display: none; }
  .field-row { grid-template-columns: 1fr; }
}

/* Mobile sticky booking bar — a fourth touchpoint, appearing past the hero. */
.sticky-cta {
  position: fixed; inset: auto 0 0; z-index: 90;
  padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom));
  background: rgba(252,250,247,.97); border-top: 1px solid var(--line);
  transform: translateY(110%); transition: transform var(--med) var(--ease);
}
.sticky-cta.is-in { transform: none; }
@media (min-width: 901px) { .sticky-cta { display: none; } }

/* The language switcher lives in the menu panel on small screens — four
   languages plus a wordmark and a burger will not fit a 320px bar. main.js
   moves the element; these rules dress it once it is in there. */
@media (max-width: 900px) {
  .site-nav .lang {
    display: flex; flex-wrap: wrap; gap: 4px;
    padding: 14px 0 4px; border-top: 1px solid var(--line); width: 100%;
  }
  .site-nav .lang button { font-size: 14px; padding: 10px 14px; letter-spacing: .06em; }
  .site-header .header-actions > .lang { display: none; }
}

/* ─── Scroll to top ─────────────────────────────────────────────────────── */
.to-top {
  position: fixed; right: var(--gutter); bottom: 28px; z-index: 95;
  width: 46px; height: 46px; border-radius: var(--r-pill);
  display: grid; place-items: center;
  border: 1px solid var(--line);
  background: rgba(253, 251, 249, .94);
  color: var(--ink); cursor: pointer;
  box-shadow: 0 12px 28px -14px rgba(20, 16, 14, .5);
  /* Out of the way until earned: invisible AND unfocusable, so it never
     appears as a stray tab stop at the top of the page. */
  opacity: 0; visibility: hidden; transform: translateY(10px);
  transition: opacity var(--med) var(--ease), transform var(--med) var(--ease),
              visibility var(--med), background var(--med) var(--ease),
              border-color var(--med) var(--ease);
}
.to-top.is-in { opacity: 1; visibility: visible; transform: none; }
.to-top:hover { border-color: var(--ink); background: #fff; }
@supports (backdrop-filter: blur(1px)) {
  .to-top { background: rgba(253, 251, 249, .78); backdrop-filter: saturate(150%) blur(10px); }
}
/* Clear of the mobile booking bar, which owns the bottom edge. */
@media (max-width: 900px) { .to-top { bottom: calc(84px + env(safe-area-inset-bottom)); } }
@media print { .to-top { display: none; } }

/* ─── Reduced motion ────────────────────────────────────────────────────── */
/* Everything still renders; it simply arrives without movement. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  /* Every arrival state has to be neutralised explicitly. The blanket
     transition-duration override above stops the movement, but an element
     whose resting state is opacity 0 would simply never be seen — content
     trapped invisible is a far worse outcome than the animation was. */
  .js [data-motion],
  .js [data-motion~="stagger"] > *,
  .js [data-motion~="settle"] img,
  .js [data-motion~="settle"] picture {
    opacity: 1 !important;
    transform: none !important;
    clip-path: none !important;
  }
  [data-parallax] { transform: none !important; }
  .shots__track a:hover img { transform: none; }
  .js .shots__track > a { opacity: 1 !important; transform: none !important; }
  .facts dd::before { transform: scaleX(1) !important; }
  .cal--in-right .cal__month, .cal--in-left .cal__month { animation: none; }
  .social__link:hover { transform: none; }

  /* The place backdrop holds on its first photograph: no rotation, no drift.
     place.js does not start its timer, and this stops the resting animation. */
  .place__slide img { animation: none; transform: scale(1.02); }
  .place__slide { transition: none; }
  .place__slide:not(.is-active) { opacity: 0; }
  .place__slide.is-active { opacity: 1; transform: none; }

  /* The carousel keeps working — the dashes still change slide — it simply
     cuts instead of fading, holds still instead of drifting, and (in
     carousel.js) never advances on its own. The active slide must stay at
     full opacity: a reduced-motion visitor seeing a blank hero would be a
     worse outcome than the animation itself. */
  .hero__slide { transition: none; }
  .hero__slide.is-active { opacity: 1; }
  .hero__slide.is-active img { animation: none; transform: none; }
  /* A hero video is paused by carousel.js under reduced motion; this stops the
     drift on its first frame either way. */
  .hero__slide video { animation: none; transform: none; }
  .shots__track { scroll-behavior: auto; }
}

/* ─── Print ─────────────────────────────────────────────────────────────── */
@media print {
  .site-header, .sticky-cta, .booking__form, .lightbox,
  .dock, .hero__dots, .shots__arrows { display: none; }
  body { background: #fff; }
  /* The hero is a full-height photograph on screen; on paper it collapses to
     its text so the first page is not one image. */
  .hero { min-height: 0; color: var(--ink); padding-top: 0; }
  .hero__stage, .hero__scrim { display: none; }
  .hero__wordmark { text-shadow: none; font-size: 40px; }
}

/* ─── The place — photographic band ─────────────────────────────────────── */
/* Two aerials that change with a vertical push rather than a crossfade. A fade
   would dissolve a city into an island, which reads as a glitch; a push reads
   as "and also, somewhere else". The slides keep drifting while they rest, so
   the movement is continuous instead of a long still broken by a sudden slide.

   Both width treatments are here. `band--place` bleeds edge to edge;
   `place--framed` on the same section holds it to the site's measure with
   panel corners. The framed one upscales the 900px-wide Bissau photograph far
   less, which on a large display is the difference between crisp and mushy. */
.band--place {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Near-white rather than the warm off-white used on light bands: over a
     photograph the tinted version reads as grey and washed out, which is
     exactly how this section looked before. */
  color: #FFFDFB;
  background: var(--sage);        /* seen only until the first slide paints */
  padding-block: clamp(76px, 9vw, 118px);
}

.place__stage { position: absolute; inset: 0; z-index: 0; }

.place__slide {
  position: absolute; inset: 0;
  opacity: 0;
  transform: translateY(6%);
  /* --place-fade must match FADE in place.js. */
  transition: opacity var(--place-fade, 1200ms) var(--ease),
              transform var(--place-fade, 1200ms) var(--ease);
}
.place__slide.is-active  { opacity: 1; transform: translateY(0); z-index: 2; }
/* Held opaque underneath while it travels up and out, so the band's own
   background never shows through the handover. */
.place__slide.is-leaving { opacity: 1; transform: translateY(-6%); z-index: 1; }

.place__slide img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  /* The rest drift. 13s against a 4s cycle means a slide is never at the same
     offset twice, so the movement reads as continuous rather than looped. */
  animation: place-drift 13s var(--ease) infinite alternate;
}
@keyframes place-drift {
  from { transform: scale(1.08) translateY(-1.4%); }
  to   { transform: scale(1.08) translateY(1.4%); }
}

/* Heavier than the hero's scrim: this copy is body size, so it needs the 4.5:1
   normal-text threshold rather than the 3:1 large-text one, and it sits over
   pale sand and bright turquoise rather than a façade in shade. */
.place__scrim {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  /* Weighted to the middle, not to one side, because this copy is centred —
     a left-heavy gradient like the hero's would put its darkest part where
     there is no text and its lightest where the paragraph ends.
     Heavier than the hero's overall: that copy is display size and only needs
     3:1, this is body size and needs 4.5:1, and it sits over pale sand and
     bright turquoise rather than a façade in shade.

     Measured, like the hero's: `node scripts/check-hero-contrast.mjs --place`
     in api/ composites these exact stops over both photographs and samples the
     worst pixel under the copy. These values give 5.96:1 against the 4.5 it
     needs. An earlier pass sat at 9.5:1, which cleared the bar twice over and
     buried the photograph for nothing; re-run the script after any change here
     rather than guessing which way it moved. */
  background:
    radial-gradient(ellipse 82% 78% at 50% 52%,
                    rgba(22, 25, 21, .72), rgba(22, 25, 21, .53) 62%,
                    rgba(22, 25, 21, .34) 100%),
    linear-gradient(to bottom, rgba(22, 25, 21, .32), rgba(22, 25, 21, .13) 42%,
                               rgba(22, 25, 21, .38) 100%);
}

.place__copy { position: relative; z-index: 4; }
/* Brighter than the site's other light-on-dark text. Over a moving photograph
   a 92%-opacity warm white reads as grey; at full strength with a shadow to
   separate it from the water behind, it reads as text. */
.band--place .eyebrow { color: #F0C97A; letter-spacing: .2em; }
.band--place .display { color: #FFFDFB; text-shadow: 0 2px 24px rgba(16, 20, 15, .55); }
.band--place .lede {
  color: #FFFDFB;
  text-shadow: 0 1px 16px rgba(16, 20, 15, .6);
  font-weight: 400;
}

/* The hero's dashes, turned upright, because this carousel moves vertically and
   a horizontal indicator would contradict the motion. */
.place__dots {
  position: absolute; z-index: 4;
  right: clamp(16px, 3vw, 40px); top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; gap: 10px; align-items: center;
}
.place__dots button {
  position: relative; width: 44px; height: 46px;    /* the target, not the mark */
  background: none; border: 0; padding: 0; cursor: pointer;
}
.place__dots button::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%;
  width: 2px; margin-left: -1px; border-radius: var(--r-pill);
  background: rgba(255, 255, 255, .42);
  box-shadow: 0 1px 4px rgba(20, 16, 14, .75);
  transition: background var(--med) var(--ease), width var(--med) var(--ease);
}
.place__dots button:hover::after { background: rgba(255, 255, 255, .75); }
.place__dots button[aria-current="true"]::after { background: #fff; width: 3px; }
.place__dots button:focus-visible { outline: 2px solid #fff; outline-offset: -8px; border-radius: var(--r-pill); }

/* The framed alternative: same photographs, held to the measure. */
.band--place.place--framed {
  overflow: visible;
  background: none;
  padding-inline: var(--gutter);
}
.place--framed .place__stage,
.place--framed .place__scrim {
  inset: clamp(24px, 4vw, 56px) max(var(--gutter), calc((100% - var(--wrap)) / 2));
  border-radius: var(--r-panel);
  overflow: hidden;
}
/* The dashes have to come inside the panel with everything else; left on the
   viewport edge they float in the margin, half off the photograph. */
.place--framed .place__dots {
  right: calc(max(var(--gutter), (100% - var(--wrap)) / 2) + clamp(14px, 2vw, 28px));
}

@media (max-width: 700px) {
  /* On a phone the copy needs the full width, so the dashes move off the side
     and sit under it rather than crowding the text. */
  .place__dots {
    position: static; transform: none;
    flex-direction: row; justify-content: flex-start;
    margin-top: 26px;
  }
  .place__dots button { width: 46px; height: 44px; }
  .place__dots button::after {
    top: 50%; bottom: auto; left: 0; right: 0; width: auto; margin: 0;
    height: 2px; margin-top: -1px;
  }
  .place__dots button[aria-current="true"]::after { height: 3px; width: auto; }
}

/* ─── Scroll progress ───────────────────────────────────────────────────── */
/* Deliberately a hairline in terracotta rather than a chunky bar: this is a
   hospitality site, not a reading app, and the point is a sense of response
   rather than a readout. */
.scroll-progress {
  position: fixed; inset: 0 0 auto 0; z-index: 120;
  height: 2px; pointer-events: none;
  background: var(--terracotta);
  transform: scaleX(var(--p, 0));
  transform-origin: left;
}
@media (prefers-reduced-motion: reduce) { .scroll-progress { display: none; } }
@media print { .scroll-progress { display: none; } }

/* ─── Sticky-scroll gallery ─────────────────────────────────────────────── */
/* Three columns; the middle one pins to the viewport while the outer two travel
   past it. The whole effect is `position: sticky` — no scroll listener, no
   animation loop — which is why it stays smooth on a phone where a JS-driven
   equivalent would drop frames.

   The proportions come from the reference component (three equal columns, a
   full-height pinned middle holding three frames). The palette, radii and
   spacing are this site's. */
.sticky-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(8px, 1vw, 14px);
  padding: 0 clamp(8px, 1vw, 14px);
  align-items: start;
}

.sticky-gallery__col {
  display: grid;
  gap: clamp(8px, 1vw, 14px);
}

/* The pin. `align-self: start` is load-bearing: a grid item stretches to the
   row height by default, and a stretched item cannot stick — the box is already
   as tall as its track, so there is nothing for it to travel within. */
.sticky-gallery__col--pinned {
  position: sticky;
  top: clamp(8px, 1vw, 14px);
  align-self: start;
  height: calc(100svh - 2 * clamp(8px, 1vw, 14px));
  grid-template-rows: repeat(3, 1fr);
}

.sticky-gallery__fig {
  margin: 0;
  position: relative;
  overflow: hidden;
  border-radius: var(--r-panel);
  background: var(--clay);
}
/* Outer columns: a fixed frame height, so the two sides keep step with each
   other however tall the photographs are. */
.sticky-gallery__col:not(.sticky-gallery__col--pinned) .sticky-gallery__fig {
  height: clamp(240px, 30vw, 384px);
}
.sticky-gallery__col--pinned .sticky-gallery__fig { height: 100%; }

.sticky-gallery__fig button {
  display: block; width: 100%; height: 100%;
  padding: 0; border: 0; background: none;
  cursor: zoom-in;
}
.sticky-gallery__fig button:focus-visible {
  outline: 2px solid var(--terracotta);
  outline-offset: -4px;
  border-radius: var(--r-panel);
}
.sticky-gallery__fig img,
.sticky-gallery__fig video {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 900ms var(--ease);
}
.sticky-gallery__fig button:hover img { transform: scale(1.05); }
.sticky-gallery__fig.is-video { cursor: default; }

.sticky-gallery__fig.is-unpublished { outline: 3px solid var(--brass); outline-offset: -3px; }
.sticky-gallery__fig.is-unpublished::after {
  content: attr(data-badge);
  position: absolute; left: 10px; top: 10px; z-index: 2;
  background: var(--brass); color: #fff;
  font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  padding: 4px 9px; border-radius: var(--r-pill);
}

/* Two columns at tablet, one on a phone. A pinned column needs something beside
   it to travel past; with a single column there is nothing, so the pin is
   dropped rather than left sticking to nothing. */
@media (max-width: 900px) {
  .sticky-gallery { grid-template-columns: repeat(2, 1fr); }
  .sticky-gallery__col--pinned {
    position: static; height: auto; grid-template-rows: none;
  }
  .sticky-gallery__col--pinned .sticky-gallery__fig { height: clamp(240px, 40vw, 340px); }
}
@media (max-width: 620px) {
  .sticky-gallery { grid-template-columns: 1fr; }
  .sticky-gallery__col:not(.sticky-gallery__col--pinned) .sticky-gallery__fig,
  .sticky-gallery__col--pinned .sticky-gallery__fig { height: clamp(280px, 78vw, 420px); }
}

@media (prefers-reduced-motion: reduce) {
  .sticky-gallery__fig button:hover img { transform: none; }
}

/* ─── Header call button ────────────────────────────────────────────────── */
/* The number a guest most often wants, one tap away rather than a scroll to the
   footer. Hidden by the script when no number is set in the admin panel. */
.call-link {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 44px; padding: 8px 14px;   /* 44px: it is dialled with a thumb */
  border: 1px solid var(--line); border-radius: var(--r-pill);
  color: var(--ink); text-decoration: none;
  font-size: 13px; letter-spacing: .02em; white-space: nowrap;
  transition: background var(--med) var(--ease), border-color var(--med) var(--ease),
              color var(--med) var(--ease);
}
.call-link:hover { border-color: var(--terracotta); color: var(--terracotta); }
.call-link[hidden] { display: none; }
/* Over the hero the header is inverted, so this has to invert with it. */
.site-header.over-hero:not(.is-stuck) .call-link {
  color: #fff; border-color: rgba(255, 255, 255, .42);
}
.site-header.over-hero:not(.is-stuck) .call-link:hover {
  border-color: #fff; background: rgba(255, 255, 255, .12);
}
/* Below the nav breakpoint the number itself goes and the icon stays: a phone
   header has no room for it, and the icon alone still dials. */
@media (max-width: 1080px) { .call-link__number { display: none; } }
@media (max-width: 1080px) { .call-link { padding: 6px 11px; } }

/* ─── Footer contact + credit ───────────────────────────────────────────── */
.contact__address { margin-top: 10px; line-height: 1.55; color: #B3B5AC; }

.footer-base {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 8px 12px;
}
.footer-base .credit { display: inline-flex; align-items: center; gap: 10px; }
.footer-base .credit > span[aria-hidden] { opacity: .45; }
.footer-base .credit a {
  display: inline-flex; align-items: center; gap: 8px;
  color: inherit; text-decoration: none;
}
.footer-base .credit img {
  width: 22px; height: 22px; border-radius: 4px; flex: 0 0 auto;
  /* The mark is dark-on-transparent and the footer is dark, so it needs
     lifting rather than sitting invisible against the sage. */
  background: rgba(255, 255, 255, .92); padding: 2px;
}
.footer-base .credit a:hover { color: #fff; }

@media (max-width: 620px) {
  .footer-base { flex-direction: column; align-items: flex-start; }
  .footer-base .credit > span[aria-hidden] { display: none; }
}

/* The room block before a home has been chosen: present, so a guest can see
   that rooms are bookable, but plainly not yet actionable. */
.pick.is-waiting .pick__label { color: var(--muted); }
.pick.is-waiting .pick__hint { font-style: italic; }

/* "The whole house" is not a fifth room, so it does not look like one. */
.room-pick__all {
  font-weight: 600;
  border-color: var(--muted) !important;
}
.room-pick__all[aria-pressed="true"] {
  background: var(--terracotta) !important;
  border-color: var(--terracotta) !important;
  color: #fff !important;
}
/* A chosen room is filled in ink; several filled pills read as a set, which is
   what multi-select needs to look like. */
.room-pick button[aria-pressed="true"] { font-weight: 600; }
