/* ==========================================================================
   KUBAY — theme stylesheet
   Design tokens, layout primitives, and component styles.
   ========================================================================== */

/* --- Fonts (self-hosted, latin subset) --- */
@font-face {
  font-family: "Fraunces";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/fraunces-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Fraunces";
  font-style: italic;
  font-display: swap;
  font-weight: 100 900;
  src: url("../fonts/fraunces-latin-wght-italic.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Manrope";
  font-style: normal;
  font-display: swap;
  font-weight: 200 800;
  src: url("../fonts/manrope-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url("../fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 500;
  src: url("../fonts/ibm-plex-mono-latin-500-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Design tokens --- */
:root {
  --ink: #0a0a0b;
  --ink-soft: #131215;
  --ink-raised: #1a181b;
  --paper: #f4efe6;
  --berry: #7a1440;
  --berry-bright: #a3245a;
  --berry-deep: #4e0c29;
  --gold: #c6a15b;
  --gold-soft: #d9c090;
  --mute: #a8a49f;
  /* Was #716d69, which gave 3.86:1 on --ink and 3.7:1 on --ink-raised — below
     the 4.5:1 AA floor, and it carries real text: stat labels, results counts,
     field labels, card locations. Lightened until the worst pairing (on
     --ink-raised) clears 4.5. Secondary labels read a little brighter now. */
  --mute-dim: #8a8480;
  --line: rgba(244, 239, 230, 0.1);
  --line-strong: rgba(244, 239, 230, 0.18);

  /* Surface roles.

     Components used to set `background: var(--ink)` directly. That reads as
     "the darkest surface", which is true in dark mode but wrong in light,
     where --ink IS the page colour — so cards, inputs, the footer and the
     mobile nav all rendered the same shade as the page behind them and
     disappeared. Each was then patched with its own [data-theme="light"]
     override, three times over, which is why the same selector appears in
     three override blocks with three different values.

     These tokens name the role instead of the colour, so a component says
     what it is once and both themes resolve it correctly. In dark mode all
     three still resolve to --ink, so dark rendering is unchanged. */
  --surface-card: var(--ink);   /* raised: cards, tiles, result rows */
  --surface-field: var(--ink);  /* inputs, selects, textareas */
  --surface-panel: var(--ink);  /* full-bleed chrome: footer, mobile nav */

  --font-display: "Fraunces", Georgia, serif;
  --font-body: "Manrope", system-ui, -apple-system, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, monospace;

  --wrap: 1280px;
  --gutter: 1.5rem;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

@media (min-width: 1024px) {
  :root {
    --gutter: 2.5rem;
  }
}

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

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

a {
  color: inherit;
  text-decoration: none;
  transition: color 0.3s ease;
}

button {
  font-family: inherit;
  cursor: pointer;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.1;
  margin: 0;
}

p {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

::selection {
  background: var(--berry);
  color: var(--paper);
}

:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 3px;
}

.screen-reader-text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}

.skip-link:focus {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 999;
  background: var(--berry);
  color: var(--paper);
  padding: 0.75rem 1.25rem;
  border-radius: 999px;
  width: auto;
  height: auto;
  clip: auto;
}

/* --- Layout primitives --- */
.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.section {
  padding-block: 7rem;
}

.section--tight {
  padding-block: 4.5rem;
}

.section--bordered {
  border-bottom: 1px solid var(--line);
}

.section--soft {
  background: var(--ink-soft);
}

.grid {
  display: grid;
  gap: 1.5rem;
}

/* A grid item defaults to min-width:auto, meaning it refuses to shrink below
   its content. A 480px-wide table inside a card was therefore stretching the
   card to 546px on a 375px screen and scrolling the whole page sideways — the
   .table-scroll wrapper could not contain it, because the wrapper itself had
   been widened. The ROI calculator had the same fault independently.

   Listing every grid container rather than the two that were caught: this is
   a whole class of bug, it only ever shows up on narrow screens, and each
   instance looks like a separate mystery. Overriding to 0 lets items shrink so
   their own overflow-x containers do the scrolling, which is the intent
   everywhere a grid appears in this theme. */
.acronym > *,
.contact-layout > *,
.content-results > *,
.footer-grid > *,
.footer-villas > *,
.forecast__grid > *,
.form-grid > *,
.form-grid__row > *,
.gallery > *,
.grid > *,
.numbered > *,
.panel-group > *,
.psearch__row > *,
.roi > *,
.spec-columns > *,
.stat-row > *,
.team-grid > *,
.type-grid > *,
.villa-grid > * {
  min-width: 0;
}

/* Cards are the usual thing being stretched, and they turn up as children of
   flex containers too, which have the same min-width:auto default. */
.card {
  min-width: 0;
}

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

@media (min-width: 1024px) {
  .grid--3 {
    grid-template-columns: repeat(3, 1fr);
  }
  .grid--2-lg {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* --- Type utilities --- */
.mono,
.code-chip {
  font-family: var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.tabular {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mute);
}

.eyebrow__index {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 0.125rem 0.5rem;
  font-size: 0.6875rem;
  color: var(--gold);
}

.section-title {
  margin-top: 1.25rem;
  font-size: clamp(2rem, 5vw, 3rem);
  max-width: 36ch;
}

.lede {
  margin-top: 1.5rem;
  color: var(--mute);
  font-size: 0.9375rem;
  line-height: 1.75;
  max-width: 60ch;
}

.text-gold {
  color: var(--gold);
}
.text-mute {
  color: var(--mute);
}
.text-dim {
  color: var(--mute-dim);
}

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border-radius: 999px;
  padding: 0.875rem 1.75rem;
  font-size: 0.875rem;
  letter-spacing: 0.02em;
  border: 1px solid transparent;
  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
  white-space: nowrap;
}

.btn--primary {
  background: var(--berry);
  color: var(--paper);
}
.btn--primary:hover {
  background: var(--berry-bright);
}

.btn--outline {
  border-color: var(--line-strong);
  color: var(--paper);
}
.btn--outline:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 2.5rem;
}

/* --- Scroll reveal (progressive enhancement) --- */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* No-JS and reduced-motion users see everything immediately. */
.no-js .reveal {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal {
    opacity: 1;
    transform: none;
  }
}

/* ==========================================================================
   Header / navigation
   ========================================================================== */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 50;
  transition: background-color 0.5s ease, border-color 0.5s ease;
  border-bottom: 1px solid transparent;
  background: linear-gradient(to bottom, rgba(10, 10, 11, 0.7), transparent);
}

.site-header.is-scrolled,
.site-header.is-open {
  background: rgba(10, 10, 11, 0.92);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--line);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block: 1rem;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.brand img {
  width: 32px;
  height: 32px;
  object-fit: contain;
}

.brand__name {
  font-family: var(--font-display);
  font-size: 1.125rem;
  letter-spacing: 0.02em;
}

.nav-primary ul {
  display: flex;
  align-items: center;
  gap: 2.25rem;
}

.nav-primary a {
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--mute);
}

.nav-primary a:hover,
.nav-primary .current-menu-item > a {
  color: var(--paper);
}

.header-cta {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 0.625rem 1.25rem;
  font-size: 0.875rem;
}

.header-cta:hover {
  border-color: var(--gold);
  color: var(--gold);
}

/* The burger is the only way to navigate below 1180px, and it was a 32x36
   target — under the 44px floor that WCAG 2.5.8 and every mobile HIG set.
   Sized up and centred; the icon inside is unchanged. */
.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin-right: -0.5rem; /* keeps the icon optically aligned to the edge */
  background: none;
  border: 0;
  color: var(--paper);
  padding: 0.25rem;
}

.nav-toggle svg {
  width: 24px;
  height: 24px;
}

.nav-mobile {
  display: none;
  border-top: 1px solid var(--line);
  background: var(--surface-panel);
}

.site-header.is-open .nav-mobile {
  display: block;
}

.nav-mobile ul {
  display: flex;
  flex-direction: column;
  padding-block: 1.5rem;
}

.nav-mobile a {
  display: block;
  padding-block: 0.875rem;
  border-bottom: 1px solid var(--line);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.nav-mobile .btn {
  margin-top: 1.5rem;
}

/* The navigation breakpoint lives in ONE place, further down, at 1180px.

   There used to be a second pair of rules here at 1024px, left over from before
   the header was widened. Between 1024px and 1179px the two disagreed: the
   1180px rule hid .nav-primary, and this one hid .nav-toggle with !important,
   which won. The result was a live site with no menu, no burger and no Enquire
   button across that entire range — iPad landscape and most small laptops.
   Removed rather than reconciled, so there is only one breakpoint to reason
   about next time the header changes. */

/* ==========================================================================
   Hero
   ========================================================================== */
.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  min-height: 640px;
  height: 100svh;
  overflow: hidden;
  background: var(--ink);
}

/* The entrance zoom lives on the image, and the container carries the scroll
   parallax. They cannot share an element: the animation runs with `both`, so
   its final transform persists forever and silently overrides any transform
   set from script — the parallax would simply never have appeared.

   The container is stretched 7% beyond the hero top and bottom so shifting it
   never exposes a strip of background. The hero clips overflow, so the extra
   height is invisible at rest. */
.hero__media {
  position: absolute;
  inset: -7% 0;
}

.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  animation: hero-zoom 2.2s var(--ease) both;
}

@keyframes hero-zoom {
  from {
    transform: scale(1.08);
  }
  to {
    transform: scale(1);
  }
}

.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
      to top,
      var(--ink) 0%,
      rgba(10, 10, 11, 0.4) 45%,
      rgba(10, 10, 11, 0.1) 100%
    ),
    linear-gradient(to right, rgba(10, 10, 11, 0.7), transparent 60%);
}

.hero__body {
  position: relative;
  z-index: 2;
  width: 100%;
  padding-top: 10rem;
  padding-bottom: 5rem;
}

.hero__title {
  margin-top: 1.5rem;
  max-width: 20ch;
  font-size: clamp(2.75rem, 7vw, 4.5rem);
  line-height: 0.98;
}

.hero__title em {
  font-style: italic;
  color: var(--gold);
}

.hero__lede {
  margin-top: 1.5rem;
  max-width: 46ch;
  color: var(--mute);
  line-height: 1.7;
}

.hero__lede strong {
  color: var(--paper);
  font-weight: 500;
}

/* Destination index strip — the signature structural device */
.code-strip {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  margin-top: 4rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  scrollbar-width: none;
}

.code-strip::-webkit-scrollbar {
  display: none;
}

.code-strip a {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  transition: border-color 0.3s ease;
}

.code-strip a:hover {
  border-color: var(--gold);
}

.code-strip__code {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--gold);
}

.code-strip__name {
  font-size: 0.75rem;
  color: var(--mute);
}

.code-strip a:hover .code-strip__name {
  color: var(--paper);
}

/* ==========================================================================
   Panels / cards
   ========================================================================== */
.panel-group {
  display: grid;
  gap: 1px;
  margin-top: 4rem;
  border: 1px solid var(--line);
  border-radius: 1rem;
  overflow: hidden;
}

@media (min-width: 768px) {
  .panel-group--2 {
    grid-template-columns: repeat(2, 1fr);
  }
  .panel-group--3 {
    grid-template-columns: repeat(3, 1fr);
  }
}

.panel {
  background: var(--ink-soft);
  padding: 2.5rem;
}

@media (min-width: 1024px) {
  .panel {
    padding: 3.5rem;
  }
}

.panel h3 {
  margin-top: 1rem;
  font-size: 1.5rem;
}

.panel p {
  margin-top: 1rem;
  color: var(--mute);
  font-size: 0.9375rem;
  line-height: 1.7;
}

.card {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background: var(--surface-card);
  padding: 2rem;
}

@media (min-width: 1024px) {
  .card {
    padding: 2.5rem;
  }
}

/* KUBAY acronym row */
.acronym {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2rem;
  margin-top: 5rem;
}

@media (min-width: 640px) {
  .acronym {
    grid-template-columns: repeat(5, 1fr);
  }
}

.acronym__item {
  border-left: 1px solid var(--line-strong);
  padding-left: 1rem;
}

.acronym__letter {
  font-family: var(--font-display);
  font-size: 1.875rem;
  color: var(--gold);
}

.acronym__word {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mute-dim);
  margin-left: 0.5rem;
}

.acronym__detail {
  margin-top: 0.5rem;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--mute);
}

/* ==========================================================================
   Villa card
   ========================================================================== */
.villa-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 1rem;
  background: var(--ink-soft);
  transition: border-color 0.5s ease;
  height: 100%;
}

.villa-card:hover {
  border-color: var(--gold);
}

.villa-card__media {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--ink-raised);
}

.villa-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s var(--ease);
}

.villa-card:hover .villa-card__media img {
  transform: scale(1.06);
}

.villa-card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(10, 10, 11, 0.8), transparent 55%);
}

.villa-card__code,
.villa-card__roi {
  position: absolute;
  z-index: 2;
  border-radius: 999px;
  padding: 0.25rem 0.625rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  backdrop-filter: blur(6px);
}

.villa-card__code {
  top: 1rem;
  left: 1rem;
  border: 1px solid var(--line-strong);
  background: rgba(10, 10, 11, 0.6);
  color: var(--gold);
}

.villa-card__roi {
  bottom: 1rem;
  right: 1rem;
  background: rgba(10, 10, 11, 0.7);
  color: var(--paper);
}

.villa-card__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.5rem;
}

.villa-card__title {
  font-size: 1.5rem;
}

.villa-card__meta {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--mute-dim);
}

.villa-card__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-top: auto;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.villa-card__price-label {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
}

.villa-card__price {
  margin-top: 0.25rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.125rem;
}

.villa-card__arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  color: var(--mute);
  transition: border-color 0.3s ease, color 0.3s ease;
}

.villa-card:hover .villa-card__arrow {
  border-color: var(--gold);
  color: var(--gold);
}

/* ==========================================================================
   Filter bar (villa archive)
   ========================================================================== */
.filters {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  border: 1px solid var(--line);
  border-radius: 1rem;
  background: var(--ink-soft);
  padding: 1.25rem;
}

@media (min-width: 1024px) {
  .filters {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    padding: 1.5rem;
  }
}

.filters__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

.filters__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
}

.filters select {
  appearance: none;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-field);
  color: var(--paper);
  padding: 0.5rem 2.25rem 0.5rem 1rem;
  font-family: inherit;
  font-size: 0.875rem;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a8a49f' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  background-size: 1rem;
}

.filters input[type="range"] {
  width: 10rem;
  accent-color: var(--gold);
}

.filters__price {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.filters__price-value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.875rem;
  min-width: 5rem;
}

.results-count {
  margin-top: 1.5rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
}

.villa-grid {
  display: grid;
  gap: 1.5rem;
  margin-top: 1.5rem;
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .villa-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .villa-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.villa-grid__item.is-hidden {
  display: none;
}

.empty-state {
  grid-column: 1 / -1;
  border: 1px dashed var(--line);
  border-radius: 1rem;
  padding: 4rem 1.5rem;
  text-align: center;
  color: var(--mute);
  font-size: 0.875rem;
}

/* ==========================================================================
   Page hero (interior pages)
   ========================================================================== */
.page-hero {
  position: relative;
  padding-top: 10rem;
  padding-bottom: 4rem;
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}

.page-hero--media {
  padding-bottom: 6rem;
}

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

.page-hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.35;
}

.page-hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, var(--ink), rgba(10, 10, 11, 0.65));
}

.page-hero__body {
  position: relative;
  z-index: 1;
}

.page-hero__title {
  margin-top: 1.25rem;
  font-size: clamp(2.5rem, 6vw, 4rem);
  max-width: 18ch;
}

/* ==========================================================================
   Single villa
   ========================================================================== */
.villa-hero {
  position: relative;
  height: 70svh;
  min-height: 460px;
  overflow: hidden;
}

.villa-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.villa-hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, var(--ink), rgba(10, 10, 11, 0.25));
}

.villa-hero__body {
  position: absolute;
  inset: auto 0 0 0;
  padding-bottom: 3rem;
}

.villa-hero__title {
  margin-top: 1rem;
  font-size: clamp(2.5rem, 6vw, 4rem);
}

.stat-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2rem;
  margin-top: 3rem;
}

@media (min-width: 768px) {
  .stat-row--4 {
    grid-template-columns: repeat(4, 1fr);
  }
  .stat-row--3 {
    grid-template-columns: repeat(3, 1fr);
  }
}

.stat__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.75rem;
  color: var(--paper);
}

.stat__value--gold {
  color: var(--gold);
}

.stat__label {
  margin-top: 0.375rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
}

/* Breakdown table */
.table-scroll {
  overflow-x: auto;
  /* Belt to the min-width:0 braces above: even in a container that has not
     been taught to shrink, the wrapper never exceeds the space it was given. */
  max-width: 100%;
  margin-top: 1.5rem;
}

.data-table {
  width: 100%;
  min-width: 480px;
  border-collapse: collapse;
  text-align: left;
  font-size: 0.875rem;
}

.data-table thead th {
  border-bottom: 1px solid var(--line);
  padding-bottom: 0.75rem;
  font-weight: 400;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
}

.data-table tbody tr {
  border-bottom: 1px solid var(--line);
}

.data-table td {
  padding-block: 0.75rem;
  color: var(--mute);
}

.data-table td:first-child {
  color: var(--paper);
}

.data-table .num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  text-align: right;
}

.data-table tr.is-total td {
  color: var(--paper);
  font-weight: 500;
}

.data-table tr.is-total .num {
  color: var(--gold);
}

/* Gallery */
.gallery {
  display: grid;
  gap: 1rem;
  grid-template-columns: 1fr;
  margin-top: 2rem;
}

@media (min-width: 640px) {
  .gallery {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .gallery {
    grid-template-columns: repeat(3, 1fr);
  }
}

.gallery figure {
  margin: 0;
  border-radius: 0.75rem;
  overflow: hidden;
  aspect-ratio: 4 / 3;
  background: var(--ink-raised);
}

.gallery img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.7s var(--ease);
}

.gallery figure:hover img {
  transform: scale(1.05);
}

/* Floor plan spec lists */
.spec-columns {
  display: grid;
  gap: 2.5rem;
  margin-top: 2rem;
}

@media (min-width: 768px) {
  .spec-columns {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .spec-columns {
    grid-template-columns: repeat(4, 1fr);
  }
}

.spec-list__title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold);
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line-strong);
}

.spec-list li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.5rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.8125rem;
  color: var(--mute);
}

.spec-list li span:last-child {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  /* Was nowrap, to stop a price splitting across lines. But a formatted price
     has no spaces in it, so normal wrapping never breaks one anyway — while
     nowrap did force any multi-word value onto a single line. A project's unit
     types ran to 554px inside a 375px screen and scrolled the whole page.
     Wrapping, right-aligned, keeps the number behaviour and fixes the rest. */
  white-space: normal;
  text-align: right;
  overflow-wrap: anywhere;
}

.spec-list li.is-total {
  border-bottom: 0;
  color: var(--paper);
  font-weight: 500;
}

/* ==========================================================================
   Investment model
   ========================================================================== */
.schedule__row {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-block: 0.75rem;
  border-bottom: 1px solid var(--line);
}

.schedule__row:last-child {
  border-bottom: 0;
}

.schedule__pct {
  width: 3.5rem;
  flex-shrink: 0;
  text-align: right;
  font-family: var(--font-display);
  font-size: 1.125rem;
  color: var(--gold);
}

.schedule__bar-track {
  height: 0.375rem;
  width: 100%;
  border-radius: 999px;
  background: var(--ink-raised);
  overflow: hidden;
}

.schedule__bar-fill {
  height: 100%;
  border-radius: 999px;
  background: var(--berry);
}

.schedule__label {
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--mute);
}

.forecast {
  margin-top: 2.5rem;
  border: 1px solid var(--line);
  border-radius: 1rem;
  background: linear-gradient(135deg, var(--berry-deep), var(--ink));
  padding: 2rem;
  overflow: hidden;
}

@media (min-width: 1024px) {
  .forecast {
    padding: 3.5rem;
  }
}

.forecast__grid {
  display: grid;
  gap: 2.5rem;
  margin-top: 1.5rem;
}

@media (min-width: 1024px) {
  .forecast__grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.forecast__value {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 2.25rem;
}

.forecast__note {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line-strong);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.875rem;
}

/* ==========================================================================
   Ventures / tiers
   ========================================================================== */
.tier__roi {
  margin-top: 1rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.5rem;
  color: var(--gold);
}

.tier__example {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
  font-size: 0.75rem;
  font-style: italic;
  color: var(--mute-dim);
}

/* Numbered market / value list */
.numbered {
  display: grid;
  gap: 2.5rem;
  margin-top: 3.5rem;
}

@media (min-width: 640px) {
  .numbered {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .numbered--5 {
    grid-template-columns: repeat(5, 1fr);
  }
  .numbered--4 {
    grid-template-columns: repeat(4, 1fr);
  }
}

.numbered__item {
  border-top: 1px solid var(--line-strong);
  padding-top: 1.25rem;
}

.numbered__index {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--gold);
}

.numbered__title {
  margin-top: 0.75rem;
  font-size: 1.125rem;
}

.numbered__text {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--mute);
}

/* Team */
.team-grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(2, 1fr);
  margin-top: 3.5rem;
}

@media (min-width: 1024px) {
  .team-grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

.team-member {
  border-top: 1px solid var(--line-strong);
  padding-top: 1.25rem;
}

.team-member__name {
  font-family: var(--font-display);
  font-size: 1.125rem;
}

.team-member__role {
  margin-top: 0.25rem;
  font-size: 0.8125rem;
  color: var(--mute);
}

/* ==========================================================================
   CTA band
   ========================================================================== */
.cta-band {
  position: relative;
  padding-block: 8rem;
  overflow: hidden;
  text-align: center;
}

.cta-band__bg {
  position: absolute;
  inset: 0;
}

.cta-band__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.cta-band__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 11, 0.85);
}

.cta-band__body {
  position: relative;
  z-index: 1;
  max-width: 48rem;
  margin: 0 auto;
}

.cta-band .btn-row {
  justify-content: center;
}

/* ==========================================================================
   Forms (contact / enquiry)
   ========================================================================== */
.form-grid {
  display: grid;
  gap: 1.25rem;
  margin-top: 2rem;
}

@media (min-width: 640px) {
  .form-grid__row {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.25rem;
  }
}

.field label {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--mute-dim);
  margin-bottom: 0.5rem;
}

.field input,
.field textarea,
.field select {
  width: 100%;
  border: 1px solid var(--line-strong);
  border-radius: 0.5rem;
  background: var(--surface-field);
  color: var(--paper);
  padding: 0.875rem 1rem;
  font-family: inherit;
  font-size: 0.9375rem;
  transition: border-color 0.3s ease;
}

.field input:focus,
.field textarea:focus,
.field select:focus {
  border-color: var(--gold);
  outline: none;
}

.field textarea {
  min-height: 9rem;
  resize: vertical;
}

.form-notice {
  border-radius: 0.5rem;
  padding: 1rem 1.25rem;
  font-size: 0.875rem;
  margin-bottom: 1.5rem;
}

.form-notice--ok {
  border: 1px solid rgba(198, 161, 91, 0.4);
  background: rgba(198, 161, 91, 0.08);
  color: var(--gold-soft);
}

.form-notice--error {
  border: 1px solid rgba(163, 36, 90, 0.5);
  background: rgba(163, 36, 90, 0.12);
  color: #e8a3bf;
}

.contact-layout {
  display: grid;
  gap: 3.5rem;
}

@media (min-width: 1024px) {
  .contact-layout {
    grid-template-columns: 1.2fr 0.8fr;
  }
}

.contact-detail {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding-block: 0.875rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.9375rem;
  color: var(--mute);
}

.contact-detail svg {
  flex-shrink: 0;
  margin-top: 0.25rem;
  color: var(--gold);
}

.contact-detail a:hover {
  color: var(--paper);
}

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer {
  border-top: 1px solid var(--line);
  background: var(--surface-panel);
  padding-block: 4rem;
}

.footer-grid {
  display: grid;
  gap: 3rem;
}

@media (min-width: 1024px) {
  .footer-grid {
    grid-template-columns: 1.2fr 1fr 1fr;
  }
}

.footer-about p {
  margin-top: 1rem;
  max-width: 30ch;
  font-size: 0.875rem;
  line-height: 1.7;
  color: var(--mute);
}

.footer-contact {
  margin-top: 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  font-size: 0.875rem;
  color: var(--mute);
}

.footer-contact a:hover {
  color: var(--paper);
}

.footer-contact li {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}

.footer-contact svg {
  flex-shrink: 0;
  margin-top: 0.25rem;
  color: var(--gold);
}

.footer-col__title {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mute-dim);
}

.footer-col ul {
  margin-top: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  font-size: 0.875rem;
}

.footer-col a:hover {
  color: var(--gold);
}

.footer-villas {
  margin-top: 1rem;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem 1rem;
  font-size: 0.875rem;
}

.footer-villas a {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.footer-villas a:hover {
  color: var(--gold);
}

.footer-villas .code {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  color: var(--gold);
}

.footer-bottom {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 3.5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--line);
  font-size: 0.75rem;
  color: var(--mute-dim);
}

@media (min-width: 1024px) {
  .footer-bottom {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

/* ==========================================================================
   WordPress editor content (pages / posts)
   ========================================================================== */
.entry-content {
  max-width: 48rem;
  color: var(--mute);
  line-height: 1.8;
}

.entry-content > * + * {
  margin-top: 1.5rem;
}

.entry-content h2 {
  color: var(--paper);
  font-size: 2rem;
  margin-top: 3rem;
}

.entry-content h3 {
  color: var(--paper);
  font-size: 1.5rem;
  margin-top: 2.5rem;
}

.entry-content a {
  color: var(--gold);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.entry-content ul {
  list-style: disc;
  padding-left: 1.25rem;
}

.entry-content ol {
  list-style: decimal;
  padding-left: 1.25rem;
}

.entry-content li + li {
  margin-top: 0.5rem;
}

.entry-content blockquote {
  border-left: 2px solid var(--gold);
  padding-left: 1.5rem;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-style: italic;
  color: var(--paper);
  margin-left: 0;
}

.entry-content img {
  border-radius: 0.75rem;
}

/* Pagination */
.pagination {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
  margin-top: 3.5rem;
}

.pagination .page-numbers {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.5rem;
  height: 2.5rem;
  padding-inline: 0.75rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.875rem;
  color: var(--mute);
}

.pagination .page-numbers:hover,
.pagination .page-numbers.current {
  border-color: var(--gold);
  color: var(--gold);
}

/* Admin bar offset */
.admin-bar .site-header {
  top: 32px;
}

@media screen and (max-width: 782px) {
  .admin-bar .site-header {
    top: 46px;
  }
}

/* ==========================================================================
   v2 — Property search
   ========================================================================== */
.psearch {
  margin-top: 2rem;
}

.psearch__row {
  display: grid;
  gap: 1px;
  background: var(--line-strong);
  border: 1px solid var(--line-strong);
  border-radius: 1rem;
  overflow: hidden;
  backdrop-filter: blur(14px);
}

.psearch--hero .psearch__row {
  background: rgba(244, 239, 230, 0.12);
}

.psearch__field {
  background: rgba(10, 10, 11, 0.82);
  padding: 0.75rem 1rem;
  min-width: 0;
}

.psearch--inline .psearch__field {
  background: var(--ink-soft);
}

.psearch__field label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--mute-dim);
  margin-bottom: 0.25rem;
}

.psearch__field input,
.psearch__field select {
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--paper);
  font-family: inherit;
  font-size: 0.9375rem;
  padding: 0;
  appearance: none;
}

.psearch__field select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23716d69' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 0.875rem;
  padding-right: 1.25rem;
}

.psearch__field select option {
  background: var(--ink-soft);
  color: var(--paper);
}

.psearch__field input:focus,
.psearch__field select:focus {
  outline: none;
}

.psearch__field:focus-within {
  background: var(--ink-raised);
}

.psearch__field input::placeholder {
  color: var(--mute-dim);
}

.psearch__submit {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border: 0;
  background: var(--berry);
  color: var(--paper);
  font-size: 0.9375rem;
  padding: 1rem 1.75rem;
  transition: background-color 0.3s ease;
}

.psearch__submit:hover {
  background: var(--berry-bright);
}

@media (min-width: 900px) {
  .psearch__row {
    grid-template-columns: 1.4fr 1fr 1fr 1fr 0.7fr 1fr auto;
    align-items: stretch;
  }
  .psearch__submit {
    padding-inline: 2rem;
  }
}

.hero--search .hero__body {
  padding-bottom: 4rem;
}

.hero__title--search {
  max-width: 16ch;
}

.hero__quicklinks {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.hero__quicklinks a {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.375rem 0.875rem;
  font-size: 0.8125rem;
  color: var(--mute);
  transition: border-color 0.3s ease, color 0.3s ease;
}

.hero__quicklinks a:hover {
  border-color: var(--gold);
  color: var(--paper);
}

/* Results bar */
.results-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  margin-top: 2rem;
}

.results-sort select {
  appearance: none;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--ink-soft);
  color: var(--paper);
  padding: 0.5rem 2.25rem 0.5rem 1rem;
  font-family: inherit;
  font-size: 0.875rem;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a8a49f' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  background-size: 1rem;
}

.results-count {
  margin-top: 0;
}

/* ==========================================================================
   v2 — Listing card additions
   ========================================================================== */
.property-card__link {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.property-card__status {
  position: absolute;
  z-index: 2;
  top: 1rem;
  right: 1rem;
  border-radius: 999px;
  padding: 0.25rem 0.625rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: rgba(10, 10, 11, 0.8);
  color: var(--paper);
  backdrop-filter: blur(6px);
}

.property-card__status--sold,
.property-card__status--rented {
  background: var(--berry);
}

/* Build-status badges on development cards. Planning and under-construction
   carry the berry accent because they signal "not buyable yet", the same
   meaning sold/rented carry on a listing. */
.property-card__status--planning,
.property-card__status--under-construction {
  background: var(--berry);
}

/* Investment-route chips. Used on development cards and the project sidebar. */
.program-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
}

.program-chip {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 0.1875rem 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold);
}

.program-chip a {
  color: inherit;
}

.program-chip a:hover {
  color: var(--paper);
}

.property-card__loc {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mute-dim);
  margin-bottom: 0.5rem;
}

.property-card__specs {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: 0.75rem;
  font-size: 0.8125rem;
  color: var(--mute);
}

.property-card__specs li {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.price-period {
  font-size: 0.75rem;
  color: var(--mute);
  margin-left: 0.125rem;
}

.villa-hero__price {
  margin-top: 0.75rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.5rem;
  color: var(--gold);
}

/* Amenities */
.amenity-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}

.amenity-list li {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.375rem 0.875rem;
  font-size: 0.8125rem;
  color: var(--mute);
}

/* Map */
.map-embed {
  margin-top: 2rem;
  border: 1px solid var(--line);
  border-radius: 1rem;
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

.map-embed iframe {
  width: 100%;
  height: 100%;
  border: 0;
  filter: grayscale(1) invert(0.92) contrast(0.85);
}

/* ==========================================================================
   v2 — Browse-by-type tiles
   ========================================================================== */
.type-grid {
  display: grid;
  gap: 1px;
  margin-top: 3rem;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 1rem;
  overflow: hidden;
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .type-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 1024px) {
  .type-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.type-tile {
  position: relative;
  display: block;
  background: var(--surface-card);
  padding: 2rem;
  transition: background-color 0.4s ease;
}

.type-tile:hover {
  background: var(--ink-raised);
}

.type-tile__index {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  color: var(--gold);
}

.type-tile__name {
  display: block;
  font-family: var(--font-display);
  font-size: 1.5rem;
  margin-top: 0.75rem;
}

.type-tile__count {
  display: block;
  margin-top: 0.375rem;
  font-size: 0.8125rem;
  color: var(--mute-dim);
}

.type-tile__arrow {
  position: absolute;
  top: 2rem;
  right: 2rem;
  color: var(--mute-dim);
  transition: color 0.3s ease;
}

.type-tile:hover .type-tile__arrow {
  color: var(--gold);
}

/* ==========================================================================
   v2 — Entertainment band
   ========================================================================== */
.entertainment-band {
  border: 1px solid var(--line);
  border-radius: 1rem;
  background: linear-gradient(135deg, var(--berry-deep), var(--ink) 70%);
  padding: 3rem 2rem;
}

@media (min-width: 1024px) {
  .entertainment-band {
    padding: 4rem 3.5rem;
  }
}

.eightone-band {
  background: var(--ink-soft);
}

/* ==========================================================================
   v2 — Dropdown navigation
   ========================================================================== */
.nav-primary li {
  position: relative;
}

.nav-primary a {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.nav-primary .submenu {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 15rem;
  padding: 0.5rem 0;
  margin-top: 0.75rem;
  background: var(--ink-soft);
  border: 1px solid var(--line-strong);
  border-radius: 0.75rem;
  display: block;
  flex-direction: column;
  gap: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity 0.25s ease, transform 0.25s ease, visibility 0.25s;
  z-index: 60;
}

.nav-primary li:hover > .submenu,
.nav-primary li:focus-within > .submenu {
  opacity: 1;
  visibility: visible;
  transform: none;
}

.nav-primary .submenu li {
  display: block;
}

.nav-primary .submenu a {
  display: block;
  padding: 0.625rem 1.25rem;
  font-size: 0.8125rem;
  text-transform: none;
  letter-spacing: 0.01em;
  color: var(--mute);
}

.nav-primary .submenu a:hover {
  color: var(--gold);
}

/* Mobile: submenus render inline, indented */
/* Submenus start folded. They used to render expanded, so the mobile menu
   opened at full height with every child of every section on screen at once —
   six sections and fourteen children is far more than a phone can show, and
   the top-level items it is meant to present were buried. */
.nav-mobile .submenu {
  display: none;
  padding-left: 1rem;
  border-left: 1px solid var(--line);
  margin-left: 0.25rem;
}

/* The fold control that sits beside a parent link in the mobile menu. */
.nav-mobile .has-submenu {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
}

.nav-mobile .has-submenu > .submenu {
  grid-column: 1 / -1;
}

.submenu-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  border: 0;
  background: none;
  color: var(--mute);
  transition: transform 0.25s var(--ease), color 0.25s ease;
}

.nav-mobile .submenu-toggle {
  display: inline-flex;
}

.nav-mobile .has-submenu.is-open > .submenu-toggle {
  transform: rotate(180deg);
  color: var(--paper);
}

/* The parent link keeps its own caret on desktop; in the mobile menu the
   button carries it, so the inline one would be a duplicate. */
.nav-mobile .has-submenu > a svg {
  display: none;
}

.nav-mobile .submenu a {
  border-bottom: 0;
  padding-block: 0.625rem;
  font-size: 0.875rem;
  text-transform: none;
  color: var(--mute);
}

.nav-mobile .has-submenu > a {
  justify-content: space-between;
  display: flex;
}

/* Header logo sizing for the real wordmark */
.brand img {
  width: auto;
  height: 30px;
  max-width: 190px;
  object-fit: contain;
}

@media (max-width: 480px) {
  .brand img {
    height: 24px;
    max-width: 140px;
  }
}

.brand--markonly img {
  width: 32px;
  height: 32px;
}

/* ==========================================================================
   v2 — ROI calculator
   ========================================================================== */
.roi {
  display: grid;
  gap: 2rem;
  grid-template-columns: 1fr;
}

@media (min-width: 1024px) {
  .roi {
    grid-template-columns: 0.85fr 1.15fr;
    align-items: start;
  }
}

.roi__field {
  margin-top: 1.5rem;
}

.roi__field label {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--mute-dim);
  margin-bottom: 0.5rem;
}

.roi__field label span {
  color: var(--gold);
}

.roi__field input[type="number"] {
  width: 100%;
  border: 1px solid var(--line-strong);
  border-radius: 0.5rem;
  background: var(--surface-field);
  color: var(--paper);
  padding: 0.75rem 1rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 1rem;
}

.roi__field input[type="number"]:focus {
  border-color: var(--gold);
  outline: none;
}

.roi__field input[type="range"] {
  width: 100%;
  accent-color: var(--gold);
}

.roi__results .btn-row {
  margin-top: 1.5rem;
}

/* ==========================================================================
   v2.1 — Custom select dropdowns
   Native select popups are OS-drawn and cannot be themed, so they flash a
   white system menu on a dark site. These replace the popup while the real
   <select> stays in the DOM for submission and no-JS fallback.
   ========================================================================== */
.kselect {
  position: relative;
  width: 100%;
}

/* Visually hidden, still focusable by assistive tech and still submitted */
.kselect__native {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Without JS the enhancement never runs, so show the native control */
.no-js .kselect__native {
  position: static;
  width: 100%;
  height: auto;
  clip: auto;
  margin: 0;
}

.kselect__button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--paper);
  font-family: inherit;
  font-size: 0.9375rem;
  padding: 0;
  text-align: left;
  line-height: 1.4;
}

.kselect__label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.kselect__caret {
  display: flex;
  flex-shrink: 0;
  color: var(--mute-dim);
  transition: transform 0.25s ease, color 0.25s ease;
}

.kselect.is-open .kselect__caret {
  transform: rotate(180deg);
  color: var(--gold);
}

.kselect__list {
  position: absolute;
  z-index: 80;
  top: calc(100% + 0.75rem);
  left: -1rem;
  right: -1rem;
  max-height: 17rem;
  overflow-y: auto;
  padding: 0.375rem;
  margin: 0;
  list-style: none;
  background: var(--ink-raised);
  border: 1px solid var(--line-strong);
  border-radius: 0.75rem;
  box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.7);
  animation: kselect-in 0.18s var(--ease) both;
}

.kselect.is-flipped .kselect__list {
  top: auto;
  bottom: calc(100% + 0.75rem);
}

@keyframes kselect-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.kselect__option {
  padding: 0.625rem 0.875rem;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  color: var(--mute);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.kselect__option:hover {
  background: rgba(244, 239, 230, 0.06);
  color: var(--paper);
}

.kselect__option[aria-selected="true"] {
  background: var(--berry);
  color: var(--paper);
}

/* Scrollbar inside the dropdown */
.kselect__list::-webkit-scrollbar {
  width: 8px;
}
.kselect__list::-webkit-scrollbar-track {
  background: transparent;
}
.kselect__list::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 999px;
}

/* The search field no longer needs the native caret background image */
.psearch__field select,
.results-sort select {
  background-image: none;
  padding-right: 0;
}

.results-sort .kselect {
  min-width: 12rem;
}

.results-sort .kselect__button {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--ink-soft);
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
}

.results-sort .kselect__list {
  left: 0;
  right: 0;
}

/* ==========================================================================
   v2.1 — Smart search
   ========================================================================== */
.smartsearch {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.72);
  backdrop-filter: blur(14px);
  padding: 0.375rem 0.375rem 0.375rem 1.125rem;
  transition: border-color 0.3s ease;
}

.smartsearch:focus-within {
  border-color: var(--gold);
}

.smartsearch--inline {
  background: var(--ink-soft);
}

.smartsearch__icon {
  display: flex;
  flex-shrink: 0;
  color: var(--mute-dim);
}

.smartsearch:focus-within .smartsearch__icon {
  color: var(--gold);
}

.smartsearch input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--paper);
  font-family: inherit;
  font-size: 1rem;
  padding: 0.75rem 0;
}

.smartsearch input:focus {
  outline: none;
}

.smartsearch input::placeholder {
  color: var(--mute-dim);
}

.smartsearch input::-webkit-search-cancel-button {
  filter: invert(0.7);
}

.smartsearch__submit {
  flex-shrink: 0;
  border: 0;
  border-radius: 999px;
  background: var(--berry);
  color: var(--paper);
  font-family: inherit;
  font-size: 0.875rem;
  padding: 0.75rem 1.5rem;
  transition: background-color 0.3s ease;
}

.smartsearch__submit:hover {
  background: var(--berry-bright);
}

@media (max-width: 560px) {
  .smartsearch__submit span {
    display: none;
  }
  .smartsearch__submit {
    padding-inline: 1.125rem;
  }
}

/* Divider between the smart bar and the structured filters */
.search-divider {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 1.5rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mute-dim);
}

.search-divider::before,
.search-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Parsed interpretation chips */
.parsed-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

.parsed-chip {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 0.3125rem 0.75rem;
  font-size: 0.75rem;
  color: var(--gold);
  background: rgba(198, 161, 91, 0.08);
}

.parsed-chip--kw {
  color: var(--mute);
  background: transparent;
}

.page-hero__search {
  margin-top: 2rem;
  max-width: 40rem;
}

/* Suggestions */
.suggestion-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1.5rem;
}

.suggestion-row a {
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.5rem 1rem;
  font-size: 0.8125rem;
  color: var(--mute);
  transition: border-color 0.3s ease, color 0.3s ease;
}

.suggestion-row a:hover {
  border-color: var(--gold);
  color: var(--paper);
}

/* ==========================================================================
   v2.1 — Site content results
   ========================================================================== */
.content-results {
  display: grid;
  gap: 1px;
  margin-top: 2rem;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 1rem;
  overflow: hidden;
}

.content-result {
  position: relative;
  display: grid;
  gap: 0.25rem;
  background: var(--surface-card);
  padding: 1.5rem 3.5rem 1.5rem 1.5rem;
  transition: background-color 0.3s ease;
}

.content-result:hover {
  background: var(--ink-raised);
}

.content-result__type {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--gold);
}

.content-result__title {
  font-family: var(--font-display);
  font-size: 1.125rem;
  color: var(--paper);
}

.content-result__excerpt {
  font-size: 0.8125rem;
  line-height: 1.6;
  color: var(--mute);
}

.content-result__arrow {
  position: absolute;
  top: 1.5rem;
  right: 1.5rem;
  color: var(--mute-dim);
  transition: color 0.3s ease;
}

.content-result:hover .content-result__arrow {
  color: var(--gold);
}

/* 8-One band background */
.eightone-band {
  position: relative;
  overflow: hidden;
}

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

.eightone-band__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.16;
}

.eightone-band__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--ink-soft), rgba(19, 18, 21, 0.7));
}

.eightone-band > .wrap {
  position: relative;
  z-index: 1;
}

/* Header search toggle */
.header-actions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

@media (max-width: 1023px) {
  .header-actions {
    display: none;
  }
}

.header-search-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--paper);
  transition: border-color 0.3s ease, color 0.3s ease;
}

.header-search-toggle:hover,
.header-search-toggle[aria-expanded="true"] {
  border-color: var(--gold);
  color: var(--gold);
}

.header-search {
  border-top: 1px solid var(--line);
  background: var(--surface-panel);
  padding-block: 1rem;
}

.header-search .smartsearch {
  max-width: 44rem;
  margin-inline: auto;
  background: var(--ink-soft);
}

/* ==========================================================================
   v2.2 — Light theme
   Every colour is a token, so light mode is a token swap rather than a
   parallel stylesheet. Components inherit automatically.
   ========================================================================== */
[data-theme="light"] {
  /* The light palette. There is one of these, and this is it.

     Until now the stylesheet declared three complete [data-theme="light"]
     token blocks — one here, one at "v2.3 — Light theme, reworked", one at
     "v2.4 — Light theme, second pass". Only the last one ever rendered; each
     rework added a palette rather than editing this one, so two thirds of the
     light-mode tokens in the file were dead, and reading any of them told you
     nothing about what the site actually looked like.

     The values below are the v2.4 ones — the set that was live — moved up to
     where the dark palette is declared, so the two sit together. */
  --ink: #e8e0d3;
  --ink-soft: #ddd3c2;
  --ink-raised: #fdfbf7;
  --paper: #13100f;
  --berry: #6e1139;
  --berry-bright: #8d1b4c;
  --berry-deep: #ecd9e1;
  /* Gold at #c6a15b fails contrast on paper, so light mode uses a deep aged
     bronze for the same accent role. Berry carries most accents; this is kept
     for the places where gold itself is the meaning. */
  --gold: #6f4e18;
  --gold-soft: #5a3f13;
  --mute: #4a423b;
  /* Was #766d63 — 3.88:1 on the page and 3.43:1 on the footer's sand, both
     below AA. Darkened until the worst pairing (on --ink-soft) clears 4.5. */
  --mute-dim: #5c554d;
  --line: rgba(19, 16, 15, 0.18);
  --line-strong: rgba(19, 16, 15, 0.32);

  /* The whole light-mode surface fix, in three lines. Anything that used to
     need a per-component override for its background is covered here. */
  --surface-card: var(--ink-raised);
  --surface-field: var(--ink-raised);
  --surface-panel: var(--ink-soft);
}

[data-theme="light"] body {
  background: var(--ink);
  color: var(--paper);
}

/* The header's light background is set once, in the v2.4 surfaces block at the
   end of this file. The pair of rules that used to sit here specified a
   different tint and was overridden by it, so it never rendered. */

/* The hero keeps a dark scrim in both modes — it sits over photography, and
   light text on an image is correct either way. The scrim gradient itself is
   defined once, further down; the copy that stood here was overridden. */
[data-theme="light"] .hero__title,
[data-theme="light"] .hero__lede,
[data-theme="light"] .villa-hero__title,
[data-theme="light"] .page-hero--media .page-hero__title,
[data-theme="light"] .page-hero--media .lede,
[data-theme="light"] .cta-band__body,
[data-theme="light"] .cta-band__body .section-title {
  color: #f7f4ef;
}

[data-theme="light"] .hero__lede,
[data-theme="light"] .page-hero--media .lede {
  color: rgba(247, 244, 239, 0.82);
}

[data-theme="light"] .villa-hero__scrim {
  background: linear-gradient(to top, rgba(23, 20, 26, 0.9), rgba(23, 20, 26, 0.25));
}

[data-theme="light"] .page-hero__bg::after {
  background: linear-gradient(to top, rgba(23, 20, 26, 0.85), rgba(23, 20, 26, 0.6));
}

[data-theme="light"] .page-hero--media .eyebrow {
  color: rgba(247, 244, 239, 0.75);
}

[data-theme="light"] .cta-band__bg::after {
  background: rgba(23, 20, 26, 0.78);
}

[data-theme="light"] .cta-band .lede {
  color: rgba(247, 244, 239, 0.82);
}

/* Panels and cards need a visible edge on a light background */
[data-theme="light"] .card,
[data-theme="light"] .villa-card,
[data-theme="light"] .panel,
[data-theme="light"] .type-tile,
[data-theme="light"] .content-result {
  box-shadow: 0 1px 2px rgba(23, 20, 26, 0.04);
}

/* .villa-card, .kselect__list, .smartsearch, .psearch--hero .psearch__row,
   .psearch__field, .forecast and .entertainment-band were all restyled here
   and then restyled again by the v2.4 surfaces block, which is later in the
   file and therefore won every time. Their rules are removed rather than
   reconciled — the values that were actually rendering are the v2.4 ones. */
[data-theme="light"] .kselect__option:hover {
  background: rgba(23, 20, 26, 0.05);
}

[data-theme="light"] .eightone-band__bg img {
  opacity: 0.1;
}

[data-theme="light"] .eightone-band__bg::after {
  background: linear-gradient(to bottom, var(--ink-soft), rgba(239, 233, 224, 0.75));
}

[data-theme="light"] .map-embed iframe {
  filter: none;
}

/* .parsed-chip is restyled in berry further down; the bronze fill that stood
   here was overridden. */
[data-theme="light"] .smartsearch input::-webkit-search-cancel-button {
  filter: none;
}

[data-theme="light"] .villa-card__code,
[data-theme="light"] .villa-card__roi,
[data-theme="light"] .property-card__status {
  background: rgba(23, 20, 26, 0.72);
  color: #f7f4ef;
}

/* ==========================================================================
   v2.2 — Theme toggle control
   ========================================================================== */
.theme-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--paper);
  transition: border-color 0.3s ease, color 0.3s ease;
  flex-shrink: 0;
}

.theme-toggle:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.theme-toggle svg {
  width: 17px;
  height: 17px;
}

/* Only one icon shows at a time */
.theme-toggle .icon-sun {
  display: none;
}
[data-theme="light"] .theme-toggle .icon-sun {
  display: block;
}
[data-theme="light"] .theme-toggle .icon-moon {
  display: none;
}

.nav-mobile .theme-toggle {
  width: auto;
  gap: 0.625rem;
  padding-inline: 1.25rem;
  height: 2.75rem;
  margin-top: 1rem;
  font-family: inherit;
  font-size: 0.875rem;
}

/* Logo variants swap with the theme */
.brand__logo--light {
  display: block;
}
.brand__logo--dark {
  display: none;
}
[data-theme="light"] .brand__logo--light {
  display: none;
}
[data-theme="light"] .brand__logo--dark {
  display: block;
}

/* ==========================================================================
   v2.2 — Header / hero seam
   The header had a hairline that read as a rule between the nav and the hero.
   Separation now comes from the blur and background alone.
   ========================================================================== */
.site-header,
.site-header.is-scrolled,
.site-header.is-open {
  border-bottom: 0;
}

.site-header.is-scrolled {
  box-shadow: 0 1px 0 rgba(244, 239, 230, 0.06);
}

[data-theme="light"] .site-header.is-scrolled {
  box-shadow: 0 1px 0 rgba(23, 20, 26, 0.08);
}

.hero {
  margin-top: 0;
  border-top: 0;
}

/* ==========================================================================
   v2.2 — Search bar, enlarged
   The search is the site's primary action, so it gets real presence: a wide
   pill, generous height, one visible edge. Weight comes from scale and space
   rather than decoration.
   ========================================================================== */
.smartsearch--hero {
  max-width: 46rem;
  margin-top: 2.5rem;
  padding: 0.5rem 0.5rem 0.5rem 1.5rem;
  border-width: 1px;
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.55);
  backdrop-filter: blur(18px) saturate(1.1);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.65);
  transition: border-color 0.35s var(--ease), box-shadow 0.35s var(--ease),
    background-color 0.35s var(--ease);
}

.smartsearch--hero:focus-within {
  background: rgba(10, 10, 11, 0.78);
  box-shadow: 0 22px 50px -18px rgba(0, 0, 0, 0.8);
}

/* Background and shadow are both set again later; only the focus state below
   is unique to this point in the file. */
[data-theme="light"] .smartsearch--hero:focus-within {
  background: rgba(255, 255, 255, 0.96);
}

.smartsearch--hero .smartsearch__icon svg {
  width: 21px;
  height: 21px;
}

.smartsearch--hero input {
  font-size: 1.0625rem;
  padding-block: 1.0625rem;
}

@media (min-width: 768px) {
  .smartsearch--hero input {
    font-size: 1.1875rem;
  }
}

.smartsearch--hero .smartsearch__submit {
  padding: 1rem 2.25rem;
  font-size: 0.9375rem;
  letter-spacing: 0.01em;
}

/* Hint line under the hero search */
.search-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
  font-size: 0.8125rem;
  color: var(--mute);
}

.hero .search-hint {
  color: rgba(244, 239, 230, 0.65);
}

[data-theme="light"] .hero .search-hint {
  color: rgba(247, 244, 239, 0.72);
}

.search-hint__label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.75;
}

.search-hint a {
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  transition: border-color 0.25s ease, color 0.25s ease;
}

.search-hint a:hover {
  border-bottom-color: currentColor;
  color: var(--gold);
}

.hero .search-hint a:hover {
  color: var(--gold);
}

/* Collapse the structured filters behind a disclosure so the hero stays clean */
/* Was 14px tall with zero padding — the label's own line-box and nothing more.
   The vertical padding is the tap target; the negative inline margin keeps the
   text visually flush with the column it sits under. */
.filters-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 44px;
  margin-top: 1.25rem;
  margin-inline: -0.5rem;
  border: 0;
  background: none;
  padding: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mute);
  transition: color 0.25s ease;
}

.hero .filters-toggle {
  color: rgba(244, 239, 230, 0.7);
}

.filters-toggle:hover {
  color: var(--gold);
}

.filters-toggle svg {
  transition: transform 0.3s var(--ease);
}

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

.filters-panel[hidden] {
  display: none;
}

.filters-panel {
  animation: filters-in 0.3s var(--ease) both;
}

@keyframes filters-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.hero .psearch {
  margin-top: 1rem;
}

/* Quicklinks sit tighter to the search now */
.hero__quicklinks {
  margin-top: 1.75rem;
}

/* ==========================================================================
   v2.2.2 — Light mode surface separation

   The background half of this block is gone. The --surface-card /
   --surface-field / --surface-panel tokens at the top of the file now carry
   it, for both themes at once. What remains is the part no token expresses:
   light-only borders and the focus ring.

   Every background rule removed here was already being overridden by the
   v2.4 surfaces block further down this same file, so none of them affected
   the rendered page — three generations of light-mode patches had stacked up,
   each re-styling the same selectors with slightly different values.
   ========================================================================== */
[data-theme="light"] .schedule__bar-track {
  background: var(--ink-soft);
}

[data-theme="light"] .field input,
[data-theme="light"] .field textarea,
[data-theme="light"] .field select,
[data-theme="light"] .roi__field input[type="number"],
[data-theme="light"] .filters select {
  border-color: var(--line-strong);
}

[data-theme="light"] .field input:focus,
[data-theme="light"] .field textarea:focus,
[data-theme="light"] .roi__field input[type="number"]:focus {
  border-color: var(--gold);
}

/* A raised panel needs a visible edge on paper; the dark-mode hairline all
   but disappears. type-grid, content-results and panel-group are not listed
   because the v2.4 block below sets their border-color to a stronger value. */
[data-theme="light"] .card,
[data-theme="light"] .villa-card {
  border-color: rgba(23, 20, 26, 0.14);
}

[data-theme="light"] .empty-state {
  border-color: rgba(23, 20, 26, 0.18);
}

/* The zebra stripe is set again further down, with the rest of the table
   treatment, so the rule that stood here never applied. */

/* ==========================================================================
   v2.3 — Header proportions

   The nav was overrunning the header: long labels wrapped to two lines and
   collided with the logo. Fix is smaller type, tighter tracking, a wider
   wrapper, and an earlier collapse to the mobile menu.
   ========================================================================== */
.site-header__inner {
  gap: 1.5rem;
  padding-block: 0.875rem;
}

.brand {
  flex-shrink: 0;
}

.brand img {
  height: 26px;
  max-width: 158px;
}

.nav-primary {
  flex: 1;
  display: flex;
  justify-content: center;
  min-width: 0;
}

.nav-primary > ul {
  gap: 1.75rem;
  flex-wrap: nowrap;
}

.nav-primary > ul > li > a {
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  white-space: nowrap;
}

.header-actions {
  flex-shrink: 0;
  gap: 0.5rem;
}

.header-cta {
  padding: 0.5rem 1.125rem;
  font-size: 0.8125rem;
}

.theme-toggle,
.header-search-toggle {
  width: 2.125rem;
  height: 2.125rem;
}

.theme-toggle svg,
.header-search-toggle svg {
  width: 15px;
  height: 15px;
}

/* Give the header more room than the content wrapper */
.site-header__inner {
  max-width: 96rem;
}

/* Collapse to the burger before the nav has a chance to crowd */
@media (max-width: 1179px) {
  .nav-primary,
  .header-actions {
    display: none;
  }
  .nav-toggle {
    display: inline-flex;
  }
}

@media (min-width: 1180px) {
  .nav-toggle,
  .nav-mobile {
    display: none !important;
  }
  .nav-primary,
  .header-actions {
    display: flex;
  }
}

@media (min-width: 1180px) and (max-width: 1339px) {
  .nav-primary > ul {
    gap: 1.25rem;
  }
  .nav-primary > ul > li > a {
    font-size: 0.6875rem;
    letter-spacing: 0.07em;
  }
}

/* Submenus align under their parent rather than the viewport edge */
.nav-primary .submenu {
  left: 50%;
  transform: translateX(-50%) translateY(-6px);
  min-width: 13rem;
}

.nav-primary li:hover > .submenu,
.nav-primary li:focus-within > .submenu {
  transform: translateX(-50%);
}

/* ==========================================================================
   v2.3 — Light theme, reworked

   The first pass was a mechanical inversion and read as generic. Two changes
   give it back the character of the dark theme:

   1. A warmer, deeper paper (#f2ece2 rather than near-white) so raised white
      surfaces genuinely lift off it. Pale-grey-on-white has no depth.
   2. Berry carries the accent role instead of gold. Gold on paper is muddy at
      any value that passes contrast; berry is the actual brand colour and
      reads as confident rather than washed out.
   ========================================================================== */
/* The palette this section used to redeclare has been folded into the single
   [data-theme="light"] block at the top of the file. The rules below are the
   part of this rework that still does something: routing accents to berry. */

/* Accents move to berry: eyebrows, index chips, code labels, hovers */
[data-theme="light"] .eyebrow__index,
[data-theme="light"] .code-strip__code,
[data-theme="light"] .numbered__index,
[data-theme="light"] .type-tile__index,
[data-theme="light"] .content-result__type,
[data-theme="light"] .acronym__letter,
[data-theme="light"] .spec-list__title,
[data-theme="light"] .tier__roi,
[data-theme="light"] .stat__value--gold,
[data-theme="light"] .search-hint__label {
  color: var(--berry);
}

[data-theme="light"] .parsed-chip {
  color: var(--berry);
  background: rgba(122, 20, 64, 0.07);
  border-color: rgba(122, 20, 64, 0.24);
}

[data-theme="light"] a:hover,
[data-theme="light"] .nav-primary .submenu a:hover,
[data-theme="light"] .footer-col a:hover,
[data-theme="light"] .footer-villas a:hover,
[data-theme="light"] .content-result:hover .content-result__arrow,
[data-theme="light"] .type-tile:hover .type-tile__arrow,
[data-theme="light"] .search-hint a:hover {
  color: var(--berry);
}

[data-theme="light"] .villa-card:hover,
[data-theme="light"] .btn--outline:hover,
[data-theme="light"] .theme-toggle:hover,
[data-theme="light"] .header-search-toggle:hover,
[data-theme="light"] .header-cta:hover,
[data-theme="light"] .suggestion-row a:hover,
[data-theme="light"] .hero__quicklinks a:hover,
[data-theme="light"] .code-strip a:hover {
  border-color: var(--berry);
  color: var(--berry);
}

[data-theme="light"] .villa-card:hover .villa-card__arrow {
  border-color: var(--berry);
  color: var(--berry);
}

[data-theme="light"] :focus-visible {
  outline-color: var(--berry);
}

/* Depth: raised surfaces cast a real shadow instead of relying on a hairline */
[data-theme="light"] .card,
[data-theme="light"] .villa-card,
[data-theme="light"] .panel,
[data-theme="light"] .content-result,
[data-theme="light"] .type-tile {
  box-shadow: 0 1px 2px rgba(25, 19, 22, 0.05), 0 8px 24px -16px rgba(25, 19, 22, 0.18);
}

/* .villa-card:hover is set again in the v2.4 surfaces block, so its rule here
   is gone. .smartsearch--hero keeps only its shadow: the v2.4 block sets the
   background and border but not this. */
[data-theme="light"] .smartsearch--hero {
  box-shadow: 0 20px 44px -20px rgba(25, 19, 22, 0.4);
}

[data-theme="light"] .kselect__list {
  box-shadow: 0 24px 48px -14px rgba(25, 19, 22, 0.24);
}

/* Section rhythm, and the berry-tinted feature bands, are both set in the
   v2.4 surfaces block. The duplicate rules that stood here did not render. */
[data-theme="light"] .forecast .forecast__value,
[data-theme="light"] .forecast .mono {
  color: var(--berry);
}

[data-theme="light"] .forecast__note {
  border-top-color: rgba(122, 20, 64, 0.18);
}

/* The footer's background comes from --surface-panel now; only its edge is
   light-specific. */
[data-theme="light"] .site-footer {
  border-top-color: rgba(25, 19, 22, 0.12);
}

[data-theme="light"] .footer-villas .code {
  color: var(--berry);
}

/* Table headers and zebra tuned for paper */
[data-theme="light"] .data-table thead th {
  color: var(--mute);
  border-bottom-color: rgba(25, 19, 22, 0.2);
}

[data-theme="light"] .data-table tbody tr:nth-child(even) {
  background: rgba(25, 19, 22, 0.022);
}

[data-theme="light"] .data-table tr.is-total .num {
  color: var(--berry);
}

/* Schedule bars keep berry fill on a sand track */
[data-theme="light"] .schedule__pct {
  color: var(--berry);
}

/* The light-mode hero scrim that used to sit here started its gradient at
   var(--ink), which is the cream page colour in light mode — it faded the
   photograph to cream instead of darkening it. It is superseded by the fixed
   dark version in the v3.16 block near the end of this file, so the broken
   rule is removed rather than left to be read as intent. */

[data-theme="light"] .hero .search-hint__label {
  color: rgba(255, 252, 247, 0.7);
}

[data-theme="light"] .hero .filters-toggle {
  color: rgba(255, 252, 247, 0.8);
}

[data-theme="light"] .hero__quicklinks a {
  border-color: rgba(255, 252, 247, 0.3);
  color: rgba(255, 252, 247, 0.9);
}

/* ==========================================================================
   v2.3.2 — Portalled dropdown

   While open, the list is moved to <body> and positioned with fixed
   coordinates. The search bar sets overflow:hidden and backdrop-filter, and
   the hero clips overflow — any one of those would crop the menu or bury it
   behind the surrounding interface. Positioning off the body sidesteps all
   clipping and stacking contexts, whatever the ancestors do.
   ========================================================================== */
.kselect__list--portal {
  position: fixed;
  z-index: 9999;
  top: auto;
  left: auto;
  right: auto;
  transform: none;
  margin: 0;
  box-shadow: 0 28px 60px -16px rgba(0, 0, 0, 0.75);
}

[data-theme="light"] .kselect__list--portal {
  box-shadow: 0 28px 60px -18px rgba(25, 19, 22, 0.32);
}

/* The flip class is no longer used — placement is computed in JS */
.kselect.is-flipped .kselect__list--portal {
  bottom: auto;
}

/* Filters panel is visible on load, so it needs breathing room */
.filters-toggle {
  margin-top: 1.5rem;
}

.filters-panel {
  margin-top: 0.25rem;
}

/* On short viewports the open panel shouldn't push content off-screen */
@media (max-height: 780px) and (min-width: 900px) {
  .hero {
    height: auto;
    min-height: 100svh;
  }
  .hero__body {
    padding-top: 8rem;
    padding-bottom: 3rem;
  }
}

/* Three-across numbered grid, used by Conciergerie */
@media (min-width: 1024px) {
  .numbered--3 {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ==========================================================================
   v2.4 — Light theme, second pass

   The previous light background was still too close to white, which is why it
   read as generic: with nothing between the page and a white card, there is no
   depth to see. This moves the base to a warm sand so raised surfaces lift
   properly, deepens the berry, and pushes navigation to near-black so the
   primary links carry the weight they should.
   ========================================================================== */
/* These are the values the site has been rendering. They now live in the one
   [data-theme="light"] block at the top of the file, next to the dark palette,
   instead of being redeclared 3,000 lines below it. */

/* --- Navigation: primary links get full weight, not muted grey ----------- */
[data-theme="light"] .nav-primary > ul > li > a {
  color: var(--paper);
  font-weight: 500;
}

[data-theme="light"] .nav-primary > ul > li > a:hover,
[data-theme="light"] .nav-primary .current-menu-item > a {
  color: var(--berry);
}

[data-theme="light"] .nav-primary .submenu a {
  color: var(--mute);
}

[data-theme="light"] .nav-primary .submenu a:hover {
  color: var(--berry);
}

[data-theme="light"] .nav-mobile a {
  color: var(--paper);
}

/* Dark theme nav also gains a little presence */
.nav-primary > ul > li > a {
  color: rgba(244, 239, 230, 0.82);
}

.nav-primary > ul > li > a:hover,
.nav-primary .current-menu-item > a {
  color: var(--paper);
}

/* --- Buttons ------------------------------------------------------------ */
[data-theme="light"] .btn--primary {
  background: var(--berry);
  color: #fdfbf7;
}

[data-theme="light"] .btn--primary:hover {
  background: var(--berry-bright);
}

[data-theme="light"] .btn--outline {
  border-color: var(--line-strong);
  color: var(--paper);
  background: rgba(253, 251, 247, 0.55);
}

[data-theme="light"] .btn--outline:hover {
  border-color: var(--berry);
  color: var(--berry);
  background: var(--ink-raised);
}

[data-theme="light"] .header-cta {
  border-color: var(--line-strong);
  color: var(--paper);
  background: rgba(253, 251, 247, 0.6);
}

[data-theme="light"] .header-cta:hover {
  border-color: var(--berry);
  color: var(--berry);
  background: var(--ink-raised);
}

[data-theme="light"] .theme-toggle,
[data-theme="light"] .header-search-toggle {
  border-color: var(--line-strong);
  color: var(--paper);
  background: rgba(253, 251, 247, 0.6);
}

[data-theme="light"] .smartsearch__submit,
[data-theme="light"] .psearch__submit {
  background: var(--berry);
  color: #fdfbf7;
}

[data-theme="light"] .smartsearch__submit:hover,
[data-theme="light"] .psearch__submit:hover {
  background: var(--berry-bright);
}

/* --- Surfaces -----------------------------------------------------------
   Backgrounds that the surface tokens already resolve are not repeated here.
   .card, .content-result and .type-tile take --surface-card; the three below
   keep an explicit background because their dark value is --ink-soft, not
   --ink, so one shared token cannot serve both. */
[data-theme="light"] .villa-card,
[data-theme="light"] .panel,
[data-theme="light"] .empty-state {
  background: var(--ink-raised);
}

[data-theme="light"] .card,
[data-theme="light"] .villa-card,
[data-theme="light"] .panel,
[data-theme="light"] .content-result,
[data-theme="light"] .type-tile,
[data-theme="light"] .empty-state {
  box-shadow: 0 1px 2px rgba(19, 16, 15, 0.06), 0 10px 28px -18px rgba(19, 16, 15, 0.28);
}

[data-theme="light"] .villa-card:hover {
  box-shadow: 0 2px 6px rgba(19, 16, 15, 0.09), 0 20px 44px -20px rgba(19, 16, 15, 0.36);
}

[data-theme="light"] .type-grid,
[data-theme="light"] .content-results,
[data-theme="light"] .panel-group {
  background: rgba(19, 16, 15, 0.16);
  border-color: rgba(19, 16, 15, 0.16);
}

/* The form controls take --surface-field. psearch__field is listed on its own
   because its dark value is a translucent black, not a token. */
[data-theme="light"] .psearch__field {
  background: var(--ink-raised);
}

[data-theme="light"] .results-sort .kselect__button {
  background: var(--ink-raised);
  border-color: var(--line-strong);
}

[data-theme="light"] .smartsearch--hero,
[data-theme="light"] .smartsearch {
  background: rgba(253, 251, 247, 0.95);
  border-color: rgba(19, 16, 15, 0.2);
}

[data-theme="light"] .psearch--hero .psearch__row {
  background: rgba(19, 16, 15, 0.24);
}

[data-theme="light"] .forecast,
[data-theme="light"] .entertainment-band {
  background: linear-gradient(135deg, #e6cfd9, var(--ink-raised) 78%);
  border-color: rgba(110, 17, 57, 0.2);
}

/* .site-footer is absent: it takes --surface-panel, which already resolves to
   --ink-soft in light mode. */
[data-theme="light"] .section--soft,
[data-theme="light"] .eightone-band {
  background: var(--ink-soft);
}

[data-theme="light"] .site-header {
  background: linear-gradient(rgba(232, 224, 211, 0.9), rgba(232, 224, 211, 0));
}

[data-theme="light"] .site-header.is-scrolled,
[data-theme="light"] .site-header.is-open {
  background: rgba(232, 224, 211, 0.96);
}

/* .nav-mobile and .header-search take --surface-panel, which resolves to
   --ink-soft here. The override that used to sit at this point is gone. */

[data-theme="light"] .kselect__list {
  background: var(--ink-raised);
}

/* Section titles and stats keep maximum weight */
[data-theme="light"] .section-title,
[data-theme="light"] .stat__value,
[data-theme="light"] .villa-card__title,
[data-theme="light"] .villa-card__price,
[data-theme="light"] .type-tile__name,
[data-theme="light"] .content-result__title {
  color: var(--paper);
}

/* ==========================================================================
   v2.5 — Touch and small-screen ergonomics

   Two separate concerns, deliberately keyed off two different things:

   - `pointer: coarse` covers *how* the page is being touched. A finger needs a
     44px target whether it is a 390px phone or a 1024px tablet, and a small
     laptop with a trackpad does not — so target sizing keys off input type,
     not viewport width. Sizing these by breakpoint would have loosened the
     desktop search bar for no reason and still missed large tablets.
   - `max-width` covers *layout*, which really is about available space.
   ========================================================================== */

@media (pointer: coarse) {
  /* The custom selects rendered a 21px-tall button inside a padded field, so
     most of what looks tappable was the parent's padding and did nothing.
     Moving the height onto the control makes the visible field the target. */
  .kselect__button {
    min-height: 44px;
    padding-block: 0.5rem;
  }

  /* Hero quicklink chips were 23px tall. */
  .hero__quicklinks a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding-block: 0.5rem;
  }

  /* Pagination numbers, card arrows and inline text links all sit close to
     other targets; 44px keeps them separable by thumb. */
  .pagination a,
  .pagination .page-numbers {
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* The logo doubles as the home link once the menu collapses. */
  .brand {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* The example searches sit inline inside a hint sentence, which WCAG 2.5.8
     exempts from the 44px floor — forcing them to full size would turn one
     line of prose into six stacked rows. They get breathing room instead, so
     two adjacent suggestions cannot be mistaken for each other by thumb. */
  .search-hint {
    line-height: 2.4;
  }

  .search-hint a {
    padding: 0.25rem 0.125rem;
  }

  /* Native selects and form controls: iOS zooms the page in on focus when the
     font is under 16px, which then leaves the layout scrolled sideways and is
     the most common cause of "the site is broken on my iPhone". */
  .field input,
  .field textarea,
  .field select,
  .filters select,
  .roi__field input[type="number"] {
    font-size: 16px;
    min-height: 44px;
  }
}

@media (max-width: 767px) {
  /* The hero was a fixed-height flex box, bottom-aligned, with overflow hidden.
     On a phone its contents ran to 1310px inside an 844px box, so everything
     above the fold overflowed *upwards* and was clipped: the eyebrow, the
     headline and the lede were simply not on the page, and the search bar sat
     underneath the fixed header, overlapping the logo. That is the "barely
     visible text" over the wordmark.

     On small screens the hero becomes an ordinary block that grows with its
     content and starts below the header, so nothing can be clipped again. */
  .hero {
    height: auto;
    min-height: 0;
    align-items: flex-start;
  }

  .hero__body {
    padding-top: 6.5rem; /* clears the 72px fixed header, plus breathing room */
    padding-bottom: 3rem;
  }

  /* The headline was eating the screen before the search bar appeared. The
     search is the primary job of this page on a phone, so the type comes down
     and the search moves up. */
  .hero__title {
    font-size: clamp(1.875rem, 8.5vw, 2.5rem);
    margin-top: 0.75rem;
    max-width: none;
  }

  .hero__lede {
    font-size: 0.9375rem;
  }

  .smartsearch--hero {
    margin-top: 1.5rem;
  }

  /* Buttons stack rather than crowd: two 48px pills side by side on a 390px
     screen leaves each one too narrow to read its own label. */
  .btn-row {
    flex-direction: column;
    align-items: stretch;
  }

  .btn-row .btn {
    width: 100%;
  }

  /* Long menus need to scroll inside the panel rather than pushing the page:
     six items with submenus open is taller than a phone viewport. */
  .nav-mobile {
    max-height: calc(100vh - 4rem);
    max-height: calc(100dvh - 4rem);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* Sections carry desktop-scale vertical rhythm; on a phone it reads as dead
     space and pushes real content below three screens of scrolling. */
  .section {
    padding-block: 3.5rem;
  }

  .section--tight {
    padding-block: 2.5rem;
  }

  /* Stat rows were fixed multi-column and squeezed four figures onto 390px. */
  .stat-row--3,
  .stat-row--4 {
    grid-template-columns: repeat(2, 1fr);
  }

  /* The per-plot inventory table is seven columns wide. It scrolls inside its
     own container, but the hint that it scrolls has to be visible. */
  .table-scroll {
    -webkit-overflow-scrolling: touch;
  }
}

@media (max-width: 480px) {
  /* A single column below this: two cards side by side leaves each ~170px,
     narrower than the photo needs to read as a property. */
  .villa-grid {
    grid-template-columns: 1fr;
  }

  .stat-row--3,
  .stat-row--4 {
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem;
  }
}

/* ==========================================================================
   v2.6 — Motion

   The house style is restraint: long easing, short distances, opacity doing
   most of the work. Nothing bounces, spins or overshoots — on a page asking
   someone to consider a 60-million-baht villa, motion has to read as
   confidence, not decoration.

   Every rule here is transform/opacity only, so nothing in this section can
   move layout or cost a CLS point, and all of it collapses under
   prefers-reduced-motion via the global block near the top of the file.
   `.js-motion` is set by theme.js, so anything that starts hidden is only
   ever hidden when there is JavaScript running to reveal it again.
   ========================================================================== */

/* --- Staggered reveal ---------------------------------------------------
   Cards in a grid used to fade in together, which reads as a page loading
   rather than as content arriving. JS assigns each child an index and the
   delay is derived from it, so a six-card grid resolves over ~400ms. */
.reveal {
  transition-delay: var(--reveal-delay, 0s);
}

/* --- Headline mask ------------------------------------------------------
   The one deliberately expressive move: a heading wipes up from behind its
   own baseline. Done with clip-path on the block rather than by splitting
   text into spans, so it survives <em>, <br> and translation without any
   markup change and cannot leave orphaned nodes if JS fails mid-way. */
.js-motion .reveal-mask {
  clip-path: inset(0 0 105% 0);
  transform: translateY(14px);
  transition: clip-path 1.05s var(--ease), transform 1.05s var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}

.js-motion .reveal-mask.is-visible {
  clip-path: inset(0 0 -20% 0); /* -20% so descenders are never clipped */
  transform: none;
}

/* --- Scroll progress ----------------------------------------------------
   Project pages run long — Patong Bay Park is a 116-row inventory table.
   A hairline of progress tells you how much is left without adding an
   element anyone has to look at. */
.scroll-progress {
  position: fixed;
  z-index: 60;
  top: 0;
  left: 0;
  width: 100%;
  height: 2px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: linear-gradient(to right, var(--berry), var(--gold));
  pointer-events: none;
}

/* --- Lazy image fade ----------------------------------------------------
   Images resolve in rather than snapping. Scoped to .js-motion so a JS
   failure can never leave a page of invisible photography. */
.js-motion img[data-motion-img] {
  opacity: 0;
  transition: opacity 0.9s var(--ease);
}

.js-motion img[data-motion-img].is-loaded {
  opacity: 1;
}

/* --- Card polish --------------------------------------------------------
   The arrow already changed colour on hover; now it moves the way the
   cursor is about to. 2px is enough to register and not enough to notice. */
.villa-card__arrow svg {
  transition: transform 0.4s var(--ease);
}

.villa-card:hover .villa-card__arrow svg {
  transform: translate(2px, -2px);
}

.villa-card {
  transition: border-color 0.5s ease, transform 0.5s var(--ease);
}

.villa-card:hover {
  transform: translateY(-3px);
}

/* --- Counters -----------------------------------------------------------
   Held at a fixed width while counting so the surrounding layout cannot
   shuffle as digits change. The font is already tabular-nums. */
.stat__value[data-counting] {
  display: inline-block;
  min-width: 3ch;
}

/* --- Underline links ----------------------------------------------------
   A wipe rather than a fade: reads as deliberate. Used on prose links only,
   never on buttons or cards. */
.entry-content a:not(.btn) {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size 0.45s var(--ease);
  text-decoration: none;
}

.entry-content a:not(.btn):hover,
.entry-content a:not(.btn):focus-visible {
  background-size: 100% 1px;
}

@media (prefers-reduced-motion: reduce) {
  .js-motion .reveal-mask {
    clip-path: none;
    transform: none;
  }
  .js-motion img[data-motion-img] {
    opacity: 1;
  }
  .villa-card:hover {
    transform: none;
  }
  .scroll-progress {
    display: none;
  }
}

/* ==========================================================================
   v2.7 — Key Features

   Two columns of icon + name + one supporting line, matching the reference
   KUBAY supplied. The icon column is a fixed width so every name starts on
   the same vertical line — the single thing that makes a grid like this read
   as designed rather than assembled.
   ========================================================================== */
.features {
  margin-top: 3rem;
}

.features__title {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gold);
  padding-bottom: 0.875rem;
  border-bottom: 1px solid var(--line-strong);
  margin: 0;
}

.features__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.75rem 2.5rem;
  margin: 1.75rem 0 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 560px) {
  .features__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

.feature {
  display: grid;
  grid-template-columns: 2.25rem 1fr;
  gap: 0.875rem;
  align-items: start;
  min-width: 0;
}

.feature__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gold);
  /* Optical alignment: the icon reads as centred against the two lines of
     text beside it rather than against the first line only. */
  margin-top: 0.125rem;
}

.feature__body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}

.feature__name {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--paper);
  line-height: 1.3;
}

.feature__desc {
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--mute);
}

/* --- Category hero imagery ----------------------------------------------
   Commercial and Land have no listings to show, so the page has to carry
   itself on one image and the explanation beneath it. */
.page-hero--category {
  position: relative;
  overflow: hidden;
}

.page-hero--category .page-hero__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.page-hero--category .page-hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.page-hero--category .page-hero__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Text over photography stays light-on-dark in both themes, by design. */
  background: linear-gradient(to top, var(--ink) 4%, rgba(10, 10, 11, 0.55) 60%, rgba(10, 10, 11, 0.35));
}

.page-hero--category .page-hero__body {
  position: relative;
  z-index: 1;
  padding-block: 7rem 4rem;
}

.page-hero--category .page-hero__title,
.page-hero--category .lede {
  color: #f7f4ef;
}

.page-hero--category .lede {
  max-width: 60ch;
}

@media (max-width: 767px) {
  .page-hero--category .page-hero__body {
    padding-block: 6rem 2.5rem;
  }
}

/* Three-across variant, used for the commercial building formats. */
@media (min-width: 720px) {
  .features__grid--three {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ==========================================================================
   v3.0 — Contact bar

   Enquiring meant scrolling a long page to a form. Most first contact in this
   market is WhatsApp, and the phone number sat three taps away in the footer.
   Both are now one tap away for the whole scroll.

   Fixed to the bottom on phones, where the thumb already is; a quiet inline
   row on desktop, where a floating bar would just cover the content.
   ========================================================================== */
.contact-bar {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem var(--gutter) calc(0.75rem + env(safe-area-inset-bottom));
  background: var(--surface-panel);
  border-top: 1px solid var(--line-strong);
}

.contact-bar__btn {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 48px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  font-size: 0.8125rem;
  letter-spacing: 0.02em;
  color: var(--paper);
  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

.contact-bar__btn:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.contact-bar__btn--primary {
  background: var(--berry);
  border-color: var(--berry);
  color: #fff;
}

.contact-bar__btn--primary:hover {
  background: var(--berry-bright);
  border-color: var(--berry-bright);
  color: #fff;
}

/* WhatsApp keeps its own green: it is a recognised affordance, and restyling
   it into the brand palette costs more in recognition than it gains. */
.contact-bar__btn--whatsapp {
  border-color: rgba(37, 211, 102, 0.5);
  color: #25d366;
}

.contact-bar__btn--whatsapp:hover {
  background: rgba(37, 211, 102, 0.12);
  border-color: #25d366;
  color: #25d366;
}

@media (max-width: 767px) {
  .contact-bar {
    position: fixed;
    z-index: 55;
    left: 0;
    right: 0;
    bottom: 0;
    transform: translateY(110%);
    transition: transform 0.45s var(--ease);
    backdrop-filter: blur(10px);
  }

  /* Held back until the hero has been passed, so it never covers the first
     screen — the price and the headline earn the tap, not the bar. */
  .contact-bar.is-visible {
    transform: none;
  }

  /* Room for the bar so it never sits on top of the footer's last line. */
  body:has(.contact-bar) .site-footer {
    padding-bottom: 6rem;
  }
}

@media (min-width: 768px) {
  .contact-bar {
    max-width: var(--wrap);
    margin: 0 auto 4rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: transparent;
    justify-content: center;
  }

  .contact-bar__btn {
    flex: 0 1 14rem;
  }
}

/* ==========================================================================
   v3.1 — Social icons, deal flags, quick filters
   ========================================================================== */

/* --- Social icons -------------------------------------------------------- */
.social-links {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.social-links a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 999px;
  color: var(--mute);
  transition: color 0.3s ease, background-color 0.3s ease;
}

.social-links a:hover {
  color: var(--gold);
  background: rgba(198, 161, 91, 0.1);
}

.social-links--header {
  margin-right: 0.25rem;
}

/* On touch the icons need the full target, and the mobile panel has room. */
@media (pointer: coarse) {
  .social-links a {
    width: 44px;
    height: 44px;
  }
}

.social-links--mobile {
  margin-top: 1.25rem;
  gap: 0.5rem;
}

.social-links--footer {
  flex-wrap: wrap;
  margin-top: 0.25rem;
}

/* --- Deal flags ---------------------------------------------------------
   Sale and rent used to be a choice you made in the menu before you had seen
   anything. Stating it on the card lets one listing be both, which several
   genuinely are, and removes a decision from the start of the journey. */
.deal-flags {
  position: absolute;
  z-index: 2;
  left: 1rem;
  bottom: 1rem;
  display: flex;
  gap: 0.375rem;
}

.deal-flag {
  border-radius: 999px;
  padding: 0.1875rem 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
}

.deal-flag--sale {
  background: rgba(10, 10, 11, 0.78);
  color: var(--gold-soft);
}

.deal-flag--rent {
  background: var(--berry);
  color: #fff;
}

[data-theme="light"] .deal-flag--sale {
  background: rgba(19, 16, 15, 0.82);
  color: #f0d9a8;
}

/* --- Quick filters ------------------------------------------------------
   Plain links, not a JS widget: shareable, bookmarkable, works without
   scripts, and nothing to maintain when stock changes. */
.quick-filters {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  margin-bottom: 2rem;
}

.quick-filters__row {
  display: flex;
  align-items: baseline;
  gap: 0.875rem;
  min-width: 0;
}

.quick-filters__label {
  flex: none;
  width: 3.5rem;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mute-dim);
}

.quick-filters__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  min-width: 0;
}

.qf-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 0.375rem 0.75rem;
  font-size: 0.8125rem;
  color: var(--mute);
  transition: border-color 0.3s ease, color 0.3s ease, background-color 0.3s ease;
}

.qf-chip:hover {
  border-color: var(--gold);
  color: var(--paper);
}

.qf-chip.is-active {
  border-color: var(--berry);
  background: var(--berry);
  color: #fff;
}

.qf-chip__count {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  opacity: 0.65;
}

@media (pointer: coarse) {
  .qf-chip {
    min-height: 40px;
  }
}

@media (max-width: 560px) {
  .quick-filters__row {
    flex-direction: column;
    gap: 0.5rem;
    align-items: stretch;
  }
  .quick-filters__label {
    width: auto;
  }
  /* Chips scroll sideways rather than stacking into a wall of rows.
     min-width:0 alone did not hold the scroller inside the page: as a
     stretched flex item it still sized to its content and pushed the whole
     document 204px wide on a 375px screen. The explicit max-width is what
     actually binds it — the same class of fault as the grid items further
     up this file. */
  .quick-filters__chips {
    display: flex;
    width: 100%;
    max-width: 100%;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    padding-bottom: 0.25rem;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .quick-filters__chips::-webkit-scrollbar {
    display: none;
  }
  .qf-chip {
    flex: 0 0 auto;
    white-space: nowrap;
  }
}

/* ==========================================================================
   v3.1 — Quick contact, brief block, card hover
   ========================================================================== */

/* --- Quick contact -------------------------------------------------------
   A footnote under the form, not a second call to action. Text weight, no
   competing colour: the form stays the primary path and this catches the
   people who would simply rather message. */
.quick-contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1.25rem;
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  font-size: 0.8125rem;
}

.quick-contact__label {
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mute-dim);
}

.quick-contact__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  color: var(--mute);
  transition: color 0.3s ease;
}

.quick-contact__link:hover {
  color: var(--gold);
}

@media (pointer: coarse) {
  .quick-contact__link {
    min-height: 44px;
  }
}

/* --- Commercial brief block ---------------------------------------------- */
.brief-block {
  margin-top: 3rem;
  gap: 3.5rem;
}

.numbered-steps {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  margin: 1.75rem 0 0;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.numbered-steps li {
  display: grid;
  grid-template-columns: 2.5rem 1fr;
  gap: 1rem;
  align-items: start;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--mute);
}

.numbered-steps__n {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--gold);
  padding-top: 0.125rem;
}

.numbered-steps strong {
  display: block;
  color: var(--paper);
  font-weight: 600;
  margin-bottom: 0.125rem;
}

/* --- Card hover ----------------------------------------------------------
   The whole card lifts and its image drifts; the arrow already moved. Kept to
   transform and opacity so it stays on the compositor and costs nothing. */
.villa-card__media img {
  transition: transform 0.9s var(--ease);
}

.villa-card:hover .villa-card__media img {
  transform: scale(1.045);
}

@media (prefers-reduced-motion: reduce) {
  .villa-card:hover .villa-card__media img {
    transform: none;
  }
}

/* ==========================================================================
   v3.2 — Social icon fit and recognition
   ========================================================================== */

/* Tighter in the header so five icons still fit beside the nav at 1180px,
   where the header is at its most crowded before collapsing to a burger. */
.social-links--header a {
  width: 2rem;
  height: 2rem;
}

.social-links--header {
  gap: 0.125rem;
  padding-right: 0.5rem;
  margin-right: 0.25rem;
  border-right: 1px solid var(--line);
}

/* WhatsApp and Facebook keep their own colour on hover. These two are
   recognised by colour before they are recognised by shape, and WhatsApp is
   the channel most buyers here actually use — worth the small break from the
   palette. The rest stay gold, so the row still reads as one set. */
.social-links a[aria-label="WhatsApp"]:hover {
  color: #25d366;
  background: rgba(37, 211, 102, 0.12);
}

.social-links a[aria-label="Facebook"]:hover {
  color: #1877f2;
  background: rgba(24, 119, 242, 0.12);
}

/* At rest the header icons sit slightly brighter than body muted text so they
   register as controls rather than decoration, without shouting. */
.social-links--header a {
  color: var(--mute);
}

.social-links--footer a,
.social-links--mobile a {
  border: 1px solid var(--line);
}

.social-links--footer a:hover,
.social-links--mobile a:hover {
  border-color: currentColor;
}

/* The header row is the first thing to go when space runs out — the icons are
   a convenience, the navigation is not. */
@media (max-width: 1339px) and (min-width: 1180px) {
  .social-links--header {
    display: none;
  }
}

/* ==========================================================================
   v3.3 — Light-mode control affordance, and a footer row instead of a column
   ========================================================================== */

/* The five catalogue filters are custom selects: .kselect__button is
   deliberately borderless and transparent because .psearch__field around it is
   what you are meant to see. In dark mode that field is a near-black panel on a
   near-black page and the 1px grid gaps read as bright hairlines, so the group
   is obviously a control. Light mode inverted both halves of that: the field
   became white on cream — 1.27:1 — and the hairlines became the only edge,
   at 0.32 alpha. The sort select beside them carries its own border and so
   stayed looking like the one real control on the page.

   The fix is on the group, not the button: darken the row so the outer edge and
   the dividers both hold on paper, and let the field lift off it. */
[data-theme="light"] .psearch__row {
  background: rgba(19, 16, 15, 0.45);
  border-color: rgba(19, 16, 15, 0.45);
  box-shadow: 0 1px 3px rgba(19, 16, 15, 0.09);
}

/* The caret was --mute-dim, the same grey as the label above it. It is the only
   mark that says "this opens", so it carries the accent instead. */
[data-theme="light"] .kselect__caret {
  color: var(--berry);
}

/* Focus was a background swap from --ink-raised to --ink-raised — the same
   colour, so keyboard focus moved through the row invisibly. */
[data-theme="light"] .psearch__field:focus-within {
  background: #fff;
  box-shadow: inset 0 0 0 2px var(--berry);
}

/* Chips and page numbers sat on --line (0.18 alpha, 1.46:1 against the page).
   Next to the solid berry active chip an unselected one read as loose text
   rather than a control. --line-strong is the same token the other light-mode
   edges already use. */
[data-theme="light"] .qf-chip,
[data-theme="light"] .pagination .page-numbers {
  border-color: var(--line-strong);
}

/* --- Footer social row ---------------------------------------------------
   The icons used to hold a .footer-col of their own, which bought a heading
   and a quarter of the grid for what is currently two links. They now sit
   under the contact list in the about column; the grid is back to the three
   columns its 1.2fr 1fr 1fr template was written for. */
.social-links--footer {
  margin-top: 1.5rem;
  gap: 0.375rem;
}

.social-links--footer a {
  width: 2rem;
  height: 2rem;
}

/* Touch still needs the full target, but the row is inside a column now, so it
   takes the standard 44px rather than growing the footer further. */
@media (pointer: coarse) {
  .social-links--footer a {
    width: 44px;
    height: 44px;
  }
}

/* Dropping the Follow column left four children in a three-column grid, so
   8-One Villas sat alone on a second row and the footer was no shorter than
   before. Four columns puts all of them on one line — which is where the
   height saving actually comes from. The about column keeps the wider share. */
@media (min-width: 1024px) {
  .footer-grid {
    grid-template-columns: 1.4fr 1fr 1fr 1fr;
  }
}

/* ==========================================================================
   v3.4 — Light theme over photography, and the empty-cell grid
   ========================================================================== */

/* The hero scrim's bottom stop was var(--ink). In dark mode that is #0a0a0b, so
   the photo fades to black and the white type over it reads — which is the
   intent. In light mode --ink IS the cream page colour, so the identical rule
   faded the photo to cream instead, and everything white sitting low in the
   hero went with it: the "Try" suggestion links, the quicklink chips, the lede.
   That is one token doing two opposite jobs, the same fault the surface tokens
   were introduced to fix.

   The scrim sits over photography, so like .villa-hero__scrim and
   .page-hero__bg::after it is dark in both themes and only its strength
   changes. Light mode keeps it a little softer so the photo still carries. */
[data-theme="light"] .hero__scrim {
  background: linear-gradient(
      to top,
      rgba(25, 19, 22, 0.9) 0%,
      rgba(25, 19, 22, 0.6) 48%,
      rgba(25, 19, 22, 0.5) 100%
    ),
    linear-gradient(to right, rgba(25, 19, 22, 0.5), transparent 62%);
}

/* The unscrolled header laid a 0.9-alpha cream wash across the top of the hero
   photo. It kept the dark nav type legible but bleached the image under it.
   Softening it to 0.72 keeps the type readable — it still sits on cream, and
   the brand mark still swaps to its light-theme variant — while letting the
   photograph through. The solid bar on scroll is unchanged. */
[data-theme="light"] .site-header {
  background: linear-gradient(rgba(232, 224, 211, 0.72), rgba(232, 224, 211, 0) 92%);
}

/* Submenu items were --mute against the dropdown panel. The top-level items are
   --paper; the children now match them, so the whole menu reads at one weight. */
[data-theme="light"] .nav-primary .submenu a {
  color: var(--paper);
}

/* The suggestion row under the search is the one place in the hero that is
   meant to be tapped, and it was the faintest thing in it — 0.72 alpha white
   with no underline and no edge. Full-strength type, and a visible edge so it
   reads as a control rather than as a caption. */
[data-theme="light"] .hero .search-hint a,
.hero .search-hint a {
  color: #fffcf7;
  border-bottom: 1px solid rgba(255, 252, 247, 0.45);
}

[data-theme="light"] .hero .search-hint a:hover,
.hero .search-hint a:hover {
  border-bottom-color: currentColor;
}

[data-theme="light"] .hero .search-hint__label,
.hero .search-hint__label {
  color: rgba(255, 252, 247, 0.85);
}

/* --- The empty-cell grey block -----------------------------------------
   .type-grid drew its hairlines by painting the container and letting a 1px
   gap show through. That only holds while the grid is exactly full. The
   rentals page has four rental types in a three-column grid, so the second row
   carried one tile and two empty cells — and those cells painted the container
   colour as a single 800x167 grey block, which is the "dark grey spot".

   Painting the line on the tile instead means an empty cell paints nothing.
   outline is used rather than box-shadow because the tiles already carry an
   elevation box-shadow in light mode, and outline does not fight it. */
.type-grid,
[data-theme="light"] .type-grid {
  background: transparent;
}

.type-tile {
  outline: 1px solid var(--line);
}

/* Keyboard focus must still win over the resting outline. */
.type-tile:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: -2px;
}

/* --- Direct-contact actions on a listing --------------------------------
   Sits under the enquiry button in the Key Facts card. The form is still the
   primary action, so these are chip-height rather than full buttons — but they
   carry their channel's own colour, because WhatsApp and Messenger are
   recognised by colour before they are read, and that recognition is the whole
   point of putting them here. */
.contact-actions {
  margin-top: 1.25rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}

.contact-actions__label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--mute-dim);
}

.contact-actions__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

.contact-action {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  min-height: 40px;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: 0.8125rem;
  color: var(--paper);
  transition: border-color 0.25s ease, background-color 0.25s ease,
    color 0.25s ease;
}

.contact-action svg {
  flex-shrink: 0;
}

.contact-action--whatsapp:hover {
  color: #25d366;
  border-color: #25d366;
  background: rgba(37, 211, 102, 0.12);
}

.contact-action--facebook:hover {
  color: #1877f2;
  border-color: #1877f2;
  background: rgba(24, 119, 242, 0.12);
}

.contact-action--email:hover {
  color: var(--gold);
  border-color: var(--gold);
  background: rgba(198, 161, 91, 0.12);
}

[data-theme="light"] .contact-action:hover {
  background: var(--ink-raised);
}

[data-theme="light"] .contact-action--email:hover {
  color: var(--berry);
  border-color: var(--berry);
}

/* Touch targets: the row is inside a card, so 44px is the ceiling rather than
   something that would push the card taller than the copy beside it. */
@media (pointer: coarse) {
  .contact-action {
    min-height: 44px;
  }
}

/* ==========================================================================
   v3.5 — Icons in the key-facts list
   Each row in the Key Facts / Investment Routes card now leads with the icon
   for what it states. The list is scanned rather than read — a buyer is
   looking for one number — and an icon gives the eye something to land on.
   The icon is decorative: the label beside it already carries the meaning,
   so it is not announced separately to a screen reader.
   ========================================================================== */
.spec-list li span:first-child {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

.spec-list li span:first-child svg {
  flex-shrink: 0;
  color: var(--gold);
  opacity: 0.85;
}

[data-theme="light"] .spec-list li span:first-child svg {
  color: var(--berry);
  opacity: 1;
}

/* The value column must keep priority when a label wraps on a narrow card —
   but `flex-shrink: 0` was too blunt a way to say it. A value long enough to
   need the whole row (a project's unit-type list runs to six names) then
   refused to shrink at all, so it kept its intrinsic width, drove straight
   through the label beside it and out of the card. That is the bug in the
   screenshot: berry text laid over the words "Unit types" and spilling past
   the card's right edge.

   `flex: 1 1 8rem` says the same thing without the absolutism. The value still
   takes the space it needs while there is room; once the row cannot give it
   8rem, `flex-wrap` drops it onto its own full-width line underneath the label
   — right-aligned, wrapping normally, contained. A price, which is one short
   token, never reaches that point and still sits on the label's line. */
.spec-list li {
  flex-wrap: wrap;
  align-items: baseline;
}

.spec-list li span:first-child {
  flex: 0 1 auto;
}

.spec-list li span:last-child {
  flex: 1 1 8rem;
  min-width: 0;
  max-width: 100%;
  text-align: right;
  overflow-wrap: anywhere;
}

/* ==========================================================================
   v3.6 — Gallery roller
   The old gallery was a fixed grid, so a development with sixteen photographs
   rendered sixteen tall rows and a listing with none rendered nothing at all.
   A horizontal roller shows the same set in one screen's height, and scales
   from one photograph to twenty without changing the page's shape.

   Scroll-snap does the work. The buttons only nudge the same scroll container,
   so touch, trackpad, keyboard and the buttons all drive one mechanism.
   ========================================================================== */
.roller {
  margin-top: 1.5rem;
}

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

.roller__nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.roller__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--paper);
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.25s ease,
    color 0.25s ease, opacity 0.25s ease;
}

.roller__btn:hover:not(:disabled) {
  border-color: var(--gold);
  color: var(--gold);
}

.roller__btn:disabled {
  opacity: 0.35;
  cursor: default;
}

/* One icon, mirrored — a second path in the sprite for "the same arrow backwards"
   is a thing to keep in sync for no gain. */
[data-roller-prev] svg {
  transform: rotate(180deg);
}

.roller__count {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--mute-dim);
  min-width: 3.5rem;
  text-align: center;
}

.roller__track {
  display: flex;
  gap: 1rem;
  margin-top: 1.25rem;
  padding-bottom: 0.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: thin;
  /* The recurring overflow trap: a flex item defaults to min-width:auto and
     refuses to shrink, so this container never binds and the page scrolls
     instead of the roller. Both properties are needed — min-width alone has
     not been enough anywhere else in this file either. */
  min-width: 0;
  max-width: 100%;
  -webkit-overflow-scrolling: touch;
}

.roller__track:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 4px;
}

.roller__slide {
  flex: 0 0 min(100%, 34rem);
  scroll-snap-align: start;
  min-width: 0;
}

.roller__slide img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 0.75rem;
  background: var(--surface-card);
}

@media (min-width: 1024px) {
  .roller__slide {
    flex-basis: min(100%, 42rem);
  }
}

/* Smooth scrolling is motion. Honour the same preference the rest of the
   theme does rather than inventing an exception for this component. */
@media (prefers-reduced-motion: reduce) {
  .roller__track {
    scroll-behavior: auto;
  }
}

@media (pointer: coarse) {
  .roller__btn {
    width: 44px;
    height: 44px;
  }
}

/* ==========================================================================
   v3.7 — Roller, second pass: no system scrollbar, thumbnails, lightbox
   The first pass left the native scrollbar showing, which made a gallery read
   as an overflow box that had failed to fit. Hiding it means the affordances
   have to carry the message instead — hence the count badge, the zoom cursor,
   the hover glyph, the edge fade and the thumbnail strip.
   ========================================================================== */
.roller__viewport {
  position: relative;
  margin-top: 1.25rem;
  min-width: 0;
  max-width: 100%;
}

/* Hidden, not disabled: the track still scrolls by touch, wheel, keyboard and
   the buttons. Firefox and WebKit need separate declarations. */
.roller__track {
  scrollbar-width: none;
  margin-top: 0;
  padding-bottom: 0;
}

.roller__track::-webkit-scrollbar {
  display: none;
}

/* A consistent peek of the next photograph reads as "there is more this way".
   Without it a full-bleed slide looks like the only slide. */
.roller__slide {
  flex-basis: 86%;
}

@media (min-width: 768px) {
  .roller__slide {
    flex-basis: 62%;
  }
}

@media (min-width: 1180px) {
  .roller__slide {
    flex-basis: 48%;
  }
}

/* Softens the cut at the right edge so the peek reads as continuation rather
   than a cropped image. Never intercepts a click. */
.roller__fade {
  position: absolute;
  inset: 0 0 0 auto;
  width: 4rem;
  pointer-events: none;
  background: linear-gradient(to right, transparent, var(--ink));
  opacity: 0.9;
}

.roller__open {
  display: block;
  position: relative;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}

.roller__zoom {
  position: absolute;
  right: 0.75rem;
  bottom: 0.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.72);
  color: #f7f4ef;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.25s var(--ease), transform 0.25s var(--ease);
}

.roller__open:hover .roller__zoom,
.roller__open:focus-visible .roller__zoom {
  opacity: 1;
  transform: none;
}

/* Hover never fires on touch, so the glyph is always shown there. */
@media (pointer: coarse) {
  .roller__zoom {
    opacity: 1;
    transform: none;
  }
}

/* The badge does the most work of anything here: it announces the gallery
   before the visitor has scrolled or hovered anything. */
.roller__badge {
  position: absolute;
  left: 0.75rem;
  bottom: 0.75rem;
  padding: 0.375rem 0.75rem;
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.72);
  backdrop-filter: blur(6px);
  color: #f7f4ef;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  transition: opacity 0.4s var(--ease);
}

.roller.is-touched .roller__badge {
  opacity: 0;
}

.roller__thumbs {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.75rem;
  overflow-x: auto;
  scrollbar-width: none;
  min-width: 0;
  max-width: 100%;
}

.roller__thumbs::-webkit-scrollbar {
  display: none;
}

.roller__thumb {
  display: block;
  width: 72px;
  height: 48px;
  padding: 0;
  border: 0;
  border-radius: 0.375rem;
  overflow: hidden;
  cursor: pointer;
  opacity: 0.45;
  outline: 1px solid transparent;
  transition: opacity 0.25s ease, outline-color 0.25s ease;
}

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

.roller__thumb:hover,
.roller__thumb.is-active {
  opacity: 1;
}

.roller__thumb.is-active {
  outline-color: var(--gold);
}

/* --- Lightbox ------------------------------------------------------------
   Themed from the same tokens as everything else so it does not read as a
   bolted-on plugin. Dark in both themes: it sits over photography. */
.lightbox {
  width: min(96vw, 88rem);
  max-width: none;
  height: min(94vh, 60rem);
  max-height: none;
  padding: 0;
  border: 0;
  border-radius: 1rem;
  background: rgba(16, 13, 15, 0.96);
  color: #f7f4ef;
  overflow: hidden;
}

.lightbox::backdrop {
  background: rgba(10, 10, 11, 0.82);
  backdrop-filter: blur(10px);
}

.lightbox__bar {
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.25rem;
  background: linear-gradient(rgba(10, 10, 11, 0.65), transparent);
}

.lightbox__count {
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: rgba(247, 244, 239, 0.75);
}

.lightbox__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid rgba(247, 244, 239, 0.25);
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.5);
  color: #f7f4ef;
  cursor: pointer;
  transition: border-color 0.25s ease, background-color 0.25s ease;
}

.lightbox__close:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.lightbox__figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  margin: 0;
  padding: 3.5rem 1rem 2.5rem;
}

.lightbox__figure img {
  max-width: 100%;
  max-height: 100%;
  /* contain, never upscaled: a 900px photo blown up to 1600 looks worse than
     the same photo shown at its own size. */
  object-fit: contain;
  border-radius: 0.5rem;
}

.lightbox__caption {
  margin-top: 0.75rem;
  font-size: 0.8125rem;
  color: rgba(247, 244, 239, 0.7);
  text-align: center;
  max-width: 60ch;
}

.lightbox__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid rgba(247, 244, 239, 0.25);
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.55);
  color: #f7f4ef;
  cursor: pointer;
  transition: border-color 0.25s ease, opacity 0.25s ease;
}

.lightbox__nav:hover:not(:disabled) {
  border-color: var(--gold);
  color: var(--gold);
}

.lightbox__nav:disabled {
  opacity: 0.25;
  cursor: default;
}

.lightbox__nav--prev {
  left: 1rem;
}

.lightbox__nav--prev svg {
  transform: rotate(180deg);
}

.lightbox__nav--next {
  right: 1rem;
}

@media (max-width: 600px) {
  .lightbox__nav {
    display: none;
  }
  .lightbox__figure {
    padding: 3.5rem 0.5rem 2rem;
  }
}

/* ==========================================================================
   v3.7 — Category tiles: photograph behind, motion on hover
   The tiles were flat panels with a number and a name. A photograph behind
   each one tells the visitor what the category actually looks like before
   they click, which is the whole job of a category tile.

   The photograph is deliberately far back — heavily dimmed at rest, lifting
   on hover. The text has to stay the loudest thing in the tile at every
   moment, so the scrim is what animates, not the type.
   ========================================================================== */
.type-tile {
  isolation: isolate;
  overflow: hidden;
}

/* Set from the template as --tile-bg. A tile with no photograph simply has no
   custom property and keeps the flat surface it has now — nothing breaks. */
.type-tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background-image: var(--tile-bg, none);
  background-size: cover;
  background-position: center;
  opacity: 0.16;
  transform: scale(1.04);
  transition: opacity 0.55s var(--ease), transform 0.7s var(--ease);
}

/* A wash between photo and text. Without it, a light patch in the photograph
   puts pale type on pale ground somewhere in the tile. */
.type-tile::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to top,
    var(--surface-card) 12%,
    rgba(0, 0, 0, 0) 92%
  );
  opacity: 0.92;
  transition: opacity 0.55s var(--ease);
}

.type-tile:hover::before,
.type-tile:focus-visible::before {
  opacity: 0.42;
  transform: scale(1);
}

.type-tile:hover::after,
.type-tile:focus-visible::after {
  opacity: 0.78;
}

/* The arrow is the only element that moves — a short travel along its own
   diagonal, which is the direction the glyph already points. */
.type-tile__arrow {
  transition: transform 0.4s var(--ease), color 0.3s ease;
}

.type-tile:hover .type-tile__arrow,
.type-tile:focus-visible .type-tile__arrow {
  transform: translate(3px, -3px);
}

/* Light mode needs a stronger wash: the photograph sits on white card stock
   rather than near-black, so the same opacity reads much brighter. */
[data-theme="light"] .type-tile::before {
  opacity: 0.1;
}

[data-theme="light"] .type-tile:hover::before,
[data-theme="light"] .type-tile:focus-visible::before {
  opacity: 0.3;
}

/* Hover is a pointer idiom. On touch the photograph sits at a middle opacity
   permanently, so those visitors get the image rather than nothing. */
@media (hover: none) {
  .type-tile::before {
    opacity: 0.28;
    transform: none;
  }
  [data-theme="light"] .type-tile::before {
    opacity: 0.18;
  }
}

@media (prefers-reduced-motion: reduce) {
  .type-tile::before,
  .type-tile::after,
  .type-tile__arrow {
    transition: none;
  }
  .type-tile:hover::before {
    transform: scale(1.04);
  }
}

/* ==========================================================================
   v3.8 — The gallery is the hero
   The photographs used to sit below the specification table, which is the
   wrong way round: the photographs are what a buyer came for. The hero is now
   the gallery — same roller markup, so the same script drives arrows, keyboard,
   drag, swipe and the lightbox with no second implementation.

   It is deliberately shorter than the viewport. The stat row underneath stays
   partly in frame so the page reads as a page that opens with photographs
   rather than as a slideshow to be got past.
   ========================================================================== */
.villa-hero--gallery {
  height: auto;
  min-height: 0;
}

.roller--hero {
  position: relative;
  margin: 0;
}

.roller__track--hero {
  gap: 0;
  margin: 0;
  padding: 0;
  border-radius: 0;
}

.roller__slide--hero {
  flex: 0 0 100%;
  height: 78svh;
  min-height: 420px;
  max-height: 860px;
}

.roller__slide--hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  aspect-ratio: auto;
}

/* The controls sit clear of the title block, which occupies the lower left. */
.hero-roller__ui {
  position: absolute;
  right: 1.25rem;
  bottom: 1.5rem;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.roller__btn--hero {
  background: rgba(10, 10, 11, 0.55);
  backdrop-filter: blur(8px);
  border-color: rgba(247, 244, 239, 0.28);
  color: #f7f4ef;
}

.roller__btn--hero:hover:not(:disabled) {
  border-color: var(--gold);
  color: var(--gold);
}

.roller__count--hero {
  padding: 0.25rem 0.625rem;
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.55);
  backdrop-filter: blur(8px);
  color: rgba(247, 244, 239, 0.9);
  min-width: 0;
}

.hero-roller__dots {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.75rem;
  z-index: 3;
  display: flex;
  justify-content: center;
  gap: 0.375rem;
  padding: 0;
}

.hero-roller__dot {
  width: 22px;
  height: 3px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(247, 244, 239, 0.35);
  cursor: pointer;
  transition: background-color 0.3s ease, width 0.3s var(--ease);
}

.hero-roller__dot.is-active {
  background: var(--gold);
  width: 34px;
}

/* The scrim has to clear the dots and the controls, and it must not swallow a
   drag that started on the photograph. */
.villa-hero--gallery .villa-hero__scrim {
  pointer-events: none;
  z-index: 2;
}

.villa-hero--gallery .villa-hero__body {
  z-index: 3;
  pointer-events: none;
}

.villa-hero--gallery .villa-hero__body a {
  pointer-events: auto;
}

@media (max-width: 767px) {
  .roller__slide--hero {
    height: 68svh;
    min-height: 360px;
  }

  /* On a narrow screen the arrows compete with the title, so the dots carry
     navigation and swipe does the rest. */
  .hero-roller__ui {
    right: 0.75rem;
    bottom: auto;
    top: 0.75rem;
  }

  .roller__btn--hero {
    width: 36px;
    height: 36px;
  }
}

/* --- Back link ---------------------------------------------------------- */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  margin-bottom: 1rem;
  padding: 0.4375rem 0.875rem;
  border: 1px solid rgba(247, 244, 239, 0.28);
  border-radius: 999px;
  background: rgba(10, 10, 11, 0.5);
  backdrop-filter: blur(8px);
  color: rgba(247, 244, 239, 0.9);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: border-color 0.25s ease, color 0.25s ease;
}

.back-link:hover {
  border-color: var(--gold);
  color: var(--gold);
}

.back-link__icon {
  display: inline-flex;
  transform: rotate(180deg);
}

@media (pointer: coarse) {
  .back-link {
    min-height: 44px;
  }
}

/* ==========================================================================
   v3.8 — Light theme, softened
   The light palette moved between flat blocks of cream and near-white with a
   hard line at every join, which reads as sharp next to the dark theme's long
   tonal falloff. These add the same gradual transitions the dark theme gets
   for free from its own depth — nothing changes colour, the steps between
   colours are just no longer abrupt.
   ========================================================================== */
[data-theme="light"] body {
  background:
    linear-gradient(180deg, #efe8dc 0%, var(--ink) 34%, var(--ink) 100%);
  background-attachment: fixed;
}

/* Section joins were a 1px line between two near-identical creams. A soft
   vertical wash reads as depth instead of as a seam. */
[data-theme="light"] .section--soft {
  background: linear-gradient(180deg, #ece5d8, #f3ede3);
}

[data-theme="light"] .site-footer {
  background: linear-gradient(180deg, var(--ink), #ddd3c2 70%);
}

/* Cards were pure white on cream — the sharpest edge on the page. A faint
   top-down warm gradient and a softer shadow settle them onto the paper. */
[data-theme="light"] .card,
[data-theme="light"] .villa-card,
[data-theme="light"] .panel,
[data-theme="light"] .type-tile,
[data-theme="light"] .content-result {
  background: linear-gradient(180deg, #fffdfa, #faf6ef);
}

[data-theme="light"] .psearch__row {
  background: linear-gradient(180deg, rgba(19, 16, 15, 0.4), rgba(19, 16, 15, 0.5));
}

/* The hero scrim already fades to dark; in light mode it now eases into the
   page colour underneath instead of stopping against it. */
[data-theme="light"] .villa-hero__scrim {
  background: linear-gradient(
    to top,
    rgba(23, 20, 26, 0.92) 0%,
    rgba(23, 20, 26, 0.62) 42%,
    rgba(23, 20, 26, 0.3) 100%
  );
}

/* ==========================================================================
   v3.9 — Listing intro below the photograph
   The title, price and back link sat on top of the hero image and covered the
   half of it a buyer most wanted to see. Moving them below means the
   photograph is unobstructed, the type needs no scrim to stay legible, and the
   heading is set on the page's own paper at full contrast.
   ========================================================================== */
.listing-intro {
  padding-block: 2.25rem 0;
}

.listing-intro__title {
  font-family: var(--font-display);
  font-size: clamp(1.875rem, 4.4vw, 3.25rem);
  line-height: 1.08;
  letter-spacing: -0.015em;
  margin-top: 0.625rem;
  max-width: 22ch;
}

.listing-intro__row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  margin-top: 1.125rem;
}

.listing-intro__price {
  font-family: var(--font-mono);
  font-size: clamp(1.125rem, 2.4vw, 1.5rem);
  color: var(--gold);
  font-variant-numeric: tabular-nums;
}

[data-theme="light"] .listing-intro__price {
  color: var(--berry);
}

/* The facts a buyer scans for before anything else, beside the price rather
   than in a table further down. */
.listing-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  min-width: 0;
}

.listing-fact {
  display: inline-flex;
  align-items: center;
  gap: 0.4375rem;
  padding: 0.375rem 0.75rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: 0.8125rem;
  color: var(--mute);
}

.listing-fact svg {
  flex-shrink: 0;
  color: var(--gold);
}

[data-theme="light"] .listing-fact svg {
  color: var(--berry);
}

.listing-intro__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

/* The back link is no longer over the image, so it takes the page's own
   colours rather than a translucent dark pill. */
.listing-intro .back-link {
  background: transparent;
  backdrop-filter: none;
  border-color: var(--line-strong);
  color: var(--mute);
  margin-bottom: 0.75rem;
}

.listing-intro .back-link:hover {
  border-color: var(--gold);
  color: var(--gold);
}

/* --- Hero, without the overlay --------------------------------------------
   With nothing on top of it the hero no longer needs to be tall enough to
   hold a heading, so it gets shorter and the intro below comes into view
   sooner. An aspect ratio rather than a viewport height also stops a very
   wide monitor from cropping the photograph to a letterbox. */
.roller__slide--hero {
  height: auto;
  aspect-ratio: 16 / 9;
  min-height: 0;
  max-height: 76svh;
}

/* Above 2000px there is no larger crop to serve — kubay-hero is 2000 wide —
   so the image is capped rather than upscaled, and the page background shows
   at the sides instead of a soft, stretched photograph. */
.villa-hero--gallery {
  max-width: 2000px;
  margin-inline: auto;
}

/* The controls sat top-right on mobile, directly under the fixed header, so
   they collided with the menu button. They belong at the bottom of the frame
   at every width, clear of both the header and the dots. */
@media (max-width: 767px) {
  .hero-roller__ui {
    top: auto;
    bottom: 1.25rem;
    right: 0.75rem;
  }

  .roller__slide--hero {
    aspect-ratio: 4 / 3;
    max-height: 62svh;
  }
}

/* --- Header navigation in light mode --------------------------------------
   The links are --paper, which is near-black and correct once the header is a
   solid bar. Unscrolled it is a wash over a photograph, and where that wash
   fades the same near-black type sits on a dark image. Holding the wash at
   full strength across the nav row and only then fading gives the type a
   consistent ground, whatever photograph is behind it. */
[data-theme="light"] .site-header {
  background: linear-gradient(
    rgba(232, 224, 211, 0.94) 0%,
    rgba(232, 224, 211, 0.9) 62%,
    rgba(232, 224, 211, 0) 100%
  );
}

[data-theme="light"] .nav-primary > ul > li > a {
  color: #17130f;
  font-weight: 600;
}

[data-theme="light"] .nav-primary > ul > li > a:hover,
[data-theme="light"] .nav-primary .current-menu-item > a {
  color: var(--berry);
}

/* The icon buttons in the header row were --mute against the same wash. */
[data-theme="light"] .social-links--header a,
[data-theme="light"] .theme-toggle,
[data-theme="light"] .header-search-toggle {
  color: #17130f;
}

/* Any SVG inside a fixed-size round control must not be stretched by the
   flex container it sits in — this is what made the hero next-arrow render as
   a long bar rather than an arrow. */
.roller__btn svg,
.back-link svg,
.lightbox__nav svg,
.lightbox__close svg {
  flex-shrink: 0;
  width: auto;
  height: auto;
}

/* ==========================================================================
   v3.10 — The type grid divides evenly
   Three fixed columns left four rental categories as a row of three plus one
   lonely tile beside two empty cells. --type-cols is set from the item count
   in the template, so four items lay out 2x2 and six stay 3x2 — every row
   full, at every width.

   The media queries still cap the value: a count of six must not become six
   columns on a tablet. min() takes whichever is smaller, the divisor or what
   the width can carry.
   ========================================================================== */
@media (min-width: 640px) {
  .type-grid {
    grid-template-columns: repeat(min(var(--type-cols, 2), 2), 1fr);
  }
}

@media (min-width: 1024px) {
  .type-grid {
    grid-template-columns: repeat(min(var(--type-cols, 3), 3), 1fr);
  }
}

/* ==========================================================================
   v3.12 — Unit types
   PROJECT → UNIT TYPE → AVAILABLE UNIT, made visible.

   The tab row is a scroller rather than a wrap, because four chips wrapping to
   two lines on a phone reads as two groups of choices rather than one. It
   scrolls horizontally and snaps, which is the same mechanism the gallery
   roller uses — one behaviour to learn, not two.

   `min-width: 0` on the scroller is the fix that keeps having to be
   re-learned: a grid or flex item defaults to min-width auto and refuses to
   shrink below its content, so the inner overflow-x never binds and the whole
   page scrolls sideways instead. The explicit max-width is what actually holds
   it at 390px; min-width alone has not been enough here before.
   ========================================================================== */
.unit-types {
  margin-top: 2.5rem;
}

.unit-types__tabs {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  min-width: 0;
  max-width: 100%;
  padding-bottom: 0.5rem;
  scrollbar-width: thin;
}

.unit-tab {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex: 0 0 auto;
  scroll-snap-align: start;
  padding: 0.75rem 1.125rem;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: transparent;
  color: var(--mute);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

.unit-tab svg {
  flex-shrink: 0;
  opacity: 0.6;
}

.unit-tab:hover {
  color: var(--paper);
  border-color: var(--gold);
}

.unit-tab.is-active {
  color: var(--paper);
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}

.unit-tab.is-active svg {
  color: var(--gold);
  opacity: 1;
}

[data-theme="light"] .unit-tab.is-active {
  background: color-mix(in srgb, var(--berry) 8%, transparent);
  border-color: var(--berry);
}

[data-theme="light"] .unit-tab.is-active svg {
  color: var(--berry);
}

/* Without JavaScript every panel is visible and stacked, each under its own
   heading — a longer page, but a complete one. The selector only becomes a
   selector once the script has claimed it. */
.unit-panel {
  padding-top: 2.5rem;
  border-top: 1px solid var(--line);
  margin-top: 0.5rem;
}

.unit-types[data-unit-types-ready] .unit-panel[hidden] {
  display: none;
}

.unit-panel__grid {
  display: grid;
  gap: 2.5rem;
  align-items: start;
}

@media (min-width: 900px) {
  .unit-panel__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    gap: 3rem;
  }
}

.unit-panel__title {
  font-size: clamp(1.5rem, 3vw, 2rem);
  line-height: 1.15;
  margin: 0;
}

.unit-panel__meta {
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--mute);
}

.unit-panel__price {
  margin-top: 1rem;
  font-size: 1.375rem;
  color: var(--paper);
}

.unit-panel__specs {
  margin-top: 1.5rem;
  display: grid;
  gap: 0.5rem;
}

.unit-panel__specs li {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  font-size: 0.875rem;
  color: var(--mute);
}

.unit-panel__specs svg {
  flex-shrink: 0;
  color: var(--gold);
}

[data-theme="light"] .unit-panel__specs svg {
  color: var(--berry);
}

.unit-panel__actions {
  margin-top: 1.75rem;
}

.unit-panel__plan {
  background: var(--paper);
  border: 1px solid var(--line);
  padding: 1rem;
}

.unit-panel__plan img {
  display: block;
  width: 100%;
  height: auto;
}

.unit-panel__plan--photo {
  padding: 0;
  background: transparent;
}

.unit-panel__plan figcaption {
  margin-top: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink);
  text-align: center;
}

.unit-panel__units {
  margin-top: 3rem;
}

.unit-panel__subhead {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 1rem;
}

[data-theme="light"] .unit-panel__subhead {
  color: var(--berry);
}

/* A status is the column people scan for, so it is the one thing in the table
   that carries colour. Available reads forward, reserved reads as a pause,
   sold reads as closed — and the same three words mean the same three things
   on every development. */
.unit-status {
  display: inline-block;
  padding: 0.125rem 0.5rem;
  border-radius: 2px;
  font-size: 0.75rem;
  white-space: nowrap;
  background: color-mix(in srgb, currentColor 12%, transparent);
}

.unit-status--available {
  color: #6fcf97;
}

.unit-status--reserved {
  color: var(--gold);
}

.unit-status--sold,
.unit-status--rented {
  color: var(--dim);
}

.unit-status--under-construction {
  color: var(--gold);
}

[data-theme="light"] .unit-status--available {
  color: #1f7a4d;
}

/* Unit-type chips on a development card. Deliberately quieter than the
   investment-route chips they sit in place of: these are descriptive, and a
   card should have one thing shouting on it, not two. */
.unit-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.875rem;
  min-width: 0;
}

.unit-chip {
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.02em;
  color: var(--mute);
  max-width: 100%;
  overflow-wrap: anywhere;
}

.unit-chip--more {
  border-style: dashed;
  color: var(--dim);
}

/* ==========================================================================
   v3.13 — ROI investment routes
   Three buttons that reprice the model. They read as a choice rather than as
   navigation, so they are sized like the inputs beneath them and share the
   card's rhythm — a route is an assumption, and it sits with the assumptions.

   Two lines each, name over price, because the price is the thing being
   compared and stacking it lets all three prices align down the row.
   ========================================================================== */
.roi__routes {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
  margin-top: 1.25rem;
}

.roi-route {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
  padding: 0.75rem 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: transparent;
  color: var(--mute);
  text-align: center;
  cursor: pointer;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

.roi-route:hover {
  color: var(--paper);
  border-color: var(--gold);
}

.roi-route.is-active {
  color: var(--paper);
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 12%, transparent);
}

[data-theme="light"] .roi-route.is-active {
  background: color-mix(in srgb, var(--berry) 8%, transparent);
  border-color: var(--berry);
}

.roi-route__name {
  font-size: 0.75rem;
  line-height: 1.2;
  overflow-wrap: anywhere;
}

.roi-route__price {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  color: var(--gold);
  white-space: nowrap;
}

[data-theme="light"] .roi-route__price {
  color: var(--berry);
}

.roi__hint {
  margin-top: 0.375rem;
  font-size: 0.6875rem;
  line-height: 1.5;
  color: var(--dim);
}

/* Three routes will not fit side by side on a narrow phone without the names
   fragmenting a word at a time. Two columns and a wrap is the shape that
   keeps each label readable. */
@media (max-width: 420px) {
  .roi__routes {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ==========================================================================
   v3.14 — Controls
   Everywhere the browser was still drawing its own furniture.

   A number input's spinner is the worst of them: Chrome hides two grey
   chevrons until you hover, then draws them in system blue at system size,
   over a dark card, in a typeface that is not ours. On the ROI calculator that
   is the control a visitor touches most. Replaced with a pair of buttons that
   belong to the same family as the roller arrows and the unit tabs — square,
   hairline, gold on hover — built in script so the markup stays a plain
   `<input type=number>` and keeps its keyboard and mobile behaviour.
   ========================================================================== */
.num-field {
  position: relative;
  display: block;
}

/* The native spinner goes, in both engines. */
.num-field input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
  padding-right: 3.25rem;
}

.num-field input[type="number"]::-webkit-outer-spin-button,
.num-field input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

.num-field__steps {
  position: absolute;
  top: 50%;
  right: 0.5rem;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.num-field__step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 18px;
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 2px;
  background: transparent;
  color: var(--mute);
  cursor: pointer;
  transition: color 0.18s ease, border-color 0.18s ease, background-color 0.18s ease,
    transform 0.12s ease;
}

.num-field__step svg {
  width: 10px;
  height: 10px;
  pointer-events: none;
}

.num-field__step:hover {
  color: var(--gold);
  border-color: var(--gold);
  background: color-mix(in srgb, var(--gold) 10%, transparent);
}

/* A press should be felt, not watched. 60ms and 1px is the whole animation. */
.num-field__step:active {
  transform: translateY(1px);
}

[data-theme="light"] .num-field__step:hover {
  color: var(--berry);
  border-color: var(--berry);
  background: color-mix(in srgb, var(--berry) 8%, transparent);
}

/* --- Range inputs --------------------------------------------------------
   Three sliders on the ROI calculator and one on the villa browser, each
   drawn by the operating system in its own accent colour — blue on Windows,
   the user's chosen tint on macOS. On a berry-and-gold site that is the one
   element that never matched. */
input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 2px;
  background: var(--line-strong);
  border-radius: 999px;
  outline: none;
  cursor: pointer;
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: var(--gold);
  border: 2px solid var(--bg);
  cursor: grab;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}

input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 999px;
  background: var(--gold);
  border: 2px solid var(--bg);
  cursor: grab;
  transition: transform 0.16s ease, box-shadow 0.16s ease;
}

input[type="range"]:hover::-webkit-slider-thumb,
input[type="range"]:focus-visible::-webkit-slider-thumb {
  transform: scale(1.15);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--gold) 22%, transparent);
}

input[type="range"]:hover::-moz-range-thumb,
input[type="range"]:focus-visible::-moz-range-thumb {
  transform: scale(1.15);
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--gold) 22%, transparent);
}

input[type="range"]:active::-webkit-slider-thumb {
  cursor: grabbing;
}

[data-theme="light"] input[type="range"]::-webkit-slider-thumb,
[data-theme="light"] input[type="range"]::-moz-range-thumb {
  background: var(--berry);
}

[data-theme="light"] input[type="range"]:hover::-webkit-slider-thumb {
  box-shadow: 0 0 0 6px color-mix(in srgb, var(--berry) 18%, transparent);
}

/* --- Focus -------------------------------------------------------------- */
:where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: 2px;
}

[data-theme="light"] :where(button, a, input, select, textarea, [tabindex]):focus-visible {
  outline-color: var(--berry);
}

/* --- Zoomable images ----------------------------------------------------- */
.is-zoomable img {
  cursor: zoom-in;
}

.hero-single {
  display: block;
  width: 100%;
  height: 100%;
}

.hero-single img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The viewer's own arrows were only rendered by the roller's lightbox. The
   shared one reuses the same class, so this only fills the gaps: hiding a nav
   button on a single-image group, and dimming the one at the end of a run. */
.lightbox__nav[hidden] {
  display: none;
}

.lightbox__nav:disabled {
  opacity: 0.3;
  cursor: default;
}

.lightbox__figure img {
  /* Restated because the shared viewer sets no width or height attribute: the
     image must show whole, at its own shape, never stretched to the frame. */
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

@media (prefers-reduced-motion: reduce) {
  .num-field__step,
  input[type="range"]::-webkit-slider-thumb,
  input[type="range"]::-moz-range-thumb {
    transition: none;
  }
}

/* ==========================================================================
   v3.15 — Spacing, plans, and the header menu
   ========================================================================== */

/* The results line sat flush against the filter card above it — no gap at all,
   so "8 OF 8 VILLAS" read as part of the card's bottom edge rather than as a
   caption for the grid below. A later rule had zeroed the margin the base rule
   set, which is why it looked deliberate and was not.

   It belongs to the grid it counts, so it takes space above and gives space
   below, and its baseline is the one the first row of cards aligns to. */
.results-count {
  margin-top: 1.75rem;
  margin-bottom: 0.5rem;
}

/* --- Plans and drawings --------------------------------------------------
   A floor plan is a document, not a photograph. Rendered through the
   photograph gallery at full container width it became a two-metre-wide
   architectural sheet — the "big random picture" on Patong Bay Residence.

   Plans are capped at a readable width and centred, on the paper the drawing
   was made for, so a page of them reads as a set of documents. */
.gallery--plans {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: 1fr;
  justify-items: center;
}

@media (min-width: 900px) {
  .gallery--plans {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .gallery--plans:has(> :only-child) {
    grid-template-columns: 1fr;
  }
}

.gallery--plans figure {
  width: 100%;
  max-width: 48rem;
  margin: 0;
  padding: 1.25rem;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 0.25rem;
}

.gallery--plans img {
  display: block;
  width: 100%;
  height: auto;
  /* A plan is line art: it must never be cropped to a card's shape. */
  object-fit: contain;
}

/* --- Header submenu ------------------------------------------------------
   It was a plain rounded rectangle that faded in: correct, and anonymous. It
   is the first interactive thing most visitors touch, so it now belongs to the
   same family as the rest of the site — a hairline gold rule along the top, a
   pointer tying it to the item that opened it, and items that shift a few
   pixels toward the reader on hover with a rule drawn under the one they are
   on. Movement is small and fast; a menu that performs is a menu that delays. */
.nav-primary .submenu {
  min-width: 16rem;
  padding: 0.5rem;
  margin-top: 0.625rem;
  background: color-mix(in srgb, var(--ink-soft) 92%, transparent);
  backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid var(--line-strong);
  border-radius: 0.625rem;
  box-shadow: 0 18px 44px -22px rgba(0, 0, 0, 0.85);
  transform: translateY(-8px) scale(0.985);
  transform-origin: top left;
  transition: opacity 0.22s cubic-bezier(0.22, 1, 0.36, 1),
    transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), visibility 0.22s;
}

/* The gold hairline reads as the KUBAY rule, not as a border. */
.nav-primary .submenu::before {
  content: "";
  position: absolute;
  inset: -1px 1rem auto 1rem;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--gold), transparent);
  opacity: 0.8;
}

/* A pointer, so the panel is visibly attached to the word that opened it —
   and a bridge across the gap, so the pointer does not close it on the way. */
.nav-primary .submenu::after {
  content: "";
  position: absolute;
  inset: -0.75rem 0 100% 0;
}

.nav-primary li:hover > .submenu,
.nav-primary li:focus-within > .submenu {
  transform: translateY(0) scale(1);
}

.nav-primary .submenu a {
  position: relative;
  padding: 0.5625rem 0.875rem;
  border-radius: 0.375rem;
  transition: color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
}

.nav-primary .submenu a::before {
  content: "";
  position: absolute;
  left: 0.875rem;
  right: 0.875rem;
  bottom: 0.25rem;
  height: 1px;
  background: currentColor;
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left;
  transition: opacity 0.18s ease, transform 0.24s cubic-bezier(0.22, 1, 0.36, 1);
}

.nav-primary .submenu a:hover {
  color: var(--gold);
  background: color-mix(in srgb, var(--gold) 8%, transparent);
  transform: translateX(3px);
}

.nav-primary .submenu a:hover::before {
  opacity: 0.45;
  transform: scaleX(1);
}

/* The parent's own chevron turns while its panel is open — the one piece of
   feedback that says which menu you are inside. */
.nav-primary .has-submenu > a svg {
  transition: transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), color 0.18s ease;
}

.nav-primary .has-submenu:hover > a svg,
.nav-primary .has-submenu:focus-within > a svg {
  transform: rotate(180deg);
  color: var(--gold);
}

[data-theme="light"] .nav-primary .submenu a:hover {
  color: var(--berry);
  background: color-mix(in srgb, var(--berry) 7%, transparent);
}

[data-theme="light"] .nav-primary .submenu::before {
  background: linear-gradient(90deg, transparent, var(--berry), transparent);
}

[data-theme="light"] .nav-primary .has-submenu:hover > a svg {
  color: var(--berry);
}

@media (prefers-reduced-motion: reduce) {
  .nav-primary .submenu,
  .nav-primary .submenu a,
  .nav-primary .submenu a::before,
  .nav-primary .has-submenu > a svg {
    transition-duration: 0.01ms;
  }

  .nav-primary .submenu a:hover {
    transform: none;
  }
}

/* ==========================================================================
   v3.16 — Light mode: the scrims that flipped
   ========================================================================== */

/* Three scrims were built as `linear-gradient(to top, var(--ink), rgba(10,10,11,…))`
   — one theme token mixed with one hard-coded dark. In dark mode both ends are
   near-black and the gradient does its job. In light mode `--ink` becomes
   #e8e0d3, so the same gradient runs from pale sand at the bottom to dark at
   the top, over a photograph, under text that is white in both themes. The
   bottom of every hero put white type on sand: unreadable, and it read as a
   broken page rather than a colour mistake.

   A scrim over a photograph is not a surface — it belongs to the photograph,
   and the photograph does not change when the theme does. So these are fixed
   dark values in both themes, and the white text on them is legible in both.
   The page around them still lightens; only the panel behind the hero type
   stays dark, which is what every editorial site does. */
.page-hero__bg::after,
[data-theme="light"] .page-hero__bg::after {
  background: linear-gradient(to top, rgba(10, 10, 11, 0.92), rgba(10, 10, 11, 0.55));
}

.page-hero--category .page-hero__bg::after,
[data-theme="light"] .page-hero--category .page-hero__bg::after {
  background: linear-gradient(
    to top,
    rgba(10, 10, 11, 0.9) 4%,
    rgba(10, 10, 11, 0.55) 60%,
    rgba(10, 10, 11, 0.35)
  );
}

.villa-hero__scrim,
[data-theme="light"] .villa-hero__scrim {
  background: linear-gradient(to top, rgba(10, 10, 11, 0.88), rgba(10, 10, 11, 0.25));
}

/* --- Browse-by-type tiles ------------------------------------------------
   The wash is `--surface-card` at 92% opacity across the whole tile. In dark
   mode that is a near-black veil over a near-black photograph and reads as
   depth. In light mode `--surface-card` is #fdfbf7, so it became a near-opaque
   white sheet: with the photograph already dimmed to a tenth, the image was
   erased and six tiles rendered as flat empty cards.

   Rebalanced rather than removed. The photograph carries three times the
   weight it did, and the veil becomes a diagonal anchored at the top-left
   corner where the index, the name and the count actually sit — heavy where
   the type is, clearing by the far corner so the picture is visible again. */
[data-theme="light"] .type-tile::before {
  opacity: 0.34;
}

[data-theme="light"] .type-tile:hover::before,
[data-theme="light"] .type-tile:focus-visible::before {
  opacity: 0.6;
}

[data-theme="light"] .type-tile::after {
  background: linear-gradient(
    155deg,
    color-mix(in srgb, var(--surface-card) 94%, transparent) 0%,
    color-mix(in srgb, var(--surface-card) 80%, transparent) 45%,
    color-mix(in srgb, var(--surface-card) 30%, transparent) 100%
  );
  opacity: 1;
}

[data-theme="light"] .type-tile:hover::after,
[data-theme="light"] .type-tile:focus-visible::after {
  opacity: 0.86;
}

/* The count sat at --mute-dim on a photograph rather than on flat card stock,
   which is a different contrast problem from the one that token was tuned for. */
[data-theme="light"] .type-tile__count {
  color: var(--mute);
}

@media (hover: none) {
  [data-theme="light"] .type-tile::before {
    opacity: 0.3;
  }
}

/* ==========================================================================
   v3.17 — Light mode, rebuilt as glass
   Why the light theme read as cheap while the dark one read as luxury.

   The dark theme gets its quality from depth: near-black surfaces separated by
   a hairline, a gold that glows because there is darkness to glow against, and
   long soft shadows. The light theme was built by inverting the tokens — sand
   page, off-white cards, a hairline at 18% black — and inversion is exactly
   what does not carry over. Flat cream panels butted against a flat cream page
   with a grey line between them is the visual language of a printed form.

   What luxury looks like in light: fewer, softer edges; surfaces that are
   translucent rather than opaque, so a card floats over the page instead of
   being pasted onto it; warmth in the whites; and shadow doing the work the
   hairline was doing. That is what this does — with `backdrop-filter`, which
   is the one property that makes a light surface read as material rather than
   as fill.

   Structured as a token override plus a short list of component tweaks, so
   the dark theme is untouched and there is one place to adjust.
   ========================================================================== */
[data-theme="light"] {
  /* Warmer, deeper page. The old #e8e0d3 was close enough to the card colour
     that the two read as one flat field; pulling it down and adding warmth
     gives the cards something to sit on. */
  --ink: #e3dacb;
  --ink-soft: #d8cdba;
  --ink-raised: #fffdfa;

  /* Glass, not fill. 72% lets the page tint through a card, which is the whole
     effect — the surface picks up warmth from what is behind it. Components
     that must stay solid (fields, dropdowns) opt out below. */
  --surface-card: rgba(255, 253, 250, 0.72);
  --surface-panel: rgba(248, 243, 235, 0.78);
  --surface-field: #fffdfa;

  /* Hairlines carry less of the structure now that shadow does, so they can be
     lighter and warmer — a grey line on cream is the cheapest-looking element
     in a light UI. */
  --line: rgba(94, 72, 46, 0.14);
  --line-strong: rgba(94, 72, 46, 0.24);
}

/* --- Cards, panels, tiles ------------------------------------------------
   One rule, because they are one material. The inset highlight along the top
   edge is what sells glass: it reads as light catching a bevel. */
[data-theme="light"] .card,
[data-theme="light"] .panel,
[data-theme="light"] .villa-card,
[data-theme="light"] .content-result,
[data-theme="light"] .type-tile,
[data-theme="light"] .empty-state,
[data-theme="light"] .roi__inputs {
  border-color: rgba(255, 255, 255, 0.55);
  background: var(--surface-card);
  -webkit-backdrop-filter: blur(18px) saturate(1.35);
  backdrop-filter: blur(18px) saturate(1.35);
  border: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.9),
    0 1px 2px rgba(94, 72, 46, 0.05),
    0 18px 40px -24px rgba(94, 72, 46, 0.3);
}

[data-theme="light"] .villa-card:hover,
[data-theme="light"] .content-result:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    0 2px 6px rgba(94, 72, 46, 0.07),
    0 30px 60px -26px rgba(94, 72, 46, 0.38);
}

/* The soft band between sections was two near-identical creams. Given a real
   gradient and a hairline top edge it now reads as a change of surface. */
[data-theme="light"] .section--soft {
  background: linear-gradient(180deg, #ddd2be 0%, #eae1d1 55%, #f2ebdf 100%);
  border-top: 1px solid rgba(255, 255, 255, 0.6);
}

/* --- The header ----------------------------------------------------------
   The one element on screen at every moment. Frosted, so the photograph
   underneath shows through as colour rather than being covered. */
/* The header has two states — a transparent gradient over the hero at rest,
   a solid bar once scrolled — so both need saying, and the resting one must
   stay a gradient or it covers the photograph it is supposed to sit on. */
[data-theme="light"] .site-header {
  background: linear-gradient(to bottom, rgba(227, 218, 203, 0.85), transparent);
}

[data-theme="light"] .site-header.is-scrolled,
[data-theme="light"] .site-header.is-open {
  background: rgba(250, 246, 239, 0.72);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  backdrop-filter: blur(20px) saturate(1.4);
  border-bottom-color: rgba(94, 72, 46, 0.12);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7), 0 10px 30px -22px rgba(94, 72, 46, 0.35);
}

[data-theme="light"] .nav-primary .submenu {
  background: rgba(255, 253, 250, 0.82);
  -webkit-backdrop-filter: blur(22px) saturate(1.4);
  backdrop-filter: blur(22px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, 0.7);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    0 22px 50px -26px rgba(94, 72, 46, 0.42);
}

/* --- Inputs --------------------------------------------------------------
   Fields stay opaque: text has to sit on a known colour. They get the inset
   shadow instead, which is the light-mode equivalent of depth. */
[data-theme="light"] input,
[data-theme="light"] select,
[data-theme="light"] textarea,
[data-theme="light"] .psearch__field {
  background: var(--surface-field);
  border-color: rgba(94, 72, 46, 0.18);
  box-shadow: inset 0 1px 2px rgba(94, 72, 46, 0.07);
}

[data-theme="light"] input:focus,
[data-theme="light"] select:focus,
[data-theme="light"] textarea:focus {
  border-color: var(--berry);
  box-shadow:
    inset 0 1px 2px rgba(94, 72, 46, 0.05),
    0 0 0 3px color-mix(in srgb, var(--berry) 14%, transparent);
}

/* --- Accents -------------------------------------------------------------
   The berry button was flat fill. A two-stop gradient and a top highlight give
   it the same dimension the cards now have, without changing the colour. */
[data-theme="light"] .btn--primary {
  background: linear-gradient(180deg, var(--berry-bright), var(--berry));
  border-color: transparent;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 10px 24px -14px rgba(110, 17, 57, 0.6);
}

[data-theme="light"] .btn--primary:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.3),
    0 16px 32px -14px rgba(110, 17, 57, 0.7);
}

[data-theme="light"] .btn--outline {
  background: rgba(255, 253, 250, 0.6);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

/* Chips and badges are small surfaces; they get the same material so the page
   does not mix glass with fill. */
[data-theme="light"] .program-chip,
[data-theme="light"] .unit-chip,
[data-theme="light"] .unit-tab,
[data-theme="light"] .roi-route,
[data-theme="light"] .filters,
[data-theme="light"] .chip {
  background: rgba(255, 253, 250, 0.62);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-color: rgba(94, 72, 46, 0.16);
}

/* --- Tables and footer --------------------------------------------------- */
[data-theme="light"] .data-table thead th {
  background: rgba(216, 205, 186, 0.5);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

[data-theme="light"] .data-table tbody tr:hover {
  background: rgba(255, 253, 250, 0.55);
}

[data-theme="light"] .site-footer {
  background: linear-gradient(180deg, #d8cdba, #cabda5 70%);
  border-top: 1px solid rgba(255, 255, 255, 0.5);
}

/* Safari and Firefox without backdrop-filter fall back to a solid surface
   rather than a see-through one — checked here rather than assumed, because a
   72% card with no blur behind it is unreadable over a photograph. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  [data-theme="light"] {
    --surface-card: #fffdfa;
    --surface-panel: #f8f3eb;
  }

  [data-theme="light"] .site-header,
  [data-theme="light"] .nav-primary .submenu,
  [data-theme="light"] .program-chip,
  [data-theme="light"] .unit-chip,
  [data-theme="light"] .unit-tab,
  [data-theme="light"] .roi-route,
  [data-theme="light"] .btn--outline,
  [data-theme="light"] .filters {
    background: #fffdfa;
  }
}

/* ==========================================================================
   v3.17.1 — Hero base colour
   ==========================================================================

   `.hero` sets `background: var(--ink)`. In dark mode that is near-black and
   correct: the hero photograph loads on top of it, and while it is still in
   flight the white headline sits on black and reads. In light mode --ink is
   the cream page colour, so the same rule paints the hero pale sand — and the
   white headline, the lede and the quicklinks all vanish into it for as long
   as the photograph takes to arrive. If the image ever 404s they never come
   back.

   The scrim above this layer is already fixed-dark in both themes, so making
   the base dark too changes nothing once the photo has painted. It only
   removes the window in which the type is invisible. Same reasoning as the
   .hero__scrim and .villa-hero__scrim fixes: this surface is photography, not
   page, so it does not follow the page token. */
[data-theme="light"] .hero {
  background: #191316;
}

/* ==========================================================================
   v3.17.2 — Light-mode type over photography
   ==========================================================================

   Light mode is a token swap, and that is the right architecture — but it
   assumes every surface follows the page. Photographic surfaces do not. The
   hero, the media page-heroes and the villa hero stay dark in both themes
   (their scrims were fixed to fixed-dark for exactly this reason), so any
   token that flips dark for the sand page goes dark-on-dark the moment it is
   used over one of them.

   Measured on the rendered pixels, not inferred:

     projects, light   .eyebrow span "Developments"   1.01 : 1
     8-one,    light   .eyebrow__index "8-ONE"        1.08 : 1
     home,     light   .text-gold eyebrow             1.20 : 1
     home,     light   .hero__title em "Phuket"       1.60 : 1   (needs 3)
     home,     light   .text-dim "Popular"            2.35 : 1

   --gold goes to #6f4e18, --berry to #6e1139 and --mute/--mute-dim to near
   black, all correct against cream and all invisible against a dark photo.
   The white type beside them was never affected because it is set in fixed
   rgba, which is why this survived every review that looked at the headline.

   The fix is the same shape as the scrim fix: a photographic container is not
   the page, so inside one the accent and muted tokens revert to their dark
   theme values. Everything nested inherits, so no component needs to know.

   --paper and the surface tokens are deliberately NOT reverted here: the
   search field and filter panel inside the hero are genuine light surfaces
   sitting on top of the photograph, and their labels and inputs must stay
   dark on white. The block after this one puts those two back explicitly. */
[data-theme="light"] .hero,
[data-theme="light"] .page-hero--media,
[data-theme="light"] .page-hero--category,
[data-theme="light"] .villa-hero {
  --gold: #c6a15b;
  --gold-soft: #d9c090;
  --berry: #8d1b4c;
  --berry-bright: #a3245a;
  --mute: #a8a49f;
  --mute-dim: #8a8480;
  --line: rgba(244, 239, 230, 0.22);
  --line-strong: rgba(244, 239, 230, 0.42);
}

/* The search form and the filter panel are white cards laid over the hero
   photograph. They are light surfaces in light mode, so they take the light
   palette back — otherwise the field labels would follow the block above and
   turn pale grey on white, trading one invisible-text bug for another. */
[data-theme="light"] .hero .smartsearch,
[data-theme="light"] .hero .filters-panel,
[data-theme="light"] .villa-hero .filters,
[data-theme="light"] .page-hero--media .filters {
  --gold: #6f4e18;
  --gold-soft: #5a3f13;
  --berry: #6e1139;
  --berry-bright: #8d1b4c;
  --mute: #4a423b;
  --mute-dim: #5c554d;
  --line: rgba(19, 16, 15, 0.18);
  --line-strong: rgba(19, 16, 15, 0.32);
}

/* Light mode routes accent chips and labels to berry — right on cream, wrong
   on a photograph, where deep berry is as dark as the scrim behind it. Inside
   a photographic hero those accents go back to gold, which is what they are
   in dark mode and what the scrim was tuned for. */
[data-theme="light"] .hero .eyebrow__index,
[data-theme="light"] .hero .search-hint__label,
[data-theme="light"] .hero .numbered__index,
[data-theme="light"] .hero .stat__value--gold,
[data-theme="light"] .page-hero--media .eyebrow__index,
[data-theme="light"] .page-hero--media .numbered__index,
[data-theme="light"] .page-hero--media .stat__value--gold,
[data-theme="light"] .page-hero--category .eyebrow__index,
[data-theme="light"] .villa-hero .eyebrow__index,
[data-theme="light"] .villa-hero .numbered__index,
[data-theme="light"] .villa-hero .stat__value--gold {
  color: var(--gold);
}

/* ==========================================================================
   v3.17.3 — The rest of the painted-contrast findings
   ==========================================================================

   Measured on rendered pixels, glyph-tight, after v3.17.2:

     home,   light desktop  .text-gold "Your Gateway…"   1.20 : 1
     project,light desktop  "WhatsApp"                   1.98 : 1
     home,   dark  mobile   hero eyebrow                 2.86 : 1
     home,   dark  mobile   .hero__lede                  3.06 : 1
     8-one,  light mobile   .eyebrow__index              3.18 : 1
     home,   light desktop  footer © line                3.96 : 1
     home,   light mobile   .footer-col__title           3.96 : 1
   ========================================================================== */

/* The closing CTA band is a photographic surface too — its light-mode scrim
   is rgba(23,20,26,0.78), i.e. dark — so it takes the same token treatment as
   the heroes. This is what left the CTA eyebrow at 1.20: deep bronze gold on
   a dimmed photograph.

   The 8-One band is deliberately NOT in this list, and that is worth stating
   because it looks like an omission. In light mode its photo drops to 0.1
   opacity under a *cream* gradient, so it is a light surface that happens to
   have a picture in it. Adding it here flipped its eyebrow and lede to the
   dark-theme greys and drove them to 1.62 and 1.72 on cream — measured, not
   predicted. Membership of this list is decided by the scrim's colour, not by
   whether the section contains an image. */
[data-theme="light"] .cta-band {
  --gold: #c6a15b;
  --gold-soft: #d9c090;
  --berry: #8d1b4c;
  --berry-bright: #a3245a;
  --mute: #a8a49f;
  --mute-dim: #8a8480;
  --line: rgba(244, 239, 230, 0.22);
  --line-strong: rgba(244, 239, 230, 0.42);
}

/* WhatsApp green is kept deliberately — it is recognised by colour before it
   is read — but #25d366 was chosen against a dark background and gives 1.98:1
   as *text* on a white card. The glyph and the border keep the familiar
   green; only the word darkens, to the same hue at a legible value. */
[data-theme="light"] .contact-bar__btn--whatsapp,
[data-theme="light"] .contact-action--whatsapp {
  color: #107c41;
}

[data-theme="light"] .contact-bar__btn--whatsapp svg,
[data-theme="light"] .contact-action--whatsapp svg {
  color: #25d366;
}

/* The footer's light surface is the darkest sand on the page, and --mute-dim
   was tuned against the lighter page background, so it lands at 3.96 here.
   Scoped darkening rather than moving the global token, which is correct
   everywhere else. */
[data-theme="light"] .site-footer {
  --mute-dim: #453e37;
}

/* Mobile heroes are short: the type sits high, where every scrim in the file
   is at its weakest because on desktop nothing is up there. On a 390px
   viewport the eyebrow and lede land in the thinnest part of the gradient and
   drop to 2.86 and 3.06. Strengthening the top stop on small screens only —
   the desktop composition, where the photograph carries the top third, is
   untouched. */
@media (max-width: 767px) {
  .hero__scrim {
    background: linear-gradient(
        to top,
        var(--ink) 0%,
        rgba(10, 10, 11, 0.62) 45%,
        rgba(10, 10, 11, 0.55) 100%
      ),
      linear-gradient(to right, rgba(10, 10, 11, 0.7), transparent 75%);
  }

  [data-theme="light"] .hero__scrim {
    background: linear-gradient(
        to top,
        rgba(25, 19, 22, 0.9) 0%,
        rgba(25, 19, 22, 0.72) 45%,
        rgba(25, 19, 22, 0.66) 100%
      ),
      linear-gradient(to right, rgba(25, 19, 22, 0.6), transparent 75%);
  }

  .page-hero__bg::after {
    background: linear-gradient(to top, rgba(10, 10, 11, 0.95), rgba(10, 10, 11, 0.8));
  }

  [data-theme="light"] .page-hero__bg::after {
    background: linear-gradient(to top, rgba(23, 20, 26, 0.92), rgba(23, 20, 26, 0.78));
  }
}

/* Small mono eyebrows and index chips over photography move to the lighter
   gold. --gold is #c6a15b, tuned as an accent on near-black; at 11–12px over
   a scrimmed photograph it reaches only 3.4–3.7:1, and dropping the scrim
   further to rescue it would flatten the picture the section exists to show.
   --gold-soft is the same hue two steps up and clears 4.5 with headroom. */
[data-theme="light"] .cta-band .text-gold,
.hero__body > .mono.text-gold,
.page-hero--media .eyebrow__index,
.page-hero--category .eyebrow__index,
.villa-hero .eyebrow__index,
[data-theme="light"] .page-hero--media .eyebrow__index,
[data-theme="light"] .page-hero--category .eyebrow__index,
[data-theme="light"] .villa-hero .eyebrow__index {
  color: var(--gold-soft);
}

/* The light page-hero scrim carried the eyebrow chip at 3.36. A little more
   depth at both stops; the photo behind still reads. */
[data-theme="light"] .page-hero__bg::after {
  background: linear-gradient(to top, rgba(23, 20, 26, 0.88), rgba(23, 20, 26, 0.68));
}

/* Desktop dark hero: the lede sits at mid-height where the scrim was thinnest
   and measured 4.08 against 4.5. The middle stop and the left wash take a
   step up — the top of the frame, which carries the composition, is unchanged. */
@media (min-width: 768px) {
  .hero__scrim {
    background: linear-gradient(
        to top,
        var(--ink) 0%,
        rgba(10, 10, 11, 0.52) 45%,
        rgba(10, 10, 11, 0.12) 100%
      ),
      linear-gradient(to right, rgba(10, 10, 11, 0.74), transparent 64%);
  }
}

/* The category archive hero — /properties/, /projects/, every taxonomy term —
   scrims its photo to only 0.35 at the top, which is where kubay_eyebrow()
   puts its label. --mute is a light grey in this container (it is a
   photographic surface) and light grey on a half-lit photograph measures
   2.47:1. Brightening the label is the right lever here rather than dropping
   the scrim again: --media heroes already pin this label to a fixed light
   value for the same reason, and this simply extends that to the other two.

   The category scrim also takes a little depth at its two upper stops, which
   carries the h1 as well as the eyebrow. Both themes share the rule. */
.page-hero--media .eyebrow,
.page-hero--category .eyebrow,
.villa-hero .eyebrow,
[data-theme="light"] .page-hero--media .eyebrow,
[data-theme="light"] .page-hero--category .eyebrow,
[data-theme="light"] .villa-hero .eyebrow {
  color: rgba(247, 244, 239, 0.9);
}

.page-hero--category .page-hero__bg::after,
[data-theme="light"] .page-hero--category .page-hero__bg::after {
  background: linear-gradient(
    to top,
    rgba(10, 10, 11, 0.92) 4%,
    rgba(10, 10, 11, 0.64) 60%,
    rgba(10, 10, 11, 0.52)
  );
}

/* ==========================================================================
   v3.18.0 — Gallery: plan sheets, selection tint, swipe
   ========================================================================== */

/* A floor plan is line art printed on paper, so .gallery--plans figure gave it
   a paper-coloured sheet — via var(--paper). But --paper is the *text* token:
   cream on the dark theme, near-black on the light one. The sheet therefore
   inverted with the theme and plans were being shown on a black card in light
   mode, which is why the section looked broken there.

   A sheet of paper is white in both themes. It does not follow the page. */
.gallery--plans figure {
  background: #fbf8f3;
  border-color: rgba(19, 16, 15, 0.12);
}

/* Swiping the lightbox painted the whole photograph berry.

   Not an overlay and not a blend mode: `::selection` is `var(--berry)` at full
   opacity, a drag across the dialog starts a text selection spanning the
   figure, and Chrome tints a *selected image* with the selection colour.
   Reproduced in isolation — a forced selection over a page carrying only that
   one rule renders the count chip in solid berry and the picture washed red,
   which is exactly the reported screenshot.

   Images are never usefully selected as text, and neither is the chrome of a
   viewer you are swiping. Turning selection off on both removes the cause
   rather than the symptom, and leaves ::selection alone for body copy, where
   it is wanted. */
img {
  -webkit-user-select: none;
  user-select: none;
  -webkit-user-drag: none;
}

.lightbox,
.roller,
.gallery,
.hero-roller {
  -webkit-user-select: none;
  user-select: none;
}

/* Horizontal drags inside the viewer must reach the swipe handler instead of
   being consumed as a browser pan, and the long-press callout on iOS has to be
   off or a swipe that starts on the photo opens a share sheet. */
.lightbox__figure,
.lightbox__figure img {
  touch-action: pan-y;
  -webkit-touch-callout: none;
}

/* ==========================================================================
   v3.18.0 — Standalone custom selects
   ==========================================================================

   .kselect__button is borderless and unpadded because the two places that
   used it — the property search row and the archive sort — already draw a
   field around it. The enquiry form's "I am interested in" and the 8-One
   archive's sort are standalone controls: they were native <select>s carrying
   their own border, so switching them to .kselect left a bare word with a
   caret floating where a field used to be.

   These two give the custom button the same chrome the native control had, so
   the enhancement is invisible except for the part that was the point — the
   popup is ours instead of the operating system's. */
.enquiry-form .kselect__button {
  border: 1px solid var(--line-strong);
  border-radius: 0.5rem;
  background: var(--surface-field);
  padding: 0.875rem 1rem;
  transition: border-color 0.3s ease;
}

.enquiry-form .kselect.is-open .kselect__button,
.enquiry-form .kselect__button:focus-visible {
  border-color: var(--gold);
}

.enquiry-form .kselect__list {
  left: 0;
  right: 0;
}

/* The archive filter is a pill, and it sizes to its content rather than
   filling the row the way a form field does. */
.filters .kselect {
  width: auto;
  min-width: 13rem;
}

.filters .kselect__button {
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface-field);
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
  transition: border-color 0.3s ease;
}

.filters .kselect.is-open .kselect__button,
.filters .kselect__button:focus-visible {
  border-color: var(--gold);
}

.filters .kselect__list {
  left: 0;
  right: 0;
  border-radius: 0.875rem;
}

/* ==========================================================================
   v3.19.0 — Master plan and floor plan sheets
   ==========================================================================

   A plan sheet is a .gallery figure, so it inherited two rules written for
   photographs and wrong for drawings:

     aspect-ratio: 4 / 3   — a master plan is whatever shape the site is. Forced
                             into a card ratio inside overflow:hidden, the
                             drawing was simply cropped; measured on the
                             cottages plan, a 588x441 frame around an image that
                             wanted to be far taller.
     :hover img scale(1.05) — a photograph gains from a slow push-in. A plan
                             loses by it: the zoom pushes the edges of the
                             drawing — which is where the legend, the zone keys
                             and the north arrow live — out through the crop.

   So the sheet sizes to its drawing and holds still. The hover affordance
   moves to the sheet itself, which is the honest signal anyway: the whole
   sheet is the click target that opens the lightbox. */
.gallery--plans figure {
  aspect-ratio: auto;
  overflow: visible;
  transition: border-color 0.3s var(--ease), box-shadow 0.3s var(--ease),
    transform 0.3s var(--ease);
}

.gallery--plans figure:hover img,
.gallery--plans figure:focus-within img {
  transform: none;
}

.gallery--plans figure:hover,
.gallery--plans figure:focus-within {
  border-color: rgba(198, 161, 91, 0.55);
  box-shadow: 0 10px 30px -18px rgba(19, 16, 15, 0.45);
  transform: translateY(-2px);
}

@media (prefers-reduced-motion: reduce) {
  .gallery--plans figure,
  .gallery--plans figure:hover,
  .gallery--plans figure:focus-within {
    transition: none;
    transform: none;
  }
}
