/* ==========================================================================
   Caribbean palette — PREVIEW OVERRIDE
   Loaded only by index.html, after styles.css.

   It redefines the same custom properties styles.css already uses, so every
   component (buttons, cards, footer, labels, forms) picks up the new colours
   with no duplicated component CSS. Only two things need a real override:
   the CTA button, whose text colour has to flip on a bright background, and
   the photo scrims, which were hardcoded green-black.

   Rolling it out to the whole site later = link this file from the other
   seven pages, or paste the :root block into styles.css and delete this file.

   Colours are drawn from the property itself: the turquoise pool, the cobalt
   furniture, the butter-yellow walls, the terracotta roof.
   Every pair below is contrast-computed; ratios are noted inline.
   ========================================================================== */

:root {
  /* --- Neutrals: warm white and sun-bleached cream --- */
  --bone:      #FDFBF6;   /* page background */
  --sand:      #FFF6E3;   /* alternating section — pale sun */
  --sand-deep: #FBE9C4;   /* image placeholder, subtle fills */
  --line:      #EFE3CC;
  --surface:   #FFFFFF;

  /* --- Ink: deep sea rather than neutral black --- */
  --ink:       #16232B;   /* 15.5:1 on --bone */
  --ink-soft:  #4A5A63;   /*  6.9:1 */
  --ink-muted: #62727B;   /*  4.8:1 */

  /* --- Primary: the deep end of the pool --- */
  --palm:      #0B6E7F;   /*  5.8:1 on --bone · white on it 6.0:1 */
  --palm-soft: #0E8598;   /* hover */
  --palm-deep: #06323C;   /* dark sections + footer — bone on it 13.3:1 */

  /* --- Accents ---
     --gold is the token styles.css uses for eyebrow labels and the primary
     CTA. Here it becomes the sea teal for labels (readable on every light
     surface: 5.8:1 on bone, 5.6:1 on sand), while the CTA button is given
     its own treatment below. */
  --gold:      #0B6E7F;
  --gold-ink:  #0B6E7F;   /* same teal works on sand — no second tone needed */
  --gold-lume: #FFC93C;   /* sun yellow on dark — 9.0:1 on --palm-deep */

  /* --- Extra Caribbean tones (used by this file only) --- */
  --turquoise:   #12B3C7; /* pool bright — decorative, or text on dark (5.4:1) */
  --aqua-bright: #7FE3F0; /* pale pool tint — display text over photography */
  --sun:        #FFC93C;  /* butter yellow — decorative, or text on dark */
  --mango:      #F5A623;  /* CTA fill — ink on it is 7.9:1 */
  --mango-deep: #E08A0B;  /* CTA hover — ink on it is 6.0:1 */
  --coral:      #D2542A;  /* terracotta roof — decorative only */
  --coral-ink:  #B8431E;  /* text-safe terracotta — 5.3:1 on --bone */

  --success:    #0F7B52;
  --success-bg: #EDF7F1;
}

/* --------------------------------------------------------------------------
   CTA button
   A bright mango fill cannot carry white text (2.0:1). It carries dark ink
   very comfortably instead (7.9:1) — and reads far more Caribbean than a
   muted brass would.
   -------------------------------------------------------------------------- */
.btn--gold {
  background: var(--mango);
  color: var(--ink);
  font-weight: 600;
}
.btn--gold:hover { background: var(--mango-deep); color: var(--ink); }

/* On dark sections the ghost button borrows the sun yellow. */
.on-dark .btn--ghost:hover { border-color: var(--sun); color: var(--sun); }

/* --------------------------------------------------------------------------
   Photo scrims
   styles.css hardcodes a green-black (20,38,29). Over a bright turquoise pool
   that reads muddy, so they are restated in deep ocean (6,50,60) at the same
   opacities — the contrast maths from the base file still holds.
   -------------------------------------------------------------------------- */
/* This photograph is far brighter than the one the base scrim was tuned for —
   white walls and full sun. The hero copy occupies roughly the bottom 55% of
   the frame, so the scrim is held near 0.83 through that band (the level that
   keeps bone text at 4.5:1 even over near-white pixels) and then released
   sharply above it, so the sky and palms still read bright and Caribbean. */
.hero__media::after {
  background: linear-gradient(to top,
    rgba(6, 50, 60, 0.90) 0%,
    rgba(6, 50, 60, 0.87) 45%,
    rgba(6, 50, 60, 0.82) 68%,
    rgba(6, 50, 60, 0.32) 92%,
    rgba(6, 50, 60, 0.26) 100%);
}

/* Eyebrows over photography.
   --gold is now sea teal, which disappears on a dark scrim. Sun yellow was the
   obvious swap, but measured against the actual hero pixels it only reaches
   2.9:1 — short of the 4.5:1 this 12px label needs. So the label text is bone
   (4.9:1 measured) and the little rule beside it carries the yellow, which is
   decorative and free to be vivid. */
.hero .t-label,
.page-hero .t-label,
.scroll-hero__content .t-label { color: var(--bone); }
.hero .t-label::before,
.page-hero .t-label::before,
.scroll-hero__content .t-label::before { background: var(--sun); }
/* The lit pool in this frame is bright cyan, and the copy sits right on top of
   it. Measured at the base 0.80 centre the eyebrow only reached 3.0:1, so the
   centre is taken to 0.90 — a night photograph absorbs that without going flat. */
.hero--center .hero__media::after {
  background: radial-gradient(ellipse 100% 100% at 50% 50%,
    rgba(6, 50, 60, 0.90) 0%,
    rgba(6, 50, 60, 0.84) 60%,
    rgba(6, 50, 60, 0.62) 100%);
}
/* Nav sits over open sky here, which is the brightest part of the frame.

   The gradient now holds its strength across the whole header and does its
   fading *below* it — hence `bottom: -48px`. The old one had faded to almost
   nothing by 60%, which was survivable while a hero scrim was always painted
   underneath. The villa hero now shows the photograph clean at scroll 0, and
   measured against the brightest part of that frame the 11px tagline came out
   at 2.94:1 — a fail. Held this way it measures 6.7:1. */
.header[data-transparent='true']::before {
  bottom: -48px;
  background: linear-gradient(to bottom,
    rgba(6, 50, 60, 0.74) 0%,
    rgba(6, 50, 60, 0.60) 62%,
    transparent 100%);
}
.hero__title, .hero .t-lead, .hero__meta, .hero--center h2,
.scroll-hero__content .t-lead {
  text-shadow: 0 1px 3px rgba(4, 30, 36, 0.38);
}

/* Translucent bars track the new warm white. */
.header[data-solid='true'] { background: rgba(253, 251, 246, 0.92); }
.book-bar { background: rgba(253, 251, 246, 0.96); }
::selection { background: var(--palm); color: var(--bone); }

.social a:hover { border-color: var(--sun); background: rgba(255, 201, 60, 0.14); }

/* --------------------------------------------------------------------------
   Caribbean detailing
   -------------------------------------------------------------------------- */

/* The hero italic picks up the pool, not the old brass.
   Full-strength --turquoise measures only 1.9:1 against the scrimmed photo, so
   the display text uses a paler tint of the same hue — 3.4:1, clearing the 3:1
   large-text threshold while still reading as pool water. */
.hero__title em { color: var(--aqua-bright); font-style: italic; }

/* A thin four-colour band under the hero — yellow, orange, turquoise, blue.
   Decorative only, so it is hidden from assistive tech. */
.reef-rule {
  height: 4px;
  border: 0;
  margin: 0;
  background: linear-gradient(to right,
    var(--sun)      0%,  var(--sun)      25%,
    var(--mango)   25%,  var(--mango)    50%,
    var(--turquoise) 50%, var(--turquoise) 75%,
    var(--palm)    75%,  var(--palm)    100%);
}

/* --------------------------------------------------------------------------
   SCROLL VIDEO HERO
   A tall scroll container with a sticky, viewport-sized stage inside it. The
   video's timeline is scrubbed by scroll position — see
   assets/js/scroll-video-hero.js, which also sets the container height.

   Sticky rather than a JS pin on purpose: it is handled by the compositor,
   it survives layout changes, and it never takes the scroll away from the
   reader.
   -------------------------------------------------------------------------- */
/* One screen is the safe default: with JavaScript off, a 220svh container
   would be mostly empty sticky space. scroll-video-hero.js sets the tall
   height inline, per breakpoint, only when it is actually going to scrub. */
.scroll-hero {
  position: relative;
  height: 100svh;
  background: var(--palm-deep);
}

/* The poster sits on the viewport itself, permanently, behind the video.
   If the video has no decoded frame to paint — still buffering, seeking past
   what it has, a codec it won't touch — the daytime still shows through
   instead of a void. Previously the <video> carried an opaque background of
   its own, so an unpainted video read as a solid dark block. */
.scroll-hero__viewport {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: clip;
  display: flex;
  align-items: flex-end;
  color: var(--bone);
  background: url('../img/hero-poster.webp') center / cover no-repeat var(--palm-deep);
}

/* The frame keeps its shot composition: cover, centred, never stretched. */
.scroll-hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  z-index: 0;
  pointer-events: none;
  background: transparent;   /* never mask the poster underneath */
}

/* No full-frame scrim.
   Every version of this that covered the whole frame — bottom-weighted, then
   side-weighted — tinted the entire photograph teal. It read as a filter over
   the architecture, which is the one thing that must stay untouched. The
   readability treatment is now a panel behind the copy and nothing else; the
   rest of the frame is the photograph, clean. */
.scroll-hero__overlay { display: none; }

/* The panel behind the copy.
   Not a rectangle with a hard edge — a blurred shape that dissolves into the
   photograph. The blur is what removes the edge; a border-radius alone still
   reads as a card sitting on top of the image.

   Geometry matters for legibility: the blob is inset NEGATIVE, so it extends
   past the text block, and the padding then keeps the text at least one blur
   radius inside it. That way the text sits in the solid core at full opacity
   while only the outer rim is soft. Shrink the padding without shrinking the
   blur and the text drifts into the falloff, where contrast collapses. */
.scroll-hero__state .stack {
  position: relative;
  isolation: isolate;          /* keeps the blob behind the text, above the video */
  background: none;
  /* Breathing room around the text — reaches ~2cm (75px at 96dpi) on a large
     screen and eases off on smaller ones, where that much padding would eat
     the column the text needs. */
  padding: clamp(1.5rem, 4.5vw, 4.7rem);
}
.scroll-hero__state .stack::before {
  content: '';
  position: absolute;
  inset: -28px;
  z-index: -1;
  background: rgba(6, 50, 60, 0.90);
  border-radius: 72px;
  filter: blur(36px);
  pointer-events: none;
}

@media (min-width: 768px) {
  .scroll-hero__content .stack { max-width: min(46rem, 64%); }
}

/* The display size is tuned for a full-width hero. Inside a panel roughly
   half that wide it broke the headline into four lines and pushed the whole
   block taller than the screen, so it steps down here. */
.scroll-hero__state .hero__title {
  font-size: clamp(2.25rem, 3.4vw + 0.6rem, 3.75rem);
}
.scroll-hero__state .t-h2 {
  font-size: clamp(1.875rem, 2.4vw + 0.6rem, 2.75rem);
}
.scroll-hero__content { padding-block: var(--s-5) var(--s-5); }

.scroll-hero__content {
  position: relative;
  z-index: 2;
  width: 100%;
  padding-block: var(--s-7) var(--s-6);
}

/* Default (and the no-JS state): the two copy blocks simply stack, both
   readable. Only once the script has taken over do they share a single grid
   cell, so the handover can cross-fade in place — nothing slides, nothing
   reflows. Keyed off an attribute the script sets, not off the `js` class,
   because that class is dropped under reduced motion. */
.scroll-hero__states > * + * { margin-top: var(--s-5); }
[data-scroll-video-ready='true'] /* Both copy blocks sit on photography, so lead text takes the light colour.
   Set here rather than inline: the after-dark block was relying on an inline
   style it lost when it moved into the hero, and fell back to the dark body
   grey — near-invisible against the night frame. */
.scroll-hero__content .t-lead { color: rgba(253, 251, 246, 0.90); }

.scroll-hero__states { display: grid; }
[data-scroll-video-ready='true'] .scroll-hero__states > * + * { margin-top: 0; }
[data-scroll-video-ready='true'] .scroll-hero__state { grid-area: 1 / 1; align-self: end; }
[data-scroll-video-ready='true'] .scroll-hero__state[data-scroll-video-night] {
  opacity: 0;
  visibility: hidden;
}

/* The cue retires once the reader has clearly started. */
.scroll-hero .scroll-cue {
  z-index: 2;
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}
.scroll-hero .scroll-cue[data-hidden='true'] {
  opacity: 0;
  transform: translate(-50%, 8px);
  pointer-events: none;
}

/* The after-dark copy sits in the same narrow left column as the daytime
   copy, so it stays inside the protected band of the scrim. */
@media (min-width: 768px) {
  .scroll-hero__state[data-scroll-video-night] .stack { max-width: min(36rem, 48%); }
}

/* --------------------------------------------------------------------------
   FLIP CARDS
   Photo and title on the front; the detail is on the back and arrives on
   hover, focus or tap. Built on a <button> so the keyboard reaches it and
   assistive tech announces it — a div with a hover style would leave both out.
   -------------------------------------------------------------------------- */
.flip {
  perspective: 1200px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: var(--r-lg);
  aspect-ratio: 4 / 3;
  display: block;
  width: 100%;
  text-align: left;

  /* The pointer tilt lives here, on the outer element, while the 180deg flip
     lives on .flip__inner. Two effects, two elements, so neither has to know
     the other exists — put both on one node and whichever writes `transform`
     last wins and the card either stops tilting or stops flipping. */
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  transform: rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transition: transform 400ms var(--ease-out);
}
.flip:hover { transition-duration: 120ms; }   /* keep up while tracking */
.flip__inner {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 620ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* :focus as well as :focus-visible — a card reached by Tab must turn, and
   relying on :focus-visible alone leaves some paths out. */
.flip:hover .flip__inner,
.flip:focus .flip__inner,
.flip:focus-visible .flip__inner,
.flip[aria-pressed='true'] .flip__inner { transform: rotateY(180deg); }

.flip__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-radius: var(--r-lg);
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  border: 1px solid var(--line);
}
.flip__face--front img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* Just enough gradient for the title to sit on the photograph. */
/* The scrim the title sits on.

   The stops are not decorative. Titles wrap to two lines and reach 37.5% of
   the card's height, and the first version had already fallen to 0.29 alpha
   by then — which is fine over a dark garden photograph and fails badly over
   a bright one. Measured against the actual rendered pixels, the bedroom and
   sailing cards came out at 1.88–2.61:1 against a 3:1 requirement.

   These stops hold ~0.64 alpha at 37.5%, which is 4.37:1 even if the photo
   behind is pure white — so the card stays legible whatever is shot next.
   Anything above ~82% is untouched, so the photograph still reads as a
   photograph rather than a dark slab. If a title ever grows to three lines
   it reaches ~51%, still covered at ~0.52 alpha. */
.flip__face--front::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
    rgba(6, 50, 60, 0.90) 0%,
    rgba(6, 50, 60, 0.74) 26%,
    rgba(6, 50, 60, 0.58) 45%,
    rgba(6, 50, 60, 0.30) 65%,
    rgba(6, 50, 60, 0.10) 82%,
    rgba(6, 50, 60, 0) 100%);
}
.flip__face--back {
  transform: rotateY(180deg);
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-4);
  background: var(--palm-deep);
  border-color: var(--palm-deep);
}

.flip__title {
  position: relative;
  z-index: 1;
  display: block;
  padding: var(--s-3);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.2;
  color: var(--bone);
}
.flip__face--back .flip__title { padding: 0; color: var(--sun); }
.flip__text {
  display: block;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: rgba(253, 251, 246, 0.86);
}

/* Touch devices have no hover, so the flip is driven by a real tap; main.js
   toggles aria-pressed. Also a hint that there is more on the back. */
@media (hover: none) {
  .flip__face--front::before {
    content: '';
    position: absolute;
    top: var(--s-2); right: var(--s-2);
    z-index: 2;
    width: 26px; height: 26px;
    border-radius: 50%;
    background: rgba(253, 251, 246, 0.9)
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2306323C' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M12 8v8M8 12h8'/%3E%3C/svg%3E") center / 16px no-repeat;
  }
}

/* The global reduced-motion rule collapses every transition to ~0, which would
   make these snap between faces instead of turning. The turn is short, it only
   happens when you deliberately point at or tap a card, and it is the whole
   point of the component — so it is kept, just quicker. Consistent with the
   sunset scrub, which also runs under reduced motion by your choice. */
@media (prefers-reduced-motion: reduce) {
  .flip__inner { transition-duration: 420ms !important; }
}

/* --------------------------------------------------------------------------
   CAROUSEL
   Native scroll-snap does the scrolling and the touch swipe; the script only
   adds mouse-drag and the arrow buttons.
   -------------------------------------------------------------------------- */
.section-head--split {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  max-width: none;
}
.carousel__nav { display: flex; gap: var(--s-1); flex: none; }
.carousel__btn {
  width: 48px; height: 48px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  transition: border-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out),
              opacity var(--dur-base) var(--ease-out);
}
.carousel__btn:hover { border-color: var(--palm); color: var(--palm); }
.carousel__btn[disabled] { opacity: 0.35; cursor: not-allowed; }
.carousel__btn svg { width: 20px; height: 20px; }
@media (max-width: 639px) { .carousel__nav { display: none; } }

.carousel {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  /* `proximity`, not `mandatory`. Mandatory yanks the strip to the nearest
     slide the instant you stop pushing, which over a long photo strip feels
     like being corrected rather than helped. Proximity tidies up a near-miss
     and otherwise leaves you where you put it. */
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: var(--s-1);
  cursor: grab;
}
.carousel::-webkit-scrollbar { display: none; }

/* While JS owns scrollLeft — a drag, an eased step, or the momentum after a
   drag — snapping and native smoothing both have to be out of the way. Either
   one left on will fight the animation for the same property and stutter. */
.carousel[data-dragging='true'],
.carousel[data-gliding='true'] { scroll-behavior: auto; scroll-snap-type: none; }
.carousel[data-dragging='true'] { cursor: grabbing; }

.carousel__item {
  flex: 0 0 min(78%, 30rem);
  margin: 0;
  scroll-snap-align: start;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--sand-deep);
}
.carousel__item img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}
@media (min-width: 768px)  { .carousel__item { flex-basis: min(46%, 30rem); } }
@media (min-width: 1200px) { .carousel__item { flex-basis: min(32%, 30rem); } }

@media (prefers-reduced-motion: reduce) {
  .carousel { scroll-behavior: auto; }
}

/* ==========================================================================
   Two-column text-and-media split (welcome, location)

   These two sections used to set `grid-template-columns:1fr 1fr` in a `style`
   attribute. That looks harmless and is not: an inline style cannot be
   overridden by a media query, so the layout stayed two columns all the way
   down to a phone — two ~160px columns of body text side by side. The
   responsive rules in styles.css target `.grid-2` and never applied, because
   nothing here carried that class.
   ========================================================================== */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-7);
  align-items: center;
}
@media (max-width: 899px) {
  .split { grid-template-columns: 1fr; gap: var(--s-5); }
}

/* Contact page: enquiry form beside the details card.

   This lived in a `style="grid-template-columns:1.35fr 1fr"` on the div, and
   an inline style cannot be overridden by a media query — so the phone kept
   both columns and the page scrolled 241px sideways, the one horizontal
   overflow on the whole site. Same trap `.split` was pulled out of earlier.
   The form leads, so it takes the wider column. */
.contact-grid {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: var(--s-7);
  align-items: start;
}
@media (max-width: 899px) {
  .contact-grid { grid-template-columns: 1fr; gap: var(--s-5); }
}

/* ==========================================================================
   Google Maps embed — the location section
   ========================================================================== */
.map {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-lg);
  overflow: hidden;
  /* Google's tiles are pale grey. Against the cream page the panel would have
     no edge at all, so it gets the same hairline the photographs get from
     their own contrast. */
  border: 1px solid var(--line);
  background: var(--sand-deep);
}
.map iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* The map is the one element on the page a reader can scroll *inside*. On a
   phone that is a trap: a swipe meant to move the page pans the map instead
   and the reader is stuck. Below the tablet breakpoint the iframe ignores
   pointer events until it is tapped, so the first touch scrolls past it and a
   deliberate tap hands control over. */
@media (max-width: 1023px) {
  .map:not([data-active='true']) iframe { pointer-events: none; }
  /* Anchored to the top, not the bottom. Google puts its logo and the
     "Map data ©" attribution along the bottom edge of the embed, and covering
     that breaks the Maps terms of use. */
  .map:not([data-active='true'])::after {
    content: attr(data-hint);
    position: absolute;
    inset: 0 0 auto 0;
    padding: var(--s-1) var(--s-2);
    background: rgba(6, 50, 60, 0.86);
    color: var(--bone);
    font-size: 0.8125rem;
    text-align: center;
  }
}

/* The room cards on villa.html are 3:2, matching their photographs, rather
   than the 4:3 the highlights grid uses. */
.flip--3x2 { aspect-ratio: 3 / 2; }

/* The tour cards carry a price on the back. It sits apart from the description
   and in the sun yellow, because the price is the line people are looking for
   and it should not have to be found inside a paragraph. */
.flip__price {
  display: block;
  margin-top: var(--s-2);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--sun);            /* 9.0:1 on --palm-deep */
}

/* Service cards carry a price, same idea as the tour cards: its own line, so
   nobody has to read a paragraph to find the number. Teal rather than sun
   yellow because these sit on a light card, not a dark face. */
.svc-card { display: flex; flex-direction: column; }
.svc-card .card__body { display: flex; flex-direction: column; height: 100%; }

/* The text grows a little under the pointer, same idea as the amenity rows.
   The scale is on the body rather than the card so the card's own border and
   shadow stay crisp — scaling a box with a 1px border makes the border blur. */
.svc-card .card__body {
  transform-origin: left top;
  transition: transform 320ms var(--ease-out);
}
.svc-card:hover .card__body { transform: scale(1.03); }
.svc-card { transition: box-shadow 320ms var(--ease-out), border-color 320ms var(--ease-out); }
.svc-card:hover { box-shadow: var(--e-2); border-color: var(--palm); }
.svc-card h3 { transition: color 320ms var(--ease-out); }
.svc-card:hover h3 { color: var(--palm); }

@media (prefers-reduced-motion: reduce) {
  /* Kept, like the other hover zooms: it is pointer-triggered and moves only
     the card being pointed at. The duration has to be restated because the
     global reduced-motion rule flattens it. */
  .svc-card .card__body { transition-duration: 260ms !important; }
}
.svc-card__price {
  margin-top: auto;               /* pinned to the foot, so a row lines up */
  padding-top: var(--s-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--palm);             /* 7.4:1 on --surface */
}

/* ==========================================================================
   Amenity rows — lift and grow under the pointer

   The list was correct and completely inert: thirty-odd lines of text in three
   columns with nothing to catch the eye. It now scales very slightly toward
   the pointer. `transform-origin: left` matters — growing from the centre
   makes the left edge of a text block drift inward, which reads as the type
   wobbling rather than approaching.
   ========================================================================== */
.amenity {
  transform-origin: left center;
  transition: transform 320ms var(--ease-out), color 320ms var(--ease-out);
}
.amenity:hover { transform: scale(1.035); }
.amenity:hover .amenity__title { color: var(--palm); }
.amenity__icon { transition: transform 380ms var(--ease-out), color 320ms var(--ease-out); }
.amenity:hover .amenity__icon { transform: scale(1.12) rotate(-4deg); color: var(--palm); }
.amenity__title, .amenity__desc { transition: color 320ms var(--ease-out); }

/* Kept under reduced motion, like the flip cards and for the same reason: it
   is triggered by pointing at the element, it moves only that element, and it
   never displaces something the reader is trying to hit or read. The global
   reduced-motion rule collapses transitions to ~0, so the duration has to be
   restated or the scale would snap. */
@media (prefers-reduced-motion: reduce) {
  .amenity,
  .amenity__icon { transition-duration: 260ms !important; }
  .amenity:hover .amenity__icon { transform: scale(1.08); }
}

/* ==========================================================================
   Page hero — slow drift, and moving water

   The still photograph felt frozen. Two layers fix that, both compositor-only:

   1. A very slow scale on the image itself. Twenty-eight seconds for six per
      cent, which is below the rate at which the eye reads it as an animation —
      it just stops feeling like a photograph of a stopped moment.

   2. A shimmer over the water. This is not a filter: an animated
      feTurbulence/feDisplacementMap would genuinely ripple the pixels and
      would also re-run a full-size filter every frame, which is exactly the
      kind of thing that makes a phone hot. This is two soft gradient bands
      sliding across each other under `mix-blend-mode: soft-light`, masked to
      the lower part of the frame where the pool is. It costs one composited
      transform and reads, at this opacity, as light moving on water.
   ========================================================================== */
.page-hero__media img {
  animation: hero-drift 28s ease-in-out infinite alternate;
  transform-origin: 55% 60%;
}
@keyframes hero-drift {
  from { transform: scale(1); }
  to   { transform: scale(1.06); }
}

/* The blend is contained here, so it can never reach the page behind. */
.page-hero__media { isolation: isolate; }

/* No z-index: the shimmer must paint *under* the scrim. Above it, the white
   bands would lighten the exact band the lead paragraph sits in, and the
   contrast the scrim was tuned for would no longer hold. Under it, the scrim
   still has the last word. */
.page-hero__media::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: soft-light;
  opacity: 0.5;
  background:
    repeating-linear-gradient(102deg,
      rgba(255, 255, 255, 0) 0 42px,
      rgba(255, 255, 255, 0.5) 52px,
      rgba(255, 255, 255, 0) 74px),
    repeating-linear-gradient(78deg,
      rgba(255, 255, 255, 0) 0 68px,
      rgba(255, 255, 255, 0.35) 84px,
      rgba(255, 255, 255, 0) 116px);
  /* Confined to the water. Above ~46% is sky, render and planting, where a
     shimmer would just look like a dirty screen. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 40%, #000 62%, #000 100%);
          mask-image: linear-gradient(to bottom, transparent 40%, #000 62%, #000 100%);
  animation: water 19s linear infinite;
  will-change: transform;
}
@keyframes water {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-148px, 0, 0); }   /* one full gradient period */
}

/* Ambient, continuous, and nobody asked for it — which is the clearest case
   there is for switching motion off. Both layers stop; the picture stays. */
@media (prefers-reduced-motion: reduce) {
  .page-hero__media img { animation: none; }
  .page-hero__media::before { animation: none; opacity: 0.3; }
}

/* ==========================================================================
   Scroll-revealed hero (villa.html)

   The photograph arrives clean — no scrim, no type — and both fade in as you
   scroll. Everything below reads `--hero-p`, the single 0-to-1 number
   page-hero.js writes on the section.

   The `[data-page-hero-ready]` gate matters: without JavaScript the property
   is never set, so none of this applies and the hero falls back to the plain,
   fully visible version rather than a photograph whose text never appears.
   ========================================================================== */
.page-hero[data-page-hero] { --hero-p: 0; }

.page-hero[data-page-hero-ready] {
  padding-block: 0;
  /* height comes from JS, per breakpoint */
}
.page-hero[data-page-hero-ready] .page-hero__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: clip;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding-block: calc(var(--header-h) + var(--s-5)) var(--s-6);
}

/* The scrim was there from the start before; now its opacity *is* the reveal.

   Its shape changes too. The old one ran left-to-right, which was right when
   the copy sat in the left half of a short banner. In a full-screen stage the
   copy is anchored to the bottom and runs the width, so the protection has to
   run bottom-up instead — and the crop is different at every viewport ratio,
   so it cannot depend on where the bright part of the photograph happens to
   land. Bottom-weighted covers the copy wherever the crop puts the water.

   0.80 at the foot measures ~9.6:1 against bone-white text over the worst case
   (a pure white pixel). The top half is left almost untouched, which is where
   the house, the trees and the sky are. */
.page-hero[data-page-hero-ready] .page-hero__media::after {
  opacity: var(--hero-p);
  background: linear-gradient(to top,
    rgba(6, 50, 60, 0.80) 0%,
    rgba(6, 50, 60, 0.74) 22%,
    rgba(6, 50, 60, 0.42) 44%,
    rgba(6, 50, 60, 0.16) 62%,
    rgba(6, 50, 60, 0.08) 100%);
}

/* Copy rises as it arrives. 22px — far enough to read as movement, close
   enough that it never looks like the text was somewhere else entirely. */
.page-hero[data-page-hero-ready] .page-hero__inner {
  opacity: var(--hero-p);
  transform: translate3d(0, calc((1 - var(--hero-p)) * 22px), 0);
  will-change: opacity, transform;
}

/* A thin line at the foot of the clean photograph, which retires as the copy
   takes over. It is the only thing telling a first-time visitor that the
   photograph is not the whole story. */
.page-hero__cue {
  position: absolute;
  left: 50%;
  bottom: var(--s-4);
  width: 1px;
  height: 46px;
  transform: translateX(-50%);
  background: linear-gradient(to bottom, rgba(253, 251, 246, 0), rgba(253, 251, 246, 0.85));
  opacity: 0;
}
.page-hero[data-page-hero-ready] .page-hero__cue {
  opacity: calc(1 - var(--hero-p) * 2.4);
}

/* Reduced motion keeps the reveal. It is driven entirely by the reader's own
   scrolling — nothing moves unless they move it — which is the same reasoning
   that keeps the day-to-night hero on the home page running. What it does not
   keep is the drift and the shimmer above, which run on their own. */

/* ==========================================================================
   Gallery — the caption slides up and stays as a subtitle

   It used to fade in and rise six pixels, which is a fade with a nod. This is
   a slide: the bar sits fully below the frame and travels up into it, so the
   photograph is uncovered until you ask. The item has to clip, or the bar
   would simply hang below the tile while it waits.
   ========================================================================== */
.gallery__item { overflow: hidden; border-radius: var(--r-lg); }

.gallery__caption {
  padding: var(--s-3) var(--s-3) var(--s-2);
  background: linear-gradient(to top,
    rgba(6, 50, 60, 0.94) 0%,
    rgba(6, 50, 60, 0.86) 62%,
    rgba(6, 50, 60, 0.60) 100%);
  opacity: 1;                     /* the slide does the work, not a fade */
  transform: translateY(101%);    /* 101% hides the sub-pixel seam */
  transition: transform 420ms var(--ease-out);
  line-height: 1.45;
}
.gallery__item:hover .gallery__caption,
.gallery__item:focus-visible .gallery__caption { transform: translateY(0); }

/* No hover to trigger it, so it is simply always there. Both conditions, not
   just `hover: none`: that alone also matches a stylus and some TV remotes,
   and on a laptop with a touchscreen it can flip the captions permanently on
   for someone who does have a pointer. */
@media (hover: none) and (pointer: coarse) {
  .gallery__caption { transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  /* Kept: pointer-triggered, and it moves only inside the tile being pointed
     at. The global rule flattens the duration, so it is restated. */
  .gallery__caption { transition-duration: 300ms !important; }
}

/* The tile itself lifts a little, which is what makes the caption feel like it
   belongs to the photograph rather than to the page. */
.gallery__item {
  transition: transform 420ms var(--ease-out), box-shadow 420ms var(--ease-out);
}
.gallery__item:hover { transform: translateY(-4px); box-shadow: var(--e-2); }
@media (prefers-reduced-motion: reduce) {
  .gallery__item:hover { transform: none; }
}

/* ==========================================================================
   Hover zoom on a standalone photograph
   ========================================================================== */
.media--hoverzoom { overflow: hidden; }
.media--hoverzoom img {
  transition: transform 900ms var(--ease-out);
  transform-origin: center;
}
.media--hoverzoom:hover img { transform: scale(1.06); }

@media (prefers-reduced-motion: reduce) {
  /* Kept, like the amenity rows: pointer-triggered, and it moves only the
     thing being pointed at. The global rule flattens the duration, so it has
     to be restated or the zoom would snap. */
  .media--hoverzoom img { transition-duration: 500ms !important; }
}

/* ==========================================================================
   Micro-interactions

   Everything here is transform or opacity only, so it stays on the compositor
   and never triggers layout. The JS half lives in assets/js/micro.js.
   ========================================================================== */

/* --- The cursor ring ---------------------------------------------------- */
/* Drawn around the real pointer, not instead of it. Sits above everything,
   catches nothing. */
.cursor-ring {
  position: fixed;
  top: 0; left: 0;
  width: 32px; height: 32px;
  border: 1px solid var(--palm);
  border-radius: 50%;
  pointer-events: none;
  z-index: 200;
  opacity: 0;
  transition: opacity 220ms var(--ease-out), border-color 220ms var(--ease-out);
  will-change: transform;
}
.cursor-ring[data-on='true'] { opacity: 0.55; }
.cursor-ring[data-down='true'] { opacity: 0.9; }

/* Over the dark hero and footer the teal ring disappears into the picture. */
.scroll-hero:hover ~ * .cursor-ring,
body:has(.scroll-hero:hover) .cursor-ring,
body:has(.footer:hover) .cursor-ring,
body:has(.closing:hover) .cursor-ring { border-color: var(--sun); }

/* Touch and coarse pointers never get one — the JS bails first, this is the
   belt-and-braces so a stray element can never appear. */
@media (hover: none), (pointer: coarse) {
  .cursor-ring { display: none; }
}

/* --- Nav underline, drawn rather than switched on ----------------------- */
.nav__link {
  position: relative;
  padding-block: 2px;
  text-decoration: none;
}
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;      /* retracts to the right, grows from the left */
  transition: transform 320ms var(--ease-out);
}
.nav__link:hover::after,
.nav__link:focus-visible::after { transform: scaleX(1); transform-origin: left; }

/* --- Chips lift very slightly ------------------------------------------- */
.chip {
  transition: transform 260ms var(--ease-out), border-color 260ms var(--ease-out);
}
.chip:hover { transform: translateY(-2px); border-color: var(--palm); }

/* --- Carousel photographs breathe under the pointer --------------------- */
.carousel__item img {
  transition: transform 700ms var(--ease-out), filter 700ms var(--ease-out);
}
.carousel:not([data-dragging='true']) .carousel__item:hover img {
  transform: scale(1.035);
  filter: saturate(1.06);
}

/* --- Buttons settle rather than snap ------------------------------------ */
/* The magnetic script writes `transform`, so the button's own hover cannot
   also use it. Shadow and colour carry the hover state instead. */
.btn--gold, .btn--primary, .carousel__btn {
  transition: box-shadow 260ms var(--ease-out), background-color 260ms var(--ease-out),
              border-color 260ms var(--ease-out), color 260ms var(--ease-out);
}
.btn--gold:hover, .btn--primary:hover { box-shadow: var(--e-2); }

@media (prefers-reduced-motion: reduce) {
  /* The ring stays — it is feedback, and micro.js already removes its trail.
     The travelling effects go, because those are the ones that move something
     the reader was not pointing at. */
  .chip:hover { transform: none; }
  .carousel:not([data-dragging='true']) .carousel__item:hover img { transform: none; }
  .nav__link::after { transition-duration: 1ms; }
}

/* ==========================================================================
   Availability calendar (rates.html)
   ========================================================================== */
.cal {
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  padding: clamp(1rem, 3vw, 2rem);
}
.cal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
  margin-bottom: var(--s-3);
}
.cal__label { font-family: var(--font-display); font-size: var(--fs-h3); }
.cal__nav { display: flex; gap: var(--s-1); }
.cal__navbtn {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--ink);
  transition: border-color var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
.cal__navbtn:hover:not(:disabled) { border-color: var(--palm); color: var(--palm); }
.cal__navbtn:disabled { opacity: 0.3; cursor: not-allowed; }
.cal__navbtn svg { width: 18px; height: 18px; }

.cal__grid { display: grid; gap: var(--s-5); }
@media (min-width: 720px) { .cal__grid { grid-template-columns: 1fr 1fr; } }

.cal__monthname {
  font-family: var(--font-display);
  font-size: 1.125rem;
  text-align: center;
  margin-bottom: var(--s-2);
}
.cal__days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }

/* Small phones: keep the day cells thumb-sized.

   `aspect-ratio: 1` ties a cell's height to its width, and seven columns
   inside a 375px screen come out 41px — under the touch minimum, on the one
   control that actually takes the booking. Seven 44px-wide columns cannot fit
   at this width no matter how the padding is trimmed (gutter alone eats 40px),
   so width stays as the grid gives it and the height gets a floor instead: a
   41x44 cell is comfortable under a thumb, and the month arrows go up to 44
   square to match. */
@media (max-width: 400px) {
  .cal { padding: var(--s-2) var(--s-1); }
  .cal__day { aspect-ratio: auto; min-height: 44px; }
  .cal__navbtn { width: 44px; height: 44px; }
}
.cal__weekday {
  display: grid; place-items: center;
  height: 28px;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.cal__blank { aspect-ratio: 1; }

.cal__day {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--r-md);
  background: none;
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  transition: background-color 180ms var(--ease-out), color 180ms var(--ease-out),
              transform 180ms var(--ease-out);
}
.cal__day:hover:not(:disabled) { background: var(--sand); transform: scale(1.08); }

/* Booked and past are told apart by more than colour: past simply fades, a
   booked night gets a strike through it. Colour alone would leave a
   colour-blind reader guessing which greyed cells are gone for good. */
.cal__day:disabled { cursor: not-allowed; }
.cal__day[data-state='past'] { color: var(--ink-muted); opacity: 0.35; }
.cal__day[data-state='booked'] {
  color: var(--ink-muted);
  opacity: 0.55;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

/* A booked night you are still allowed to leave on. Half-struck, so it reads
   as "partly available" rather than as either free or gone. */
.cal__day[data-state='checkout'] {
  color: var(--ink-soft);
  background: linear-gradient(135deg, transparent 50%, var(--sand-deep) 50%);
}

.cal__day[data-sel='start'],
.cal__day[data-sel='end'] {
  background: var(--palm);
  color: var(--bone);           /* 8.1:1 */
  font-weight: 600;
}
.cal__day[data-sel='between'] { background: #DCF0F3; color: var(--palm-deep); }

.cal__summary {
  margin-top: var(--s-4);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}
.cal__hint { color: var(--ink-soft); font-size: var(--fs-sm); }
.cal__hint--error { color: var(--danger); }
.cal__dates { font-size: var(--fs-sm); color: var(--ink-soft); }
.cal__nights { font-size: var(--fs-sm); color: var(--ink-muted); }
.cal__total {
  font-family: var(--font-display);
  font-size: clamp(2rem, 5vw, 2.75rem);
  line-height: 1.1;
  color: var(--palm);
  font-variant-numeric: tabular-nums;
}
.cal__note { font-size: 0.8125rem; color: var(--ink-muted); margin-top: var(--s-1); }
.cal__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.cal__clear {
  font-size: var(--fs-sm);
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}
.cal__clear:hover { color: var(--palm); }

/* The strings the script reads for its own generated text. Kept in the DOM so
   they go through the same translation pass as everything else rather than
   living in a second dictionary inside the JavaScript. */
.cal__strings { display: none; }

@media (prefers-reduced-motion: reduce) {
  .cal__day:hover:not(:disabled) { transform: none; }
}

/* ==========================================================================
   The fruit list (villa.html, the garden)

   Two columns of names with a leaf mark. Deliberately not a grid of cards:
   seven short nouns in seven boxes would give each one the visual weight of a
   feature, and they are a list of what is growing outside, not a spec sheet.
   ========================================================================== */
.fruits {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--s-1) var(--s-4);
  grid-template-columns: 1fr;
}
@media (min-width: 480px) { .fruits { grid-template-columns: 1fr 1fr; } }

/* A <button>, because pointing at one swaps the photograph beside it. All the
   button chrome has to come off by hand — appearance, border, background and
   the UA's centred, shrunk-down font. */
.fruit {
  position: relative;
  display: block;
  width: 100%;
  padding-left: 1.75rem;
  padding-block: 0.35rem;
  padding-right: 0;
  border: 0;
  background: none;
  appearance: none;
  text-align: left;
  font: inherit;
  color: var(--ink);
  font-size: var(--fs-sm);
  cursor: pointer;
  border-radius: var(--r-sm);
}
/* No focus ring of its own — styles.css:159 already rings every focusable
   thing on the site in --palm, and a second rule here would only drift. */

/* A leaf rather than a bullet or a tick. A tick reads as "included in your
   package", which is the wrong register for fruit on a tree. */
.fruit::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 12px;
  height: 12px;
  background: var(--palm);
  border-radius: 0 100% 0 100%;
  transform: rotate(-20deg);
  transition: transform 320ms var(--ease-out), background-color 320ms var(--ease-out);
}
.fruit:hover::before,
.fruit:focus-visible::before,
.fruit[aria-pressed='true']::before { transform: rotate(10deg) scale(1.15); background: var(--mango); }
.fruit__name { transition: color 320ms var(--ease-out); }
.fruit:hover .fruit__name,
.fruit:focus-visible .fruit__name,
.fruit[aria-pressed='true'] .fruit__name { color: var(--palm); }

@media (prefers-reduced-motion: reduce) {
  .fruit::before { transition-duration: 260ms !important; }
}

/* On a phone these are tap targets, not just labels — tapping one swaps the
   photograph. At the desktop line-height they came out 37px tall, under the
   44px touch minimum, and eight of them stacked make a row easy to miss. */
@media (hover: none) and (pointer: coarse) {
  .fruit {
    min-height: 44px;
    display: flex;
    align-items: center;
  }
  .fruit::before { top: 50%; margin-top: -6px; }
}

/* The photograph beside the list. `.media` already gives us position:relative
   and overflow:hidden, so the swap layer just covers it. It stays in the DOM
   with its src emptied between hovers rather than being created each time —
   the browser keeps the decoded bitmaps, so the second pass over a fruit is
   instant. Opacity, not display, so the two shots cross over each other. */
.garden-photo__swap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.04);
  transition: opacity 420ms var(--ease-out), transform 700ms var(--ease-out);
  pointer-events: none;
}
.garden-photo[data-fruit-active] .garden-photo__swap {
  opacity: 1;
  transform: scale(1);
}

/* A quiet caption so you know which fruit you are looking at once the picture
   has changed — hovering "Guavas" and seeing an unlabelled close-up of green
   fruit is not obviously an answer. */
.garden-photo__tag {
  position: absolute;
  left: var(--s-3);
  bottom: var(--s-3);
  z-index: 2;
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  background: rgba(6, 50, 60, 0.82);   /* white on this: 11.3:1 */
  color: #fff;
  font-size: var(--fs-label);
  letter-spacing: 0.04em;
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 300ms var(--ease-out), transform 300ms var(--ease-out);
  pointer-events: none;
}
.garden-photo[data-fruit-active] .garden-photo__tag {
  opacity: 1;
  transform: translateY(0);
}

/* Pointer-triggered, so it survives the reduced-motion policy — but the drift
   and the scale go, leaving a straight cut between the two photographs. */
@media (prefers-reduced-motion: reduce) {
  .garden-photo__swap {
    transform: none !important;
    transition: opacity 160ms linear !important;
  }
  .garden-photo[data-fruit-active] .garden-photo__swap { transform: none !important; }
  .garden-photo__tag { transition: opacity 160ms linear !important; transform: none !important; }
}

/* ==========================================================================
   Enquiry hand-off (contact.html)

   Shown in place of a POST when no form endpoint is configured: the message
   is already written, these two buttons just carry it. Sized and spaced like
   the calendar's send buttons so the two routes feel like one thing.
   ========================================================================== */
.form__handoff {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-3);
}
.form__handoff .btn { flex: 1 1 auto; }
@media (max-width: 479px) {
  /* Stacked, full width — two half-width buttons at this size put the labels
     on two lines each. */
  .form__handoff { flex-direction: column; }
}

/* Alternating aqua section, as a counterweight to the cream one. */
.section--aqua { background: #E6F7F9; }   /* ink 14.6:1 · teal 5.4:1 */

/* --ink-muted lands at 4.52:1 on this panel. That passes 4.5, but by 0.02 —
   near enough that a nudge to either colour would break it, and this is small
   print, which is where margin matters most. --ink-soft takes it to 7.7:1. */
.section--aqua .t-muted { color: var(--ink-soft); }

/* Fact strip — numerals in teal, label beneath. */
.section--aqua .stat__value,
.section--sand .stat__value { color: var(--palm); }

/* Cards lift onto a warm white with a slightly sunnier border on hover. */
.card--interactive:hover { border-color: var(--sand-deep); }

/* Amenity ticks in teal rather than brass. */
.amenity__icon,
.section--sand .amenity__icon { color: var(--palm); }

/* Numbered steps / feature numerals use the mango so they pop against cream. */
.numeral { color: var(--coral-ink); }

/* Pill used for small factual tags (e.g. "10 min drive"). */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 0 12px;
  border-radius: 999px;
  background: var(--sand);
  border: 1px solid var(--line);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--ink-soft);
  white-space: nowrap;
}
.chip svg { width: 15px; height: 15px; color: var(--palm); }
.section--sand .chip { background: var(--surface); }
.section--aqua .chip { background: var(--surface); border-color: #CFE9ED; }

/* Note callout — for the "we're not on the beach" honesty box. */
.note {
  border-left: 3px solid var(--turquoise);
  background: #E6F7F9;
  border-radius: 0 var(--r-md) var(--r-md) 0;
  padding: var(--s-3);
}
.note p { color: var(--ink-soft); max-width: none; }
