/* ==========================================================================
   Yovanny Hyde Real Estate — main.css
   Design tokens, rem system, type scale, grid, utilities.
   Normative source: docs/research/01-kononenko-base.md §2–§3.
   1rem === 1 design px  (canvas: desktop 1920, mobile 375).
   Single breakpoint: mobile <= 767.98px / desktop >= 767.99px.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts (01 §2.3)
   `n`  = main sans  — PP Neue Montreal Regular (COMMERCIAL, not licensed yet).
                       Stand-in self-hosted: Instrument Sans 400, registered
                       under the alias family "PP Neue Montreal Fallback" so
                       dropping in a licensed n.woff2 needs no CSS change.
   `h`  = title serif — Hedvig Letters Serif 400 (OFL).
   One weight per family for the whole site. font-display: swap.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "PP Neue Montreal Fallback";
  src: url("../fonts/instrument-sans-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Hedvig Letters Serif";
  src: url("../fonts/hedvig-letters-serif-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* --------------------------------------------------------------------------
   2. Tokens (01 §2.1, §2.2, §2.3, §2.6)
   -------------------------------------------------------------------------- */
:root {
  /* THE REM SYSTEM — everything depends on this. 1rem = 1px at 1920w. */
  font-size: 0.0520833333vw;

  /* Colors */
  --bk: #000000;   /* p / bk / tx — text, dark sections, selection bg */
  --wh: #ffffff;   /* s / wh / bg — page bg, dark-section text */
  --gr: #eeeeee;   /* grid hover outline, toolbar pill bg, frame border */
  --gr2: #e2e2e2;  /* utility */
  --gr3: #929292;  /* ALL muted text: labels, addresses, captions */
  --bg2: #f8f8f8;  /* partner-logo circles bg */

  /* Overlays (01 §2.2) */
  --sov: rgba(0, 0, 0, 0.3);
  --ovg: linear-gradient(180deg, #000, transparent);

  /* Type families */
  --f-mn: "PP Neue Montreal", "PP Neue Montreal Fallback", sans-serif;
  --f-sf: "Hedvig Letters Serif", serif;

  /* Motion system — golden-ratio timing, Si(step, offset?) (01 §2.6) */
  --d1: 0.1s;      /* Si(1)   line/word stagger */
  --d2: 0.162s;    /* Si(2)   headline stagger / default delay token */
  --d21: 0.062s;   /* Si(2,1) char stagger, digit-roll stagger */
  --d4: 0.424s;    /* Si(4)   fade page transition */
  --d5: 0.686s;    /* Si(5)   gallery enter */
  --d6: 1.109s;    /* Si(6)   DEFAULT duration everywhere */
  --d62: 0.947s;   /* Si(6,2) Lenis duration */
  --d7: 1.794s;    /* Si(7)   scroll-drag multiplier / digit roll */

  --ease: cubic-bezier(0.17, 0.84, 0.44, 1);  /* customdefault */
  --ease-wipe: cubic-bezier(0.8, 0, 0.2, 1);  /* wipeFront */

  /* Layout */
  --gap: 20rem;
  --pad: 30rem;
}

@media (max-width: 767.98px) {
  :root {
    font-size: 0.2666666667vw; /* 1rem = 1px at 375w */
    --gap: 20rem;
    --pad: 15rem;
  }
}

/* --------------------------------------------------------------------------
   3. Reset / base
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  font-kerning: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizespeed;
  /* Scrollbar completely hidden (01 §4.2) */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}

body {
  background-color: var(--wh);
  color: var(--bk);
  /* body default = .fn-b1 */
  font-family: var(--f-mn);
  font-weight: 400;
  font-size: 16rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
  overflow-x: hidden;
}
@media (max-width: 767.98px) {
  body {
    font-size: 14rem;
  }
}

::selection {
  background-color: var(--bk);
  color: var(--wh);
}

img,
picture,
video,
svg,
canvas {
  display: block;
  max-width: 100%;
}
img,
video {
  height: auto;
}

a {
  color: inherit;
  text-decoration: none;
}

button,
input,
textarea,
select {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background: none;
  border: none;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
}

ul,
ol {
  list-style: none;
}

/* Headings default to the serif face (01 §2.3) */
h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: var(--f-sf);
  font-weight: 400;
}

/* --------------------------------------------------------------------------
   4. Type scale (01 §2.4) — rem = design px
   -------------------------------------------------------------------------- */
.fn-h1 {
  font-family: var(--f-sf);
  font-size: 400rem;
  line-height: normal;
  letter-spacing: -0.03em;
}
.fn-h2 {
  font-family: var(--f-sf);
  font-size: 175rem;
  line-height: 0.7;
  letter-spacing: -0.03em;
}
.fn-h3 {
  font-family: var(--f-sf);
  font-size: 118rem;
  line-height: 0.9;
  letter-spacing: -0.03em;
}
.fn-h4 {
  font-family: var(--f-sf);
  font-size: 80rem;
  line-height: 0.8;
  letter-spacing: -0.03em;
}
.fn-h5 {
  font-family: var(--f-sf);
  font-size: 40rem;
  line-height: 1.1;
  letter-spacing: -0.03em;
}
.fn-b1 {
  font-family: var(--f-mn);
  font-size: 16rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
.fn-b2 {
  font-family: var(--f-mn);
  font-size: 14rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
/* NOTE: weight 200 is what kononenko specifies, but both our stand-in and the
   real PP Neue Montreal ship a single 400 file — this renders as 400 until a
   licensed multi-weight family is dropped in (no CSS change needed then). */
.fn-btn {
  font-family: var(--f-mn);
  font-size: 14rem;
  font-weight: 200;
  line-height: normal;
  letter-spacing: -0.02em;
}
sup {
  font-family: var(--f-sf);
  font-size: 14rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
  vertical-align: super;
}

@media (max-width: 767.98px) {
  .fn-h1 { font-size: 200rem; }
  .fn-h2 { font-size: 40rem; }
  .fn-h3 { font-size: 36rem; }
  .fn-h4 { font-size: 30rem; }
  .fn-h5 { font-size: 26rem; }
  .fn-b1 { font-size: 14rem; }
  .fn-b2 { font-size: 12rem; }
  .fn-btn { font-size: 14rem; }
  sup { font-size: 12rem; }
}

/* Family overrides (01 §2.4). Declared after the .fn-* scale, so plain source
   order wins — no !important needed. */
.f-mn { font-family: var(--f-mn); }
.f-sf { font-family: var(--f-sf); }

/* Sub-1 line-height helper: pads descenders so overflow:hidden line masks
   don't clip them (01 §2.4 / §3). */
.e-lh {
  padding-top: 0.5em;
  padding-bottom: 0.5em;
  margin-top: -0.5em;
  margin-bottom: -0.5em;
}

/* --------------------------------------------------------------------------
   5. Layout (01 §2.5)
   -------------------------------------------------------------------------- */
.grd {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: var(--gap);
}
@media (max-width: 767.98px) {
  .grd {
    display: flex;
    flex-direction: column;
  }
}

.ctr {
  padding: 0 var(--pad);
}

/* --------------------------------------------------------------------------
   6. Color utilities (01 §2.2 — cl-* / bg-* pairs)
   -------------------------------------------------------------------------- */
.cl-bk { color: var(--bk); }
.cl-wh { color: var(--wh); }
.cl-gr { color: var(--gr); }
.cl-gr2 { color: var(--gr2); }
.cl-gr3 { color: var(--gr3); }
.bg-bk { background-color: var(--bk); }
.bg-wh { background-color: var(--wh); }
.bg-gr { background-color: var(--gr); }
.bg-gr2 { background-color: var(--gr2); }
.bg-gr3 { background-color: var(--gr3); }
.bg-bg2 { background-color: var(--bg2); }

/* --------------------------------------------------------------------------
   7. Underline links (01 §3)
   ::before bar, height max(1px, .05em), currentColor, scaleX 0 origin right
   → hover scaleX 1 origin left, transition 1.109s customdefault.
   .is--a = active page: underline stays (origin left); hovering wipes it OUT.
   -------------------------------------------------------------------------- */
.link {
  position: relative;
  display: inline-block;
  color: inherit;
  text-decoration: none;
}
.link::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: max(1px, 0.05em);
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--d6) var(--ease);
  pointer-events: none;
}
.link:hover::before {
  transform: scaleX(1);
  transform-origin: left center;
}
.link.is--a::before {
  transform: scaleX(1);
  transform-origin: left center;
}
.link.is--a:hover::before {
  transform: scaleX(0);
  transform-origin: right center;
}

/* --------------------------------------------------------------------------
   8. Reveal masks (01 §3 — splittext/linereveal targets)
   `.ln-mask` is also the class GSAP SplitText generates for its line masks
   (linesClass "ln" + "-mask"), so hand-written and generated markup share it.

   The pad / negative-margin pairs below let a mask show a hair MORE than its
   line box, so descenders (line-height < 1) and the 1px `.link` underline are
   not clipped. core.js compensates by starting the hidden state that much
   lower (PAD_LH / PAD_MSK) — keep the two in sync.
   -------------------------------------------------------------------------- */
.ln-mask {
  overflow: hidden;
  display: block;
}

/* SplitText 3.13 applies `display` to the elements it generates ONLY when its
   `tag` option is not "span" — from the vendored source:
     o !== "span" && (g.style.position = "relative", g.style.display = a)
   core.js splits with `tag: 'span'` (§5 splitOne), so every generated .ln/.wd/
   .ch is left at the browser default `display: inline` — and an inline box
   IGNORES `transform`. buildReveal animates those spans with yPercent only
   (101 -> 0, no opacity), so EVERY masked reveal on the site was a silent
   no-op: the text simply sat there instead of rising into place.
   Measured in headless Chrome 2026-08-24: `gsap.set(line, {yPercent:101})`
   moved `.intro-p .ln` and `.hero-t .wd` 0px.
   These are SplitText's own per-kind defaults (line -> block, word/char ->
   inline-block), applied by hand because we ask for spans. */
.ln {
  display: block;
}
.wd,
.ch {
  display: inline-block;
}

/* generic single-line mask for chrome items that carry an underline */
.msk {
  overflow: hidden;
  padding-bottom: 2px;
  margin-bottom: -2px;
}

/* mask for sub-1 line-height text (wordmarks, the giant footer letters) */
.msk-lh {
  display: block;
  overflow: hidden;
  padding-bottom: 0.35em;
  margin-bottom: -0.35em;
}
.msk-lh > * {
  display: block;
}

.e-lh .ln-mask {
  padding: 0.35em 0;
  margin: -0.35em 0;
}

/* First-line indent that survives SplitText.
   `text-indent` INHERITS, so once a headline is split every generated line
   block indents its own first row and the whole thing re-wraps. Reset it on
   the generated lines and hand the indent back to the first line as a margin
   — the trick SplitText documents, and one that survives the line spans' own
   display mode (text-indent does not apply to them).
   Call sites set the distance once, as `--ti`, and get both halves:
     .lx-h { --ti: 125rem; }
   Used by .lx-h (main.css §19) and .ab-hero-h / .ab-lead / .ab-quote-t
   (about.css §2, §4, §5). */
.txt-indent {
  text-indent: var(--ti, 0);
}
.txt-indent .ln-mask,
.txt-indent .ln {
  text-indent: 0;
}
.txt-indent .ln-mask:first-child {
  margin-inline-start: var(--ti, 0);
}

/* --------------------------------------------------------------------------
   9. Misc helpers
   -------------------------------------------------------------------------- */
.hairline {
  height: 1px;
  width: 100%;
  background-color: currentColor;
  opacity: 0.15;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --------------------------------------------------------------------------
   10. Header (01 §4.1)
   fixed, full width, z 9999, white + mix-blend-mode difference so it stays
   legible over any background. `.hdr--solid` (= their .kvx/.isi) drops the
   blend on hero photos: home/about above scrollY 300 and every listing page.
   -------------------------------------------------------------------------- */
.hdr {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 9999;
  padding: 20rem 0;
  color: var(--wh);
  mix-blend-mode: difference;
  pointer-events: none;
  /* 01 §4.1 records a .6s transition here, but mix-blend-mode is not an
     animatable property — the blend flip is instant on their site too. No
     transition is declared rather than a dead one. */
}
@media (max-width: 767.98px) {
  .hdr { padding: 10rem 0; }
}
.hdr--solid {
  mix-blend-mode: normal;
}

.hdr-in {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
}

/* DEVIATION from 01 §4.1: their wrap is 103rem, sized for "Kononenko".
   "Yovanny Hyde" measures ~123rem at 19rem, so the wrap is 130rem here —
   otherwise the mask (whose job is the vertical reveal) truncates the name. */
.hdr-logo {
  width: 130rem;
  overflow: hidden;
}
.hdr-logo-a {
  display: block;
  pointer-events: auto;
}
.hdr-logo-t {
  font-size: 19rem;
  line-height: 1;
}
@media (max-width: 767.98px) {
  .hdr-logo-t { font-size: 14rem; }
}
.hdr-ln {
  display: block;
  white-space: nowrap;
}

.hdr-nav ul {
  display: flex;
  gap: 0.5em;
}
/* The MLS page hosts a third-party app it cannot repaint, so the blend trick
   has nothing dependable to invert against: white IDX cards scroll under the
   bar and mix-blend-difference over a cross-origin iframe is not something to
   bet legibility on. This one page gets an opaque bar instead. */
.page-mls .hdr {
  mix-blend-mode: normal;
  background-color: var(--wh);
  color: var(--bk);
}
.hdr-nav li {
  overflow: hidden;
}
.hdr-link {
  pointer-events: auto;
  display: inline-block;
  cursor: pointer;
  white-space: nowrap;
}
@media (max-width: 767.98px) {
  /* no hamburger, ever — the "Listings" slot is simply dropped (01 §4.1) */
  .hdr-m-hide { display: none; }

  /* "All Roatan Listings," is long enough that the three remaining slots
     overrun a 375 canvas. Still no hamburger: the row wraps and stays flush
     right, so the header grows by one line instead of clipping a link. */
  .hdr-nav ul {
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 0;
  }
  .hdr-nav {
    max-width: calc(100% - 140rem);
  }

  /* TOUCH TARGET. The nav links are 14rem type on a 1.25 line — a 17.5px tap
     target on a 390px phone. The height is grown on the LINK (not on the
     `.msk` li, whose overflow:hidden would clip any hit area the anchor tried
     to claim past it) and the underline is pinned back to the type, so nothing
     moves: the links stay top-aligned, the mask still hides the whole box at
     yPercent 101, and the header is fixed — a taller nav row cannot push
     anything down the page. */
  .hdr-nav .hdr-link {
    display: block;
    padding-bottom: 26rem;
  }
  .hdr-nav .hdr-link::before {
    bottom: 26rem;
  }
}

/* --------------------------------------------------------------------------
   11. Preloader (01 §4.3)
   -------------------------------------------------------------------------- */
.pre {
  position: fixed;
  inset: 0;
  z-index: 10000; /* above the header */
  background-color: var(--wh);
  display: flex;
  align-items: flex-end;
  padding: 38rem 30rem;
}
@media (max-width: 767.98px) {
  .pre { padding: 20rem 15rem; }
}
.pre-mask {
  display: block;
  overflow: hidden;
  padding-bottom: 0.2em;
  margin-bottom: -0.2em;
}
.pre-n {
  display: block;
}
body.is-loading {
  overflow: hidden;
}
/* --------------------------------------------------------------------------
   12. Page transitions (01 §4.4)
   `.ovl` = their .dyh dim/fade layer; `.pg-pin` = the pinned outgoing page
   whose clip-path curtain closes over it.
   -------------------------------------------------------------------------- */
.ovl {
  position: fixed;
  inset: 0;
  z-index: 98;
  background-color: var(--bk);
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
}
.ovl.is--on {
  pointer-events: auto;
}
.pg-pin {
  position: fixed;
  inset: 0;
  overflow: hidden;
  z-index: 90;
  background-color: var(--wh);
}

/* --------------------------------------------------------------------------
   13. Footer (01 §6, columns per plan decision 13)
   -------------------------------------------------------------------------- */
.ftr {
  padding-top: 160rem;
  padding-bottom: 12rem;
  background-color: var(--wh);
  color: var(--bk);
  /* The giant-letter masks carry .35em of descender padding (canceled by a
     negative margin); as the page's last box that padding still extends the
     document's scroll area ~76px past the footer, so the page over-scrolls
     into a white band below the dark footer. Clip it at the footer. */
  overflow: clip;
}
.ftr--dark {
  background-color: var(--bk);
  color: var(--wh);
}

.ftr-top {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: var(--gap);
}
.ftr-logo { grid-column: 1 / 4; width: 130rem; overflow: hidden; }
.ftr-logo-t { font-size: 19rem; line-height: 1; }
.ftr-logo-t span { white-space: nowrap; }
.ftr-lbl-1 { grid-column: 4 / 5; }
.ftr-list-1 { grid-column: 5 / 8; }
.ftr-lbl-2 { grid-column: 8 / 9; }
.ftr-list-2 { grid-column: 9 / 11; }
.ftr-lbl-3 { grid-column: 11 / 12; }
.ftr-list-3 { grid-column: 12 / 14; max-width: 170rem; }
.ftr-lbl-4 { grid-column: 14 / 15; }
.ftr-list-4 { grid-column: 15 / 16; }

.ftr-list li {
  overflow: hidden;
  padding-bottom: 2px;
  margin-bottom: -2px;
}
.ftr-list li > * {
  display: inline-block;
}
/* group separator inside a list (their .cfs) */
.ftr-list li.cfs {
  margin-top: 20rem;
}

.ftr-btm {
  margin-top: 300rem;
  color: var(--gr3);
  font-family: var(--f-sf);
}

/* GIANT wordmark — text stand-in for their 4 SVG letter paths (decision 13):
   4 letters justified edge to edge, each in its own overflow mask. */
.ftr-big {
  display: flex;
  justify-content: space-between;
  width: 100%;
  font-family: var(--f-sf);
  font-size: 540rem;
  line-height: 0.8;
  letter-spacing: -0.03em;
  color: var(--bk);
}
.ftr--dark .ftr-big {
  color: var(--wh);
}

.ftr-meta {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: var(--gap);
  margin-top: 60rem;
}
.ftr-copy { grid-column: 1 / 5; }
.ftr-credit { grid-column: 12 / 16; text-align: right; }

@media (max-width: 767.98px) {
  .ftr { padding-top: 80rem; }
  .ftr-top {
    grid-template-columns: 1fr 1fr;
    gap: 40rem var(--gap);
  }
  .ftr-logo { grid-column: 1 / -1; margin-bottom: 24rem; }
  .ftr-lbl-1, .ftr-lbl-2, .ftr-lbl-3, .ftr-lbl-4 { grid-column: auto; }
  .ftr-list-1, .ftr-list-2, .ftr-list-3, .ftr-list-4 {
    grid-column: auto;
    max-width: none;
  }
  .ftr-btm { margin-top: 100rem; }
  .ftr-big { font-size: 100rem; }
  .ftr-meta { grid-template-columns: 1fr; gap: 8rem; margin-top: 40rem; }
  .ftr-copy, .ftr-credit { grid-column: auto; text-align: left; }
}

/* --------------------------------------------------------------------------
   14. Starburst button (03 §1) — Storey's `ButtonMainAnimated`, re-skinned.

   GEOMETRY IS STOREY'S, VERBATIM. Their values are vw at a 1920 / 375 canvas;
   ours is the kononenko rem system where 1rem = 1 design-px at those same
   canvases, so every value below is `Xvw × 19.2` (desktop) / `Xvw × 3.75`
   (mobile). The size table is 03 §1.2; the derived circle boxes are
   `baseHeight + 2×basePadding` (rest) and `baseHeight + 2×paddingHover`
   (hover). contact.js re-derives the same numbers from the same base config —
   if you edit one, edit both.

   TYPOGRAPHY IS OURS: Storey's label is `extraSmallText` (1.1vw ≈ 21rem); we
   use the `.fn-btn` type class (14rem) — the kononenko skin wins on type. No
   font-family is set here (core.js contract (a)); the markup carries `.fn-btn`.

   Anatomy: sharp rectangle, inline-flex space-between, label left, an 8-spoke
   starburst right. Each spoke is a transparent strip anchored at the circle's
   centre (top-0 / left-50% / origin bottom) rotated i×45°, whose
   `padding-bottom` IS the spoke radius; the only visible part is the tick at
   its outer end. Animating padding-bottom pulls the ticks in and out.
   No border-radius, no border, no shadow — anywhere.
   -------------------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  width: -moz-fit-content;
  width: fit-content;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
  -webkit-appearance: none;
  appearance: none;
}
.btn-l {
  display: block;
}
/* fixed-height slot; the starburst is allowed to overflow it (03 §1.1) */
.btn-ic {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: flex-end;
  width: var(--btn-slot-w);
  height: var(--btn-slot-h);
}
.btn-cir {
  position: relative;
  display: block;
  flex: none;
  width: var(--btn-cir);
  height: var(--btn-cir);
}
.btn-sp {
  position: absolute;
  top: 0;
  left: 50%;
  display: block;
  width: var(--btn-spoke-w);
  height: -moz-fit-content;
  height: fit-content;
  padding-bottom: var(--btn-sp);
  transform-origin: bottom center;
}
.btn-tk {
  display: block;
  width: 100%;
  height: var(--btn-tick-h);
  background-color: currentColor;
}

/* Size configs — 03 §1.2, desktop column. */
.btn--small {
  --btn-sp: 9.6rem;        /* basePadding  0.5vw    */
  --btn-sp-h: 4.8rem;      /* paddingHover 0.25vw   */
  --btn-tick-h: 2.88rem;   /* baseHeight   0.15vw   */
  --btn-spoke-w: 3.264rem; /* baseWidth    0.17vw   */
  --btn-slot-h: 28.15488rem; /* baseScale  1.4664vw */
  --btn-slot-w: 23.04rem;  /* 1.2vw (no sm: variant on Storey) */
  --btn-cir: 22.08rem;     /* 1.15vw    */
  --btn-cir-h: 12.48rem;   /* 0.65vw    */
  --btn-x: 9.6rem;         /* label nudge 0.5vw */
  gap: 44.16rem;           /* 2.3vw */
  padding: 13.44rem 17.28rem; /* py 0.7vw / px 0.9vw */
}
.btn--large {
  --btn-sp: 15.2064rem;      /* 0.792vw   */
  --btn-sp-h: 7.68rem;       /* 0.4vw     */
  --btn-tick-h: 3.689472rem; /* 0.19216vw */
  --btn-spoke-w: 4.56192rem; /* 0.2376vw  */
  --btn-slot-h: 39.53664rem; /* 2.0592vw  */
  --btn-slot-w: 38.4rem;     /* 2vw       */
  --btn-cir: 34.102272rem;   /* 1.77616vw */
  --btn-cir-h: 19.049472rem; /* 0.99216vw */
  --btn-x: 9.6rem;
  gap: 96rem;                                  /* 5vw */
  padding: 15.36rem 26.88rem 15.36rem 23.04rem; /* py .8 / pr 1.4 / pl 1.2vw */
}

@media (max-width: 767.98px) {
  .btn--small {
    --btn-sp: 5.625rem;    /* 1.5vw  */
    --btn-sp-h: 3.75rem;   /* 1vw    */
    --btn-tick-h: 1.5rem;  /* 0.4vw  */
    --btn-spoke-w: 1.875rem; /* 0.5vw */
    --btn-slot-h: 7.5rem;  /* 2vw    */
    --btn-slot-w: 4.5rem;  /* 1.2vw — the starburst deliberately overflows it */
    --btn-cir: 12.75rem;   /* 3.4vw  */
    --btn-cir-h: 9rem;     /* 2.4vw  */
    --btn-x: 1.875rem;
    gap: 11.25rem;                          /* 3vw */
    padding: 3.75rem 11.25rem 3.75rem 7.5rem; /* py 1 / pr 3 / pl 2vw */
  }
  .btn--large {
    --btn-sp: 11.88rem;     /* 3.168vw   */
    --btn-sp-h: 6rem;       /* 1.6vw     */
    --btn-tick-h: 2.8824rem; /* 0.76864vw */
    --btn-spoke-w: 3.564rem; /* 0.9504vw  */
    --btn-slot-h: 30.888rem; /* 8.2368vw  */
    --btn-slot-w: 30rem;    /* 8vw       */
    --btn-cir: 26.6424rem;  /* 7.10464vw */
    --btn-cir-h: 14.8824rem; /* 3.96864vw */
    --btn-x: 1.875rem;
    gap: 45rem;               /* 12vw */
    padding: 15rem 18.75rem;  /* py 4vw / px 5vw */
  }
}

/* Colorways (03 §1.2). The ticks are `currentColor`, so colour is set once. */
.btn--light { background-color: var(--wh); color: var(--bk); }
.btn--dark { background-color: var(--bk); color: var(--wh); }
.btn--form { background-color: #3d3d3d; color: var(--wh); }

/* Degraded / reduced-motion fallback: the same three value flips, on CSS
   transitions. 0.33,1,0.68,1 ≈ power2.out (03 §1.3 vanilla note). Transitions
   exist ONLY on this variant so they can never fight the GSAP tweens. */
.btn--css .btn-sp,
.btn--css .btn-cir,
.btn--css .btn-l {
  transition: padding-bottom 0.4s cubic-bezier(0.33, 1, 0.68, 1),
    width 0.4s cubic-bezier(0.33, 1, 0.68, 1),
    height 0.4s cubic-bezier(0.33, 1, 0.68, 1),
    transform 0.4s cubic-bezier(0.33, 1, 0.68, 1);
}
.btn--css:hover .btn-sp,
.btn--css:focus-visible .btn-sp {
  padding-bottom: var(--btn-sp-h);
}
.btn--css:hover .btn-cir,
.btn--css:focus-visible .btn-cir {
  width: var(--btn-cir-h);
  height: var(--btn-cir-h);
  transform: rotate(90deg);
}
.btn--css:hover .btn-l,
.btn--css:focus-visible .btn-l {
  transform: translateX(var(--btn-x));
}

/* --------------------------------------------------------------------------
   15. Contact modal (03 §2) — Storey's "intake sheet", re-skinned.

   Mechanics are theirs verbatim (slide-up panel, 40svh sheet offset, dimming
   backdrop, own scroll container, backdrop-click close). Skin is ours: white
   sheet, #3d3d3d ink (kept — it reads as pen-on-paper and sits fine in a b/w
   system), our type classes, an SVG-turbulence paper texture instead of their
   paper.webp, and no paperclip (03 §2.2 asset we skip per plan decision 8).

   MONO SUBSTITUTE: Storey types the gutter numbers, inputs and footer strip in
   New Grotesk Mono. We ship no mono face, so `.cm-mono` is our sans (inherited
   from `.f-mn`, never set here — core.js contract (a)) with wide positive
   tracking and uppercase where Storey uppercases. It reads as "typed" without
   adding a font.
   -------------------------------------------------------------------------- */
.cm {
  --ink: #3d3d3d;
  position: fixed;
  inset: 0;
  z-index: 10001; /* above the header (9999) and the preloader (10000) */
  display: none;
  background-color: rgba(0, 0, 0, 0);
  pointer-events: none;
}
.cm-panel {
  height: 100%;
  will-change: transform;
}
.cm-scroll {
  height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  outline: none;
}
.cm-scroll::-webkit-scrollbar {
  width: 0;
  height: 0;
  display: none;
}
.cm-grid {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: var(--gap);
  padding: 0 var(--pad);
}
/* Storey: `col-span-9 sm:col-start-4` of 12 = 75% wide, 25% inset.
   On our 15-col grid that is cols 5–15 (73.3% / 26.7%). */
.cm-sheet {
  grid-column: 5 / 16;
  margin-top: 40vh;
  margin-top: 40svh;
  margin-bottom: 10vh;
  margin-bottom: 10svh;
}

/* html scroll lock — only used when there is no Lenis to stop (reduced motion
   / degraded); Lenis' own .stop() covers the normal path. */
.cm-lock,
.cm-lock body {
  overflow: hidden;
}

/* the paper ------------------------------------------------------------- */
.cm-form {
  position: relative;
  width: 100%;
  padding: 96rem 0 48rem; /* pt 5vw / pb 2.5vw */
  background-color: var(--wh);
  color: var(--ink);
}
.cm-hp {
  position: absolute;
  left: -9999px;
  top: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
/* fractalNoise tile, multiply @ 45% — Storey's paper.webp without the asset.
   z above the fields, pointer-events none, so inputs underneath still work. */
.cm-tex {
  position: absolute;
  inset: 0;
  z-index: 10;
  overflow: hidden;
  pointer-events: none;
  mix-blend-mode: multiply;
  opacity: 0.45;
  background-repeat: repeat;
  background-size: 200px 200px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.55 0 0 0 0 0.53 0 0 0 0 0.5 0 0 0 0.4 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23p)'/%3E%3C/svg%3E");
}

/* Close control (ADDITION to 03 §2 — Storey offers only the backdrop, which is
   scrolled off-screen by the time a touch user reaches row 03).
   A zero-height sticky rail: the button sits at the paper's top-right in flow,
   then pins to the top of the scroll container as the sheet scrolls, so it is
   always on the paper and always tabbable. z above .cm-tex (10). */
.cm-close-rail {
  position: sticky;
  top: 20rem;
  z-index: 20;
  display: flex;
  justify-content: flex-end;
  /* flex-start, not the default stretch: `height: 0` would otherwise clamp the
     button's own box to 0 + its padding and leave a 9px hit area. */
  align-items: flex-start;
  height: 0;
}
.cm-close {
  padding: 6rem 0;
  color: var(--ink);
  background: none;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
}

/* rows ------------------------------------------------------------------- */
.cm-row {
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  gap: var(--gap);
}
.cm-hdr {
  margin-bottom: 134.4rem; /* 7vw */
}
.cm-num {
  grid-column: 2 / 3;
}
.cm-title {
  grid-column: 3 / 7;
}
/* Storey's letterhead mark. Their doc pins no columns for it; we stop it at
   the end of col 8 so it lines up with the right edge of every field rule
   rather than hanging flush off the paper's edge. */
.cm-hdr-r {
  grid-column: 7 / 9;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 20rem;
}
.cm-star {
  width: 57.6rem; /* 3vw */
  height: 57.6rem;
  flex: none;
}
.cm-star svg {
  display: block;
  width: 100%;
  height: 100%;
}
.cm-fr {
  margin-bottom: 115.2rem; /* 6vw */
}
.cm-field {
  grid-column: 3 / 9;
}

/* fields ----------------------------------------------------------------- */
.cm-f {
  display: flex;
  flex-direction: column;
  gap: 9.6rem; /* 0.5vw */
}
.cm-in {
  width: 100%;
  padding-bottom: 4rem;
  border-bottom: 1px solid var(--ink);
  background-color: transparent;
  outline: none;
}
.cm-in:focus,
.cm-in:focus-visible {
  outline: none;
}

.cm-opts {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--gap);
  width: 100%;
}
.cm-opt {
  grid-column: span 3;
  display: flex;
  align-items: center;
  gap: 48rem; /* 2.5vw */
  cursor: pointer;
}
.cm-box {
  flex: none;
  width: 19.2rem; /* 1vw */
  height: 19.2rem;
  background-color: var(--ink);
  opacity: 0.5;
}
.cm-opt input:checked + .cm-box {
  opacity: 1;
}
.cm-opt input:focus-visible + .cm-box {
  outline: 1px solid var(--ink);
  outline-offset: 2px;
}

/* textarea over 3 ruled lines: fixed-height box, line-height == rule pitch */
.cm-ta-wrap {
  position: relative;
  width: 100%;
  height: 144rem; /* 7.5vw = 3 × 2.5vw */
}
.cm-ta {
  width: 100%;
  height: 100%;
  border: none;
  background-color: transparent;
  outline: none;
  resize: none;
  line-height: 48rem; /* 2.5vw */
}
.cm-rules {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
}
.cm-rules > span {
  display: block;
  width: 100%;
  height: 48rem;
  border-bottom: 1px solid var(--ink);
}

/* submit + printed footer strip ------------------------------------------ */
.cm-submit {
  margin-bottom: 38.4rem; /* 2vw */
}
.cm-submit-cell {
  grid-column: 3 / 9;
  display: flex;
  align-items: center;
}
.cm-status {
  margin-left: 38.4rem; /* 2vw */
}
.cm-foot {
  margin-bottom: 19.2rem; /* 1vw */
  padding-top: 38.4rem; /* 2vw */
  border-top: 1px solid var(--ink);
}
.cm-foot > span {
  display: flex;
  justify-content: center;
}
.cm-foot-1 { grid-column: 1 / 3; }
.cm-foot-2 { grid-column: 4 / 7; }
.cm-foot-3 { grid-column: 7 / 10; }

.cm-mono {
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

@media (max-width: 767.98px) {
  .cm-grid {
    display: block;
  }
  .cm-form {
    padding: 45rem var(--pad) 37.5rem; /* pt 12vw / px --margin / pb 10vw */
  }
  .cm-row {
    grid-template-columns: repeat(6, 1fr);
  }
  .cm-hdr {
    margin-bottom: 56.25rem; /* 15vw */
  }
  .cm-num {
    grid-column: 1 / 2;
  }
  .cm-title {
    grid-column: 2 / 7;
  }
  .cm-hdr-r {
    display: none; /* 03 §2.7 — hidden on mobile */
  }
  .cm-close-rail {
    top: 15rem;
  }
  .cm-close {
    padding: 14rem 0; /* 42.4rem ≈ 44px tall touch target */
  }
  /* TOUCH TARGET. The submit chip's geometry is Storey's config (03 §1.2) and
     must not be re-sized, so it gets a transparent hit box instead: 25rem of
     button + 18rem of pseudo ≈ 44px, claimed straight up and down into the
     row's own margin where there is nothing else to hit. */
  .cm-submit-cell .btn {
    position: relative;
  }
  .cm-submit-cell .btn::after {
    content: "";
    position: absolute;
    inset: -9rem 0;
  }
  .cm-fr {
    margin-bottom: 45rem; /* 12vw */
  }
  .cm-field,
  .cm-submit-cell {
    grid-column: 2 / 7;
  }
  .cm-f {
    gap: 7.5rem; /* 2vw */
  }
  .cm-opts {
    grid-template-columns: repeat(2, 1fr);
  }
  .cm-opt {
    grid-column: span 1;
    gap: 15rem; /* 4vw */
  }
  .cm-box {
    width: 13.125rem; /* 3.5vw */
    height: 13.125rem;
  }
  .cm-ta-wrap {
    height: 90rem; /* 24vw */
  }
  .cm-ta,
  .cm-rules > span {
    line-height: 30rem; /* 8vw */
    height: 30rem;
  }
  .cm-ta {
    height: 100%;
  }
  .cm-submit {
    margin-bottom: 30rem; /* 8vw */
  }
  .cm-status {
    margin-left: 7.5rem; /* 2vw */
  }
  .cm-foot {
    margin-bottom: 15rem; /* 4vw */
    padding-top: 22.5rem; /* 6vw */
    row-gap: 15rem;
  }
  .cm-foot-1 { grid-column: 1 / 4; }
  .cm-foot-2 { grid-column: 4 / 7; }
  .cm-foot-3 { grid-column: 1 / 7; }
}

/* ==========================================================================
   HOME PAGE (§16–§21) — 01 §5 section by section, plus 02 for the listings
   expand. Section CSS never sets font-family (core.js contract (a)); the
   markup carries `.f-mn` / `.f-sf`.
   ========================================================================== */

/* --------------------------------------------------------------------------
   16. Hero (01 §5.1) + Storey's 4-slide crossfade slider (06 §1–§5)

   The slider is four absolutely-stacked layers over black and one `gsap.to`
   per transition — no carousel library (06 §1). Everything below is 06's
   geometry; home.js §1 owns the motion.
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  color: var(--wh);
}
@media (max-width: 767.98px) {
  .hero {
    height: calc(100vh - var(--vhmobilebar));
  }
}
.hero-img {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: #000;                /* 06 §1: black under the layers */
}
.hero-zoom {
  position: absolute;
  inset: 0;
  will-change: transform, opacity;
}
.hero-slides {
  position: absolute;
  inset: 0;
}

/* 06 §1–§2. The hidden resting state (scale 1.05 / opacity 0) lives in the
   STYLESHEET, exactly where an outgoing tween parks a layer — so reduced
   motion and the no-JS path show slide 0 alone, with no flash and no script. */
.hero-slide {
  position: absolute;
  inset: 0;
  will-change: transform, opacity;
}
.hero-slide + .hero-slide {
  opacity: 0;
  transform: scale(1.05);
}
.hero-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.85;                   /* 06 §7 IMG — verbatim (page only; the  */
  filter: saturate(1.5);           /* OG/preload plate is untouched)        */
}

/* The overlay is a column: wordmark -> progress row -> caption + button. */
/* 06 §1: Storey's overlay is a 12-column `layout-grid`, and the whole slider
   block is ONE `col-span-6` cell — the left half. Their grid is
   column-gap/padding 1.3889vw; ours is --gap 20rem / --pad 30rem, which lands
   the 6-col block at 47.9% of the viewport at 1920 — the same 47.9% Storey's
   own tokens produce. Nothing to import. */
.hero-in {
  position: relative;
  z-index: 2;
  width: 100%;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gap);
  align-content: end;
  padding: 38rem 30rem;
}
.hero-bottom {
  grid-column: 1 / span 6;
  display: flex;
  flex-direction: column;
}

/* 06 §3. The widths here are state 0; gsap morphs them (50% <-> 16.666667%).
   27rem ~= Storey's --gutter, 1.3889vw at their 1920 canvas. */
.hero-bars {
  display: flex;
  width: 100%;
  gap: 27rem;
}
.hero-track {
  display: block;
  width: 16.666667%;
  padding: 27rem 0;                /* 06 §3 py-[var(--gutter)] — also the hit area */
  margin: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.hero-track:first-child {
  width: 50%;                      /* 06 §3: the active track is 1/2 */
}
.hero-line {
  display: block;
  height: 3px;                     /* 06 §6: a hard 3px, NOT a rem */
  background: rgba(255, 255, 255, 0.3);
}
.hero-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--wh);
  transform: scaleX(0);
  transform-origin: left center;
}

/* 06 §1: inside the col-span-6 cell, a nested 6-col grid — caption in the
   first 3, CTA right-aligned in the last 3. So the caption column is a quarter
   of the viewport, which is what makes it wrap to four or five lines. */
.hero-foot {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  column-gap: var(--gap);
  align-items: end;
}
.hero-caps {
  position: relative;
  grid-column: 1 / span 3;
}
/* 06 §6: Storey's caption is `.smallText` = 1.6vw / line-height 1.2 on their
   1920 canvas — 30.72rem here. No house type class is close (fn-b1 is 16rem),
   so the size is set outright, the way §14's starburst geometry is. */
.hero-cap {
  margin: 0;
  font-size: 30.72rem;
  line-height: 1.2;
}
/* Caption 0 sits in flow and sets the box; 1–3 are bottom-anchored over it.
   Their split lines rest below their masks (home.js), so a hidden caption
   takes no visual space. */
.hero-cap + .hero-cap {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
}
/* Two CTAs share the 06 §1 cell that used to hold one.

   THEY STACK, and that is not a fallback. The cell is 450px at the 1920
   canvas (3 of the nested 6 columns); the two large starbursts measure 266 +
   298 + 20 = 584. Side by side they would have to eat into the caption's
   columns — and the caption being exactly a quarter of the viewport is the
   measured 06 §1 fact that makes it wrap to four lines. The stack costs
   nothing; widening the CTA would cost the slider its layout.

   column-REVERSE so "View Listings" stays on the bottom row, i.e. exactly
   where the single button sat before (the row is align-items:end, so the
   block's bottom edge is the anchor). DOM order is still primary-first, which
   is what tab order should be. */
.hero-btns {
  grid-column: 4 / span 3;
  justify-self: end;
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-end;
  gap: var(--gap);
}

@media (max-width: 767.98px) {
  /* 06 §6: below 640 Storey's layout-grid drops to 6 columns and the slider
     block is `col-span-6` — i.e. full width. The caption and the CTA each take
     the whole row (their `justify-end` is an `sm:` rule), so it stacks. */
  .hero-in {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    padding: 20rem 15rem;
  }
  .hero-bottom {
    grid-column: 1 / -1;
  }
  .hero-bars {
    gap: 15rem;                    /* 06 §6: --gutter is 4vw below 640 */
  }
  .hero-track {
    padding: 15rem 0;              /* py-[var(--gutter)] at the 375 canvas */
  }
  .hero-foot {
    grid-template-columns: 1fr;
    row-gap: 24rem;
    justify-items: start;
  }
  .hero-caps {
    grid-column: 1;
  }
  /* 06 §6: `.smallText` is 4.3vw below 640 = 16rem at the 375 canvas. */
  .hero-cap {
    font-size: 16rem;
  }
  /* 06 §6: the CTA block is full-width and left-aligned below 640. Nothing is
     bottom-anchored to preserve down here (the caption is its own grid row),
     so the reverse is dropped and the primary simply reads first. */
  .hero-btns {
    grid-column: 1;
    justify-self: start;
    flex-direction: column;
    align-items: flex-start;
    gap: 15rem;
  }
}

/* Reduced motion / no engine (body.no-motion, set by core.js and home.js):
   no rotation, so no timer UI and no second caption. Slides 1–3 are already
   parked by the stylesheet resting state above. */
.no-motion .hero-bars {
  display: none;
}
.no-motion .hero-cap + .hero-cap {
  display: none;
}

/* --------------------------------------------------------------------------
   17. Intro (01 §5.2)
   -------------------------------------------------------------------------- */
.intro {
  margin-top: 220rem;
}
@media (max-width: 767.98px) {
  .intro { margin-top: 80rem; }
}
.intro-h {
  text-align: center;
}
.intro-h-l {
  display: block;
}
.intro-h-2 {
  margin-top: 24rem;
}
@media (max-width: 767.98px) {
  .intro-h-2 { margin-top: 8rem; }
}
.intro-cols {
  display: flex;
  justify-content: center;
  gap: 147rem;
  margin-top: 70rem;
  margin-bottom: 132rem;
}
.intro-p {
  width: 356rem;
  color: var(--gr3);
}
@media (max-width: 767.98px) {
  .intro-cols {
    flex-direction: column;
    gap: 24rem;
    margin-top: 32rem;
    margin-bottom: 48rem;
  }
  .intro-p { width: 100%; }
}
.intro-band {
  height: 960rem;
  overflow: hidden;
}
.intro-band img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 767.98px) {
  .intro-band { height: 250rem; }
}

/* --------------------------------------------------------------------------
   18. Info / bio (01 §5.3, `.rfi` variant: 134rem top / 160rem bottom —
       NOT the plain 220/220 the section carried before)
   -------------------------------------------------------------------------- */
.info {
  margin-top: 134rem;
  margin-bottom: 160rem;
}
.info-in {
  display: flex;
  gap: 270rem;
}
/* The width moved to .info-col when the brokerage mark was added below the
   list — the wrapper is the flex child now, not the <dl>. */
.info-col {
  width: 355rem;
  flex: none;
}
.info-row {
  display: flex;
}
.info-row + .info-row {
  margin-top: 14rem;
}
.info-k {
  width: 105rem;
  flex: none;
}
.info-body {
  flex: 1 1 auto;
  max-width: 1110rem;
}
/* Brokerage mark. Roatan Reef Realty's logo is a LIGHT-BACKGROUND asset — its
   palms and roofline are white and only read against the grey sun-disc — so it
   sits here unmodified on the white home page and needs no plate. The about
   page is black, which is why it gets a --bg2 plaque there instead. Keep it
   below the wordmark rather than replacing it: the text is what the muted
   `.info-v` type scale is aligned to, and MLS practice is to name the
   brokerage, not only show its mark. */
.info-logo {
  display: block;
  width: 200rem;
  height: auto;
  /* Indented to the value column (.info-k is 105rem) so the mark hangs off the
     same left edge as "Roatan Reef Realty" rather than the labels. */
  margin: 34rem 0 0 105rem;
}
@media (max-width: 767.98px) {
  .info-col { width: 100%; }
  .info-logo {
    width: 160rem;
    margin: 24rem 0 0 105rem;
  }
}
@media (max-width: 767.98px) {
  .info {
    margin-top: 60rem;
    margin-bottom: 60rem;
  }
  .info-in {
    flex-direction: column;
    gap: 40rem;
  }
}

/* --------------------------------------------------------------------------
   19. ★ LISTINGS INDEX — closed state 01 §5.4, expand 02 §1–§3
   -------------------------------------------------------------------------- */
.lx {
  margin-bottom: 160rem;
}
@media (max-width: 767.98px) {
  .lx { margin-bottom: 80rem; }
}
.lx-intro {
  grid-column: 1 / -1;
  margin-bottom: 100rem;
}
/* `.txt-indent` (main.css §8) turns this into the split-safe pair. */
.lx-h {
  --ti: 125rem;
}
/* The label sits in column 5, top-aligned with the list — i.e. exactly where
   the FIRST row's now full-bleed inverted band passes under it. Two
   consequences, both handled here rather than in JS:
     · pointer-events:none, so a pointer over the label still reaches the row
       underneath and the hover-to-open reads it (the label is decorative);
     · z-index above the band, and white while that first row is inverted, so
       it is legible on the black instead of disappearing into it. Scoped to
       `:first-child` because no other row reaches this high. */
.lx-eyebrow {
  grid-column: 5 / 6;
  position: relative;
  z-index: 1;
  pointer-events: none;
}
.lx-grid:has(.lx-list > .lx-row:first-child:hover) .lx-eyebrow,
.lx-grid:has(.lx-list > .lx-row:first-child.is--open) .lx-eyebrow {
  color: var(--wh);
}
.lx-list {
  grid-column: 6 / 13;
  display: flex;
  flex-direction: column;
  width: 100%;

  /* --- the full-bleed lane (02 §1) -------------------------------------
     lamalama's row is a `w-screen` button: the closed title/description
     cluster lives in its right half, and the `.js-content` gallery is a
     SIBLING BELOW that head row spanning the whole viewport — which is why
     ~3 of its 600px slides are on screen at once. Ours used to be confined
     to this 857rem grid column, and the open slider measured 597rem: not one
     600rem slide fitted, so opening a row showed a single clipped photo.

     The numbers below describe the page grid (`.ctr` padding 30rem,
     `.grd` = 15 columns with a 20rem gap, this list on columns 6–12) in
     DESIGN REM, i.e. px at 1920. Everything on this site is rem and
     1rem = 0.0520833vw, so the grid is a pure proportional scale of the
     1920 canvas and these ratios hold at every desktop width.

     They are used as PERCENTAGES OF THIS COLUMN, never as vw/100vw: a
     percentage resolves against a containing block that can never include a
     scrollbar gutter, so the bleed cannot overflow the page. (It also cannot
     under-run: `scrollbar-width:none` hides ours, but that is a platform
     promise we do not need to depend on.) */
  --lx-page: 1920;                                          /* design viewport */
  --lx-pad: 30;                                             /* = --pad, the page gutter */
  --lx-col: 857.33333;                     /* 7 columns + 6 gaps = this column */
  --lx-off: 656.66667;              /* --pad + 5 x (column + gap) = its left x */
  --lx-rgt: calc(var(--lx-page) - var(--lx-off) - var(--lx-col));   /* 406 = the right gutter */
}
@media (max-width: 767.98px) {
  .lx-intro { margin-bottom: 40rem; }
  .lx-h { --ti: 40rem; }
  .lx-eyebrow { margin-bottom: 24rem; }
}

/* one row ---------------------------------------------------------------- */
/* The row is the full-bleed element (lamalama's `w-screen` button), and its
   PADDING puts the content box back on the grid column — so .lx-head and
   .lx-band need no rules of their own and the closed row is untouched, while
   the background and the gallery below reach the screen edges.

   The negative margins carry no `width`: as a column-flex item the row
   stretches to fill its line, so its border box is exactly
   column + left bleed + right bleed. Any rounding in the 1fr tracks therefore
   lands INSIDE the viewport rather than past it, and `overflow: hidden`
   (already here for the head's yPercent:101 entrance) is the second belt —
   nothing the row contains can reach the page's scrollable width. */
.lx-row {
  position: relative;
  overflow: hidden;
  margin-left: calc(var(--lx-off) / var(--lx-col) * -100%);
  margin-right: calc(var(--lx-rgt) / var(--lx-col) * -100%);
  padding-left: calc(var(--lx-off) / var(--lx-col) * 100%);
  padding-right: calc(var(--lx-rgt) / var(--lx-col) * 100%);
}
/* Instant black/white invert (01 §5.4) — no transition property, on purpose.
   The two muted cells keep their explicit gray; name + arrow follow color.
   `.is--open` joins `:hover` so a row opened by CLICK and then left alone
   still reads as the inverted, open row rather than reverting to white. */
.lx-row:hover,
.lx-row.is--open {
  background-color: var(--bk);
  color: var(--wh);
}

/* The hairlines stay on the grid column — they are the closed list's own
   rules, and the client signed that off. Their percentages are of the row's
   PADDING box (the full 1920), not of the column, hence the second pair. */
.lx-rule {
  position: absolute;
  left: calc(var(--lx-off) / var(--lx-page) * 100%);
  height: 1px;
  width: calc(var(--lx-col) / var(--lx-page) * 100%);
  background-color: currentColor;
  opacity: 0.15;
}
.lx-rule--top { top: 0; }
.lx-rule--btm { bottom: 0; }

.lx-head {
  position: relative;
  display: flex;
  gap: 20rem;
  padding: 14rem 0;
}
.lx-name,
.lx-area,
.lx-price {
  display: block;
  width: 230rem;
  flex: none;
}
.lx-area,
.lx-price {
  color: var(--gr3);
}
.lx-arrow {
  position: absolute;
  right: 0;
  top: 50%;
  width: 10rem;
  height: 10rem;
  transform: translateY(-50%);
  opacity: 0.6;
}

/* the closed row's photo lane (02 R1/R2) --------------------------------- */
/* R1 was our biggest divergence: lamalama's strip carries NO opacity-0 and
   nothing in its JS ever touches the strip's opacity — it is on screen in
   every closed row, and `pointer-events: none` is the load-bearing part, so a
   click that lands on a thumbnail falls straight through to the row and
   toggles it. That is why a visitor experiences it as "clicking an image
   expands it". We had `opacity: 0` here, so there was nothing to see and
   nothing to click, and every symptom the client reported followed from it.

   .lx-band is the in-flow lane that gives the closed row the height to hold a
   120rem strip (it sits after .lx-head, whose own height varies — one line or
   two, depending on the listing name). The strip is absolute INSIDE it, at the
   band's top and a fixed 120rem tall, so its rect never moves while the band
   resizes — which is what keeps every Flip start/end rect honest.

   The band is ALSO lamalama's `.js-description` box: their strip is
   absolutely positioned OVER a description whose text is `opacity-0` until
   the row opens, at which point the box tweens `7.5rem -> auto` in the HEAD
   row while the gallery band sits below it. .lx-info is that description, and
   it lives here for the same reason — the panel used to put it in a 240rem
   column BESIDE the slider, which is what left the slider 597rem wide. The
   band therefore tweens 134rem -> auto now, not 134rem -> 0.

   No `overflow: hidden` here, deliberately: on CLOSE the images are appended
   back into the strip's thumbs and fly in from the slider's rect, so a clip
   on this box would blank them for the whole flight. */
.lx-band {
  position: relative;
  display: block;
  width: 100%;
  /* CLOSED HEIGHT 0 — client decision 2026-08-24: the closed row shows no
     previews. This is a deliberate departure from 02 R1 (lamalama keeps its
     strip permanently on screen); we keep their OPEN gallery and drop their
     closed strip. The strip below still has real geometry so every Flip
     start/end rect stays honest — it is just invisible and zero-height, so
     the closed list is the plain 01 §5.4 index again. */
  height: 0;
}
/* Rows with no photos: no strip, so no lane to reserve. 120rem of blank white
   in the middle of the list read as a missing image, not as a short row. They
   still open — the band goes 0 -> auto, i.e. to their text height. */
.lx-band--empty {
  height: 0;
}
.lx-strip {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  gap: 8rem;
  width: 100%;
  height: 120rem;
  pointer-events: none;
}
/* 02 R2 `lg:first:ml-auto` — the whole strip is right-aligned. */
.lx-strip .lx-thumb:first-child {
  margin-left: auto;
}
.lx-thumb {
  position: relative;
  flex: none;
  height: 100%;
}
.lx-thumb-i {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.lx-thumb-i img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* the row's description (02 §1 `.js-description`) ------------------------ */
/* In the band, under the head, ABOVE the gallery — never beside the slider.
   Invisible and untouchable while the row is closed exactly as lamalama's is
   (`.js-text-reveal opacity-0` on the text, `opacity-0 pointer-events-none`
   on the CTA); home.js tweens both back on open, 0.75 power4.out per 02 R4.
   480rem = two head cells + their gap, so the paragraph starts and ends on
   the same grid the listing name and area do. The 14rem of bottom padding is
   the band's air, moved onto the content so `height: auto` keeps it. */
.lx-info {
  width: 480rem;
  display: flex;
  flex-direction: column;
  gap: 16rem;
  align-items: flex-start;
  padding-bottom: 14rem;
  opacity: 0;
  pointer-events: none;
}
.lx-facts {
  color: var(--gr3);
}
.lx-fact + .lx-fact {
  margin-top: 4rem;
}

/* the expanding gallery band (02 §2) -------------------------------------- */
/* This is the piece that breaks out of the column. It escapes the row's
   padding with the same percentages, minus the page gutter: the gallery
   starts where the site's own content edge is (--pad, 30rem — lamalama's
   `px-[var(--ll-grid-margin)]`) and bleeds off the right screen edge, so
   three 600rem slides are on screen at 1920 with the fourth running off. */
.lx-body {
  position: relative;
  height: 0;
  overflow: visible;
  margin-left: calc((var(--lx-off) - var(--lx-pad)) / var(--lx-col) * -100%);
  margin-right: calc(var(--lx-rgt) / var(--lx-col) * -100%);
}
.lx-body-in {
  position: absolute;
  top: 0;
  left: 0;
  display: block;
  width: 100%;
  padding-top: 56rem;
  /* Client 2026-08-24: the photos used to sit flush on the band's bottom edge
     (lamalama's 28.5rem panel is exactly 56 + 400, no bottom padding — 02 R2
     verbatim). A departure, on purpose: the gallery is inset from the bottom
     the way the slides are inset from each other, just at a scale you can
     actually see. Keep it in step with LX.bodyH in home.js. */
  padding-bottom: 32rem;
}
/* 02 R2 slider height, verbatim: 400rem, against the 120rem .lx-strip above —
   a 3.333 Flip scale, the same one lamalama's 400/120 gives. The panel's own
   height follows from it and is spelled out once, in home.js LX.bodyH:
   400 (slider) + 56 (.lx-body-in padding-top) = 456rem, which is lamalama's
   28.5rem to the pixel. Slide widths are 600rem landscape / 320rem portrait
   in build.js — again verbatim, and the same 1.875 ratio as the 180/96
   thumbs. The earlier ×0.8 sizing that sat "halfway" to the reference is
   gone: the client compared it to lamalama and it read too small. */
.lx-slider {
  width: 100%;
  height: 225rem;
  cursor: grab;
}
.lx-slider .swiper-wrapper {
  height: 225rem;
}
.lx-slider.is--drag {
  cursor: grabbing;
}
.lx-slot {
  position: relative;
  flex: none;
  height: 225rem;
}
/* The slider keeps `grab` (dragging is still the main gesture), but a photo
   itself now navigates to that listing's #gallery, so it says so. Restored
   with the click-through it advertises. */
.lx-slot [data-lx-img] {
  cursor: pointer;
}
/* A click inside the open slider is inert (02 R3) — you drag it. The grab
   cursor from .lx-slider is therefore the honest one; this used to say
   `cursor: pointer`, back when a click here navigated to the gallery. */
/* row is open: the panel occupies real height */
.lx-row.is--open .lx-body-in {
  position: relative;
}

@media (max-width: 767.98px) {
  /* No expand on mobile (decision 5): rows become the 01 §5.4 2-col grid.
     The band goes with the strip and the description it holds — without this
     every mobile row would carry 120rem of empty photo lane. */
  .lx-band,
  .lx-body {
    display: none;
  }
  /* And no bleed: under 768 `.grd` is a flex column, so `.lx-list` is already
     the full width and the desktop percentages would push the rows off it. */
  .lx-row {
    margin-left: 0;
    margin-right: 0;
    padding-left: 0;
    padding-right: 0;
  }
  .lx-rule {
    left: 0;
    width: 100%;
  }
  .lx-eyebrow {
    z-index: auto;
  }
  .lx-head {
    display: grid;
    grid-template-columns: 1fr 1.2fr;
    gap: 4rem 16rem;
    padding: 12rem 28rem 12rem 0;
  }
  .lx-name {
    grid-column: 1;
    grid-row: 1 / 3;
    width: auto;
  }
  .lx-area {
    grid-column: 2;
    grid-row: 1;
    width: auto;
  }
  .lx-price {
    grid-column: 2;
    grid-row: 2;
    width: auto;
  }
  .lx-arrow {
    top: 12rem;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   20. Featured grid (01 §5.6, teaser mode)
   -------------------------------------------------------------------------- */
.ft {
  margin-top: 300rem;
  padding-bottom: 120rem;
  background-color: var(--wh);
  color: var(--bk);
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease);
}
@media (max-width: 767.98px) {
  .ft {
    margin-top: 100rem;
    padding-bottom: 60rem;
  }
}
.ft-h {
  margin-bottom: 60rem;
}
/* 01 §5.6: the count rides the headline at .55em, not the global 14rem `sup`. */
.ft-h sup {
  font-size: 0.55em;
}
.ft-grid {
  align-items: start;
}

/* Editorial collage placement — 01 §5.6, rows 1–6 of the 10-item cycle. */
.ft-item:nth-child(10n + 1) { grid-column: 1 / 8; }
.ft-item:nth-child(10n + 2) { grid-column: 9 / -1; margin-top: 170rem; }
.ft-item:nth-child(10n + 3) { grid-column: 1 / 5; margin-top: 416rem; }
.ft-item:nth-child(10n + 4) { grid-column: 6 / 12; margin-top: 188rem; }
.ft-item:nth-child(10n + 5) { grid-column: 8 / 15; margin-top: -214rem; }
.ft-item:nth-child(10n + 6) { grid-column: 5 / 9; margin-top: 178rem; }

.ft-item {
  display: block;
}
.ft-cell {
  display: block;
  height: 540rem;
  overflow: hidden;
  transition: outline-color 0.3s var(--ease);
  outline: 2px solid transparent;
  outline-offset: -2px;
}
.ft-item:nth-child(10n + 3) .ft-cell,
.ft-item:nth-child(10n + 6) .ft-cell {
  height: 700rem;
}
.ft-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--d6) var(--ease), opacity 0.3s var(--ease);
}
.ft-item:hover .ft-img {
  transform: scale(1.035);
}
/* JS cross-dim: every OTHER card becomes an empty outlined frame (01 §5.6) */
.ft-item.is--b .ft-img {
  opacity: 0;
}
.ft-item.is--b .ft-cell {
  outline-color: var(--gr);
}
/* entrance guard: no pointer events until the item's tween finishes */
.ft-item.is--a {
  pointer-events: none;
}

.ft-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 20rem;
  margin-top: 12rem;
}
.ft-name {
  display: block;
}
.ft-c {
  position: relative;
  display: block;
  overflow: hidden;
  text-align: right;
  white-space: nowrap;
}
.ft-price {
  display: block;
  transition: transform 0.6s var(--ease);
}
.ft-visit {
  position: absolute;
  right: 0;
  top: 0;
  transform: translateY(101%);
  transition: transform 0.6s var(--ease);
}
.ft-item:hover .ft-price {
  transform: translateY(-101%);
}
.ft-item:hover .ft-visit {
  transform: translateY(0);
}
.ft-item:hover .ft-visit::before {
  transform: scaleX(1);
  transform-origin: left center;
  transition-delay: 0.3s;
}

@media (max-width: 767.98px) {
  .ft-item:nth-child(n) {
    grid-column: auto;
    margin-top: 0;
  }
  .ft-item:nth-child(n + 5) {
    display: none;   /* teaser: first 4 only (01 §5.6 mobile) */
  }
  .ft-cell,
  .ft-item:nth-child(10n + 3) .ft-cell,
  .ft-item:nth-child(10n + 6) .ft-cell {
    height: 215rem;
  }
  .ft-grid {
    gap: 40rem;
    /* .grd turns into a flex column here — `start` would shrink each card to
       its meta row's width instead of the full column. */
    align-items: stretch;
  }
}

/* --------------------------------------------------------------------------
   21. Vision windmill + dark takeover (01 §5.7)
   -------------------------------------------------------------------------- */
.vis {
  position: relative;
  height: 200vh;
  background-color: var(--wh);
  color: var(--bk);
  transition: background-color 0.3s var(--ease), color 0.3s var(--ease);
}
.vis-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}
.vis-hd {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.vis-h {
  position: absolute;
  top: 50%;
  left: 50%;
  /* DEVIATION from 01 §5.7 (600rem): our headline "Born on the island." is
     one word longer than kononenko's and re-wraps at 600rem. 680rem keeps
     each written line on one row, which is the point of the crossfade. */
  width: 680rem;
  transform: translate(-50%, -50%);
  line-height: 0.875;
  text-align: center;
}
.vis-ln {
  display: block;
}
/* headline #2 starts below its mask (01 §5.7); home.js crossfades them.
   The mask (`.msk-lh`) shows 0.35em BELOW the line box so descenders survive
   the sub-1 line-height, so the hidden state has to clear that too — same
   PAD_LH compensation core.js applies to its own reveals. */
.vis-h--2 .vis-ln {
  transform: translateY(calc(101% + 0.35em));
}
/* no engine / prefers-reduced-motion: show the final state, drop headline #1 */
body.no-motion .vis-h--2 .vis-ln {
  transform: none;
}
body.no-motion .vis-h--1 {
  display: none;
}
.vis-ring {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 80vh;
  height: 80vh;
  transform: translate(-50%, -50%);
}
.vis-svg {
  width: 100%;
  height: 100%;
}
.vis-blades {
  opacity: 0.35;
}
body.is-dark .vis-blades {
  opacity: 0.5;
}
.vis-blades path {
  fill: none;
  stroke: currentColor;
  stroke-width: 0.94;
}
.vis-nums {
  position: absolute;
  inset: 0;
}
.vis-nums span {
  position: absolute;
  top: 0;
  left: 0;
  font-size: 12rem;
  line-height: 1;
  will-change: transform;
}

/* DARK TAKEOVER — from the windmill down, the page stays dark (01 §5.7).
   The footer is already `.ftr--dark` on home, so the run is continuous. */
body.is-dark .vis,
body.is-dark .ft {
  background-color: var(--bk);
  color: var(--wh);
}

@media (max-width: 767.98px) {
  /* Static reduced variant — no 200vh pin, headline #2 only, no ring. */
  .vis {
    height: auto;
    min-height: 420rem;
  }
  .vis-sticky {
    position: relative;
    height: auto;
    min-height: 420rem;
    display: flex;
    align-items: center;
  }
  .vis-hd {
    position: relative;
    inset: auto;
    width: 100%;
    padding: 60rem 15rem;
  }
  .vis-h {
    position: relative;
    top: auto;
    left: auto;
    width: 100%;
    max-width: 300rem;
    margin: 0 auto;
    transform: none;
  }
  .vis-h--1 {
    display: none;
  }
  .vis-h--2 .vis-ln {
    transform: none;
  }
  .vis-ring {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   22. Not found (01 §10: `SectionNotFound` — big fn-h1 + a link home)
   White page, no hero photo. The header is neither solid nor scroll-aware
   here (core.js: this route is not home/about/listing), so it stays on
   mix-blend-difference over white — i.e. black — for the whole page.
   -------------------------------------------------------------------------- */
.nf {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 220rem 0 180rem;
}
.nf-n {
  line-height: 0.8;
  letter-spacing: -0.04em;
}
.nf-t {
  margin-top: 60rem;
  max-width: 760rem;
}
.nf-a {
  align-self: flex-start;
  /* 13rem of padding top and bottom makes this a ~44px touch target; the
     margin is reduced by the same amount and the underline pinned back to the
     type, so the link sits exactly where 80rem/40rem would put it. */
  padding: 13rem 0;
  margin-top: 67rem;
}
.nf-a::before {
  bottom: 13rem;
}
@media (max-width: 767.98px) {
  .nf {
    padding: 140rem 0 100rem;
  }
  .nf-t {
    margin-top: 32rem;
    max-width: none;
  }
  .nf-a {
    margin-top: 27rem;
  }
}

/* --------------------------------------------------------------------------
   23. Reduced motion
   core.js drops Lenis, the reveals, the parallax and the page transitions
   entirely in this mode (nothing is ever left hidden); these rules only cut
   the CSS-side transitions.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .link::before {
    transition-duration: 0.01ms;
  }
  .btn--css .btn-sp,
  .btn--css .btn-cir,
  .btn--css .btn-l {
    transition-duration: 0.01ms;
  }
}
