/* ==========================================================================
   listing.css — the listing detail page: hero + price odometer, info,
   ★ THE GALLERY, facts, and the "Other listings" cursor-preview list.

   Normative sources:
     docs/research/05-range-listing-page.md  §1 hero, ★ §8 GALLERY (exact),
                                             §8.3 CSS values, §8.4 responsive
     docs/research/01-kononenko-base.md      §2 tokens/type (the SKIN wins),
                                             §5.3 info def-list, §8.1 odometer
     docs/research/04-base31-whats-on.md     §2/§4 other-listings row + preview

   UNIT CONVERSION — Range writes everything in vw at a 1920 / 375 canvas;
   this build uses kononenko's rem system where 1rem === 1 design px at those
   same canvases. So every Range value below is:
       desktop  X vw  →  X × 19.2 rem
       mobile   X vw  →  X × 3.75 rem   (Range's 479px phone layer)
   Each converted value carries its source vw in a comment.

   BREAKPOINTS — Range restructures at 991 / 767 / 479. We have ONE breakpoint
   (767.98px, 01 §2.1), so Range's 479px phone layer is what our mobile tier
   renders and the 991px tablet tier is dropped: between 768 and 991 we simply
   keep the desktop geometry, which scales fluidly with the rem system anyway
   (Range's tablet tier exists only because their vw type needed re-tuning).

   Section CSS never sets font-family — that is core.js contract (a); the
   markup carries `.f-mn` / `.f-sf`.
   ========================================================================== */

/* ==========================================================================
   1. HERO (05 §1, mechanics 01 §8.1)
   ========================================================================== */

.lst-hero {
  position: relative;
  height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  color: var(--wh);
}
@media (max-width: 767.98px) {
  .lst-hero {
    height: calc(100vh - var(--vhmobilebar));
  }
}

.lst-hero-media {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.lst-hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  will-change: transform;
}

/* 05 §1: two layered gradients for legibility — bottom scrim + a left-side
   wash so the name never lands on a bright patch of sky. Recoloured to our
   black (Range's is their teal #000000d9 / #000000d4). */
.lst-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0) 73%, rgba(0, 0, 0, 0.85) 97%),
    linear-gradient(68deg, rgba(0, 0, 0, 0.83) 9%, rgba(0, 0, 0, 0) 57%);
}

.lst-hero-in {
  position: relative;
  z-index: 2;
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 60rem;
  padding: 38rem 30rem; /* 01 §2.5 header/hero inner padding */
}

.lst-hero-l {
  min-width: 0;
}
.lst-eyebrow {
  margin-bottom: 16rem;
  color: var(--gr3);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.lst-title {
  max-width: 760rem;
}

.lst-hero-r {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 28rem;
}

/* --- the price odometer (01 §8.1) ----------------------------------------
   One masked column per character. Digit columns hold spans 0..d and are
   rolled up by listing.js; `$` and commas are static single spans. Column
   widths are ghost-measured per character (Range API, after fonts.ready) so
   the settled row is exactly as wide as the plain text would be.

   `.fn-h2` line-height is 0.7, which would clip the digits inside the masks —
   the odometer therefore carries its own 1.1 line-height. That number is also
   the roll geometry: every `.odo-n` is exactly 1.1em tall, so translating the
   roll by −d·(100/count)% lands the column precisely on digit d. */
.lst-price {
  text-align: right;
}
.odo {
  display: inline-block;
  line-height: 1.1;
  white-space: nowrap;
}
.odo-col {
  position: relative;
  display: inline-block;
  vertical-align: top;
  height: 1.1em;
  overflow: hidden;
}
.odo-roll {
  display: block;
  will-change: transform;
}
.odo-n {
  display: block;
  line-height: 1.1;
  text-align: center;
}
.odo-static {
  display: inline-block;
  vertical-align: top;
  line-height: 1.1;
}
/* the measuring ghost — laid out, never painted, never affects the row */
.odo-ghost {
  position: absolute;
  left: 0;
  top: 0;
  visibility: hidden;
  white-space: pre;
  pointer-events: none;
}

/* --- stat counters (05 §1) ---------------------------------------------- */
.lst-stats {
  display: flex;
  gap: 60rem;
}
.lst-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6rem;
}
.lst-stat-v {
  display: block;
}
.lst-stat-k {
  display: block;
  color: var(--gr3);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

@media (max-width: 767.98px) {
  .lst-hero-in {
    flex-direction: column;
    align-items: flex-start;
    gap: 24rem;
    padding: 20rem 15rem;
  }
  .lst-hero-r {
    align-items: flex-start;
    gap: 16rem;
  }
  .lst-price {
    text-align: left;
  }
  .lst-stats {
    gap: 24rem;
  }
  .lst-stat {
    align-items: flex-start;
  }
  .lst-eyebrow {
    margin-bottom: 10rem;
  }
}

/* ==========================================================================
   2. INFO — specs def-list + verbatim description (01 §5.3)
   ========================================================================== */

.lst-info {
  margin-top: 134rem;
  margin-bottom: 160rem;
}
.lst-info-in {
  display: flex;
  gap: 270rem;
}
.lst-specs {
  width: 355rem;
  flex: none;
}
.spec-row {
  display: flex;
}
.spec-row + .spec-row {
  margin-top: 14rem;
}
.spec-k {
  width: 105rem;
  flex: none;
}
.spec-v {
  flex: 1 1 auto;
  min-width: 0;
}
.lst-desc {
  flex: 1 1 auto;
  max-width: 1110rem;
}
.lst-p + .lst-p {
  margin-top: 30rem;
  /* the opener stays ink whatever size it is set at; the rest is muted body
     copy, the same hierarchy the home intro columns use (01 §5.2) */
  color: var(--gr3);
}
/* 16rem body copy across the full 1110rem column would run ~130 characters to
   the line. Body paragraphs get a reading measure; the lead keeps the full
   column, which is the ordinary drop-lead shape. */
.lst-p.fn-b1 {
  max-width: 620rem;
}

@media (max-width: 767.98px) {
  .lst-info {
    margin-top: 60rem;
    margin-bottom: 60rem;
  }
  .lst-info-in {
    flex-direction: column;
    gap: 40rem;
  }
  .lst-specs {
    width: 100%;
  }
  .spec-k {
    width: 110rem;
  }
  .lst-p + .lst-p {
    margin-top: 20rem;
  }
  .lst-p.fn-b1 {
    max-width: none;
  }
}

/* ==========================================================================
   3. ★ THE GALLERY (05 §8) — the client's top replication target.
   Geometry, stacking order and timings are Range's verbatim; only the palette
   is ours (their teal #092c38 → our black).
   ========================================================================== */

/* 05 §8.3 `.section-6-island` */
.lst-gal {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 100vh;
  background-color: var(--wh);
  overflow: hidden;
}
.gal-panes {
  position: absolute;
  inset: 0;
}

/* --- top scrim ------------------------------------------------------------
   The global header is white on `mix-blend-mode: difference` (01 §4.1, main
   .css §10), which is a contrast GUARANTEE only against blacks and whites.
   Against a mid-tone it paints its own near-neighbour and disappears, and a
   100vh wall of Roatán sky is exactly that mid-tone: measured across
   coral-views' 43 exteriors, sampling the two bands the header type occupies,
   30 of 43 slides put it below a 3:1 ratio and the saturated-sky frames
   (rgb 12,154,239) bottom out at 1.04:1 — invisible.

   The blend architecture is not the thing to change; what it needs is a
   backdrop that is decisively dark, which is the same answer 05 §1 already
   gives the hero (its own ::after runs an 0.85 black scrim). Re-measured over
   all 43 slides, this ramp holds the worst frame at 5.4:1.

   Only the gallery carries it: nothing else on the site parks the header over
   a full-bleed photograph for a whole screen. */
.lst-gal::before {
  content: "";
  position: absolute;
  z-index: 2; /* over the panes, under the rail (5) and the tab pill (10) */
  inset: 0 0 auto 0;
  height: 190rem;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.72) 0%,
    rgba(0, 0, 0, 0.7) 42%,
    rgba(0, 0, 0, 0) 100%
  );
}

/* 05 §8.1: the pane swap is INSTANT (Webflow tabs, data-duration 0/0) —
   a display toggle, no transition of any kind. All the motion in this
   section comes from the Swipers themselves. */
.gal-pane {
  position: absolute;
  inset: 0;
  display: none;
}
.gal-pane.is--on {
  display: block;
}

/* main swiper — absolute, inset 0, full height (05 §8.3) */
.swiper.gal-main {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.gal-main .swiper-slide {
  overflow: hidden;
}

/* ★ Ken Burns (05 §8.3): every slide's image cell sits at scale 1.1 and eases
   to 1 over 2.2s once its slide becomes active. `ease` is Range's literal
   keyword, NOT our customdefault — this one belongs to the gallery. */
.gal-cell {
  position: relative;
  width: 100%;
  height: 100%;
  transform: scale(1.1);
  transition: transform 2.2s ease;
}
.gal-main .swiper-slide-active .gal-cell {
  transform: scale(1);
}
.gal-cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* bottom legibility gradient over every slide (05 §8.3, recoloured) */
.gal-cell::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.5) 100%);
}

.gal-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* --- thumb rail (05 §8.3 `.thumbs-wrapper`) ------------------------------ */
.gal-rail {
  position: absolute;
  z-index: 5;
  top: 0;
  bottom: 0;
  right: 96rem; /* 5vw */
  display: flex;
  flex-direction: column;
  gap: 9.6rem; /* .5vw */
  justify-content: center;
  align-items: center;
}

/* `position: static` is Range's — the rail's flex column, not the swiper,
   does the vertical centring. `spaceBetween: 10` is a raw Swiper px value
   (05 §8.2) and stays px on purpose. */
.swiper.gal-thumbs {
  position: static;
  width: 153.6rem; /* 8vw  */
  height: 537.6rem; /* 28vw */
  overflow: clip;
}
/* The active thumb is a 1px white border (05 §8.3). Every slide carries a
   transparent one so switching thumbs cannot shift the rail by 2px —
   box-sizing is border-box globally, so the box never changes size. */
.gal-thumbs .swiper-slide {
  border: 1px solid transparent;
}
.gal-thumbs .swiper-slide-thumb-active {
  border-color: var(--wh);
}
/* the .5vw padding is the gutter that keeps the image off that border */
.gal-thumb {
  display: block;
  width: 100%;
  height: 96rem; /* 5vw */
  padding: 9.6rem; /* .5vw */
  cursor: pointer;
}
.gal-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.gal-nav {
  position: relative;
  z-index: 5;
  flex: none;
  width: 38.4rem; /* 2vw */
  height: 38.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--wh);
  cursor: pointer;
  transition: opacity 0.3s var(--ease);
}
.gal-chev {
  width: 100%;
  height: 100%;
}
.gal-next .gal-chev {
  transform: rotate(180deg);
}
.gal-nav.swiper-button-disabled {
  opacity: 0.3;
  cursor: default;
}

/* --- floating pill tab bar (05 §8.3 `.tabs-menu-6`) ---------------------- */
.gal-tabs {
  position: absolute;
  z-index: 10;
  left: 50%;
  bottom: 76.8rem; /* 4vw */
  transform: translateX(-50%);
  display: flex;
  gap: 19.2rem; /* 1vw */
  padding: 5.76rem; /* .3vw */
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  background-color: rgba(0, 0, 0, 0.2); /* #0003 */
  border: 1px solid rgba(255, 255, 255, 0.33); /* #ffffff54 */
  border-radius: 1000px;
}
.gal-tab {
  flex: none;
  padding: 9.6rem 28.8rem; /* .5vw 1.5vw — Webflow's base w-tab-link padding */
  color: var(--wh);
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  background-color: transparent;
  border-radius: 1000px;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.5s ease, color 0.5s ease;
}
.gal-tab:hover {
  background-color: rgba(255, 255, 255, 0.23); /* #ffffff3b */
}
.gal-tab.is--on {
  color: var(--bk);
  background-color: var(--wh);
}

/* --- caption, bottom-left (05 §8.3 `.location-slider-wrapper`) ----------- */
.gal-cap {
  position: absolute;
  z-index: 1;
  inset: auto auto 0 0;
  display: flex;
  align-items: center;
  gap: 9.6rem; /* .5vw */
  max-width: 652.8rem; /* 34vw */
  padding-left: 96rem; /* 5vw */
  padding-bottom: 172.8rem; /* 9vw */
  color: var(--wh);
  pointer-events: none;
}
.gal-pin {
  flex: none;
  width: 15.36rem; /* .8vw */
  height: 15.36rem;
}
.gal-cap p {
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* --- gallery, mobile (05 §8.4, the 479px phone layer × 3.75) ------------- */
@media (max-width: 767.98px) {
  /* the rail becomes a horizontal strip overlaid 39vw above the bottom;
     the thumbs swiper itself flips to horizontal 3-up via its Swiper
     breakpoint `0:` (05 §8.2), not via CSS. */
  .gal-rail {
    flex-direction: row;
    justify-content: center;
    gap: 7.5rem; /* 2vw */
    width: 100%;
    padding: 0 11.25rem; /* 3vw */
    inset: auto 0 146.25rem 0; /* 39vw above the bottom */
  }
  .swiper.gal-thumbs {
    width: 300rem; /* 80vw */
    height: 63.75rem; /* 17vw */
  }
  .gal-thumb {
    width: 100%;
    height: 100%;
    padding: 7.5rem; /* 2vw */
  }
  /* up/down chevrons become left/right (05 §8.4) */
  .gal-nav {
    width: 15rem; /* 4vw */
    height: 15rem;
    transform: rotate(-90deg);
  }

  /* TOUCH TARGETS (mobile only). Both controls keep their drawn size — the
     rail has no room to grow the chevrons (44 + 300 + 44 + gaps overflows the
     375rem canvas) and the pill's proportions are 05 §8.3's — so each claims a
     transparent hit box instead. Nothing here changes a single painted pixel.
     Measured on a 390×844 phone: chevrons 15.6px → 44px, tabs 26.7px → 45px. */
  .gal-nav::after,
  .gal-tab::after {
    content: "";
    position: absolute;
  }
  .gal-nav::after {
    /* 15rem drawn + 29rem = a 44rem square. Measured on a 390px phone it
       lands at 43×47: the rail parks the chevrons 12px from the screen edge,
       and on the inside the thumb strip paints over the last pixel or two.
       Widening it further only takes bites out of the neighbouring thumb —
       another target — so 43×47 (from 15.6×15.6) is where this stops. */
    inset: -14.5rem;
  }
  .gal-tab {
    position: relative;
  }
  .gal-tab::after {
    /* vertical only: the tabs sit 7.5rem apart and must not overlap */
    inset: -9rem 0;
  }
  .gal-tabs {
    bottom: 75rem; /* 20vw */
    gap: 7.5rem;
    padding: 3.75rem;
  }
  .gal-tab {
    padding: 5.625rem 11.25rem; /* 1.5vw 3vw */
  }
  /* DEVIATION from 05 §8.4's 54vw. The thumb strip occupies 39vw–56vw above
     the bottom, so a caption whose bottom edge sits at 54vw lands INSIDE it.
     Range gets away with it because their caption is 3vw type on a 1.5em
     line, and only the descender space overlaps; our `.fn-b2` runs a 1.2 line
     so half the words cross the thumbnails. Raised to clear 56vw with air. */
  .gal-cap {
    max-width: none;
    padding-left: 15rem;
    padding-bottom: 228rem; /* ≈61vw — 54vw would sit on the thumb strip */
  }
  .gal-pin {
    width: 11.25rem; /* 3vw */
    height: 11.25rem;
  }
}

/* ==========================================================================
   4. FACTS + DIRECTIONS (01 §5.3 def-list, laid out as a grid)
   ========================================================================== */

.lst-facts {
  /* the gallery is a full-bleed 100vh block with no margin of its own, so the
     air under it belongs to this section */
  margin-top: 134rem;
  margin-bottom: 160rem;
}
.lst-facts-in {
  display: grid;
  grid-template-columns: 155rem 1fr 355rem;
  gap: 20rem 60rem;
  align-items: start;
}
.lst-facts-lbl {
  color: var(--gr3);
}
.lst-facts-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14rem 60rem;
  align-content: start;
}
.fact-row {
  display: flex;
  gap: 20rem;
}
.fact-k {
  width: 130rem;
  flex: none;
}
.fact-v {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--gr3);
}
.lst-dir-l {
  margin-bottom: 10rem;
  color: var(--gr3);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

@media (max-width: 767.98px) {
  .lst-facts {
    margin-top: 60rem;
    margin-bottom: 80rem;
  }
  .lst-facts-in {
    display: flex;
    flex-direction: column;
    gap: 24rem;
  }
  .lst-facts-list {
    grid-template-columns: minmax(0, 1fr);
    gap: 10rem;
  }
  .fact-k {
    width: 110rem;
  }
}

/* ==========================================================================
   5. OTHER LISTINGS (04) — Base31 mechanics, kononenko palette.
   The row inverts to black/white (01 §5.4 offices hover) instead of Base31's
   per-item accent colour; the two muted cells keep their explicit grey, which
   is the same rule the offices rows follow.
   ========================================================================== */

.lst-other {
  margin-bottom: 160rem;
}
.ol-h {
  margin-bottom: 80rem;
}
.ol-list {
  position: relative;
  width: 100%;
}

.ol-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 20rem;
  width: 100%;
  padding: 44rem 0;
  /* 04 §4: Tailwind `transition-colors` — 150ms, cubic-bezier(.4,0,.2,1) */
  transition: background-color 0.15s cubic-bezier(0.4, 0, 0.2, 1),
    color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
/* hairlines: `border-b` + `first:border-t` in 04, rendered the way every other
   rule on this site is (currentColor at .15) so they invert with the row. */
.ol-row::after,
.ol-row:first-child::before {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 1px;
  background-color: currentColor;
  opacity: 0.15;
  pointer-events: none;
}
.ol-row::after {
  bottom: 0;
}
.ol-row:first-child::before {
  top: 0;
}

/* 04 §4: the fill is inside @media (hover:hover) so a touch device can never
   be left with a stuck highlight after a tap. */
@media (hover: hover) {
  .ol-row:hover {
    background-color: var(--bk);
    color: var(--wh);
  }
}

.ol-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20rem;
}
.ol-t {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
}
/* the triangle is parked at the title's left edge; the 44rem title indent is
   the space it slides into (04 §4) */
.ol-tri {
  position: absolute;
  left: 0;
  top: 50%;
  width: 28rem;
  height: 24rem;
  margin-top: -12rem;
  opacity: 0;
  /* Tailwind composes translate BEFORE rotate, so the 12rem slide happens on
     the unrotated X axis and the glyph points right the whole time. */
  transform: translateX(0) rotate(90deg);
  transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1),
    transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.ol-name {
  display: block;
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (hover: hover) {
  .ol-row:hover .ol-tri {
    opacity: 1;
    transform: translateX(12rem) rotate(90deg);
  }
  .ol-row:hover .ol-name {
    transform: translateX(44rem);
  }
}

.ol-meta {
  flex: none;
  display: flex;
}
.ol-area,
.ol-price {
  display: block;
  width: 240rem; /* 04 §4 `md:w-60` fixed right-aligned meta cells */
  padding: 0 12rem;
  text-align: right;
  color: var(--gr3);
}

/* 04 §1: ONE shared preview, fixed at the origin and moved only by transform,
   240×240 REAL px because the +20 / −120 cursor offsets are client px too
   (−120 = half the box, which is what centres it on the pointer). */
.ol-prev {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 50;
  display: none;
  width: 240px;
  height: 240px;
  overflow: hidden;
  border-radius: 4px;
  pointer-events: none;
}
.ol-prev img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* 04 §5: the floating preview does not exist below 1024px */
@media (min-width: 1024px) {
  .ol-prev.is--on {
    display: block;
  }
}

/* the inline thumb is the mobile substitute for the preview (04 §5) */
.ol-thumb {
  display: none;
}

@media (max-width: 767.98px) {
  .lst-other {
    margin-bottom: 80rem;
  }
  .ol-h {
    margin-bottom: 40rem;
  }
  .ol-row {
    padding: 20rem 0;
    gap: 16rem;
  }
  .ol-body {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 8rem;
  }
  .ol-tri {
    display: none;
  }
  .ol-meta {
    display: flex;
    gap: 16rem;
  }
  .ol-area,
  .ol-price {
    width: auto;
    padding: 0;
    text-align: left;
  }
  .ol-thumb {
    display: block;
    flex: none;
    width: 88rem;
    height: 88rem;
    overflow: hidden;
    border-radius: 4rem;
  }
  .ol-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* ==========================================================================
   6. Reduced motion
   core.js already drops the reveals, the parallax and the transitions, and
   listing.js skips the odometer roll and the gallery autoplay. These rules
   cut the CSS-side motion that is not under its control.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  .gal-cell,
  .gal-main .swiper-slide-active .gal-cell {
    transform: none;
    transition: none;
  }
  .ol-row,
  .ol-name,
  .ol-tri,
  .gal-tab,
  .gal-nav {
    transition-duration: 0.01ms;
  }
}
