/* ============================================================
   swv-kit.css — the design stylesheet, inlined here as the page's only
   <style>. Superset of what this file carried before: 80 selectors added,
   none dropped, so existing engine markup keeps styling as it did.
   Fonts load from assets/fonts/ (local files, not a network request).
   ============================================================ */

/* ============================================================
   WITH GRACE — VILLAGE PORTAL  ·  white-label buyer platform
   Skin: Secret Waterfall Village
   All brand tokens live here; re-skin via :root + PROJECT config.
   ============================================================ */

:root{
  /* secretwaterfallvillage.com — the SWV brand, not With Grace.
     The site is DARK: a deep forest ground under a paper texture, sand type,
     sage as the only accent. Token NAMES are kept so the ~400 var() uses across
     this file remap in one place; their VALUES are now SWV's.            */
  --cream:#1D2415;   /* page ground   — forest (was cream)                */
  --paper:#E6E1D1;   /* type on ground — sand                             */
  --ink:#E6E1D1;     /* foreground / inverse fill — sand (was near-black) */
  --well:#11150C;    /* behind media: map, hero, film, styler stage       */
  --stone:#314736;   /* raised ground — moss (the site's sticky bar)      */
  --midnight:#151A0F;/* deepest ground                                    */
  --terra:#949E86;   /* THE accent — sage. Not terracotta.                */
  --ghost:#9CAD85;   /* lighter sage                                      */
  /* Second accent — a brass yellow, added 2026-08. Sage is the voice of the
     place; --sun is the voice of the MONEY. Use it only where a number or a
     decision needs to be found: the price, the active state, the one line on
     a chart that matters. Never as a large fill, never for body type.
     --sun-ink is the type that sits ON a --sun fill.                   */
  --sun:#D8B24A;     /* brass on the forest ground — ~8.9:1 on #1D2415    */
  --sun-soft:color-mix(in srgb,var(--sun) 22%, transparent);
  --sun-ink:#1D2415; /* type on a --sun fill                              */
  /* Hover is a DIRECTION, not a colour: always away from the ground toward
     maximum contrast. On the forest ground that is white; inside the bone
     scope (which redefines --cream/--paper below) it has to invert to the
     deep forest, or a white hover fill puts sand type on white. Both halves
     are tokens so a single :hover rule is correct on either ground.     */
  --hover:#FFFFFF;   /* hover destination — fill or type, never a colour  */
  --hover-on:#1D2415;/* type sitting ON a --hover fill                    */
  /* Setting text BACK has one floor: 4.5:1 against the ground under it.
     Roughly forty rules used to reach for their own opacity:.6–.75, which
     lands between 3.2:1 and 4.2:1 — fine on a bright desktop, thin and grey
     on a phone in daylight. These two tokens are the only sanctioned way to
     mute type. Use --dim for labels and captions, --dim-soft only for the
     faintest tier; below --dim-soft there is nothing, by design.
     Both are redefined in the bone scope because that ground has far less
     headroom (sand on forest is 12:1; forest on bone only 8.3:1), so the
     same fraction that reads there is illegible here.               */
  --dim:color-mix(in srgb,var(--paper) 74%, transparent);       /* ~7.0:1 */
  --dim-soft:color-mix(in srgb,var(--paper) 58%, transparent);  /* ~4.7:1 */
  /* The boot cover, and the only surface on the site that is not the forest
     ground. It is the near white the design draws it on, so it carries its own
     three values rather than borrowing --cream and --ink: --ink is sand here,
     the inverse fill for a dark ground, and it would disappear.

     The design puts the whole wordmark at opacity 50% and the unfilled half at
     a further 24%. That is folded into these two rather than left as an
     opacity on the element, so each tone is one dial and the cover needs no
     compositing layer. Raise --splash-ink toward 100% if the name should read
     harder than the design draws it.                                      */
  --splash-ground:#F7F8F3;  /* --colors-neutral-50 in the design            */
  --splash-hue:#1A2412;    /* --colors-neutral-950 in the design            */
  --splash-ink:color-mix(in srgb,var(--splash-hue) 50%, transparent);   /* filled   */
  --splash-ghost:color-mix(in srgb,var(--splash-hue) 12%, transparent); /* unfilled */
  --ls:0.22em; --ls-wide:0.34em;   /* the site tracks 3px @13px, 8px @15px */
  --serif:'Cormorant Garamond', Georgia, serif;
  --sans:'Montserrat', system-ui, sans-serif;
  --label:'Lato', system-ui, sans-serif;
  --ease:cubic-bezier(0.83,0,0.17,1);
  --nav-h:64px;

  /* The v1 UI kit, from the Figma Colors and Typography frames Secret
     Waterfall drew first. Read only by rules under .ps-v1, .cv1 and .swv-*,
     so the released theme never sees them. These are this block's own
     defaults: a village whose DESIGN.md sets none of these renders exactly
     this palette, which is why it is Secret Waterfall's own colours and not
     a neutral placeholder. The tenant's DESIGN.md can override each one;
     apps/portal/content/temple-village/DESIGN.md is a village that does, with
     its own generated neutral and accent ramps in place of this one. */
  --swv-neutral-50:#FAFAF5; --swv-neutral-100:#EFEFE9; --swv-neutral-200:#ECEBE2;
  --swv-neutral-300:#D6D5D1; --swv-neutral-400:#A8A69E; --swv-neutral-500:#78756C;
  --swv-neutral-600:#57554E; --swv-neutral-700:#44423C; --swv-neutral-800:#292724;
  --swv-neutral-900:#1C1A17; --swv-neutral-950:#0C0B09;
  --swv-accent-50:#F5F7ED; --swv-accent-100:#E5EED7; --swv-accent-200:#CAE0B5;
  --swv-accent-300:#A9C987; --swv-accent-400:#89B35F; --swv-accent-500:#6A9840;
  --swv-accent-600:#50782F; --swv-accent-700:#3F5E28; --swv-accent-800:#364D24;
  --swv-accent-900:#2F4223; --swv-accent-950:#1A2412;
  --swv-divider:#E5E5E5; --swv-disabled:#CBCCC7;
  --swv-serif:'Cormorant Garamond', Georgia, serif;
  --swv-sans:'Satoshi', system-ui, sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html{scroll-behavior:smooth}
html,body{height:100%}
body{
  font-family:var(--serif); font-weight:400; color:var(--ink);
  background:var(--cream); font-size:19.5px; line-height:1.6;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
img{display:block;max-width:100%}
button{font-family:inherit;cursor:pointer;background:none;border:none;color:inherit}
a{color:inherit;text-decoration:none}
/* The reset above strips the UA focus ring from every button on the page, and
   nothing put one back — keyboard users had no visible focus at all. One base
   rule, in the accent, which is legible on both grounds because --terra is
   redefined inside the bone scope. :focus-visible only, so a mouse click never
   leaves a ring behind. */
:focus-visible{outline:2px solid var(--terra);outline-offset:3px;border-radius:3px}
::selection{background:var(--ink);color:var(--cream)}

/* ---------- type ---------- */
.caps{font-weight:400;text-transform:uppercase;letter-spacing:var(--ls);font-size:12.5px}
h1,h2,h3{font-weight:300;line-height:1.08}
h2,.over,.sec{scroll-margin-top:150px}
@media(max-width:860px){h2,.over,.sec{scroll-margin-top:130px}}
.display{font-size:clamp(44px,7.5vw,104px);letter-spacing:0.01em}
.title{font-size:clamp(32px,4.4vw,58px)}
.lead{font-size:clamp(21px,2.3vw,27px);font-style:italic;font-weight:300;line-height:1.4}
.body-l{font-size:clamp(19px,1.6vw,22px)}
em{font-style:italic}
.dim{opacity:0.74}
.center{text-align:center}

/* ---------- nav ---------- */
.nav{
  position:fixed;inset:0 0 auto 0;height:var(--nav-h);z-index:600;
  display:flex;align-items:center;justify-content:space-between;
  padding:0 28px;transition:background .5s,color .5s, transform .5s;
  color:var(--paper);
}
.nav.on-cream{color:var(--ink)}
.nav.scrolled{background:rgba(245,241,232,.98);color:var(--ink);
  backdrop-filter:blur(12px);border-bottom:1px solid color-mix(in srgb,var(--ink) 10%, transparent)}
/* readability: when the nav's light text sits over imagery (cover, story,
   idea) and hasn't picked up the cream scrolled/on-cream background, lay a
   soft top scrim behind it so the brand + burger never fight the photo. */
.nav::before{content:"";position:absolute;inset:0 0 auto 0;height:calc(var(--nav-h) + 22px);z-index:-1;pointer-events:none;
  background:linear-gradient(to bottom,rgba(10,9,6,.42),transparent);opacity:0;transition:opacity .5s}
.nav:not(.on-cream):not(.scrolled)::before{opacity:1}
.nav .brand{letter-spacing:var(--ls-wide);font-size:15px;font-weight:500;text-transform:uppercase;white-space:nowrap}
.nav .mine-pill{border:1px solid currentColor;border-radius:99px;padding:7px 16px 8px;letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;transition:.3s}
/* .mine-pill hover is declared once, in the gated hover block further down */
.burger{display:none;font-size:24px;line-height:1;padding:8px}
@media(max-width:960px){
  .burger{display:block}
}

/* ---------- views ---------- */
.view{display:none}
.view.active{display:block}

/* ---------- full-bleed image beat ---------- */
.beat{position:relative;min-height:100vh;min-height:100dvh}
.beat .stage{position:sticky;top:0;height:100vh;height:100dvh;overflow:hidden}
.beat .stage img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform-origin:center;will-change:transform}
.scrim{position:absolute;inset:auto 0 0 0;padding:130px 48px 46px;
  background:linear-gradient(to top,rgba(10,9,6,.82),rgba(10,9,6,.40) 55%,transparent);color:var(--paper)}
.scrim .over{letter-spacing:var(--ls-wide);font-size:12.5px;text-transform:uppercase;font-weight:500;opacity:.95;margin-bottom:10px;text-shadow:0 1px 8px rgba(0,0,0,.5)}
.scrim .line{font-size:clamp(25px,3.2vw,41px);font-weight:400;line-height:1.18;max-width:20em;text-shadow:0 1px 14px rgba(0,0,0,.45)}
.scrim .labels{display:flex;flex-wrap:wrap;gap:8px 34px;margin-top:18px}
.scrim .labels span{letter-spacing:var(--ls);font-size:12px;text-transform:uppercase;font-weight:500;opacity:.92;text-shadow:0 1px 8px rgba(0,0,0,.5)}
.scrim .labels b{font-weight:500;opacity:1}
@media(max-width:700px){.scrim{padding:90px 22px 30px}}
/* v26 §17 (M-08) — #story-cue is fixed at bottom:22px and paints over
   the caption (z-index 95 vs 60).  At <=700px the caption's .labels
   row ended at bottom-offset 30px, inside the cue caption's 22-40.4px
   band: measured glyph-on-glyph overlap up to 22.5 x 10.0px on all six
   villa stories.  Lift the caption clear; story captions only. */
@media(max-width:700px){.fstage .scap.scrim{padding-bottom:74px}}

/* ---------- generic section on cream ---------- */
.sec{padding:110px 7vw;max-width:1280px;margin:0 auto}
/* v23 A3 — chapter body gets extra bottom breathing room so terminal
   content (last P&L row, last accordion row, etc.) can always be scrolled
   clear of the fab's resting zone. */
#det-chapter-body{padding-top:52px;padding-bottom:88px}
@media(max-width:860px){#det-chapter-body{padding-top:40px;padding-bottom:120px}}
/* merged details pages: the villa-page divider + the Numbers colour bands */
/* one bottom conversion CTA (email the numbers / book a call) */
[data-ponder]{cursor:help}
.sec .over{letter-spacing:var(--ls-wide);font-size:12.5px;text-transform:uppercase;font-weight:500;color:var(--dim);margin-bottom:16px}
.sec h2{margin-bottom:26px}
.rule{height:1px;background:color-mix(in srgb,var(--ink) 14%, transparent);margin:48px 0}
/* the idea page's "what we are building" list — the site's ✶ bullets, set as a
   terracotta rule-mark rather than a glyph so it stays inside the one family */
.ethos-list{list-style:none;display:flex;flex-direction:column;gap:11px}
.ethos-list li{position:relative;padding-left:26px;font-size:18.5px;line-height:1.5}
.ethos-list li::before{content:"";position:absolute;left:0;top:.62em;width:12px;height:1px;background:var(--terra)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:56px}
@media(max-width:900px){.grid2{grid-template-columns:1fr}}

/* ============================================================
   SELECTOR — the masterplan
   ============================================================ */
#map-wrap{position:fixed;inset:0;overflow:hidden;background:var(--well);touch-action:none}
/* body.on-map stays set for as long as a villa or facility page is open —
   #story is drawn over the masterplan rather than instead of it, see
   showView — so without the second clause this cursor kept arming itself
   under a page that is not the map at all. It went unnoticed only where
   #story happened to cover every pixel; the rule itself was already wrong.
   story-open is set by setStoryVisible, which both overlays go through, so a
   facility page is covered as well as a villa one. */
@media(hover:hover) and (pointer:fine){body.on-map:not(.story-open) #map-wrap{cursor:grab}body.panning #map-wrap{cursor:grabbing}}
#map-cam{position:absolute;left:0;top:0;will-change:transform}
#map-cam img,#map-cam video{position:absolute;left:0;top:0;width:100%;height:100%}
/* v1's turn layers: two far stills and the scrubbed clip, stacked over the
   render already sized by the rule above. All three start invisible — the
   render is the resting view — and engine.js is the only thing that ever
   changes that, per pixel of drag rather than by a transition here. */
#map-turn{position:absolute;left:0;top:0;width:100%;height:100%}
#map-turn-left,#map-turn-right,#map-turn-vid{opacity:0;pointer-events:none}
#hotspots{position:absolute;left:0;top:0;width:100%;height:100%;z-index:10}
/* ---- the chosen home lights up ----------------------------------------
   Two stacked layers inside #map-cam, so they pan and scale with the village.
   --sx/--sy are the home's own image coordinates, set from posFor().
   #spot-shade eases the rest of the village into dusk; #spot-glow lifts a
   warm light out of the chosen house, as though its windows just came on. */
#spot{position:absolute;left:0;top:0;width:100%;height:100%;z-index:9;pointer-events:none;
  opacity:0;transition:opacity .75s var(--ease)}
#spot.on{opacity:1}
#spot-shade,#spot-glow{position:absolute;inset:0;
  transition:background-position .85s var(--ease)}
#spot-shade{background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),
  rgba(10,9,6,0) 0%, rgba(10,9,6,.06) var(--r1,7%), rgba(10,9,6,.40) var(--r2,26%), rgba(10,9,6,.54) 68%)}
#spot-glow{mix-blend-mode:screen;
  background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),
    rgba(255,198,120,.34) 0%, rgba(255,180,98,.18) var(--r1,7%), rgba(255,170,90,0) var(--r2,26%))}
@media(prefers-reduced-motion:reduce){#spot,#spot-shade,#spot-glow{transition:none}}
/* the dot is a zero-size anchor — the point sits EXACTLY on the coordinate,
   the tag floats above it without shifting the geometry */
.dot{position:absolute;width:0;height:0;transition:opacity .5s}
.dot .pt{position:absolute;left:0;top:0;width:17px;height:17px;border-radius:50%;background:var(--paper);
  transform:translate(-50%,-50%);
  box-shadow:0 0 0 7px color-mix(in srgb,var(--paper) 30%, transparent),0 0 22px 4px rgba(232,221,198,.55),0 1px 10px rgba(0,0,0,.5);
  transition:.35s;}
.dot .pt::after{content:"";position:absolute;inset:-13px;border-radius:50%;border:1px solid color-mix(in srgb,var(--paper) 60%, transparent);
  animation:pulse 3.2s infinite}
.dot.lit .tag{opacity:1;transform:translate(calc(-50% + var(--tagdx)),0)}
@keyframes pulse{0%{transform:scale(.6);opacity:.9}70%{transform:scale(1.5);opacity:0}100%{opacity:0}}
.dot .tag{position:absolute;bottom:18px;left:0;--tagdx:0px;transform:translate(calc(-50% + var(--tagdx)),-4px);
  background:color-mix(in srgb,var(--well) 78%, transparent);backdrop-filter:blur(6px);color:var(--paper);
  padding:7px 13px 8px;border-radius:3px;text-align:center;opacity:0;transition:.35s;pointer-events:none;
  /* v26 §15 — was white-space:nowrap, which made the widest tag 483px against a 390px
     viewport (99-character subtitle on one line).  Wrapping + a cap is half the fix;
     clampTags() below is the other half. */
  white-space:normal;width:max-content;max-width:min(268px,calc(100vw - 24px))}
.dot .tag .nm{letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;font-weight:500}
.dot .tag .st{font-size:13.5px;font-style:italic;opacity:.9}
.dot:hover .tag,.dot.picked .tag{opacity:1;transform:translate(calc(-50% + var(--tagdx)),0)}
.dot:hover .pt{transform:translate(-50%,-50%) scale(1.35)}
.dot.off{opacity:.16;pointer-events:none}
/* Getting ready. A flying villa's first clip has to have frames before the
   arrival can show anything, and a press before then scrubs a clip that is
   still arriving. The ring turns while the clip is on its way and the pin
   settles into its normal pulse once it can fly. */
.dot.warming .pt{opacity:.5}
.dot.warming .pt::after{animation:none;opacity:0}
.dot.warming .pt::before{content:"";position:absolute;inset:-9px;border-radius:50%;
  border:2px solid color-mix(in srgb,var(--paper) 26%, transparent);
  border-top-color:var(--paper);
  /* the plots are photographed in daylight, so a paper coloured ring on a pale
     roof reads as nothing without something dark behind its edge */
  filter:drop-shadow(0 0 2px rgba(0,0,0,.75));
  animation:dotready .9s linear infinite}
@keyframes dotready{to{transform:rotate(360deg)}}

.dot.reserved .pt{background:color-mix(in srgb,var(--paper) 40%, transparent);box-shadow:0 0 0 1px var(--paper) inset,0 1px 8px rgba(0,0,0,.45)}
.dot.reserved .pt::after{display:none}
.dot.show-villa .pt{background:var(--terra)}
.dot.amen{cursor:default}
.dot.amen .pt{width:8px;height:8px;background:color-mix(in srgb,var(--paper) 70%, transparent);box-shadow:0 1px 6px rgba(0,0,0,.4)}
.dot.amen .pt::after{display:none}
.dot.amen:hover .pt{transform:translate(-50%,-50%) scale(1.2)}
.dot.editing .pt{outline:2px dashed var(--terra);outline-offset:4px}

/* facility explore CTA */
#fac-cta{position:fixed;left:50%;bottom:150px;transform:translate(-50%,14px);z-index:32;
  background:var(--paper);color:var(--ink);border-radius:99px;padding:13px 28px 14px;
  letter-spacing:var(--ls);font-size:12.5px;text-transform:uppercase;font-weight:500;
  box-shadow:0 14px 44px rgba(0,0,0,.45);opacity:0;pointer-events:none;transition:.45s var(--ease)}
#fac-cta.on{opacity:1;transform:translate(-50%,0);pointer-events:auto}
#fac-cta:hover{background:var(--cream)}
@media(max-width:700px){#fac-cta{bottom:190px}}

/* selector chrome */
#sel-chrome{position:fixed;inset:0;pointer-events:none;z-index:20}
#sel-chrome::before{content:"";position:absolute;inset:0 0 auto 0;height:150px;
  background:linear-gradient(to bottom,rgba(10,9,6,.55),transparent)}
#sel-chrome::after{content:"";position:absolute;inset:auto 0 0 0;height:190px;
  background:linear-gradient(to top,rgba(10,9,6,.6),transparent)}
#sel-head{position:absolute;top:calc(var(--nav-h) + 16px);left:0;right:0;display:flex;flex-direction:column;align-items:center;z-index:2}
#sel-head .hint{color:var(--paper);letter-spacing:var(--ls-wide);font-size:12.5px;text-transform:uppercase;font-weight:500;opacity:.96;text-shadow:0 1px 4px rgba(0,0,0,.85),0 2px 16px rgba(0,0,0,.6)}

.chips{display:flex;gap:10px;pointer-events:none;flex-wrap:wrap;justify-content:center}
.chips .chip{pointer-events:auto}
.chips.sub{max-height:0;opacity:0;overflow:hidden;transition:max-height .5s var(--ease),opacity .45s;padding:0 12px}
.chips.sub.open{max-height:170px;opacity:1}
.chips.sub .chip{font-size:11.5px;padding:8px 18px 9px;font-weight:500}
.gchip{padding:11px 26px 12px;font-size:12.5px}
.gchip.active{background:var(--paper);color:var(--ink);border-color:var(--paper)}
/* facility dots grow to full presence in facilities mode */
.dot.amen.big .pt{width:15px;height:15px;background:var(--paper);
  box-shadow:0 0 0 7px color-mix(in srgb,var(--paper) 30%, transparent),0 0 22px 4px rgba(232,221,198,.55),0 1px 10px rgba(0,0,0,.5)}
.dot.amen.big .pt::after{content:"";position:absolute;inset:-12px;border-radius:50%;
  border:1px solid color-mix(in srgb,var(--paper) 60%, transparent);animation:pulse 3.2s infinite;display:block}
.dot.amen.big:hover .pt{transform:translate(-50%,-50%) scale(1.3)}
.chip{border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);color:var(--paper);border-radius:99px;
  padding:10px 22px 11px;letter-spacing:var(--ls);font-size:12.5px;text-transform:uppercase;font-weight:500;
  background:color-mix(in srgb,var(--well) 42%, transparent);backdrop-filter:blur(8px);transition:.3s}
.chip:hover{border-color:var(--paper)}
.chip.active{background:var(--paper);color:var(--ink);border-color:var(--paper)}
#sel-legend{display:flex;gap:30px;justify-content:center;color:var(--paper);pointer-events:none;flex-wrap:wrap}
#sel-legend span{display:flex;align-items:center;gap:9px;letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;font-weight:500;opacity:.95;text-shadow:0 1px 4px rgba(0,0,0,.85),0 2px 12px rgba(0,0,0,.6)}
#sel-legend i{width:10px;height:10px;border-radius:50%;background:var(--paper)}
#sel-legend i.r{background:color-mix(in srgb,var(--paper) 40%, transparent);box-shadow:0 0 0 1px var(--paper) inset}
#sel-legend i.s{background:var(--terra)}
@media(max-width:700px){
  #sel-legend{display:none}

}

/* ============================================================
   MASTERPLAN — mobile touch pass
   1) real 44px tap targets   2) Grace's bubble clears the controls
   3) less chrome             4) available vs reserved you can scan
   ============================================================ */
@media(pointer:coarse){
  /* 1 — the visible dot stays small; the touch target does not */
  .dot .pt::before{content:"";position:absolute;left:50%;top:50%;width:46px;height:46px;
    transform:translate(-50%,-50%);border-radius:50%}
  /* 4 — status you can read at a glance: available reads first, reserved recedes */
  .dot.lit .pt, .dot:not(.off):not(.reserved):not(.amen) .pt{width:21px;height:21px}
  .dot.reserved .pt{width:13px;height:13px;opacity:.5;
    box-shadow:0 0 0 1px color-mix(in srgb,var(--paper) 70%, transparent) inset,0 1px 6px rgba(0,0,0,.4)}
  .dot.show-villa .pt{width:21px;height:21px}
  /* the lit villas name themselves rather than making you tap blind */
  .dot.lit .tag{opacity:1;transform:translate(calc(-50% + var(--tagdx)),0)}
  .dot.lit .tag .st{display:none}
  .dot.lit .tag{padding:5px 10px 6px;background:color-mix(in srgb,var(--well) 72%, transparent)}
  .dot.lit .tag .nm{font-size:10.5px}
  /* 3 — one row of controls, not two: the bedroom filters go behind Villas */
  #chips-sub.open{max-height:120px}

  #sel-head .hint{font-size:11.5px;padding:0 16px;text-align:center}
}
/* 2 — Grace's bubble sat on top of the chips it was pointing at */
body.on-map #fab-bubble{bottom:calc(18px + var(--map-chrome,150px))}
@media(max-width:860px){body.on-map #fab-bubble{bottom:calc(14px + var(--map-chrome,150px))}}

/* the focused pin earns a ring so the link to its card is obvious */
html[data-alt="1"] .dot.picked .pt{box-shadow:0 0 0 4px var(--terra),0 0 0 9px color-mix(in srgb,var(--terra) 34%, transparent),0 2px 14px rgba(0,0,0,.6)}
/* the card already names the home — pin labels would say it twice */
html[data-alt="1"] .dot .tag{display:none}
/* the rail carries its own instruction, and Grace's map bubble points at
   chips this mode doesn't have */
html[data-alt="1"] body.on-map #sel-head{display:none}
html[data-alt="1"] body.on-map #fab-bubble{display:none}

/* soft "turn your phone" nudge — dims the map and suggests landscape,
   auto-clears on rotation, never blocks (see maybeShowNudge()) */
#rotate-nudge{position:fixed;inset:0;z-index:60;display:none;flex-direction:column;align-items:center;justify-content:center;
  background:color-mix(in srgb,var(--well) 74%, transparent);backdrop-filter:blur(3px);color:var(--paper);text-align:center;
  padding:0 34px;opacity:0;transition:opacity .5s}
#rotate-nudge.on{display:flex;opacity:1}
.rn-phone{width:52px;height:92px;border:2px solid var(--paper);border-radius:11px;position:relative;margin-bottom:32px;
  transform-origin:50% 50%;animation:rn-tilt 2.8s var(--ease) infinite}
.rn-phone .rn-scr{position:absolute;inset:6px;border-radius:4px;background:color-mix(in srgb,var(--paper) 22%, transparent)}
@keyframes rn-tilt{0%,12%{transform:rotate(0)}40%,62%{transform:rotate(-90deg)}90%,100%{transform:rotate(0)}}
.rn-t{letter-spacing:var(--ls-wide);font-size:13px;text-transform:uppercase;font-weight:500}
.rn-s{font-style:italic;opacity:.82;margin-top:11px;font-size:16px;max-width:18em}
.rn-x{margin-top:34px;border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:99px;
  padding:11px 24px 12px;letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;color:var(--paper);transition:.3s}
.rn-x:hover{background:var(--paper);color:var(--ink)}
@media(prefers-reduced-motion:reduce){.rn-phone{animation:none;transform:rotate(-90deg)}}

/* landscape phone — height is the scarce axis: fold the bottom chip stack
   into a slim left rail, drop the walk arrows to the bottom-centre, and
   keep the legend out. Portrait keeps its tidy bottom bar (above). */
@media (orientation:landscape) and (max-height:520px) and (pointer:coarse){
  body.on-map #sel-chrome::after{height:120px}

  body.on-map #sel-legend{display:none}
  body.on-map #sel-head{top:calc(var(--nav-h) + 6px)}
  body.on-map #fac-cta{bottom:74px}
  /* villa story: the scroll counter/cue share the bottom with the caption's
     label row on a short landscape stage — tuck them into the right corner,
     clear of the left-aligned caption. (Only one shows at a time.) */
  body #story-count{left:auto;right:18px;bottom:14px;transform:none}
  body #story-cue{left:auto;right:26px;bottom:14px;transform:none}
}

/* editor */
/* the bar was --terra under --paper: sage behind sand, about 1.6:1. It is the
   one piece of chrome whose whole job is to be read while you work. */
#editor-bar{position:fixed;bottom:0;left:0;right:0;z-index:700;background:#11150C;color:#E6E1D1;
  border-top:1px solid rgba(230,225,209,.22);
  display:none;align-items:center;gap:18px;justify-content:center;padding:13px;flex-wrap:wrap}
#editor-bar.on{display:flex}
#editor-bar .caps{font-size:11px}
#editor-bar button{border:1px solid rgba(230,225,209,.55);border-radius:3px;padding:8px 15px;
  color:#E6E1D1;letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;cursor:pointer;
  background:transparent;transition:.2s}
#editor-bar .ed-btns{display:flex;gap:9px;flex:none}
#editor-bar #ed-view{font-family:var(--serif);font-style:italic;text-transform:none;
  letter-spacing:0;font-size:14px;margin-left:12px;color:#D8B24A}
@media(hover:hover){#editor-bar button:hover{background:#E6E1D1;color:#11150C;border-color:#E6E1D1}}

/* ==========================================================================
   EDIT MODE — every pin reachable, named, and draggable.
   The design pass made reserved and sold pins pointer-events:none, faded the
   out-of-group ones to 16%, hid every label and parked a 420px rail over the
   left third of the map. All four are correct for a buyer and all four make
   the editor unusable, so edit mode undoes them — scoped to body.editing so
   none of it can leak into the real page.
   ========================================================================== */
html[data-alt="1"] body.editing .dot,
html[data-alt="1"] body.editing .dot .pt{pointer-events:auto}
html[data-alt="1"] body.editing .dot{opacity:1 !important;cursor:grab}
html[data-alt="1"] body.editing .dot.editing:active{cursor:grabbing}
html[data-alt="1"] body.editing .dot .pt{width:15px;height:15px;
  background:#D8B24A;box-shadow:0 0 0 3px rgba(17,21,12,.75),0 0 0 4px rgba(216,178,74,.5)}
html[data-alt="1"] body.editing .dot.amen .pt{background:#9BD1E8}
html[data-alt="1"] body.editing .dot .pt::after{display:none}     /* no pulse while aiming */
/* every pin says what it is, all the time — you cannot place what you cannot name */
html[data-alt="1"] body.editing .dot .tag{display:block;opacity:1;pointer-events:none;
  transform:translate(calc(-50% + var(--tagdx,0px)),0)}
html[data-alt="1"] body.editing .dot .tag .nm{font-family:var(--sans);font-size:9.5px;
  letter-spacing:var(--ls);text-transform:uppercase}
/* the rail and the reopen tab are 420px of map you cannot reach */
html[data-alt="1"] body.editing #c-fab{display:none}
body.editing #map-wrap{bottom:0}

/* ============================================================
   VILLA STORY overlay
   ============================================================ */
#story{position:relative;z-index:80;background:transparent}
#story .exit{position:fixed;top:calc(var(--nav-h) + 10px);left:26px;z-index:90;color:var(--paper);
  display:flex;gap:10px;align-items:center;letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;font-weight:400;
  background:color-mix(in srgb,var(--well) 45%, transparent);backdrop-filter:blur(8px);border-radius:99px;padding:9px 18px 10px;transition:.3s}
#story .exit:hover{background:var(--ink)}
.story-end{background:var(--cream);position:relative}

#story .fstage::before{content:"";position:absolute;inset:0 0 auto 0;height:130px;z-index:65;pointer-events:none;
  background:linear-gradient(to bottom,rgba(10,9,6,.5),transparent)}

/* scroll affordance inside a villa story */
#story-cue{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);z-index:95;color:var(--paper);
  display:flex;flex-direction:column;align-items:center;gap:11px;pointer-events:none;transition:opacity .6s;
  text-shadow:0 1px 12px rgba(0,0,0,.6)}
#story-cue .caps{font-size:11.5px;letter-spacing:var(--ls-wide);opacity:1;font-weight:500;text-shadow:0 1px 4px rgba(0,0,0,.8)}
.cue-line{width:1px;height:46px;background:color-mix(in srgb,var(--paper) 38%, transparent);position:relative;overflow:visible}
.cue-line i{position:absolute;left:50%;top:0;width:7px;height:7px;margin-left:-3.5px;border-radius:50%;
  background:var(--paper);box-shadow:0 0 10px rgba(232,221,198,.8);animation:cuedrop 2.3s infinite cubic-bezier(.45,0,.55,1)}
@keyframes cuedrop{0%{transform:translateY(-4px);opacity:0}18%{opacity:1}72%{opacity:1}100%{transform:translateY(44px);opacity:0}}
@media(prefers-reduced-motion:reduce){.cue-line i{animation:none;top:auto;bottom:0}}
#story-count{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:95;color:var(--paper);
  background:color-mix(in srgb,var(--well) 45%, transparent);backdrop-filter:blur(8px);border-radius:99px;
  padding:8px 18px 9px;font-size:11.5px;letter-spacing:var(--ls-wide);text-transform:uppercase;font-weight:500;
  pointer-events:none;transition:opacity .5s;white-space:nowrap}

/* facts sheet */
.facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:0;border-top:1px solid rgba(230,225,209,.16);border-bottom:1px solid rgba(230,225,209,.16)}
.facts>div{padding:16px 20px 17px}
/* villa spec — one tight row, no wrapping to a lonely second line */
.facts.specrow{grid-template-columns:repeat(6,1fr)}
.facts.specrow>div{padding:13px 14px 14px}
.facts.specrow .k{min-height:0;margin-bottom:5px;font-size:10.5px}
.facts.specrow .v{font-size:clamp(15px,1.35vw,19px);line-height:1.25}
.facts.specrow .v small{font-size:65%}
@media(max-width:900px){.facts.specrow{grid-template-columns:repeat(3,1fr)}}
@media(max-width:480px){.facts.specrow{grid-template-columns:repeat(2,1fr)}}
.facts .k{letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;font-weight:500;color:var(--dim);margin-bottom:6px;min-height:2.4em;line-height:1.2}
.facts .v{font-size:clamp(20px,1.9vw,26px)}
.facts .v small{font-size:70%;color:var(--dim)}
.facts .sub{margin-top:3px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent)}

/* #/story's "facts" component — a plain two column definition list, on the
   same hairline treatment as .rule/.secrule (color-mix against --paper,
   since .sec text sits on the dark ground and is --paper coloured). No
   existing list shape fit a document with just {title, items:[{k,v}]}, so
   this is new rather than reusing .facts above, which is bound to a taller
   villa-spec/ROI shape (.k/.v with min-height, small-caps sub line, specrow
   variant) that this component's props do not carry. */
.story-facts{display:grid;grid-template-columns:1fr 1fr;gap:0;margin:20px 0 0;
  border-top:1px solid color-mix(in srgb,var(--paper) 18%, transparent)}
.story-facts>div{padding:16px 20px 17px;
  border-bottom:1px solid color-mix(in srgb,var(--paper) 18%, transparent)}
.story-facts dt{margin:0 0 6px;letter-spacing:var(--ls);font-size:11.5px;
  text-transform:uppercase;font-weight:500;color:var(--dim);font-family:var(--serif)}
.story-facts dd{margin:0;font-size:clamp(18px,1.6vw,22px)}
@media(max-width:640px){.story-facts{grid-template-columns:1fr}}

/* milestones */
.miles{display:flex;gap:0;margin-top:26px;border-left:1px solid rgba(230,225,209,.2)}
.miles>div{flex:1;padding:0 18px 6px;border-right:1px solid rgba(230,225,209,.2)}
.miles .pct{font-size:clamp(26px,3vw,40px)}
.miles .k{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500;color:var(--dim);margin-top:2px}
@media(max-width:820px){.miles{flex-direction:column;border-left:none;border-top:1px solid rgba(230,225,209,.2)}
 .miles>div{border-right:none;border-bottom:1px solid rgba(230,225,209,.2);padding:14px 4px}}

/* CTA row */
.cta-row{display:flex;gap:16px;flex-wrap:wrap;margin-top:44px}
.btn{display:inline-flex;align-items:center;gap:10px;border:1px solid var(--ink);border-radius:99px;
  padding:14px 30px 15px;letter-spacing:var(--ls);font-size:12.5px;text-transform:uppercase;font-weight:400;transition:.3s}
/* NOTE — every .btn :hover declaration lives in ONE place now, the
   @media(hover:hover) block in the SWV section further down. The four that
   used to sit here were dead: the later block overrode all of them, and
   leaving them behind meant a coarse-pointer device (where the later block
   is gated off) fell back to a *different* hover skin. */
.btn.solid{background:var(--ink);color:var(--cream)}
/* terra/ghost pair — save-form "Save & send" + /talk's primary CTA (terra),
   paired with their neighboring secondary action (ghost) — scoped to these
   two spots only (design-returns-v3 §7); .btn/.btn.solid elsewhere untouched. */
.btn.terra{background:var(--terra);border-color:var(--terra);color:var(--cream);padding:16px 30px;letter-spacing:var(--ls);font-size:12px}
.btn.ghost{padding:16px 30px;letter-spacing:var(--ls);font-size:12px}

/* pack cards — unreferenced since the packs were removed; kept only
   because .packs/.pack still style nothing and cost nothing */
.packs{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;align-items:stretch}
@media(max-width:960px){.packs{grid-template-columns:1fr}}
.pack{border-radius:4px;padding:38px 34px 34px;color:var(--paper);display:flex;flex-direction:column;min-height:420px;position:relative;overflow:hidden}
.pack.stone{background:var(--stone)}.pack.midnight{background:var(--midnight)}.pack.terra{background:var(--terra)}
.pack>*{position:relative}
.pack .nm{font-size:clamp(26px,2.4vw,34px);margin:6px 0 2px}
.pack .pr{font-style:italic;opacity:.85;margin-bottom:20px}
.pack ul{list-style:none;display:flex;flex-direction:column;gap:9px;margin-bottom:26px}
.pack li{padding-left:20px;position:relative;font-size:18px;line-height:1.45}
.pack li::before{content:"—";position:absolute;left:0;opacity:.6}
.pack .pick{margin-top:auto;border:1px solid var(--paper);border-radius:99px;padding:11px 24px 12px;
  letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;color:var(--paper);align-self:flex-start;transition:.3s}
.pack .pick:hover,.pack .pick.picked{background:var(--paper);color:var(--ink)}

/* materials */
.mats{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:26px}
.mat .sw{height:150px;border-radius:3px;margin-bottom:14px;position:relative;overflow:hidden}
.mat .sw::after{content:"";position:absolute;inset:0;background:
  radial-gradient(120% 90% at 20% 0%,rgba(255,255,255,.14),transparent 55%),
  radial-gradient(130% 100% at 85% 100%,rgba(0,0,0,.18),transparent 60%)}
.mat .nm{font-size:21px}
.mat .wh{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500;color:var(--dim);margin:3px 0 6px}
.mat .no{font-size:17px;opacity:.88;line-height:1.5}

/* inclusions */
.incl{columns:2;column-gap:64px}
@media(max-width:900px){.incl{columns:1}}
.incl .cat{break-inside:avoid;margin-bottom:38px}
.incl .cat h3{font-size:24px;margin-bottom:12px}
.incl ul{list-style:none}
.incl li{padding:9px 0 9px 24px;border-bottom:1px solid rgba(230,225,209,.12);position:relative;font-size:18px}
.incl li::before{content:"—";position:absolute;left:0;opacity:.5}

/* facilities sheet (full directory over the masterplan) */
#fac-sheet{position:fixed;inset:0;z-index:900;background:color-mix(in srgb,var(--well) 45%, transparent);
  opacity:0;pointer-events:none;transition:opacity .45s;display:flex;justify-content:center;align-items:flex-end}
#fac-sheet.open{opacity:1;pointer-events:auto}
#fac-sheet .fs-inner{background:var(--cream);color:var(--ink);width:min(1060px,100vw);max-height:88vh;overflow-y:auto;
  border-radius:6px 6px 0 0;padding:56px 6vw 48px;position:relative;transform:translateY(30px);transition:transform .5s var(--ease)}
#fac-sheet.open .fs-inner{transform:none}
#fac-sheet .fs-x{position:absolute;top:18px;right:22px;font-size:17px;opacity:.6;padding:8px}
#fac-sheet .fs-x:hover{opacity:1}
#fac-sheet .fs-imgs{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:44px}
#fac-sheet .fs-imgs img{border-radius:3px;aspect-ratio:4/3;object-fit:cover;width:100%}
#fac-sheet .fs-imgs figcaption{margin-top:8px;opacity:.55;font-size:10px}
#fac-sheet .cta-row{margin-top:40px}
@media(max-width:800px){#fac-sheet .fs-imgs{grid-template-columns:1fr}}

/* the idea — founders' film */
.film-wrap{padding:40px 7vw 0;max-width:1280px;margin:0 auto}
.film{position:relative;border-radius:5px;overflow:hidden;aspect-ratio:16/8.2;min-height:340px;background:var(--well)}
.film img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.85}
.film-veil{position:absolute;inset:0;background:radial-gradient(80% 80% at 50% 55%,rgba(10,9,6,.62),rgba(10,9,6,.28))}
.film-mid{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;color:var(--paper);text-align:center;padding:0 24px}
.film-mid .lead{max-width:26em;opacity:.92}
.play{width:84px;height:84px;border-radius:50%;border:1px solid color-mix(in srgb,var(--paper) 70%, transparent);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);transition:.35s}
.play i{width:0;height:0;border-left:18px solid var(--paper);border-top:11px solid transparent;border-bottom:11px solid transparent;margin-left:5px}
.film.ready{cursor:pointer}
.film.ready:hover .play{transform:scale(1.07);background:color-mix(in srgb,var(--paper) 14%, transparent)}
.film.waiting .play{opacity:.55}
.film.waiting .play i{opacity:.7}
.film.waiting::after{content:"IN PRODUCTION";position:absolute;top:18px;right:20px;color:var(--paper);
  letter-spacing:var(--ls-wide);font-size:10px;border:1px solid color-mix(in srgb,var(--paper) 50%, transparent);
  border-radius:99px;padding:6px 13px 7px;opacity:.85}
@media(max-width:700px){.film{aspect-ratio:4/5}}

/* facilities directory */
.facl{display:grid;grid-template-columns:1fr 1fr;gap:0 64px}
@media(max-width:900px){.facl{grid-template-columns:1fr}}
.fac{padding:20px 0 21px;border-bottom:1px solid rgba(230,225,209,.12)}
.fac .fn{font-size:22px;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap}
.fac .fs{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:400;opacity:.55;border:1px solid rgba(230,225,209,.3);border-radius:99px;padding:3px 10px 4px}
.fac .fl{font-size:17.5px;opacity:.85;margin-top:4px;max-width:34em}

/* journey */
.journey{display:flex;flex-direction:column;gap:0;border-left:1px solid rgba(230,225,209,.22);margin-left:6px}
.jstep{padding:0 0 46px 40px;position:relative}
.jstep::before{content:"";position:absolute;left:-6px;top:8px;width:11px;height:11px;border-radius:50%;background:var(--ink)}
.jstep .k{letter-spacing:var(--ls-wide);font-size:11.5px;text-transform:uppercase;font-weight:500;color:var(--dim)}
.jstep h3{font-size:clamp(24px,2.4vw,32px);margin:4px 0 8px}
.jstep p{max-width:34em}
.jstep .pay{font-style:italic;opacity:.75;margin-top:8px}

/* ============================================================
   CONCIERGE
   ============================================================ */
/* the note Grace leaves by her icon — persistent, tappable (opens the chat) */
.fab-bubble{position:fixed;right:26px;bottom:96px;z-index:805;max-width:250px;
  display:flex;align-items:flex-start;gap:6px;
  background:var(--paper);color:var(--ink);font-family:var(--serif);font-size:15.5px;
  font-style:italic;line-height:1.42;padding:11px 12px 12px 16px;border-radius:14px 14px 5px 14px;
  box-shadow:0 14px 38px rgba(0,0,0,.34);opacity:0;transform:translateY(10px) scale(.96);
  transform-origin:bottom right;transition:opacity .42s var(--ease),transform .42s var(--ease),box-shadow .3s;
  pointer-events:none;cursor:pointer}
.fab-bubble.show{opacity:1;transform:none;pointer-events:auto}
.fab-bubble:hover{box-shadow:0 18px 48px rgba(0,0,0,.44)}
.fab-bubble-t{flex:1}
/* brand: one family, ever. The glyph is U+00D7, which Cormorant carries — the
   old system-ui declaration pushed the only sans face on the page. font-style
   still resets, because the bubble around it is italic. */
.fab-bubble-x{flex:none;font-style:normal;font-size:17px;line-height:1;
  opacity:.55;padding:0 2px;margin-top:1px;color:var(--ink);transition:opacity .2s}
.fab-bubble-x:hover{opacity:.8}
@media(max-width:600px){.fab-bubble{right:14px;bottom:80px;max-width:220px}}
/* the Villas / Facilities toggle pulses when Grace invites exploration */
@keyframes gblink{0%,100%{box-shadow:0 0 0 1px color-mix(in srgb,var(--paper) 28%, transparent)}
  50%{box-shadow:0 0 0 1px var(--paper),0 0 15px 2px color-mix(in srgb,var(--paper) 45%, transparent)}}
#chips-main.blink .gchip{animation:gblink 1.6s var(--ease) infinite}
#c-panel{position:fixed;right:22px;bottom:22px;z-index:810;width:min(430px,calc(100vw - 32px));
  height:min(640px,calc(100vh - 44px));height:min(640px,calc(100dvh - 44px));background:var(--midnight);color:var(--paper);border-radius:6px;
  display:flex;flex-direction:column;overflow:hidden;box-shadow:0 30px 90px rgba(0,0,0,.5);
  transform:translateY(24px);opacity:0;pointer-events:none;transition:.45s var(--ease)}
#c-panel.open{transform:none;opacity:1;pointer-events:auto}
/* When Grace is open on a wide screen her panel (fixed, bottom-right, ~452px)
   sits over the right of the reading column. The full-bleed chart band uses
   100vw and must not be reparented, so instead of shifting .sec we rein in the
   boxed figure blocks that carry right-aligned numbers — the P&L, the tiles,
   the sliders — so nothing they show slides under the chat. */
@media(min-width:1024px){
  body.chat-open .pgtiles{grid-template-columns:1fr 1fr;max-width:600px}
  body.chat-open .rt-hero{max-width:620px}
}
#c-grain{position:absolute;inset:0;opacity:.75;pointer-events:none;background-size:cover}
#c-panel>*{position:relative}
#c-head{display:flex;align-items:center;justify-content:space-between;padding:20px 22px 16px;border-bottom:1px solid color-mix(in srgb,var(--paper) 18%, transparent)}
#c-head .t .caps{font-size:11px;opacity:.75}
#c-head .t .nm{font-size:21px}
#c-head .ops{display:flex;gap:6px}
#c-head button{opacity:.7;padding:6px;font-size:15px;transition:.3s}
#c-head button:hover{opacity:1}
#c-msgs{flex:1;overflow-y:auto;padding:20px 22px;display:flex;flex-direction:column;gap:14px;scrollbar-width:thin}
/* Cormorant is a light display serif; on the dark panel it reads thin, so the
   chat body is set a touch larger, at full weight, with a little tracking and a
   hairline same-colour shadow to firm up the strokes on dark. */
.msg{max-width:88%;line-height:1.62;font-size:19px;font-weight:400;letter-spacing:.006em}
.msg.you{align-self:flex-end;background:color-mix(in srgb,var(--paper) 14%, transparent);padding:11px 16px 12px;border-radius:4px}
.msg.ag{align-self:flex-start;text-shadow:0 0 .4px color-mix(in srgb,var(--paper) 50%, transparent)}
.msg.ag .who{letter-spacing:var(--ls);font-size:10px;text-transform:uppercase;font-weight:400;opacity:.62;display:block;margin-bottom:4px}
.msg.ag a{border-bottom:1px solid color-mix(in srgb,var(--paper) 45%, transparent)}
/* block, not inline-block: as an inline box it sat on the last line of the
   answer whenever that line had room, so whether the button shared a line
   with "...email at adrian@wgrace.co." depended on where the text happened to
   wrap. width:fit-content keeps the pill hugging its label rather than
   stretching across the panel. */
.msg .act{display:block;width:fit-content;margin-top:8px;border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:99px;
  padding:6px 14px 7px;letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;transition:.3s;cursor:pointer}
.msg .act:hover{background:var(--paper);color:var(--midnight)}
.msg .gline{font-style:italic;font-size:14.5px;opacity:.72;margin-top:8px}
/* guided qualification: stacked quick-reply options + inline lead capture */

.c-capture{display:flex;flex-direction:column;gap:9px;margin-top:10px;align-self:stretch}
.c-capture input{background:color-mix(in srgb,var(--paper) 10%, transparent);border:1px solid color-mix(in srgb,var(--paper) 25%, transparent);
  border-radius:4px;color:var(--paper);font-family:inherit;font-size:16px;padding:11px 13px;outline:none}
.c-capture input::placeholder{color:color-mix(in srgb,var(--paper) 55%, transparent);font-style:italic}
.c-capture button{border:1px solid var(--paper);border-radius:99px;padding:9px 18px 10px;letter-spacing:var(--ls);
  font-size:11px;text-transform:uppercase;color:var(--paper);align-self:flex-start;transition:.3s}
.c-capture button:hover{background:var(--paper);color:var(--midnight)}
.c-capture button[disabled]{opacity:.55}
.c-capture .ok{font-style:italic;opacity:.85;font-size:14.5px}
/* Grace shares an image in-chat — tap to open the fullscreen lightbox */
.c-img{display:block;width:100%;margin-top:8px;border-radius:6px;overflow:hidden;
  border:1px solid color-mix(in srgb,var(--paper) 20%, transparent);text-align:left;transition:.25s;background:none}
.c-img:hover{border-color:color-mix(in srgb,var(--paper) 55%, transparent)}
.c-img img{width:100%;display:block}
.c-img .c-img-cap{display:block;padding:8px 12px 9px;font-size:10px;opacity:.7;letter-spacing:var(--ls);text-transform:uppercase}
#c-panel.half{height:min(46vh,420px)}
#c-panel.half #c-chips,#c-panel.half #c-note{display:none}
#c-head{cursor:pointer}
.typing{display:flex;gap:5px;padding:6px 0}
.typing i{width:6px;height:6px;border-radius:50%;background:var(--paper);opacity:.4;animation:ty 1.2s infinite}
.typing i:nth-child(2){animation-delay:.18s}.typing i:nth-child(3){animation-delay:.36s}
@keyframes ty{0%,100%{opacity:.25;transform:none}50%{opacity:1;transform:translateY(-3px)}}
#c-chips{display:flex;gap:8px;padding:0 22px 12px;flex-wrap:wrap}
#c-chips button{border:1px solid color-mix(in srgb,var(--paper) 40%, transparent);border-radius:99px;
  padding:7px 14px 8px;font-size:13.5px;font-style:italic;color:var(--paper);opacity:.9;transition:.3s}
#c-chips button:hover{background:color-mix(in srgb,var(--paper) 14%, transparent)}
#c-inrow{display:flex;gap:10px;padding:14px 18px 18px;border-top:1px solid color-mix(in srgb,var(--paper) 18%, transparent)}
#c-in{flex:1;background:color-mix(in srgb,var(--paper) 10%, transparent);border:1px solid color-mix(in srgb,var(--paper) 25%, transparent);
  border-radius:3px;color:var(--paper);font-family:inherit;font-size:16.5px;padding:12px 14px;outline:none}
#c-in::placeholder{color:color-mix(in srgb,var(--paper) 55%, transparent);font-style:italic}
#c-send{border:1px solid var(--paper);border-radius:99px;padding:0 22px;letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;color:var(--paper);transition:.3s}
#c-send:hover{background:var(--paper);color:var(--midnight)}
#c-note{padding:0 22px 14px;font-size:13.5px;font-style:italic;opacity:.82}
@media(max-width:600px){#c-panel{right:8px;bottom:8px;width:calc(100vw - 16px);height:calc(100vh - 70px);height:calc(100dvh - 70px)}}

/* v23 A2 — the fab steps out of the way while the reader is actively
   scrolling and returns 900ms after they stop (see the scroll listener near
   the end of the script) — never while the chat panel (#c-panel.open) is
   open, since that already hides the fab outright via openChat(). */
#c-fab{transition:transform .28s ease, opacity .28s ease}
#c-fab.c-fab-away{transform:translateY(96px);opacity:0;pointer-events:none}
@media (prefers-reduced-motion:reduce){
  #c-fab{transition:opacity .01s linear}
  #c-fab.c-fab-away{transform:none;opacity:0}
}

/* proactive Grace bubble — rises from the fab with a context offer, once per
   context per session, replacing the scattered "Ask Grace" buttons. */

/* ============================================================
   MY VILLA
   ============================================================ */
.mine-hero{position:relative;min-height:72vh;min-height:72dvh;display:flex;align-items:flex-end;overflow:hidden}
.mine-hero img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.mine-hero .scrim{position:absolute}
.steps{display:flex;flex-direction:column}
.step{display:flex;gap:22px;padding:20px 0;border-bottom:1px solid rgba(230,225,209,.12);align-items:baseline}
.step .n{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;opacity:.5;min-width:70px}
.step .t{font-size:20px}
.step .s{margin-left:auto;font-style:italic;opacity:.7;font-size:16px;white-space:nowrap}
.step.done .t{opacity:.5;text-decoration:line-through;text-decoration-thickness:.5px}

/* villa details — gallery + lightbox */
#lightbox{position:fixed;inset:0;z-index:950;background:color-mix(in srgb,var(--well) 94%, transparent);
  display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .35s}
#lightbox.open{opacity:1;pointer-events:auto}
#lightbox .lb-stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;touch-action:none}
#lightbox img{max-width:92vw;max-height:84vh;object-fit:contain;box-shadow:0 30px 90px rgba(0,0,0,.6);
  will-change:transform;cursor:zoom-in;user-select:none;-webkit-user-drag:none}
#lightbox.zoomed img{cursor:grab}
#lightbox.grabbing img{cursor:grabbing}
#lightbox .lb-hint{position:absolute;top:18px;left:0;right:0;text-align:center;color:var(--paper);font-size:10px;letter-spacing:var(--ls-wide);opacity:.5;pointer-events:none;transition:opacity .3s}
#lightbox.zoomed .lb-hint{opacity:0}
#lightbox .lb-cap{position:absolute;bottom:26px;left:0;right:0;text-align:center;color:var(--paper);font-size:11px;opacity:.85;pointer-events:none}
#lightbox button{position:absolute;color:var(--paper);font-size:26px;padding:14px;opacity:.75;transition:.3s;z-index:2}
#lightbox button:hover{opacity:1}
#lightbox .lb-x{top:16px;right:20px;font-size:20px}
#lightbox .lb-p{left:14px;top:50%;transform:translateY(-50%)}
#lightbox .lb-n{right:14px;top:50%;transform:translateY(-50%)}
.plans-grid svg{background:var(--cream)}

/* villa details */
/* clean text header for the individual study chapters (no image) */
.dhead{background:var(--cream);color:var(--ink);padding:calc(var(--nav-h) + 30px) 7vw 22px}
.dhead .over{letter-spacing:var(--ls-wide);font-size:12px;text-transform:uppercase;font-weight:500;color:var(--terra);margin-bottom:10px}
.dhead .dhead-line{font-family:var(--serif);font-size:clamp(27px,4vw,40px);line-height:1.1;margin-bottom:18px}
.dhead .labels{display:flex;flex-wrap:wrap;gap:8px 30px}
.dhead .labels span{letter-spacing:var(--ls);font-size:12px;text-transform:uppercase;font-weight:500;opacity:.82}
.dhead .labels b{font-weight:600;opacity:1}
.dhero{position:relative;height:54vh;height:54dvh;min-height:340px;overflow:hidden}
.dhero img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.plans-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:30px}
.plans-grid figure{background:color-mix(in srgb,var(--ink) 3%, var(--cream));border:1px solid rgba(230,225,209,.12);
  border-radius:4px;padding:26px 26px 20px;display:flex;flex-direction:column;gap:16px}
.plans-grid img{width:100%;max-height:56vh;object-fit:contain}
.plans-grid figcaption{font-size:16px;font-style:italic;opacity:.75;line-height:1.5}

/* footer */
footer{background:var(--cream);padding:100px 7vw 60px;border-top:1px solid rgba(230,225,209,.1)}
footer .inner{max-width:1280px;margin:0 auto}
.smallprint{font-size:14px;color:var(--dim);font-style:italic;margin-top:26px;max-width:52em}

/* reveal-on-scroll */
.rv{opacity:0;transform:translateY(26px);transition:opacity .9s var(--ease),transform .9s var(--ease)}
.rv.in{opacity:1;transform:none}

/* reduced motion */
@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
  .rv{opacity:1;transform:none}
}

/* ============================================================
   V15 — THE STUDY (chaptered villa details)
   ============================================================ */
.chbar{position:sticky;top:var(--nav-h);z-index:560;background:rgba(17,21,12,.96);
  backdrop-filter:blur(10px);border-bottom:1px solid rgba(230,225,209,.14);
  display:flex;align-items:center;gap:8px;padding:0 7vw}
.chbar-scroll{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;padding:14px 0;flex:1}
.chbar-scroll::-webkit-scrollbar{display:none}
.cbhome,.cbchip{white-space:nowrap;letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;
  padding:14px 16px 15px;border-radius:99px;border:1px solid rgba(230,225,209,.22);color:var(--ink);opacity:.78;transition:.3s;
  box-sizing:border-box;display:inline-flex;align-items:center;position:relative}
.cbhome:hover,.cbchip:hover{opacity:1;border-color:rgba(230,225,209,.5)}
.cbchip.active{background:var(--ink);color:var(--cream);opacity:1;border-color:var(--ink)}
.cbchip.visited::after{content:"";position:absolute;left:50%;bottom:-8px;transform:translateX(-50%);
  width:4px;height:4px;border-radius:50%;background:var(--terra)}
@media(max-width:700px){.chbar{padding:0 5vw}}

/* villa switcher — the leftmost chip toggles a dropdown: this villa's study,
   the six for sale, the founders'. #vsw-btn stays INSIDE .chbar-scroll (it's
   simply the first scrollable chip, so that row keeps its original flex:1
   width share on narrow screens); #vsw-menu is an independent position:fixed
   sibling of .chbar, positioned in JS from getBoundingClientRect() at
   open-time. position:fixed escapes .chbar-scroll's overflow-x:auto (which
   per spec also forces overflow-y to compute as auto, clipping any
   absolutely-positioned descendant) without stealing layout width from the
   chip row the way an extra non-scrolling flex sibling would. */
.vsw-menu{position:fixed;z-index:570;background:var(--cream);
  border:1px solid rgba(230,225,209,.22);border-radius:6px;box-shadow:0 18px 44px -20px rgba(23,20,15,.4);
  max-width:340px;min-width:280px;max-height:70vh;overflow-y:auto;padding:8px 0}
.vsw-menu[hidden]{display:none}
.vsw-item{display:flex;width:100%;justify-content:space-between;align-items:baseline;gap:16px;
  text-align:left;padding:11px 22px;font-size:13px;transition:.2s}
.vsw-item:hover{background:rgba(230,225,209,.05)}
.vsw-item.vsw-study{letter-spacing:var(--ls);font-size:11.5px;text-transform:uppercase;padding:13px 22px;opacity:.85}
.vsw-item.vsw-study:hover{opacity:1}
.vsw-name{letter-spacing:.03em;font-size:12.5px;text-transform:uppercase;display:inline-flex;align-items:center;gap:8px}
.vsw-item.mine .vsw-name{color:var(--terra)}
.vsw-dot{width:6px;height:6px;border-radius:50%;background:var(--terra);flex:none}
.vsw-price{font-family:var(--serif);font-size:15px;white-space:nowrap}
.vsw-price.on-request{font-style:italic;color:color-mix(in srgb,var(--paper) 62%, transparent);font-size:13px;text-transform:none;letter-spacing:normal}
.vsw-head{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent);opacity:.85;padding:14px 22px 6px}
.vsw-rule{height:1px;background:rgba(230,225,209,.14);margin:6px 0}
@media(max-width:700px){
  .vsw-menu{left:0;right:0;max-width:none;min-width:0;border-radius:0;
    box-shadow:0 18px 44px -12px rgba(23,20,15,.4)}
}

/* Grace "?" point-question dot — reuses wireDetailsCommon's generic
   [data-ask] delegate; any copy carrying one inside a fragment that
   recompute() re-injects must be re-wired by hand (see wireReturns). */
.gdot{display:inline-flex;align-items:center;justify-content:center;flex:none;width:16px;height:16px;
  border-radius:50%;border:1px solid rgba(230,225,209,.35);background:none;cursor:pointer;
  font-family:var(--serif);font-size:11px;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;
  color:var(--ink);opacity:.6;transition:.25s;vertical-align:middle;margin-left:6px}
.gdot:hover{opacity:1;border-color:var(--terra);color:var(--terra)}

/* compare-the-six — ported .vcard grid (details-design-recon.md §2),
   re-themed to this file's own --ink/--paper/--terra + rgba/color-mix
   idiom (not the recon's own --ink-12/--paper-24 tokens, which don't exist
   in this file's :root) */
.vgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(258px,1fr));gap:18px;margin-top:26px}
.vcard{display:block;width:100%;text-align:left;cursor:pointer;position:relative;overflow:hidden;
  border:1px solid rgba(230,225,209,.12);border-radius:5px;background:rgba(232,221,198,.32);
  padding:24px 24px 22px;transition:transform .25s,box-shadow .25s,border-color .25s}
.vcard:hover{transform:translateY(-4px);box-shadow:0 16px 40px -22px rgba(23,20,15,.5);border-color:rgba(230,225,209,.4)}
.vcard.active{border-color:var(--terra);background:color-mix(in srgb,var(--terra) 7%, transparent)}
.vcard .ytag{position:absolute;top:20px;right:20px;text-align:right}
.vcard .ytag b{display:block;font-size:24px;font-weight:400;line-height:1;color:var(--terra)}
.vcard .ytag small{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;opacity:.6}
.vcard .vtag{display:inline-block;margin-bottom:16px;letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;color:var(--terra)}
.vcard .vname{font-family:var(--serif);font-size:26px;line-height:1.08;margin-bottom:.15em;max-width:72%}
.vcard .vspec{opacity:.72;font-size:14.5px;margin-bottom:16px}
.vcard .vprice{font-family:var(--serif);font-size:28px;font-weight:400;line-height:1}
.vcard .vmeta{display:flex;gap:16px;margin-top:16px;padding-top:16px;border-top:1px solid rgba(230,225,209,.12);opacity:.92;flex-wrap:wrap}
.vcard .vmeta div span{display:block;font-family:var(--serif);font-size:18px;line-height:1}
.vcard .vmeta div small{letter-spacing:.14em;font-size:10px;text-transform:uppercase;opacity:.62}
.vgridnote{font-style:italic;font-size:15px;color:color-mix(in srgb,var(--paper) 62%, transparent);opacity:.85;margin-top:22px}

.quietlink{display:inline-block;margin-top:22px;letter-spacing:var(--ls);font-size:12px;text-transform:uppercase;
  opacity:.6;border-bottom:1px solid currentColor;padding-bottom:2px;transition:.3s}
.quietlink:hover{opacity:1}
.smallprint button{font-style:italic;opacity:.85;border-bottom:1px solid currentColor}

.chhead{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px}
.curchip{display:inline-flex;border:1px solid rgba(230,225,209,.3);border-radius:99px;overflow:hidden}
.curchip button{padding:8px 16px 9px;font-size:11.5px;letter-spacing:var(--ls);text-transform:uppercase;transition:.3s}
.curchip button.active{background:var(--ink);color:var(--cream)}

/* "what makes the price" — donut + ledger (details-design-recon.md §3.3 /
   §3.4.1, ranked port items 2-3), re-themed to this file's own tokens/rgba
   idiom rather than the recon's own --ink-12/rem scale. Two columns desktop
   (donut 40% / ledger 60%, vertically centered), stacks at this file's own
   700px breakpoint on mobile (donut ~220px, centered, above the ledger). */
.invbreak{display:grid;grid-template-columns:2fr 3fr;gap:6vw;align-items:center;margin-top:10px}
.invbreak .donutside{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px}
/* the price, under the chart — an HTML box, so it wraps and grows instead of
   overflowing a fixed SVG hole */
.dprice{text-align:center;max-width:100%}
.dprice-k{font-family:var(--sans);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--stone)}
.dprice-v{font-family:var(--serif);font-size:clamp(24px,2.6vw,30px);font-weight:300;line-height:1.15;
  margin-top:6px;color:var(--ink);font-variant-numeric:lining-nums;overflow-wrap:anywhere}
.dprice-alt{font-family:var(--serif);font-size:12.5px;margin-top:4px;color:var(--dim);
  font-variant-numeric:lining-nums;overflow-wrap:anywhere}
.donut{width:100%;max-width:260px;height:auto;display:block}
.donut text{font-family:var(--serif);text-anchor:middle}
.dseg{cursor:pointer;transition:opacity .2s ease}
.dhole-caps{font-size:11px;letter-spacing:.18em;text-transform:uppercase;fill:var(--stone)}
/* .dhole-price / .dhole-idr are gone with the in-hole figure — see .dprice */
@media(max-width:700px){
  .invbreak{grid-template-columns:1fr;gap:30px}
  .invbreak .donutside{order:-1}
  .donut{max-width:220px;margin:0 auto}
}

.bdlist{width:100%}
.bd-row{display:flex;align-items:baseline;gap:14px;padding:14px 6px;margin:0 -6px;
  border-bottom:1px solid rgba(230,225,209,.18);cursor:pointer;transition:background .2s ease;border-radius:4px}
.bd-row:last-of-type{border-bottom:0}
.bd-sw{width:10px;height:10px;border-radius:3px;flex:0 0 auto;transform:translateY(3px)}
.bd-name{flex:1;min-width:0}
.bd-name .t{font-size:17px;color:var(--ink);display:block}
.bd-name .d{font-size:13.5px;opacity:.68;display:block;margin-top:4px;line-height:1.42}
.bd-row.margin .d{font-style:italic;color:color-mix(in srgb,var(--paper) 62%, transparent);opacity:1}
.bd-val{font-size:19px;white-space:nowrap;text-align:right;font-variant-numeric:lining-nums}
.bd-total{position:relative;display:flex;justify-content:space-between;align-items:baseline;gap:14px;
  margin-top:16px;padding-top:16px;border-top:1px solid var(--ink)}
.bd-total::before{content:"";position:absolute;left:0;right:0;top:-4px;height:1px;background:var(--ink)}
.bd-total .t{font-size:19px}
.bd-total .v{font-size:26px;text-align:right;font-variant-numeric:lining-nums}
.bd-total .v small{font-size:13px;font-weight:400;display:block;margin-top:4px;font-style:normal}

.pathmiles .amt{font-size:15px;opacity:.7;margin-top:4px}

.sumcard{border:1px solid rgba(230,225,209,.16);border-radius:6px;padding:32px 30px;margin:36px 0 40px}
.sumcard .sumname{font-size:clamp(24px,2.6vw,32px);margin-bottom:6px}
.sumcard .sumprice{font-size:19px;opacity:.85;margin-bottom:10px}
.sumcard .sumnums{font-style:italic;opacity:.72;font-size:16px}

/* the 30-year hero — the first thing the eye lands on in the returns
   chapter. Left = the compounded headline number; right = the two-line
   mechanism (year one vs year thirty, both live) that keeps the big number
   from reading as a trick. The hairline below the whole block is supplied
   by the tiles row that follows (.pgtiles>div already carries its own
   border-top) — deliberately not a separate <hr>. */
.rt-hero{display:grid;grid-template-columns:1.15fr 1fr;gap:64px;align-items:start;
  padding:8px 0 0;margin:0 0 56px}
@media (max-width:860px){ .rt-hero{grid-template-columns:1fr;gap:36px;margin-bottom:40px} }
.rt-hero-over{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent)}
.rt-hero-num{font-family:'Cormorant Garamond',serif;font-weight:300;
  font-size:clamp(84px,11vw,148px);line-height:.86;letter-spacing:-.02em;
  color:var(--ink);font-variant-numeric:lining-nums;margin-top:14px;display:block}
.rt-hero-num .pct{font-size:.44em;letter-spacing:0;margin-left:3px;
  color:rgba(230,225,209,.55);vertical-align:baseline}
.rt-hero-sub{font-style:italic;font-size:clamp(19px,1.5vw,21px);line-height:1.45;color:color-mix(in srgb,var(--paper) 62%, transparent);
  max-width:30ch;margin-top:18px}

/* stat tiles — "the facts of the money": same ledger construction as .facts,
   a hairline above each number, context subline underneath in stone italic */
.pgtiles{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin:34px 0}
.pgtiles>div{border-top:1px solid rgba(230,225,209,.22);padding:18px 24px 0 0}
.pgtl{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent)}
.pgtv{font-family:var(--serif);font-weight:300;font-size:clamp(40px,3.4vw,54px);line-height:1.05;color:var(--ink);
  font-variant-numeric:lining-nums;margin-top:6px}
.pgtsub{font-style:italic;font-size:15px;color:color-mix(in srgb,var(--paper) 62%, transparent);margin-top:6px}

/* scenario chips — the site's one chip system, matching the chapter-bar chip */
.schips{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:12px 10px}
.schip-row{display:flex;flex-wrap:wrap;gap:10px}
.schip{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;padding:12px 20px;border:1px solid rgba(230,225,209,.28);
  border-radius:999px;background:transparent;color:var(--ink);transition:.25s}
.schip:hover{border-color:var(--ink)}
.schip.active{background:var(--ink);color:var(--cream);border-color:var(--ink)}
.pgreset{font-size:15px;font-style:italic;color:color-mix(in srgb,var(--paper) 62%, transparent);opacity:.85;border-bottom:1px solid currentColor;padding-bottom:1px;transition:.3s}
.pgreset:hover{opacity:1}
.schips-note{font-style:italic;font-size:15px;color:color-mix(in srgb,var(--paper) 62%, transparent);margin-top:14px}

/* sliders — hairline track + node-dot thumb, result-first label hierarchy */
/* teaser gate — the analytical chapters until Grace opens them */
.chtease{max-width:34em;padding:8px 0 40px}
.chtease .over{letter-spacing:var(--ls-wide);font-size:12px;text-transform:uppercase;font-weight:500;color:var(--terra);margin-bottom:12px}
.chtease .title{margin-bottom:22px}
/* the Numbers teaser hero — the multiple is the headline, with the journey
   from price paid to total collected shown as a proportional bar. */

/* the three-scenario range — the number as a spectrum to interrogate */

/* founders' note — the energy behind the price, in their own voice */
.fnote{margin-top:38px;padding:clamp(26px,3.2vw,42px) clamp(22px,3vw,40px);border-radius:10px;
  background:color-mix(in srgb,var(--terra) 7%, var(--cream));
  border-left:2px solid color-mix(in srgb,var(--terra) 55%, transparent);max-width:760px}
.fnote .over{letter-spacing:var(--ls-wide);font-size:10.5px;text-transform:uppercase;font-weight:600;color:var(--terra);margin-bottom:12px}
.fnote h3{font-family:var(--serif);font-weight:400;font-size:clamp(23px,2.6vw,32px);line-height:1.14;margin:0 0 18px;max-width:18em}
.fnote p{font-size:17px;line-height:1.6;margin:0 0 15px;max-width:40em;opacity:.9}
.fnote p em{font-style:italic;opacity:1}
.fnote .fnote-close{margin-top:20px;padding-top:18px;border-top:1px solid color-mix(in srgb,var(--ink) 12%, transparent);font-style:italic;opacity:.82}
.fnote-sign{margin-top:16px;letter-spacing:var(--ls-wide);font-size:11px;text-transform:uppercase;font-weight:500;color:var(--terra);opacity:.9}
/* the margin contrast — the one number that makes the case on its own */
.fcontrast{display:flex;align-items:center;gap:clamp(16px,3vw,34px);flex-wrap:wrap;
  margin:26px 0 24px;padding:22px 24px;border-radius:9px;background:color-mix(in srgb,var(--cream) 78%, transparent);
  border:1px solid color-mix(in srgb,var(--ink) 10%, transparent)}
.fcontrast b{display:block;font-family:var(--serif);font-weight:400;line-height:1;font-variant-numeric:lining-nums}
.fcontrast span{display:block;margin-top:8px;letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:500;opacity:.6;max-width:15em}
@media(max-width:520px){.fcontrast{gap:14px}}
.teaser-num{font-family:var(--serif);font-size:clamp(40px,7vw,60px);line-height:.95;color:var(--ink)}
/* --sans was never defined, so this declaration was invalid and dropped — the
   label has always rendered in the inherited serif. Removed rather than
   defined: the brand has one family. */
.teaser-num span{display:block;font-size:13px;letter-spacing:var(--ls);text-transform:uppercase;opacity:.6;margin-top:10px;font-weight:500}
.teaser-sub{font-style:italic;opacity:.78;margin-top:14px;max-width:26em}
.teaser-body{font-size:18px;line-height:1.55;margin:24px 0 30px;opacity:.9}
.teaser-body em{font-style:italic}
.teaser-actions{display:flex;flex-wrap:wrap;gap:14px 20px;align-items:center}
.teaser-actions .talklink{letter-spacing:var(--ls);font-size:12px;text-transform:uppercase;color:var(--dim)}
.teaser-actions .talklink:hover{color:var(--ink)}

/* "Tuned by Grace" banner + optional three-scenario compare on the returns page */
.qtuned{background:color-mix(in srgb,var(--terra) 12%, var(--cream));border:1px solid color-mix(in srgb,var(--terra) 34%, transparent);
  border-radius:6px;padding:15px 18px;margin:0 0 30px;display:flex;flex-wrap:wrap;gap:5px 14px;align-items:baseline}
.qtuned .qt-k{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:600;color:var(--terra);width:100%}
.qtuned .qt-v{font-size:15.5px}.qtuned .qt-v b{font-weight:600}
.qtuned .qt-note{width:100%;font-style:italic;opacity:.7;font-size:13.5px;margin-top:4px}
.qcompare{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:0 0 32px}
@media(max-width:560px){.qcompare{grid-template-columns:1fr 1fr}}

/* own-nights pull-line — sits directly below the midnight band, on cream;
   the money value stands upright (non-italic) inside the italic line */

/* the P&L is a ledger, not a full-width table — cap it so the figure sits a
   readable distance from its label instead of stranding across the whole
   column (and, on desktop, so a right-aligned number never lands under the
   Grace panel). */
.pnlgroup{margin-top:22px}
.pnlhead{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent);border-top:1px solid rgba(230,225,209,.22);padding-top:14px;margin-bottom:10px}
.pnlrow{display:flex;justify-content:space-between;gap:20px;padding:9px 0;border-bottom:1px solid rgba(230,225,209,.08);font-size:17px}
.pnlrow .pnl-lbl small{display:block;font-size:12px;opacity:.55;font-style:italic;margin-top:2px}
.pnlrow.pnlbold{font-weight:500}
.pnl-val.pnl-cost{color:color-mix(in srgb,var(--paper) 62%, transparent)}
.pnlnoi{position:relative;margin-top:18px;padding-top:14px;border-top:1px solid var(--ink);border-bottom:none;font-size:22px;color:var(--terra);font-weight:500}
.pnlnoi::before{content:"";position:absolute;left:0;right:0;top:-4px;height:1px;background:var(--ink)}

/* the 30-year picture — full-bleed midnight panel, the one composed dark moment */

/* the management-proof band — full-bleed WARM PAPER (never a second dark
   panel; the midnight chart stays the chapter's one dark moment). Same
   full-bleed technique as .mnband, inner column pinned to the same 820px
   text column via .rt-proof-inner. Stats mirror .pgtiles' own ledger
   construction (hairline above each number) at the spec's own sizes. */
.rt-proof{background:var(--paper);color:var(--ink);width:100vw;
  margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw);padding:76px 0 68px}
.rt-proof-inner{max-width:820px;margin:0 auto;padding:0 24px}
.rt-proof-over{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:rgba(230,225,209,.55)}
.rt-proof-head{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(28px,3vw,34px);
  line-height:1.25;color:var(--ink);max-width:24ch;margin-top:16px}
.rt-proof-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:44px}
.rt-proof-stats>div{border-top:1px solid rgba(230,225,209,.22);padding:18px 24px 0 0}
.rt-proof-l{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent);min-height:2.4em;line-height:1.2}
.rt-proof-n{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(34px,3vw,44px);
  line-height:1.05;color:var(--ink);font-variant-numeric:lining-nums;margin-top:6px}
.rt-proof-s{font-style:italic;font-size:14px;color:color-mix(in srgb,var(--paper) 62%, transparent);margin-top:6px;text-wrap:pretty}
.rt-proof-caveat{border-top:1px solid rgba(230,225,209,.22);padding-top:20px;margin-top:32px;
  font-style:italic;font-size:16px;line-height:1.55;color:color-mix(in srgb,var(--paper) 62%, transparent);max-width:60ch}
.rt-proof-close{font-family:'Cormorant Garamond',serif;font-weight:300;font-size:clamp(19px,1.7vw,21px);
  line-height:1.45;color:var(--ink);margin-top:28px;max-width:46ch}
@media(max-width:700px){
  .rt-proof{padding:52px 0 48px}
  .rt-proof-inner{padding:0 22px}
  .rt-proof-stats{grid-template-columns:1fr 1fr}
}

.accordion{margin-top:36px}
.accordion summary{cursor:pointer;letter-spacing:.18em;font-size:12px;text-transform:uppercase;padding:16px 0;
  border-top:1px solid rgba(230,225,209,.22);border-bottom:1px solid rgba(230,225,209,.22);
  list-style:none;color:var(--ink)}
.accordion summary::-webkit-details-marker{display:none}
.accordion summary::marker{content:""}
.accordion summary::after{content:"+";float:right;font-family:var(--serif);font-size:16px}
.accordion[open] summary::after{content:"–"}

/* ---------- furnishing v2: the styler stage ---------- */
.styler{margin-top:30px}
.styler .fstage{position:relative;border:1.5px solid rgba(230,225,209,.24);overflow:hidden;background:var(--well);border-radius:2px}
.styler .fcanvas{position:relative;aspect-ratio:16/9;overflow:hidden}
.styler .fcanvas img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .7s var(--ease)}
.styler .fcanvas img.on{opacity:1}
.styler .floadbar{position:absolute;top:0;left:0;right:0;height:3px;z-index:6;opacity:0;transition:opacity .25s;pointer-events:none;background:color-mix(in srgb,var(--paper) 20%, transparent)}
.styler .fstage.loading .floadbar{opacity:1}
.styler .floadbar i{position:absolute;top:0;bottom:0;left:-32%;width:32%;background:var(--terra);animation:fsweep 1.1s var(--ease) infinite}
@keyframes fsweep{0%{left:-32%}65%{left:100%}100%{left:100%}}
@media(prefers-reduced-motion:reduce){.styler .floadbar i{animation:none;left:0;width:100%}}
.styler .flive{position:absolute;top:13px;left:15px;display:flex;align-items:center;gap:8px;color:var(--paper);letter-spacing:var(--ls);font-size:10px;text-transform:uppercase;font-weight:500;text-shadow:0 1px 6px rgba(0,0,0,.6)}
.styler .flive i{width:7px;height:7px;border-radius:50%;background:var(--terra);animation:fpl 2.4s infinite}
@keyframes fpl{0%{box-shadow:0 0 0 0 rgba(168,90,62,.55)}70%{box-shadow:0 0 0 8px rgba(168,90,62,0)}100%{box-shadow:0 0 0 0 rgba(168,90,62,0)}}
.styler .fcap{position:absolute;left:0;right:0;bottom:0;display:flex;justify-content:space-between;flex-wrap:wrap;gap:6px 18px;padding:56px 20px 15px;background:linear-gradient(to top,rgba(10,9,6,.82),transparent);color:var(--paper)}
.styler .fcap span{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500}
.styler .fcap .r{opacity:.85}
.styler .fnoncap{font-size:14px;margin-top:12px;margin-bottom:0}

.styler .ftabs{display:flex;gap:8px;margin-top:24px;flex-wrap:wrap}
.styler .ftabs button{border:1px solid rgba(230,225,209,.28);padding:10px 18px 11px;letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500;opacity:.75;transition:.2s}
.styler .ftabs button:hover{opacity:1;border-color:var(--ink)}
.styler .ftabs button.on{background:var(--ink);color:var(--cream);border-color:var(--ink);opacity:1}

.styler .fpal{display:flex;gap:12px;margin-top:20px;flex-wrap:wrap}
.styler .fpal button{display:flex;align-items:center;gap:9px;border:1px solid rgba(230,225,209,.28);padding:9px 16px 9px 10px;border-radius:99px;font-size:13px;letter-spacing:.02em;opacity:.8;transition:.2s}
.styler .fpal button:hover{opacity:1;border-color:var(--ink)}
.styler .fpal button.on{border-color:var(--ink);background:var(--ink);color:var(--cream);opacity:1}
.styler .fsw{width:16px;height:16px;border-radius:50%;flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)}
.styler .fsw.teak{background:linear-gradient(135deg,#a07c48,#6f5330)}
.styler .fsw.pale{background:linear-gradient(135deg,#e6dcc4,#c3b48f)}
.styler .fsw.dark{background:linear-gradient(135deg,#4a4b47,#23231f)}

/* ---------- configurator layout — top bar + two columns (render / room panel) ---------- */
.styler .fvbar{display:flex;flex-wrap:wrap;gap:20px 34px;align-items:center;justify-content:space-between;margin:26px 0 4px;padding:20px 0;border-top:1px solid rgba(230,225,209,.16);border-bottom:1px solid rgba(230,225,209,.16)}
.styler .fvbar .fvgrp{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
.styler .fvbar .fvlab{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500;color:var(--dim)}
.styler .fvbar .fpal{margin-top:0}
.styler .fvtot{text-align:right;margin-left:auto}
.styler .fvtot .k{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:500;color:var(--dim)}
.styler .fvtot .n{font-size:clamp(26px,3vw,31px);font-weight:300;line-height:1.15}
.styler .fvtot .tier{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:500;color:var(--dim)}
.styler .fcfg{display:grid;grid-template-columns:1.5fr 1fr;gap:46px;margin-top:30px;align-items:start}
@media(max-width:1000px){.styler .fcfg{grid-template-columns:1fr;gap:28px}}
.styler .fcfg .ftabs{margin-top:16px}
.styler .ftabs button.override{position:relative}
.styler .ftabs button.override::after{content:"";position:absolute;top:5px;right:6px;width:6px;height:6px;border-radius:50%;background:var(--terra)}

/* room panel (right column) */
.styler .fpanel .rhead{display:flex;align-items:baseline;justify-content:space-between;gap:14px;margin-bottom:6px}
.styler .fpanel .rhead h3{font-family:var(--serif);font-size:clamp(24px,2.6vw,30px);font-weight:300}
.styler .fpanel .rhead .rn{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:500;opacity:.55;white-space:nowrap}
.styler .fpanel .frp{margin:20px 0 10px}
.styler .fpanel .frp .lab{letter-spacing:var(--ls);font-size:10.5px;text-transform:uppercase;font-weight:500;color:var(--dim);margin-bottom:11px}
.styler .frp-set{display:flex;gap:7px;flex-wrap:wrap}
.styler .frp-chip{display:flex;align-items:center;gap:8px;border:1px solid rgba(230,225,209,.3);padding:8px 13px 9px;letter-spacing:var(--ls);font-size:10px;text-transform:uppercase;font-weight:500;transition:.2s}
.styler .frp-chip .fsw{width:12px;height:12px}
.styler .frp-chip:hover{border-color:var(--ink)}
.styler .frp-chip.on{background:var(--ink);color:var(--cream);border-color:var(--ink)}
.styler .fdivline{height:1px;background:rgba(230,225,209,.16);margin:24px 0 4px}
.styler .fitem{padding:18px 0;border-bottom:1px solid rgba(230,225,209,.13)}
.styler .fitem .ftop{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;user-select:none;width:100%;text-align:left}
.styler .fitem .ftop:hover .l{opacity:.75}
.styler .fitem .ftop .l{font-family:var(--serif);font-size:22px;transition:.2s}
.styler .fitem .ftop .tr{display:flex;align-items:center;gap:14px}
.styler .fitem .ftop .cur{letter-spacing:var(--ls);font-size:10px;text-transform:uppercase;font-weight:500;opacity:.55;text-align:right}
.styler .fitem .ftop .chev{width:9px;height:9px;border-right:1.4px solid rgba(230,225,209,.55);border-bottom:1.4px solid rgba(230,225,209,.55);transform:rotate(45deg) translate(-1px,-1px);transition:transform .4s var(--ease);flex:none}
.styler .fitem.open .ftop .chev{transform:rotate(225deg)}
.styler .foptwrap{overflow:hidden;max-height:0;transition:max-height .5s var(--ease)}
.styler .fitem.open .foptwrap{max-height:400px}
.styler .fopts{display:flex;flex-direction:column;gap:7px;padding-top:14px}
.styler .fopt{display:flex;align-items:center;gap:12px;border:1px solid rgba(230,225,209,.2);padding:10px 14px;transition:.2s;text-align:left;width:100%}
.styler .fopt:hover{border-color:rgba(230,225,209,.55)}
.styler .fopt.on{border-color:var(--ink);background:color-mix(in srgb,var(--ink) 5%, transparent)}
.styler .fopt .fsw{width:20px;height:20px}
.styler .fopt .fsw.none{background:repeating-linear-gradient(45deg,transparent,transparent 3px,rgba(230,225,209,.3) 3px,rgba(230,225,209,.3) 4px);box-shadow:inset 0 0 0 1px rgba(23,20,15,.3)}
.styler .fopt .nm{flex:1;font-family:var(--serif);font-size:18px;line-height:1.25}
.styler .fopt .pr{letter-spacing:var(--ls);font-size:11px;text-transform:uppercase;font-weight:500;color:var(--dim);white-space:nowrap}
.styler .fopt.on .pr{color:var(--paper)}
.styler .fopt .tick{width:19px;height:19px;border-radius:50%;border:1.5px solid rgba(230,225,209,.3);display:flex;align-items:center;justify-content:center;font-size:11px;color:transparent;flex:none;transition:.2s}
.styler .fopt.on .tick{background:var(--ink);border-color:var(--ink);color:var(--cream)}
.styler .frnote{font-style:italic;color:var(--dim);font-size:15.5px;line-height:1.5;margin-top:18px}

/* name-your-villa prompt — the gate before the configurator opens */
.styler .fname-prompt{border-top:1px solid rgba(230,225,209,.16);border-bottom:1px solid rgba(230,225,209,.16);padding:56px 0;margin:26px 0 4px}
.styler .fnp-inner{max-width:34em}
.styler .fnp-inner .over{margin-bottom:12px}
.styler .fnp-h{font-family:var(--serif);font-size:clamp(30px,4vw,46px);font-weight:300;line-height:1.05}
.styler .fnp-sub{font-style:italic;opacity:.7;font-size:18px;line-height:1.5;margin:14px 0 30px;max-width:30em}
.styler .fnp-row{display:flex;gap:14px;flex-wrap:wrap;align-items:stretch}
.styler .fnp-row input{flex:1;min-width:240px;font-family:var(--serif);font-size:22px;background:transparent;border:none;border-bottom:1.5px solid rgba(230,225,209,.35);padding:10px 4px;color:var(--ink)}
.styler .fnp-row input::placeholder{opacity:.4}
.styler .fnp-row input:focus{outline:none;border-color:var(--ink)}
.styler .fnp-row input.err{border-color:var(--terra)}
.styler .fnp-row .btn{white-space:nowrap;align-self:center}
@media(max-width:600px){.styler .fnp-row .btn{align-self:stretch;justify-content:center}}

/* the villa name, once given — sits above the configurator */
.styler .fnamebar{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-top:26px}

/* the tuner (inside the .accordion pattern already used by the returns chapter) */

/* "save my selections" lead capture */
.leadwrap{max-width:640px}
.leadh{font-family:var(--serif);font-weight:300;font-size:clamp(26px,3vw,34px);margin:10px 0 14px}
.leadfields{display:flex;gap:32px;flex-wrap:wrap;margin:30px 0 6px}
.fld{flex:1;min-width:220px;display:flex;flex-direction:column;gap:8px}
.fld span{letter-spacing:.18em;font-size:11px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent)}
.fld input{font-family:var(--serif);font-size:20px;background:transparent;border:none;border-bottom:1px solid rgba(230,225,209,.35);padding:9px 2px;color:var(--ink);min-height:22px}
.fld input:focus{outline:none;border-color:var(--terra)}
.fld.err input{border-color:var(--terra)}
.fld-warn{color:var(--terra);font-size:13px;font-style:italic;margin-top:12px;display:none}
.fld-warn.show{display:block}
.leadconfirm{font-size:19px;font-style:italic;max-width:34em;line-height:1.6}
.leadconfirm .talklink{display:inline-block;margin-top:14px;font-size:13px;font-style:normal;letter-spacing:.02em}

@media(max-width:700px){
  .pgtiles{grid-template-columns:1fr 1fr}
  .sumcard{padding:24px 22px}
  /* scenario chips + reset link were cramping into one crowded row — give the
     reset link its own line beneath them */
  .pgreset{flex-basis:100%;margin-top:2px}
  /* the node-dot thumb reads a touch bigger on mobile — still the same
     hairline+dot instrument, just an easier target */

  /* styler + tuner + lead form: bigger tap targets, single columns */
  .styler .ftabs button{padding:13px 16px 14px}
  .styler .fpal button{padding:12px 18px 12px 12px;font-size:14px}
  .leadfields{flex-direction:column;gap:22px}
  .fld input{padding:13px 2px;font-size:17px}
  /* Fault 3: disabled until there is a name to save (see wireLead's
     syncSaveDisabled in engine.js) — dimmed rather than recoloured, since
     .btn.terra's fill is the only "this is the button" cue on this page. */
  #lead-save:disabled{opacity:.45;cursor:not-allowed}
}
@media(max-width:430px){
  /* v23 E2 — "US$46,486"-class values were crowding the 2x2 stat-tile
     grid's right edge on narrow phones; shrink just the number. */
  .pgtv{font-size:clamp(30px,8.4vw,40px)}
}

/* ═══ v26 · PASS 3 readability · fixes 1-12 ══════════════════════════════
   Appended at the end of the sheet on purpose: media queries carry no
   specificity of their own, so same-specificity conflicts are decided by
   source order and every rule below must sit after the rule it overrides. */

/* FIX 1 — prose measure (A1, 45 findings).
   54ch body/lead prose, 52ch .smallprint.
   No margin-inline:auto: all 91 A1 elements compute text-align:start. */
.smallprint{max-width:52ch}
.body-l,
.jstep p,
.pricenotes p,
.accordion .methline,
.styler .froomnote,
.tuner-disc,
.tuner-room .trnote,
/* .rt-hero-close is deliberately NOT a member of this group (v27).
   v23's E1 fix sizes that closing paragraph to fill the ladder column it sits
   under on the returns page, so it pairs visually with .rt-ladder.  A 54ch
   measure clamp narrows it to ~378px against a ~472px ladder and breaks that
   pairing.  pw_v23 s16 and pw_v24 s6 both assert the two widths match to <2px.
   Do not re-add it here. */
.schips-note,
p.dim{max-width:54ch}
/* A1's D-23 is NOT closed by the #pg-acc-irr rule above.  D-23's evidence line
   ("the average above is the mean of thirty annual returns; ...") lives in a
   <small> CHILD, and `.methrow .v small{...;max-width:44em}` (block 1, 0,2,1)
   caps that child independently.  54ch on the parent is computed against the
   parent's 17px font (1ch=8px -> 432px); the child renders at 13px, so 432px
   still held 93 chars — measured, over the doc's own 78 bar.  1,0,1 here beats
   0,2,1.  52ch at 13px (1ch=6px -> 312px, 4.613px/char measured) = ~68 chars.
   .doorcard .chs and .plans-grid figcaption are NOT among A1's named 19 — they
   appear in the audit only as font findings (D-74, D-40/41/43), which are not
   mine and whose font-size I do not touch.  But fix 1's verify clause is global
   ("no prose element on any audited route exceeds 78 characters per line"), and
   measured they run 107 and 89.  max-width alone changes neither font-size nor
   colour, so this does not collide with D-74/D-40-43 or with fix 6.
   All five elements compute text-align start/left, so no margin-inline:auto —
   same conditional the doc sets for the rest of A1. */
.plans-grid figcaption{max-width:54ch}
/* 21 of the 23 <p class="lead"> carry an INLINE max-width (28em-34em =
   756-918px = 79-95 chars).  No normal-weight rule of any specificity can
   reach a style attribute, and the repeated inline strings are not unique
   enough to splice one at a time, so this is the one place !important is
   the correct tool.  The two intentionally-narrower hero leads are then
   restored at higher specificity, also !important, so they keep 26em. */
.lead{max-width:54ch!important}
.film-mid .lead{max-width:26em!important}

/* FIX 2 — journey grid (A2 / M-01).  Was an inline style on an unclassed
   div; a media query cannot reach an inline style, so it moved to a class.
   The fractional pixel columns are not preserved: they were `1fr auto`
   computing to 153px 210.125px, 60px wider than the 335.4px content box. */
.jgrid{display:grid;grid-template-columns:1fr auto;gap:60px;align-items:start}
@media(max-width:720px){
  .jgrid{grid-template-columns:1fr;gap:34px}
  .jgrid .tree-aside{text-align:center}
}

/* FIX 3 — starved columns (A3).  X measured, see apply_v26_css.py.
   .incl stays CSS multi-column on purpose: grid would flip the list from
   column-major to row-major reading order. */
.packs{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
@media(max-width:960px){.packs{grid-template-columns:1fr}}
.incl{columns:440px;column-gap:64px}
@media(max-width:900px){.incl{columns:1}}

/* FIX 3 addendum — the price-ladder cards share one set of interior rows, so
   the price line and the first list item sit at the same y however many lines
   the title takes.  `:has(.pr)` picks out the two price ladders and leaves the
   "A rule we keep" trio alone.  row-gap:0 because a subgrid would otherwise
   inherit the parent's 26px gutter between all five rows.
   justify-self:start on the button because the source sets align-self:flex-start
   on .pick — in a column flex container that is the INLINE axis and gives the
   pill its shrink-to-fit width, but in grid align-self is the BLOCK axis, so
   without this the button would stretch the full column (measured 146.3px to
   267.4px at 390).  justify-self:start restores the measured original width. */
@supports(grid-template-rows:subgrid){
  .packs:has(.pr)>.pack{display:grid;grid-template-rows:subgrid;grid-row:span 5;row-gap:0}
  .packs:has(.pr)>.pack>.pick{justify-self:start}
}

/* FIX 4 — tap targets (A4).  Coarse pointers only: the desktop site keeps
   its delicate sizing, which is the design, not an oversight. */
:root{--tap-min:44px}
@media(pointer:coarse){
  /* the 16px info dot keeps its visual size — the hit area grows, not the dot.
     SPEC DELTA: the doc's snippet uses inset:-14px and claims "44x44 of
     touchable area".  Measured, it yields 42x42.  `*{box-sizing:border-box}`
     (block 1 L21) + `.gdot{width:16px;height:16px;border:1px ...}` (L577-580)
     means the .gdot PADDING box — which is the containing block for an
     absolutely positioned ::after — is 14x14, not 16x16.  14 + 2*14 = 42.
     getComputedStyle(el,'::after').width read back "42px" and a 0.1px-step
     elementsFromPoint sweep gave a 41.8px hittable span.
     -15px restores the doc's own binding requirement of >=44x44: 14 + 2*15 = 44. */
  /* SPEC DELTA 2: a 44x44 box is not 44x44 of TOUCHABLE area if something
     paints over it.  With the inset alone, elementFromPoint (topmost — that is
     what a tap actually hits, unlike elementsFromPoint) reached only 43x36 on
     the invest chapter and 43x33 on the path chapter: inert later siblings
     (SPAN.d, DIV.amt) painted over the bottom band of the pseudo.
     z-index:1 lifts the dot over same-context siblings and fixed invest
     (36 -> 43).  It did NOT fix path, because the dot's parent
     `.pathmiles .k` computes opacity:.72, which CREATES A STACKING CONTEXT
     and traps the dot's z-index inside it; the later sibling .amt then paints
     above the whole context.  Raising .k itself is the only way out and is
     visually inert — measured, the boxes do not overlap (.k 412.1-430,
     .amt 434-458), and both rules are inside pointer:coarse.
     After both: all 6 .gdot sites measure tapW 43 / tapH 43, i.e. the full
     44x44 box (43 = largest whole-pixel span strictly inside 44px: 22 up +
     21 down from the centre). */
  .gdot{position:relative;z-index:1}
  .pathmiles .k{position:relative;z-index:1}
  .gdot::after{content:"";position:absolute;inset:-15px}
  .talklink,
  .curchip button,
  .chip,
  .schip,
  .pack .pick,
  .styler .ftabs button,
  .styler .fpal button{min-height:var(--tap-min)}
  /* 24px glyph + symmetric padding = exactly 44x44 (was 38x40). */
  .burger{padding:10px 11px}
  /* hairline track preserved exactly: 44-2*21 = 2px content box, same as
     the 30-2*14 it replaces, and background-clip:content-box is unchanged */
}

/* FIX 5 — orphan control (A5), partial by decision.  No hard <br> anywhere:
   portal widths vary continuously and a break that fixes 1440 breaks 1100. */
p,li,figcaption,blockquote{text-wrap:pretty}
h1,h2,h3,h4,.title,.nm{text-wrap:balance}

/* FIX 6 — caps off full sentences (A6).  Modifier class on the site that
   actually carries the treatment; the shared microlabel class is untouched.
   Specificity is deliberate: plain `.lowcaps` would lose to `.miles .k`.
   font-size and colour are unchanged, so the type hierarchy still reads. */

/* FIX 7 — .smallprint 14px -> 15px (A7).  "One value, everywhere."
   `.styler .fnoncap{font-size:14px}` (block 1 L837, specificity 0,2,0) outranks
   a bare `.smallprint` (0,1,0), so #styler-block>p.fnoncap.smallprint — one of
   A1's own 19 named selectors — stayed at 14px in the first build.  Measured on
   #/villa/2br-1/details/furnishing at 1440 and 390.  Matched here at equal
   specificity, later in source order, so it wins without !important. */
.smallprint{font-size:15px}
.styler .fnoncap{font-size:15px}

/* FIX 8 — invest-ledger description 13.5px -> 15px (A8). */
.bd-name .d{font-size:15px}

/* FIX 9 — .invbreak overflow (A9).  The doc's prescription (row normal,
   nowrap on the numeric span) was already true in the source; these two
   declarations restate it so it cannot regress. */
.invbreak{white-space:normal}
.bd-val{white-space:nowrap}
/* The actual 6px: .bd-row{margin:0 -6px} bleeds the hi-row background past
   the text column.  6px of inline padding on the list gives the bleed room
   inside the box, keeping the effect and losing the overflow. */
.bdlist{padding-inline:6px}

/* FIX 10 — terracotta text grounds (A10+A11).  --terra itself is untouched:
   it is correct for rules, marks and the signature. */
:root{--terracotta-deep:#949E86}  /* retired: now sage. SWV has no terracotta. */
/* .pack text is --paper, not cream: paper on #9A5138 is 4.321, still under
   the floor, so the ground AND the foreground both move. */
.pack.terra{background:var(--terracotta-deep);color:var(--cream)}
/* was an inline opacity:.7 on two sites; on the terra ground 70% cream
   composites to 3.395, so terra opts out and keeps the label at full. */
.pack .caps{opacity:.7}
.pack.terra .caps{opacity:1}
/* D-119 `.pack.terra .pick`: `.pack .pick{color:var(--paper)}` (block 1 L288-289,
   specificity 0,2,0) matches the button directly, so it beats the cream that
   `.pack.terra` sets for inheritance.  Measured 4.321 in the first build — the
   exact paper-on-#9A5138 figure.  0,3,0 here, so it wins.  The border moves with
   it or the pill reads as two different whites. */
.pack.terra .pick{color:var(--cream);border-color:var(--cream)}
.pack.terra .pick:hover,.pack.terra .pick.picked{background:var(--cream);color:var(--ink)}
.btn.terra{background:var(--terracotta-deep);border-color:var(--terracotta-deep)}
.vcard .vtag{color:var(--terracotta-deep)}
.bd-total .v small{color:var(--terracotta-deep)}
/* M-23a: ink at a cumulative .92*.62 = .5704 measured 4.193 on cream. */
.vcard .vmeta div small{opacity:.7}
/* M-23c and its untagged sibling .mn-micro: paper at 55% on midnight. */

/* FIX 11 — doorcard .over (A12).  Opacity alone tops out at 4.073 on this
   ground, so the fix-10 token is used with it. */

/* FIX 12 — 55% ink -> 70% (A13). */
.rt-proof-over{color:rgba(230,225,209,.7)}
.pnlrow .pnl-lbl small{opacity:.7}

/* ===================== v29 ===================== */
/* v29 FIX A2 — cream masthead scrim over the hero render.  At scroll 0 the
   nav is ink on the photograph with no text-shadow (nav.on-cream,
   background:none), measured 2.81 median / 1.85 min on desk /details.  A dark
   scrim would make that worse; the scrim has to be cream.  Heights are cut to
   the measured .over top on each hero variant so the cream never reaches the
   light .over text below (see v29-report.md). */
.dhero{isolation:isolate}
.dhero::before{
  content:"";position:absolute;inset:0 0 auto 0;height:132px;z-index:1;
  pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(245,241,232,.88) 0,
    rgba(245,241,232,.80) 40px,
    rgba(245,241,232,.46) 78px,
    rgba(245,241,232,.14) 110px,
    rgba(245,241,232,0) 132px);
}
/* v29 FIX A2b — .over is paper on the photograph, mid-hero, above the bottom
   scrim's reach: measured 2.58 median / 1.49 min on mobile /gallery, the worst
   text on the site.  Scoped to .dhero .scrim because the bare .scrim class is
   shared with the cover hero, both story stages and the mine-hero. */
.dhero .scrim .over{color:var(--cream);opacity:1;text-shadow:0 1px 4px rgba(10,9,6,.85)}
@media(max-width:820px){
  .dhero .scrim{background:linear-gradient(to top,
    rgba(10,9,6,.90) 0%,
    rgba(10,9,6,.84) 45%,
    rgba(10,9,6,.72) 74%,
    rgba(10,9,6,.26) 90%,
    rgba(10,9,6,0) 100%)}
}
@media(min-width:821px){
  .dhero .scrim{background:linear-gradient(to top,
    rgba(10,9,6,.88) 0%,
    rgba(10,9,6,.80) 40%,
    rgba(10,9,6,.64) 62%,
    rgba(10,9,6,.22) 86%,
    rgba(10,9,6,0) 100%)}
}
/* end v29 FIX A2 */

/* v29 FIX B — .chbar-scroll is 250.953 wide with scrollWidth 1063 and no mask,
   so chips are cut mid-glyph at both edges.  scroll-padding-inline fixes the
   real bug behind the right-hand clip: scrollIntoView({inline:"center"}) was
   leaving scrollLeft:62 with .cbhome at x -42.5. */
@media(max-width:820px){
  .chbar-scroll{
    -webkit-mask-image:linear-gradient(to right,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%);
            mask-image:linear-gradient(to right,transparent 0,#000 20px,#000 calc(100% - 20px),transparent 100%);
  }
}
.chbar-scroll{scroll-padding-inline:24px}
/* end v29 FIX B */

/* v29 FIX C — the drawings plan renders 281.406 x 198.969 at 390px from a
   2400x1697 source.  Reclaiming 32px of figure padding takes it to ~313 wide;
   the lightbox does the rest.  .plans-zoom is set only when v.plans is real,
   so the "plan is being drawn" placeholder never advertises a zoom. */
/* end v29 FIX C */

/* v29 FIX D — keep the "Ask Grace —" label in small caps, demote the quoted
   question to a readable italic.  (0,2,0) beats .g-ask's (0,1,0). */
/* end v29 FIX D */

/* v29 FIX E — tertiary links.  .smallprint .talklink is (0,2,0): a bare
   .talklink (0,1,0) loses font-style to .smallprint button (0,1,1). */
.smallprint .talklink{
  font-size:16px;font-style:normal;letter-spacing:.015em;opacity:.8;
  text-decoration:none;border-bottom:1px solid rgba(230,225,209,.28);
  padding-bottom:3px;line-height:1.5;
}
.quietlink{font-size:13px;opacity:.78}
@media(max-width:820px){
  .quietlink{min-height:44px;display:inline-flex;align-items:center}
}
/* end v29 FIX E */

/* v29 FIX F — six terracotta/hairline rectangles with no legend are a puzzle,
   not a diagram.  The swatches use var(--terra) so they stay in lockstep with
   the rects, which are the only place in the file still literally on --terra.
   svg{display:block} removes the inline line-box descender that was 10.19px of
   the measured 36.19px svg->caption gap. */
/* end v29 FIX F */

/* v29 FIX G — stage 1 was the only .miles cell wearing `lowcaps`, because
   lowcaps was applied iff st.note was truthy.  Promote the stage name, demote
   only the note.  The spec's opacity:.72 / font-size:.94em demotion is NOT
   applied: .miles .k already carries opacity .72 and nested opacity multiplies
   (.72*.72 = .518), and a real-pixel decode of the rendered glyph cores showed
   even opacity:.88 (-> .634) drops the note to 2.08:1 min / 2.36:1 median,
   BELOW its own uppercase siblings at 2.67 / 2.97.  Casing alone is enough
   demotion and costs no contrast (see v29-report.md FIX G). */
/* end v29 FIX G */

/* v29 FIX H — both CTA pairs are shrink-to-fit, so stacked buttons come out
   ragged (277.094 / 201.734 on /talk, 153.125 / 219.484 on /furnishing).
   align-items:stretch alone makes the children full width.  line-height:1.2 is
   the actual cause of the 5.19px height delta: <a> inherits body's 1.6 while
   <button> uses the UA's `normal`. */
@media(max-width:820px){
  .cta-row{flex-direction:column;align-items:stretch}
}
.btn{line-height:1.2}
/* end v29 FIX H */
/* v29b FIX F2 — the legend row was flexed to space-between across the full
   1078px text column while the <svg> it labels is max-width:520px, so the key
   floated ~560px clear of the bars and read as an unrelated element. Match the
   svg's own max-width so eyebrow and key bracket the diagram. */

/* v29b FIX D2 — on mobile the label text node is an anonymous flex item narrow
   enough to break "GRACE —" across two lines, orphaning the em-dash. Stacking
   the pill's two parts gives the label its own line and left-aligns the quoted
   question under it. */
/* end v29b */

/* ==========================================================================
   v30 — see /home/claude/swv/v30-report.md for the PRE/POST decode numbers
   that justify every value below.  Every contrast figure quoted in these
   comments is a real-pixel decode MINIMUM (A/B screenshot pair, canvas
   decode, p92-of-nonzero-delta stroke-core threshold), never arithmetic.
   Two rigs, always labelled: VIS suppresses the element outright
   (visibility:hidden) and is the recon-comparable default; INK suppresses
   only the glyph fill (color + -webkit-text-fill-color: transparent) and is
   the ONLY valid rig where the measured element owns its own background —
   VIS would delete the very surface under test.  FIX N (.gal figcaption owns
   its scrim) and FIX P (.btn.terra owns its fill) are therefore INK.
   ========================================================================== */

/* v30 FIX K — three stacked hairlines with two ~64px voids on /details:
   .facts' own border-bottom at docY 1204, a decorative .rule at 1268
   (margin 64/64, collapsed from the .accordion's 36px), and the compare
   accordion's summary border-top at 1333.  Exactly two `.rule` divs in the
   file are immediately followed by a details.accordion (the compare block
   on /details and "The assumptions, in the open" on /returns) — those are
   the redundant ones, and :has() adjacency reaches both without touching
   the other ten .rule instances or the markup.
   :has() is already in use in this sheet (:has(.pr) scopes the subgrid
   rule), so no new platform dependency. */
.rule:has(+ details.accordion){display:none}
.facts:has(+ .rule + details.accordion){border-bottom:0}
.rule + details.accordion{margin-top:64px}

/* v30 FIX L — "Compare the six" never read as one control: the label ink sat
   at x=181 and the + at x~1254, ~1070px apart, on a 1078px-wide summary whose
   hairlines were indistinguishable from the decorative .rule hairlines above
   it.  Three moves, per spec order of preference:
     1. cap the interactive row at the same 520px discipline as FIX F2 and let
        flex put the + immediately after the label instead of floating it to
        the far edge;
     2. make the + unmistakably a control — 22px instead of 16px, terracotta
        instead of ink, with a hover state (cursor:pointer was already set);
     3. differentiate the lid: the summary's top border becomes terracotta so
        it cannot be confused with the ink-14% decorative rules.
   Still a <details>/<summary>; no custom widget.  Applies to all three
   accordions in the file (#compare-six, #f-tuner, /returns assumptions) —
   the longest label, "The assumptions, in the open", fits inside 520px. */
.accordion summary{
  display:flex;
  align-items:baseline;
  gap:14px;
  max-width:520px;
  border-top-color:var(--terracotta-deep);
  border-bottom-color:rgba(230,225,209,.14);
}
.accordion summary::after{
  float:none;
  font-size:22px;
  line-height:1;
  color:var(--terracotta-deep);
  transition:transform .25s var(--ease);
}
.accordion summary:hover{color:var(--terracotta-deep)}
.accordion summary:hover::after{transform:scale(1.18)}

/* v30 FIX M — the slim hero was extinguished: 74.07% of its CSS rows below
   luminance 52 at 1440 and 71.15% at 390 (row-mean Rec.709, device rows
   collapsed by dSF so the metric is breakpoint-comparable), with ~37px of
   crushed black below the last text baseline.  Scoped to .dhero.slim only —
   the non-slim mobile treatment is deliberately left alone.

   The stops are px-from-bottom, not percentages, because the scrim's height
   is content-driven and fixed (291.56 at desktop, 243.88 at mobile) and its
   bottom edge is flush with the hero's, so px-from-bottom of the gradient is
   px-from-bottom of the hero.  Percentages would drift with the label wrap.
   Breakpoint is 700/701 to match the scrim's own padding media query
   (130px 48px 46px vs 90px 22px 30px) — that padding is what sets the text
   rows the stops are cut to.

   Text rows, px-from-bottom: desktop .over 142-162, .line 83-132, .labels
   46-65; mobile .over 134-154, .line 94-124, .labels 57-76 and 30-49.
   The dead zone is the scrim's bottom padding exactly, so alpha is pulled
   down to .12/.03 below pfb ~20 and the darkest band is lifted to start at
   pfb 48/32.

   POST, real decode, on the reference hero (/villa/2br-1/details/gallery):
     desktop 74.07% -> 55.93% rows below L52; .over min 4.75 -> 6.38,
       .line 13.51 -> 13.62, .labels 5.64/5.54/4.98 -> 5.69/5.47/5.03
     mobile  71.15% -> 54.55% rows below L52; .over min 8.36 -> 6.45,
       .line 13.96 -> 13.59, .labels 10.79/9.99/10.19 -> 10.55/9.53/9.35
   Every run stays above the 4.5 floor at both breakpoints. */

/* v30 FIX N — the gallery captions got none of v29's .over treatment: 10px
   --paper with text-shadow:none over a thin rgba(10,9,6,.65)->transparent
   wash, on unpredictable photography.  INK rig (the caption owns its scrim,
   so VIS is invalid here — it would delete the wash being measured).
   PRE minima 3.47 / 4.09 / 3.52 / 2.56 at 1440 and 5.11 / 9.31 / 4.63 / 3.69
   at 390 — "THE LIVING ROOM" over the near-white interior is the worst of
   them at both breakpoints.  POST: 6.86 / 7.54 / 7.25 / 5.86 at 1440 and
   12.69 / 14.73 / 12.74 / 12.96 at 390.
   The v29 .over recipe (cream + a TIGHT 4px shadow, never an 8px smear) plus
   a real bottom scrim of the same family as .dhero .scrim.  11px costs
   nothing here: the caption is absolutely positioned inside the figure, so
   the grid cannot reflow. */

/* v30 FIX O — belt and braces so the Grace dot can never orphan onto a line
   of its own again after the label is shortened.  CSS.supports('text-wrap',
   'balance') is true in this Chromium (asserted in pw_v30.js), so the
   balance rule is live rather than inert. */
.miles .k{text-wrap:balance}
.miles .k .gdot{white-space:nowrap}

/* v30 FIX R — .gal figure and .plans-zoom figure are now role=button
   tabindex=0; they need a visible focus ring, and the UA default outline is
   invisible against a photograph. */
/* end v30 */

/* ==========================================================================
   v31 — see /home/claude/swv/v31-report.md for the PRE/POST decode numbers
   behind every value below.  Every contrast figure quoted in these comments
   is a real-pixel decode MINIMUM (A/B screenshot pair, canvas decode,
   p92-of-nonzero-delta stroke-core threshold), never arithmetic, and every
   one is VIS (visibility:hidden suppression, the recon-comparable rig) at
   dSF 1 unless the line says otherwise.
   ========================================================================== */

/* v31 V3 — the wordmark and both nav links sit directly on whatever photograph
   the villa happens to own, and v29's 70px slim plate was too thin and too
   transparent to carry them.  PRE, desk1440 dSF1, four villas:
       2br-1    wordmark 5.76   THE IDEA 3.21   THE VILLAGE 3.01
       3br-1    wordmark 4.22   THE IDEA 3.66   THE VILLAGE 3.87
       2br-4    wordmark 4.26   THE IDEA 3.75   THE VILLAGE 3.40
       laurent  wordmark 5.47   THE IDEA 4.41   THE VILLAGE 3.38
   Ten of those twelve are under 4.5, and the measured background luminance
   under the nav swings 219.16 -> 241.08 across the four, so whether the brand
   name is readable depended on the photograph.  Pinning the top 56px to a
   fully opaque cream stop takes the photograph out of the calculation: the
   figures stop varying by villa.  Only the slim variant changes here — the
   non-slim .dhero::before (132px) is deliberately untouched. */

/* v31 V5 — the only pure-black text-shadows still rendering on the swept
   routes.  rgba(0,0,0,...) is not the ink token; rgba(10,9,6,...) is.  The
   base .scrim .line / .scrim .labels span rules are shared with the cover
   hero, the story stages and the mine-hero, so — as in v29 FIX A2b, which
   already did this for .dhero .scrim .over — the correction is scoped to
   .dhero .scrim (specificity 0,3,0 / 0,3,1, so it wins on both).  The second
   rule also drops opacity .92 -> 1 and tightens the blur 8px -> 4px; both were
   re-measured and neither costs anything. */
.dhero .scrim .line{text-shadow:0 1px 14px rgba(10,9,6,.45)}
.dhero .scrim .labels span{text-shadow:0 1px 4px rgba(10,9,6,.60);opacity:1}

/* v31 V6 — v30's FIX M scrim still over-reaches the text it exists to carry.
   The defect reproduces exactly as specified.  Scrim on vs scrim off, same
   photograph, per-CSS-row mean luminance of the 270px hero, desk1440 dSF1:
       route                        rows below y224 lifted >10 L   deepest
       2br-1  /gallery                    40 / 46                   y=263
       3br-1  /drawings                   45 / 46                   y=268
       anders /talk                       46 / 46                   y=269
       2br-4  /gallery                    34 / 46                   y=257
   The last text row ends at y=224.0 and the scrim's bottom is pinned to the
   hero's bottom (270) at EVERY width 701-1920, so that 46px gap is a constant.
   TWO SPEC DELTAS HERE, both forced by measurement — see v31-report.md.
   (1) The gradient is `to top`, so stop offsets are measured UP FROM THE
       SCRIM'S BOTTOM EDGE, which is the hero's bottom edge.  The band that
       over-reaches (hero y 224-269) is therefore gradient positions 0-46px —
       governed by the .12/0, .30/20px and .86/48px stops.  The spec's rule
       moved only the 186px stop to 172px and the 232px stop to 198px, i.e.
       hero rows 38-98, at the TOP of the hero, under the cream plate V3 adds.
       Measured with both builds' plates suppressed so the scrim edit is
       isolated: it moves rows y=39..226 (max 44.99 L at 3br-1) and only 2-3
       rows at y>=224 at all, none by more than 0.55 L, which is gradient
       dither.  Shipped verbatim it is a no-op on the defect it names.
   (2) So the correction is applied to the stops that actually govern the band,
       and EVERY STOP FROM 48px UPWARD IS BYTE-IDENTICAL TO v30's.  That is
       deliberate and checkable: the backing behind .over, .line and .labels is
       untouched by construction, and the five vis minimums measured identical
       to the shipped v31 build on all four routes (over 6.24-7.83, line
       12.72-13.97, labels 5.49-6.53).  Result on the defect:
       rows lifted >10 L  40->14 / 45->16 / 46->17 / 34->11, deepest row
       263->237 / 268->239 / 269->240 / 257->234, bottom row 6.45/9.27/11.33/
       5.43 -> 0.00 exactly.  The residual overhang is 10-16px, which is the
       same order as the mobile branch's 6-10px that v30 already accepts.
   (3) Scoped to min-width:701px, not the spec's 821px: FIX M itself splits at
       700/701 and the geometry is identical at 701, 820, 821, 1024 and 1440,
       so 821 would leave 701-820px carrying the uncorrected gradient.
   The mobile branch is deliberately untouched.  Judged on the render at all
   four villas: no banding at the ramp, and the foreground the scrim was
   flattening (ferns on 2br-1, pool deck on anders, planting on 3br-1) comes
   back. */

/* ============================================================
   SWV SKIN — secretwaterfallvillage.com
   Everything above was authored for the With Grace system: cream ground,
   ink type, terracotta accent, one serif. The SWV brand is none of those.
   The site is dark forest under a paper texture, sand type, sage accent,
   and it sets display in Cormorant against body copy in Montserrat.
   The :root values already carry the palette across; this block carries the
   things a colour swap cannot: the typeface split, the button shape, and the
   tracking. It sits last so it wins on source order.
   ============================================================ */

/* ---- type: Cormorant displays, Montserrat body, Lato field labels ---- */
body{font-family:var(--sans);font-weight:500;font-size:16px;line-height:1.6;letter-spacing:.01em}
h1,h2,h3,.display,.title,.lead,.nm,.vname,.vprice,.pack .nm,.teaser-num,
.scrim .line,.msg,.fab-bubble,#c-head .t .nm{font-family:var(--serif)}
/* the site sets its displays huge and leads them TIGHTER than their own size */
.display{font-size:clamp(50px,7.4vw,104px);line-height:.94;letter-spacing:0;font-weight:400}
.title{font-size:clamp(36px,5vw,80px);line-height:.98;font-weight:400}
h1,h2,h3{font-weight:400;line-height:1.0}
.lead{font-size:clamp(19px,1.9vw,26px);line-height:1.32;font-style:italic;font-weight:400;color:var(--terra)}
.body-l,p.body-l{font-family:var(--sans);font-weight:500;font-size:clamp(15px,1.1vw,16.5px);line-height:1.62}
.sec .over,.over{font-family:var(--sans);font-weight:400}
/* sage is the accent: the site colours the italic half of every heading */
em{color:var(--terra)}
.pack em,.scrim em{color:inherit}

/* ---- uppercase labels: Montserrat, the site's 3px tracking ---- */
.caps,.nav .brand,.btn,.over,.scrim .over,.vcard .vtag,
.schip,.quietlink{font-family:var(--sans);font-weight:400}
.nav .brand{letter-spacing:.34em;font-weight:400}
input,textarea,label{font-family:var(--label);font-weight:300}

/* ---- buttons: the site's shape — 3px radius, sage hairline, 3px tracking ---- */
.btn,.nav .mine-pill,.msg .act,.schip{
  border-radius:3px;font-family:var(--sans);font-weight:300;letter-spacing:.22em}
.btn{border:1px solid var(--terra);color:var(--paper);background:transparent;
  padding:14px 25px 15px;font-size:13px;line-height:19px}
/* .solid / .terra are the site's filled beige button */
.btn.solid,.btn.terra{
  background:var(--paper);border-color:var(--paper);color:var(--cream);font-size:13px}
.btn.ghost{padding:14px 25px 15px;font-size:13px;letter-spacing:.22em}
.nav .mine-pill{border-color:var(--terra)}

/* ---- THE button hover set. One block, two rules that matter, gated.

   Gated on (hover:hover) because a tap on a touch screen leaves :hover
   latched until the next tap somewhere else — that is the mechanism behind
   "buttons get the same colour as the background after clicking". Coarse
   pointers get :active feedback instead, which releases with the finger.

   --hover / --hover-on rather than #fff, because these rules also apply
   inside the bone scope, where a white fill under sand type rendered the
   label at 1.25:1. Hover is a direction (away from the ground), not a
   colour: white on forest, deep forest on bone. ---- */
@media(hover:hover){
  .btn:hover{
    background:transparent;color:var(--hover);border-color:var(--hover)}
  .btn.solid:hover,.btn.terra:hover{
    background:var(--hover);border-color:var(--hover);color:var(--hover-on)}
  .nav .mine-pill:hover{
    background:var(--hover);color:var(--hover-on);border-color:var(--hover)}
}
.btn:active,.nav .mine-pill:active{opacity:.72}

/* ---- the ground carries the site's paper texture over the forest ---- */
body{background-color:var(--cream)}
body::before{content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;
  opacity:.5;background-size:cover;background-position:center}
::selection{background:var(--terra);color:var(--cream)}

/* ---- surfaces that assumed a light page ---- */
.nav.scrolled{background:color-mix(in srgb,var(--cream) 94%, transparent);color:var(--paper)}
.nav.on-cream{color:var(--paper)}
.sec{color:var(--paper)}
.vcard{background:color-mix(in srgb,var(--paper) 5%, transparent);
  border-color:color-mix(in srgb,var(--paper) 20%, transparent);border-radius:3px;color:var(--paper)}
.vcard:hover{border-color:var(--terra);box-shadow:none;transform:translateY(-3px)}
.vcard.active{border-color:var(--terra);background:color-mix(in srgb,var(--terra) 14%, transparent)}
.vcard .vtag,.vcard .ytag b{color:var(--terra)}
.ethos-list li::before{background:var(--terra)}
.rule{background:color-mix(in srgb,var(--paper) 16%, transparent)}
/* the pack cards: the site would set these as moss / deep forest / sage */
.pack.stone{background:var(--stone)}
.pack.midnight{background:var(--midnight)}
.pack.terra{background:var(--terra);color:var(--cream)}
.pack.terra .caps,.pack.terra .nm,.pack.terra p{color:var(--cream)}
.footer,#foot{color:var(--paper)}

/* ---- inversion fallout: anything filled with sand needs forest type ----
   These were `background:var(--ink); color:var(--paper)` — dark chip, light
   label. Both tokens are sand now, so the label has to flip to the ground. */
.fab-bubble{background:var(--paper);color:var(--cream)}
.fab-bubble-x{color:var(--cream)}
.cbchip.active,.curchip button.active,.schip.active,
.styler .ftabs button.on,.styler .fpal button.on,.styler .frp-chip.on{
  background:var(--paper);color:var(--cream);border-color:var(--paper)}
#story .exit:hover{background:var(--paper);color:var(--cream)}

/* ---- the site's displays are leaded tighter than their own size, which
        clips ascenders on a two-line heading. Give the box the room back. ---- */
.display,.title{padding-top:.14em;padding-bottom:.06em}
h2.display,h2.title{overflow:visible}

/* ---- Grace's panel is already dark; keep it forest, not midnight-blue ---- */
#c-panel{background:var(--midnight)}
.msg .act{border-color:color-mix(in srgb,var(--paper) 55%, transparent);border-radius:3px}
.msg .act:hover{background:var(--paper);color:var(--midnight)}

/* ---- the SWV marks (secretwaterfallvillage.com uses the monogram in the
        nav) ---- */
.nav .brand{display:inline-flex;align-items:center;gap:11px}
.nav .brand-mono{height:26px;width:auto;display:block;flex:none}
@media(max-width:860px){ .nav .brand-mono{height:22px} }
@media(max-width:520px){ .nav .brand-mono{height:20px} }

/* ---- the Idea, laid out the way secretwaterfallvillage.com lays out its own
        invitation: the opener centred, the body in a narrow measure, and the
        pillars as open Cormorant type on the ground rather than boxed cards. ---- */

/* ---- the site's own outline buttons ---- */
.nav .brand-mono{height:44px}
@media(max-width:860px){
  .nav .brand-mono{height:34px}
}

/* ---- buttons: the site's exact .button metrics — Montserrat 300, 13px on a
        19px line, 3px of tracking, a 1px sage hairline and a 3px radius.
        300 is deliberate here; the small caps elsewhere need 400 to hold up
        on the dark ground, but the buttons are the site's own weight. ---- */
.btn{
  font-family:var(--sans);font-weight:300;font-size:13px;line-height:19px;
  letter-spacing:3px;text-transform:uppercase;border-radius:3px;
  border:1px solid var(--terra);color:var(--paper);background:transparent;
  padding:15px 25px 16px}
/* hover intentionally NOT redeclared here — see the single gated hover block
   above. A copy at this point in the file would outrank it by source order
   and re-break the bone ground. */

/* ---- the pillars, beat for beat as the site sets them: the name, an italic
        sage sub-line, the paragraph, then the list under its own small label ---- */

/* ---- shape: SWV is 3px, never a pill. The base rules were authored for the
        old brand and still carried 99px across the whole chrome — chips, the
        FAB, Grace's controls, the filters, the tap card. border-radius:50%
        stays where it is (dots and pins are meant to be circles). ---- */
.nav .mine-pill,#fac-cta,.chip,.rn-x,#editor-bar button,
#story .exit,#story-count,.pack .pick,.fac .fs,#c-fab,.msg .act,.c-capture button,
#c-chips button,#c-send,.cbhome,.cbchip,.curchip,.curchip button,
.schip,.styler .fpal button,.styler .ftabs button,
.styler .frp-chip,#lightbox button{border-radius:3px}
.film.waiting::after{border-radius:3px}

/* ---- the chapter bar sits on the dark band now, so its chips read as sand
        with a sage hairline, and the active one inverts to a sand fill ---- */
.cbhome,.cbchip{color:var(--paper);opacity:.82}
.cbhome:hover,.cbchip:hover{opacity:1;border-color:var(--terra)}
.cbchip.active{background:var(--paper);color:var(--cream);border-color:var(--paper);opacity:1}
.chbar{border-bottom:1px solid color-mix(in srgb,var(--paper) 14%, transparent)}

/* the multiplier glyph beside the headline number sat at 2.95:1 — nudged over
   the line without changing its quiet role */
.pgtl span{opacity:.72}

/* ---- the furniture page: palettes shown through their own rooms ---- */
.palgrid{display:flex;flex-direction:column;gap:64px}
.palcard-head{display:flex;align-items:center;gap:12px;margin-bottom:18px}
.palcard-nm{font-family:var(--serif);font-size:clamp(26px,2.6vw,34px);line-height:1}
.palcard-shots{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.palcard-shots figure{margin:0;position:relative;border-radius:3px;overflow:hidden;background:var(--well)}
.palcard-shots img{display:block;width:100%;height:100%;aspect-ratio:4/3;object-fit:cover}
.palcard-shots figcaption{position:absolute;left:0;right:0;bottom:0;padding:22px 10px 8px;
  font-size:9.5px;color:var(--paper);
  background:linear-gradient(to top,rgba(10,9,6,.72),transparent)}
@media(max-width:860px){ .palcard-shots{grid-template-columns:repeat(2,1fr)} }

/* ---- the pillar head, as the deck sets it: *With* in italic run straight into
        the word in roman caps, no space, both the same cream. The contrast is
        italic-vs-roman and lower-vs-upper case — NOT colour. ---- */

/* ============================================================
   THE SECOND GROUND — bone
   The deck alternates two grounds by the job of the page: feeling on forest,
   reading on bone. Redefining the palette custom properties inside this scope
   re-resolves every var() underneath it, so the whole page inverts from one
   place rather than a per-rule rewrite.
   ============================================================ */
body.on-bone #v-included,body.on-bone #v-materials,body.on-bone #v-journey,body.on-bone #v-details{
  --cream:#E8E6DA;        /* the ground */
  --paper:#2E4034;        /* type on it — deep forest */
  --ink:#2E4034;
  --terra:#3E5443;        /* the accent has to darken to hold on a light ground */
  --sun:#8A6A12;          /* brass darkens too — ~5.6:1 on the bone ground */
  --sun-soft:color-mix(in srgb,#D8B24A 30%, transparent);
  --sun-ink:#E8E6DA;
  --stone:#5A6B57;
  --well:#D6D4C6;         /* behind media */
  --midnight:#2E4034;
  --hover:#1D2415;        /* hover goes DOWN here, not up to white */
  --hover-on:#E8E6DA;
  /* less headroom on bone, so the muted tiers have to sit closer to full */
  --dim:color-mix(in srgb,var(--paper) 88%, transparent);
  --dim-soft:color-mix(in srgb,var(--paper) 76%, transparent);
  background:var(--cream);
  color:var(--paper);
}
/* hairlines were converted to sand for the dark ground; on bone they must
   come back down to the ink side or they vanish */
body.on-bone .rule{
  background:color-mix(in srgb,var(--paper) 20%, transparent)}
body.on-bone li{border-color:color-mix(in srgb,var(--paper) 18%, transparent)}
body.on-bone .ethos-list li::before,body.on-bone .jstep::before{background:var(--terra)}
/* .incl li::before is an em-dash glyph, not a rule — it takes colour, not a
   background, or it paints a block behind itself */
body.on-bone .incl li::before{color:var(--terra);opacity:.75}
body.on-bone .incl li{border-bottom-color:color-mix(in srgb,var(--paper) 16%, transparent)}
/* the page ground itself, behind the view */
body.on-bone{background:#E8E6DA}
body.on-bone::before{opacity:.34}
/* the nav rides the bone ground on those pages */
.nav.over-bone{color:#2E4034}
.nav.over-bone.scrolled{background:color-mix(in srgb,#E8E6DA 94%, transparent);color:#2E4034}
.nav.over-bone .brand-mono{filter:brightness(0) saturate(100%) invert(19%) sepia(18%) saturate(900%) hue-rotate(88deg)}
.nav.over-bone::before{opacity:0}
/* dark type at 50% washes out further on a light ground than light type does on
   a dark one — the spine needs more presence on bone to sit at the same weight */
body.on-bone .secspine{opacity:.68}
/* ---- the deck's paper: heavy grain with white dust flecks and the odd hair
        scratch, tiling across the whole field. Sits above the ground and below
        everything else. ---- */
body::after{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
  background-image:var(--speckle);background-repeat:repeat;background-size:420px 420px;opacity:.5;mix-blend-mode:screen}
body.on-bone::after{opacity:.22;mix-blend-mode:multiply;filter:invert(1)}
#app{position:relative;z-index:1}

/* ---- the deck's list: a round dot, and the keyword in bold ---- */

/* ---- the deck's slide frame: a big hairline number with a full stop, the
        section label beside it, a hairline rule under, and the section name
        set large and rotated up the left edge ---- */
.sec{position:relative}
.secnum{display:flex;align-items:baseline;gap:20px;margin-bottom:6px}
.secnum .n{font-family:var(--serif);font-weight:300;font-size:clamp(38px,4.6vw,64px);
  line-height:.9;color:var(--paper);opacity:.92}
.secnum .n::after{content:"."}
.secnum .l{font-family:var(--sans);font-weight:400;font-size:11px;letter-spacing:var(--ls);
  text-transform:uppercase;color:var(--dim)}
.secrule{height:1px;background:color-mix(in srgb,var(--paper) 18%, transparent);margin:0 0 46px}
.secspine{position:absolute;left:-88px;top:0;writing-mode:vertical-rl;transform:rotate(180deg);
  font-family:var(--serif);font-weight:400;font-size:clamp(30px,3.4vw,52px);line-height:1;
  text-transform:uppercase;color:var(--paper);opacity:.5;letter-spacing:.04em;
  pointer-events:none;user-select:none;white-space:nowrap}
@media(max-width:1180px){ .secspine{display:none} }

/* ---- the deck's photo treatment: two or three images overlapping at offsets,
        hard corners, no captions — a collage, not a grid ---- */
/* the pillars stack under the two-column breakpoint; the collage leads */

/* NOTE the selector: the original is `#lightbox .lb-hint`, so a single-class
   override here loses on specificity no matter how late it sits. Match the
   original weight or the rule silently does nothing. */
#lightbox .lb-hint{font-size:11px}
.palcard-shots figcaption{font-size:10.5px}

/* ---- the details pages on bone ----
   The chapter bar carries a hard-coded dark ground, so when the chips inverted
   to forest type they went dark-on-dark. The bar has to come across too. ---- */
body.on-bone .chbar{background:rgba(232,230,218,.96);
  border-bottom-color:color-mix(in srgb,#2E4034 16%, transparent)}
body.on-bone .cbhome,body.on-bone .cbchip{
  color:#2E4034;border-color:color-mix(in srgb,#2E4034 26%, transparent);opacity:.9}
body.on-bone .cbhome:hover,body.on-bone .cbchip:hover{
  opacity:1;border-color:#3E5443}
body.on-bone .cbchip.active{background:#2E4034;color:#E8E6DA;border-color:#2E4034;opacity:1}
body.on-bone .cbchip.visited::after{background:#3E5443}

/* dimmed labels that held up on forest wash out on bone — lift the floor */
body.on-bone .talklink,
body.on-bone .rt-proof-s{opacity:.92}
body.on-bone .schips-note{opacity:.85}

/* ---- text that sits on PHOTOGRAPHY keeps its light colour on both grounds.
   The gallery caption rides a dark gradient over the image, so inverting it
   with the page turned it dark-on-dark. Note the contrast sweep cannot see
   this: a gradient is not an opaque background, so the sweep measures the
   caption against the page ground and passes it. Only looking finds it. ---- */
body.on-bone .scrim .over,body.on-bone .scrim .line,body.on-bone .scrim .labels span,
body.on-bone .film-mid .over,body.on-bone .mine-hero .scrim *{color:#E6E1D1}

/* ============================================================
   MASTERPLAN SPOTLIGHT — quiet the surroundings, don't black them out
   The original shade ramped to rgba(10,9,6,.54), so focusing a villa dropped
   most of the render into near-dark. This keeps the map bright and pulls focus
   by DESATURATING the surroundings instead: the chosen villa keeps full colour
   while everything past it goes quiet and slightly cool. The warm bloom stays,
   softened, because it no longer has to fight a dark field.
   ============================================================ */
/* fallback for engines without backdrop-filter: the same idea as before, but a
   third of the darkness */
#spot-shade{
  background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),
    rgba(10,9,6,0) 0%, rgba(10,9,6,.02) var(--r1,7%),
    rgba(10,9,6,.13) var(--r2,26%), rgba(10,9,6,.20) 72%)}
/* the bloom can be gentler now that it is not competing with a dark field */
#spot-glow{
  background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),
    rgba(255,205,138,.22) 0%, rgba(255,188,112,.11) var(--r1,7%),
    rgba(255,176,96,0) var(--r2,26%))}

/* the colour drain. mix-blend-mode:saturation replaces the backdrop's
   saturation with this layer's — grey has none, so wherever this paints grey
   the map goes monochrome, and where it is transparent the map keeps its
   colour. The gradient IS the mask, so there is no mask property involved:
   backdrop-filter + mask measured as barely applying, this measures cleanly. */
#spot-desat{
  position:absolute;left:0;top:0;width:100%;height:100%;z-index:8;
  pointer-events:none;opacity:0;mix-blend-mode:saturation;
  transition:opacity .75s var(--ease);
  background:radial-gradient(circle at var(--sx,50%) var(--sy,50%),
    rgba(128,128,128,0) 0%, rgba(128,128,128,0) var(--r1,7%),
    rgba(128,128,128,.5) var(--r2,26%), rgba(128,128,128,.6) 100%)}
body.spot-on #spot-desat{opacity:1}

/* ---- the My-villa pill is the one persistent handle on a chosen villa, so it
        carries where it will take you: the villa, then the chapter left open ---- */
.nav .mine-pill{gap:0;display:inline-flex;align-items:center;white-space:nowrap}
.nav .mine-pill .mp-ch{position:relative;margin-left:9px;padding-left:10px;opacity:.8}
.nav .mine-pill .mp-ch::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
  width:1px;height:11px;background:currentColor;opacity:.45}
.nav .mine-pill.resumable::after{content:"→";margin-left:9px;opacity:.75}
@media(max-width:700px){ .nav .mine-pill .mp-ch{display:none} }

/* ---- the resume pill sits OUTSIDE #navlinks so it survives the burger
        collapse: on a phone it is the only way back to an unfinished villa,
        and it was hidden behind the menu. ---- */
.nav{position:relative}
.nav .nav-right{display:flex;align-items:center;gap:14px;margin-left:auto}
@media(min-width:961px){ .nav .nav-right{order:4} }
@media(max-width:960px){
  .nav .nav-right{gap:10px}
  .nav .mine-pill{padding:9px 14px 10px;font-size:11px}
}
/* a real touch target on coarse pointers */
@media(pointer:coarse){ .nav .mine-pill{min-height:44px;padding:11px 16px} }

/* buyer-given names can run to 40 characters; the pill truncates rather than
   pushing the burger off the bar */
.nav .mine-pill .mp-nm{max-width:16ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@media(max-width:700px){ .nav .mine-pill .mp-nm{max-width:12ch} }

/* ---- the configurator on bone: these labels sat at .5-.55 opacity, which held
        on the forest ground and washed out on parchment. The tick was a sand
        glyph inside a sand ring at 1:1 — invisible. ---- */
/* NOTE the depth: the originals are `.styler .fvtot .tier`, `.styler .froomhead
   .rn` and `.styler .fitem .ftop .cur` — three classes each. A two-class
   override loses on specificity however late it sits. */
/* .fvtot .tier is off this list: it sets itself back with --dim now, which is
   already ground-aware, and stacking .82 on top of it dropped it under AA. */
body.on-bone .styler .fpanel .rhead .rn,
body.on-bone .styler .fitem .ftop .cur{opacity:.82}
body.on-bone .tick{border-color:color-mix(in srgb,var(--paper) 40%, transparent);color:var(--paper)}
body.on-bone .fopt.on .tick{background:var(--paper);color:var(--cream);border-color:var(--paper)}

/* ============================================================
   MASTERPLAN — the map keeps its own light
   Anders: the render should not be dimmed, desaturated or bloomed. The pin on
   the house is the only thing that marks the chosen villa. The #spot layers
   stay in the DOM so the geometry code has something to write to, but they
   paint nothing.
   ============================================================ */
#spot-desat,#spot-shade,#spot-glow{display:none !important}

/* the pin now carries the whole highlight, so it earns a stronger mark */
html[data-alt="1"] .dot.picked .pt{
  box-shadow:0 0 0 3px var(--terra),
             0 0 0 8px color-mix(in srgb,var(--terra) 42%, transparent),
             0 0 22px 6px color-mix(in srgb,#FFCB8A 55%, transparent),
             0 2px 14px rgba(0,0,0,.55);
  transform:scale(1.18)}
html[data-alt="1"] .dot.picked{z-index:40}
.dot .pt{transition:box-shadow .35s var(--ease), transform .35s var(--ease)}

/* two blinks each, villas first and then facilities — never together */
@keyframes gblink2{
  0%,100%{box-shadow:0 0 0 1px color-mix(in srgb,var(--paper) 28%, transparent)}
  50%{box-shadow:0 0 0 2px var(--terra), 0 0 16px 2px color-mix(in srgb,var(--terra) 55%, transparent)}}
#chips-main.blink .gchip{animation:none}
#chips-main.blink2 .gchip[data-g="villas"]{animation:gblink2 .85s var(--ease) 2}
#chips-main.blink2 .gchip[data-g="facilities"]{animation:gblink2 .85s var(--ease) 2;animation-delay:1.7s}

/* the drawer is a bottom sheet on phones, so an open rail sat straight on top
   of the very chips that close it — the second tap never reached them */

/* on desktop the drawer is a left column, so the sub-filters and the legend
   move out from under it rather than being covered */

/* ---- the active chip was `background:var(--paper); color:var(--ink)` — a dark
        fill with a light label under the old brand. Both tokens are sand now,
        so the active Villas / Facilities / All-homes chips measured 1.00:1:
        completely invisible. It went unseen because these chips were hidden
        while cards mode had no chip UI. ---- */
.chip.active,#chips-main .gchip.active{background:var(--paper);color:var(--cream);
  border-color:var(--paper)}
/* end SWV skin */

/* ==========================================================================
   design/ split only — the same faces as the data-URI @font-face rules above,
   served from design/assets/fonts/ so a designer can open them in a type tool.
   Appended, not substituted: every rule above is untouched and still works.
   These are local files, so no network font request is introduced.
   ========================================================================== */
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:300;font-display:swap;src:url("assets/fonts/CormorantGaramond-300-normal.woff2") format("woff2");}
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:300;font-display:swap;src:url("assets/fonts/CormorantGaramond-300-italic.woff2") format("woff2");}
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:400;font-display:swap;src:url("assets/fonts/CormorantGaramond-400-normal.woff2") format("woff2");}
@font-face{font-family:'Cormorant Garamond';font-style:italic;font-weight:400;font-display:swap;src:url("assets/fonts/CormorantGaramond-400-italic.woff2") format("woff2");}
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:600;font-display:swap;src:url("assets/fonts/CormorantGaramond-600-normal.woff2") format("woff2");}
@font-face{font-family:'Jost';font-style:normal;font-weight:300;font-display:swap;src:url("assets/fonts/Jost-300-normal.woff2") format("woff2");}
@font-face{font-family:'Jost';font-style:normal;font-weight:400;font-display:swap;src:url("assets/fonts/Jost-400-normal.woff2") format("woff2");}
@font-face{font-family:'Montserrat';font-style:normal;font-weight:100 900;font-display:swap;src:url("assets/fonts/Montserrat-100_900-normal.woff2") format("woff2");}
@font-face{font-family:'Lato';font-style:normal;font-weight:300;font-display:swap;src:url("assets/fonts/Lato-300-normal.woff2") format("woff2");}
/* Satoshi, Secret Waterfall v1 only: see docs/decisions.md. Self hosted, ITF
   Free Font License, only these two weights (the Figma type system uses no
   others for UI text). */
@font-face{font-family:'Satoshi';font-style:normal;font-weight:400;font-display:swap;src:url("assets/fonts/Satoshi-400-normal.woff2") format("woff2");}
@font-face{font-family:'Satoshi';font-style:normal;font-weight:500;font-display:swap;src:url("assets/fonts/Satoshi-500-normal.woff2") format("woff2");}

/* ==========================================================================
   NAVIGATION, mobile pass.
   Three things were fighting at 390: the 24-character wordmark, the villa
   pill and the burger all claimed the 64px bar; the burger had no handler
   at all; and the map filters sat as a third layer of chrome over the card
   rail. One system per job now — the bar names the place, the sheet
   navigates, the rail filters itself.
   ========================================================================== */

/* --- the sheet: full height, forest ground, real tap targets ------------- */
@media(max-width:960px){
  /* the villa you have open is a place to go back to, so it leads the sheet
     instead of crowding the bar */
  /* the bar names the place and opens the sheet — nothing else */
  .nav{padding:0 8px 0 18px}
  .nav .brand{font-size:10.5px;letter-spacing:.2em}
  .nav .mine-pill{display:none}
  .burger{width:44px;height:44px;display:grid;place-items:center;padding:0;font-size:22px}
}

/* --- nav v2 fixes -------------------------------------------------------- */
/* the sheet's own chrome belongs to the sheet; the desktop bar has a pill */

/* ==========================================================================
   Only what you can buy earns a pin.
   Three near-identical dot states asked the reader to decode a legend before
   they could read the land. Available villas are marked; reserved and sold
   carry no pin at all.
   ========================================================================== */
html[data-alt="1"] .dot.reserved,html[data-alt="1"] .dot.sold{pointer-events:none}
html[data-alt="1"] .dot.reserved .pt,html[data-alt="1"] .dot.sold .pt{
  opacity:0;transform:translate(-50%,-50%) scale(.55);transition:opacity .35s,transform .35s}
html[data-alt="1"] .dot.reserved .pt::after,html[data-alt="1"] .dot.sold .pt::after{display:none}
/* with one pin state left, the available marker can carry full confidence */
html[data-alt="1"] .dot:not(.reserved):not(.sold):not(.amen) .pt{background:var(--terra);
  box-shadow:0 0 0 7px color-mix(in srgb,var(--terra) 26%, transparent),
             0 0 22px 4px color-mix(in srgb,var(--terra) 45%, transparent),0 1px 10px rgba(0,0,0,.5)}
html[data-alt="1"] .dot:not(.reserved):not(.sold):not(.amen) .pt::after{
  border-color:color-mix(in srgb,var(--terra) 70%, transparent)}

/* the nav sheet and the map layout switched at different widths, leaving a
   band with a desktop drawer and a phone burger */
@media(min-width:861px) and (max-width:960px){
  .nav .mine-pill{display:flex}
  .burger{display:none}
}

/* ==========================================================================
   DESKTOP pass — the map should be readable on its own, the drawer should
   be sized for the screen it is on, and the sold plots should not cost a
   third of the scroll to walk past.
   ========================================================================== */

/* the tag carries the numbers now, not just a name */
@media(min-width:861px){
  /* the base rule hides every tag; these are the moments it earns
     its place — chosen, or hovered directly */
  html[data-alt="1"] .dot.picked .tag,
  html[data-alt="1"] .dot:hover .tag{display:block;opacity:1;pointer-events:none;
    transform:translate(calc(-50% + var(--tagdx,0px)),0)}
  html[data-alt="1"] .dot.picked .tag .nm,
  html[data-alt="1"] .dot:hover .tag .nm{font-family:var(--serif);font-size:15px;
    letter-spacing:0;text-transform:none;font-weight:400}
}

/* ==========================================================================
   NAVIGATION — one bar, one way in, at every width.
   The bar had a wordmark, two links, a villa pill and a burger competing in
   64px. Now it holds the name and a two-rule mark; the menu comes down from
   the top as a full-width plate on the forest ground. Same behaviour on a
   phone and a desktop, so there is one thing to learn.
   ========================================================================== */
.nav{display:flex;align-items:center;justify-content:space-between;padding:0 22px}
.nav .brand{font-size:11px;letter-spacing:.24em}
.nav .mine-pill{display:none}

.burger{display:flex;flex-direction:column;justify-content:center;gap:5px;
  width:44px;height:44px;padding:0 9px;align-items:stretch;background:none;border:none;cursor:pointer}
.burger i{display:block;height:1px;background:currentColor;transform-origin:50% 50%;
  transition:transform .38s var(--ease),opacity .38s var(--ease)}

/* the plate drops from under the bar at every width */
.nav{z-index:601}

@media(max-width:860px){
  .nav{padding:0 14px 0 18px}
}
@media(prefers-reduced-motion:reduce){.burger i{transition:none}
}

/* ==========================================================================
   PERFORMANCE — the map was asking the compositor for more than it could
   give: 28 blurred tag backdrops, both pin layers alive at once, and the
   idle layer still pulsing. None of it was visible work.
   ========================================================================== */
/* a near-solid plate reads the same over a photograph as a blurred one, and
   costs nothing per frame. 28 of the 31 backdrop filters on this page. */
.dot .tag{backdrop-filter:none;background:color-mix(in srgb,var(--well) 94%, transparent)}

/* the hidden layer is hidden: no paint, no hit-testing, no animation */
#hotspots{transition:opacity .35s var(--ease)}
/* only the plots you can still buy breathe; the rest were animating unseen */
.dot.sold .pt::after,.dot.reserved .pt::after,.dot.amen .pt::after{animation:none}
@media(prefers-reduced-motion:reduce){.dot .pt::after{animation:none}}

/* the drop-down plate inherited the old inline bar's offset and sat 24px
   short of the left edge, out of line with the bar above it */

/* the back arrow was pinned to the old 370px drawer and now sits under the
   wider one */

/* ==========================================================================
   WALKING INTO A VILLA — the camera pushes into the plot until the village
   fills the frame, then the villa's own pictures take over. Leaving reverses
   the same move, so you come back standing on the plot you left.
   ========================================================================== */

/* MAP_FLYIN, mounted only for the run of the clip: full bleed above the map
   (#map-wrap carries no z-index of its own) and above #story, which is still
   invisible at this point since swvCrossfadeToStory has not opened it yet.
   Its own opacity is toggled from engine.js once canplay says a frame is
   actually there — this transition is the reveal, not a fade the map does. */
#swv-flyin{position:fixed;inset:0;z-index:150;width:100%;height:100%;
  max-width:none;object-fit:cover;transition:opacity .15s linear}

#story{position:fixed;inset:0;z-index:80;opacity:0;pointer-events:none;
  transition:opacity .6s var(--ease);background:var(--well)}

#story .exit{opacity:0;transition:opacity .5s var(--ease) .2s}
#story-cue,#story-count{opacity:0;transition:opacity .5s var(--ease)}
#story-count{backdrop-filter:none;background:color-mix(in srgb,var(--well) 82%, transparent)}
#story .exit{backdrop-filter:none;background:color-mix(in srgb,var(--well) 78%, transparent)}
@media(max-width:860px){
  #story .exit{top:14px;left:14px;font-size:10.5px;padding:8px 15px 9px}
}
@media(prefers-reduced-motion:reduce){
  #story,#story .exit,#story-cue,#story-count{transition:none}
}

/* the cue and the counter sat centred, straight through the left-aligned
   caption; the caption owns the bottom-left, so they move to the corner */

/* the walk-through scrolls the way the rest of the village does: each image
   holds on a sticky stage while its caption reads over it, and you leave it
   by scrolling on — no snapping, no paging */

/* ==========================================================================
   The walk-through scrolls the PAGE, like every other chapter of the
   village — an inner overflow box scrolls differently under a wheel and a
   thumb, and that is what felt wrong. The map stays fixed behind it.
   ========================================================================== */
#story{position:relative;inset:auto;z-index:80;background:var(--well);
  display:none;opacity:1;pointer-events:auto}
#story .exit{opacity:1}

/* the story is the page while it is open. The nav is sticky, so it still
   held a 64px line of flow above it — the one thing left in the document. */

/* ==========================================================================
   VILLA DETAILS — the 1b design, on the site's own ground.
   1b's structure (image hero, spec tiles, go-deeper panel, next-chapter
   block) with the kit's terracotta rather than the exploration's gold.
   ========================================================================== */

/* the chapter header is a picture of the villa, not a line of text over bone */
.dhead.dhero{position:relative;min-height:66vh;min-height:66dvh;display:flex;
  flex-direction:column;justify-content:flex-end;padding:0;overflow:hidden;
  background:var(--well)}
.dhero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.dhero-veil{position:absolute;inset:0;background:linear-gradient(to bottom,
  rgba(10,9,6,.5) 0%,rgba(10,9,6,.12) 22%,rgba(10,9,6,.42) 52%,
  rgba(10,9,6,.8) 78%,var(--cream) 100%)}
.dhero-in{position:relative;display:flex;flex-direction:column;gap:14px;
  padding:clamp(70px,10vh,110px) clamp(22px,5vw,64px) clamp(26px,4vh,40px);max-width:60em}
.dhead.dhero .dhead-line{font-family:var(--serif);font-weight:300;
  font-size:clamp(42px,7.4vw,82px);line-height:1.02;color:var(--paper);
  text-shadow:0 1px 20px rgba(10,9,6,.5);margin:0}
.dhero-price{display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;margin-top:2px}
.dhero-price .k{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:color-mix(in srgb,var(--paper) 72%, transparent)}
.dhero-price .v{font-family:var(--serif);font-weight:300;
  font-size:clamp(28px,4vw,42px);line-height:1;color:var(--terra)}
.dhero-price .a{font-family:var(--sans);font-size:10.5px;
  color:color-mix(in srgb,var(--paper) 56%, transparent)}
@media(max-width:700px){.dhead.dhero{min-height:60vh;min-height:60dvh}}

/* the specification reads as a set of tiles, one fact each */
.facts.spectiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:8px}
.facts.spectiles>div{border:1px solid color-mix(in srgb,var(--ink) 14%, transparent);
  border-radius:3px;padding:16px 16px 17px;display:flex;flex-direction:column;gap:6px;
  background:color-mix(in srgb,var(--paper) 32%, transparent)}
.facts.spectiles .k{font-family:var(--sans);font-size:9px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;opacity:.62;margin:0}
.facts.spectiles .v{font-family:var(--serif);font-weight:300;font-size:22px;line-height:1.15;margin:0}
.facts.spectiles .v.is-rs{color:var(--terra)}
.facts.spectiles .v small{font-size:14px;opacity:.7}

/* the go-deeper panel — one accented block per chapter, always Grace */
.godeep{margin-top:40px;display:flex;flex-direction:column;gap:14px;align-items:flex-start;
  border:1px solid color-mix(in srgb,var(--terra) 34%, transparent);border-radius:3px;
  padding:clamp(20px,3vw,28px);background:color-mix(in srgb,var(--terra) 5%, transparent)}
.gd-k{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--terra)}
.gd-p{font-family:var(--serif);font-weight:300;font-size:21px;line-height:1.45;
  margin:0;max-width:26em;text-wrap:pretty}
.gd-p.big{font-size:clamp(21px,3vw,27px);line-height:1.35}
/* em is emphasis. Dimming it to .82 put the line at 4.34:1 and said the
   opposite of what the tag means — the italic already carries the stress. */
.gd-p em{font-style:italic}
.gd-btn{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls);
  text-transform:uppercase;font-weight:500;color:var(--cream);background:var(--terra);
  border:1px solid var(--terra);border-radius:3px;padding:15px 22px 16px;cursor:pointer;transition:.25s}
.gd-btn:hover{background:var(--ink);border-color:var(--ink);color:var(--paper)}

/* the end of a chapter is a door to the next one, not a link */
.nextblock{display:flex;flex-direction:column;gap:9px;align-items:flex-start;margin-top:26px}
.nb-k{font-family:var(--sans);font-size:9.5px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--dim)}
.nb-h{font-family:var(--serif);font-weight:300;font-size:clamp(30px,5.4vw,44px);
  line-height:1.1;text-align:left;background:none;border:0;padding:0;cursor:pointer;
  color:inherit;transition:color .25s}
@media(hover:hover){.nb-h:hover{color:var(--hover)}}
.nb-h.as-text{cursor:default;max-width:22em;text-wrap:pretty}
.nb-p{font-family:var(--serif);font-weight:300;font-size:18px;line-height:1.55;
  color:var(--dim);margin:2px 0 6px;max-width:28em;text-wrap:pretty}
.nextblock .talklink{margin-top:10px}

/* v28 — the hero sits on a photograph, so it is dark ground no matter what
   the page around it is. On #v-details the kit flips --paper/--terra to ink
   colours for the bone ground; the hero must not inherit them. Literals. */
.dhead.dhero{background:#11150C}
.dhead.dhero .dhead-line{color:#E6E1D1}
.dhero-price .k{color:rgba(230,225,209,.72)}
.dhero-price .v{color:#949E86}
.dhero-price .a{color:rgba(230,225,209,.56)}
/* and the spec tile is a light card on bone, not a 32% ink wash */
.facts.spectiles>div{background:color-mix(in srgb,#FFFFFF 46%, transparent)}
.facts.spectiles .k{opacity:.78}

/* v28.1 — the spec tile label is a label like any other on this page: full
   opacity, the kit's 11.5px, not a dimmed 9px exception. */
.facts.spectiles .k{font-size:11.5px;opacity:1}

/* ==========================================================================
   VILLA DETAILS — direction 1b, "Cinema".
   Forest ground, image-led: the render is the page and the type lives in the
   scrim. Brass carries the price and the live chapter; everything else is
   sand on forest. Supersedes the bone-ground pass above.
   ========================================================================== */
.dhead.dhero{min-height:0;height:clamp(430px,58vh,620px);background:#11150C}
.dhero-veil{background:linear-gradient(to bottom,
  rgba(17,21,12,.55) 0%,rgba(17,21,12,.08) 34%,rgba(17,21,12,.72) 74%,var(--cream) 100%)}
.dhero-top{position:absolute;top:0;left:0;right:0;z-index:2;display:flex;align-items:center;
  justify-content:space-between;gap:12px;padding:18px clamp(22px,4vw,40px)}
.dhero-mark{height:30px;width:auto;opacity:.9}
.dhero-in{gap:12px;padding:0 clamp(22px,4vw,40px) clamp(24px,3.4vh,34px)}
.dhero-k{font-family:var(--sans);font-size:9.5px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:var(--sun)}
.dhead.dhero .dhead-line{font-size:clamp(38px,6vw,64px);line-height:1.06}
.dhero-price{gap:12px;margin-top:4px}
.dhero-price .v{color:var(--sun);font-size:clamp(24px,3.2vw,34px)}

/* the three quiet plates the direction opens on */
.plates{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:0 0 30px}
.plates>div{background:var(--midnight);border:1px solid rgba(230,225,209,.1);
  border-radius:3px;padding:16px 14px 17px;display:flex;flex-direction:column;gap:6px}
.pl-k{font-family:var(--sans);font-size:9.5px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:rgba(230,225,209,.72)}
.pl-v{font-family:var(--serif);font-weight:300;font-size:clamp(24px,3vw,30px);line-height:1}
.pl-v small{font-size:.52em}
.pl-v.brass{color:var(--sun)}
.pl-s{font-family:var(--sans);font-size:9.5px;line-height:1.5;color:rgba(230,225,209,.58)}
@media(max-width:520px){.plates{gap:7px}.plates>div{padding:13px 11px}}

/* brass on the live chapter, sage stays the general accent */
.chbar .cbchip.active{background:var(--sun);color:#1D2415;border-color:var(--sun)}
.chbar .cbchip.active:hover{background:var(--paper);color:#1D2415;border-color:var(--paper)}

/* the go-deeper panel and the spec tiles return to forest ground */
.godeep{border-color:rgba(216,178,74,.3);background:var(--midnight)}
.gd-k{color:var(--sun)}
.gd-btn{background:var(--sun);border-color:var(--sun);color:#1D2415}
.gd-btn:hover{background:var(--paper);border-color:var(--paper);color:#1D2415}
.facts.spectiles>div{background:var(--midnight);border-color:rgba(230,225,209,.12)}
.facts.spectiles .k{color:rgba(230,225,209,.7);opacity:1;font-size:9.5px;letter-spacing:var(--ls-wide)}
.facts.spectiles .v{color:var(--paper)}
.facts.spectiles .v.is-rs{color:var(--sun)}

/* v29 — the mark is a mark, not the render: the full-bleed rule is scoped to
   the hero image only, so .dhero-mark keeps its own box. */
.dhead.dhero .dhero-mark{position:static;inset:auto;width:auto;height:30px;
  object-fit:contain;flex:none}

/* v29 — Cinema pins the chapter bar to the BOTTOM of the frame, on moss:
   the chapters are a foot to the page, not a header. */
.dhead.dhero + .chbar{position:sticky;top:auto;bottom:0;z-index:34;
  background:var(--stone);border-top:1px solid rgba(230,225,209,.12);
  border-bottom:0;padding:14px clamp(22px,4vw,40px);backdrop-filter:none}
.dhead.dhero + .chbar .chbar-scroll{gap:8px}
.dhead.dhero + .chbar .cbchip{flex:1;justify-content:center;text-align:center}
/* the last thing in a chapter must clear the pinned bar */
.dhead.dhero ~ .sec{padding-bottom:clamp(96px,14vh,140px)}

/* ==========================================================================
   THE NUMBERS, in Cinema.
   Two grounds only: forest for the page, midnight for anything that is a
   plate. Brass is reserved — the headline return, the sales price, the live
   scenario, the currency in play. Everything else is sand and sage.
   ========================================================================== */

/* the three bands stop being tinted washes and become plates, like the
   spec tiles and the opening three */

/* the proof panel was an inverted sand card — on forest that painted sand
   type on a sand ground. One ground, one direction. */
#v-details .rt-proof{background:var(--midnight);color:var(--paper);
  border-top:1px solid rgba(230,225,209,.12);border-bottom:1px solid rgba(230,225,209,.12)}
#v-details .rt-proof-head,#v-details .rt-proof-n,#v-details .rt-proof-close{color:var(--paper)}
#v-details .rt-proof-n{color:var(--sun)}

/* the 30-year chart keeps its dark moment, but has to declare an edge now
   that the page around it is dark too */

/* the donut was mixed for a bone ground: midnight and stone vanish on
   forest. Re-cut the wheel in sand → sage → moss, brass on the margin. */
#v-details .dseg[data-seg="build"]{fill:#E6E1D1}
#v-details .dseg[data-seg="own"]{fill:#949E86}
#v-details .dseg[data-seg="shared"]{fill:#5A6B57}
#v-details .dseg[data-seg="community"]{fill:#C9B489}
#v-details .dseg[data-seg="margin"]{fill:#D8B24A}
#v-details .bd-row[data-seg="build"] .bd-sw{background:#E6E1D1 !important}
#v-details .bd-row[data-seg="own"] .bd-sw{background:#949E86 !important}
#v-details .bd-row[data-seg="shared"] .bd-sw{background:#5A6B57 !important}
#v-details .bd-row[data-seg="community"] .bd-sw{background:#C9B489 !important}
#v-details .bd-row[data-seg="margin"] .bd-sw{background:#D8B24A !important}
#v-details .dprice-v{color:var(--sun)}
/* --dim-soft is paper at 76% on bone, which lands these two at 4.48:1 against
   a 4.5 bar — under, by a rounding error's width. Both are small text, so they
   take --dim (88%) like every other small label on this ground. */
#v-details .dprice-k,#v-details .dprice-alt{color:var(--dim)}
#v-details .dhole-caps{fill:rgba(230,225,209,.62)}

/* brass, used four times and no more */
#v-details .rt-hero-num{color:var(--sun)}
#v-details .bd-total .v{color:var(--sun) !important}
#v-details .curchip button.active{background:var(--sun);color:#1D2415}
#v-details .schip.active{background:var(--sun);color:#1D2415;border-color:var(--sun)}
#v-details .qtuned{border-left-color:var(--sun)}
#v-details .qtuned .qt-k{color:var(--sun)}
#v-details .pgtiles>div:first-child .pgtv{color:var(--sun)}

/* compare-the-six cards were a light wash — plates here like everything else */
#v-details .vcard{background:var(--midnight);border-color:rgba(230,225,209,.12)}
#v-details .vcard.active{background:rgba(216,178,74,.07);border-color:rgba(216,178,74,.5)}
#v-details .vcard .vtag,#v-details .vcard .ytag b{color:var(--sun)}

/* the founders' note is the one warm aside — sage, on the page ground */
#v-details .fnote{background:rgba(148,158,134,.07);
  border-left-color:rgba(148,158,134,.55)}

/* the P&L and the payment path read as ledgers on forest, not cards */#v-details .miles{color:var(--paper)}
#v-details .pathmiles .pct{color:var(--sun)}

/* ==========================================================================
   THE NUMBERS — the spine.
   The chapter is three arguments, not twenty modules: a sticky line that
   keeps the price and the live scenario in view, a numbered mark opening
   each band, and one panel holding every slider.
   ========================================================================== */
#v-details .nsticky{position:sticky;top:0;z-index:28;display:flex;align-items:baseline;
  gap:14px;flex-wrap:wrap;margin:0 0 26px;padding:13px clamp(16px,2.4vw,22px) 14px;
  background:color-mix(in srgb,var(--midnight) 94%, transparent);backdrop-filter:blur(10px);
  border:1px solid rgba(230,225,209,.12);border-radius:4px}
.ns-p{font-family:var(--serif);font-weight:300;font-size:23px;line-height:1;color:var(--sun)}
.ns-v,.ns-s{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:rgba(230,225,209,.72)}
.ns-s{color:rgba(230,225,209,.5)}

/* each band opens by saying which of the three arguments it is */

/* every slider in one instrument, income above the exit */

/* v30 — rupiah is the page's currency; the dollar is a question you ask.
   The compare link is a quiet ask, not a second toggle. */
.ns-price{display:flex;flex-direction:column;gap:3px}

/* one hero number, one paragraph carrying the rest of the ladder */
.rt-hero.one{grid-template-columns:1fr;gap:0;margin-bottom:44px}
.rt-hero.one .rt-hero-sub{max-width:52ch}
.rt-hero.one .rt-hero-sub b{font-family:var(--serif);font-weight:400;
  font-size:1.12em;color:var(--paper)}

/* the payment path closes the cost band rather than owning one of its own */
#v-details .miles.pathmiles{margin-top:22px}

/* v31 — the hero owns net yield, so the ledger row is the three things it
   does not say. Three columns, not four. */
#v-details .pgtiles{grid-template-columns:repeat(3,1fr)}
@media(max-width:760px){#v-details .pgtiles{grid-template-columns:1fr 1fr}}

/* ==========================================================================
   VILLA DETAILS — sand ground.
   The kit already owns the light-ground flip (body.on-bone #v-details) and
   its contrast compensations; the body carries it again. What follows is
   only what the kit has no rule for: the pieces this design added, and the
   two places that must stay DARK ground because they sit on a photograph.
   ========================================================================== */

/* --- over the render: dark ground, whatever the page is --- */
#v-details .dhero-k{color:#D8B24A}
#v-details .dhero-price .v{color:#D8B24A}
#v-details .dhero-price .k{color:rgba(230,225,209,.75)}
#v-details .dhero-veil{background:linear-gradient(to bottom,
  rgba(17,21,12,.55) 0%,rgba(17,21,12,.08) 34%,rgba(17,21,12,.72) 74%,var(--cream) 100%)}

/* --- the chapter foot stays forest: a page needs a bottom --- */
#v-details .chbar{background:#1D2415}
#v-details .chbar .cbchip,#v-details .chbar .cbhome{
  color:#E6E1D1;border-color:rgba(230,225,209,.28)}
#v-details .chbar .cbchip.active{background:#D8B24A;color:#1D2415;border-color:#D8B24A}

/* --- pieces the kit has no rule for --- */
#v-details .plates>div,#v-details .facts.spectiles>div{
  background:color-mix(in srgb,#FFFFFF 44%, transparent);
  border:1px solid color-mix(in srgb,var(--ink) 15%, transparent)}
#v-details .pl-k,#v-details .pl-s,#v-details .facts.spectiles .k{color:var(--ink);opacity:.92}
#v-details .pl-v,#v-details .facts.spectiles .v{color:var(--ink);opacity:1}
#v-details .pl-v.brass,#v-details .facts.spectiles .v.is-rs{color:var(--sun)}
#v-details .nsticky{position:sticky;top:0;z-index:28;
  background:color-mix(in srgb,var(--cream) 94%, transparent);
  border:1px solid color-mix(in srgb,var(--ink) 15%, transparent)}
#v-details .ns-p,#v-details .rt-hero-num,
#v-details .pathmiles .pct,#v-details .qt-k{color:var(--sun)}
#v-details .ns-v,#v-details .ns-s{color:var(--ink);opacity:.92}
#v-details .godeep{background:color-mix(in srgb,var(--sun) 7%, transparent);
  border:1px solid color-mix(in srgb,var(--sun) 40%, transparent)}
#v-details .gd-k{color:var(--sun)}
#v-details .gd-btn{background:var(--sun);border-color:var(--sun);color:var(--cream)}
#v-details .gd-btn:hover{background:var(--ink);border-color:var(--ink);color:var(--cream)}

/* --- the wheel, cut for paper --- */
#v-details .dseg[data-seg="build"]{fill:#2E4034}
#v-details .dseg[data-seg="own"]{fill:#5A6B57}
#v-details .dseg[data-seg="shared"]{fill:#9AA894}
#v-details .dseg[data-seg="community"]{fill:#C2B183}
#v-details .dseg[data-seg="margin"]{fill:#8A6A12}
#v-details .bd-row[data-seg="build"] .bd-sw{background:#2E4034 !important}
#v-details .bd-row[data-seg="own"] .bd-sw{background:#5A6B57 !important}
#v-details .bd-row[data-seg="shared"] .bd-sw{background:#9AA894 !important}
#v-details .bd-row[data-seg="community"] .bd-sw{background:#C2B183 !important}
#v-details .bd-row[data-seg="margin"] .bd-sw{background:#8A6A12 !important}
#v-details .dhole-caps{fill:var(--ink);fill-opacity:.7}
#v-details .bd-total .v{color:var(--sun) !important}

/* --- brass on the live scenario, and the two 1.0-ratio bugs --- */
#v-details .schip.active{background:var(--sun);color:var(--cream);border-color:var(--sun)}
#v-details .qtuned{border-left-color:var(--sun)}
#v-details .vcard .vtag,#v-details .vcard .ytag b{color:var(--sun)}
#v-details .rt-hero-num .pct{color:var(--sun)}
#v-details .styler .fopt.on .pr,#v-details .styler .fopt.on .nm{color:var(--cream)}#v-details .vgridnote,#v-details .rn,#v-details .cur,
#v-details .nb-k,#v-details .quietlink,#v-details .gdot{opacity:1;color:var(--ink)}
#v-details .quietlink,#v-details .talklink{color:var(--terra)}
#v-details .gdot{border-color:color-mix(in srgb,var(--ink) 42%, transparent)}

/* v32 — sand ground, the leftovers.
   Four places kept a dark-ground fill or literal after the flip. */

/* compare-the-six: --midnight is ink on this ground, so the card was
   painting forest under forest text */
#v-details .vcard{background:color-mix(in srgb,var(--ink) 5%, transparent);
  border-color:color-mix(in srgb,var(--ink) 16%, transparent)}
#v-details .vcard.active{background:color-mix(in srgb,var(--sun) 8%, transparent);
  border-color:color-mix(in srgb,var(--sun) 55%, transparent)}

/* a selected furnishing option is ink-filled, so its text is cream */
#v-details .styler .fopt.on{background:var(--ink);border-color:var(--ink)}
#v-details .styler .fopt.on .nm,#v-details .styler .fopt.on .pr,
#v-details .styler .fopt.on .tick{color:var(--cream)}

/* gallery captions were never inverted */
#v-details .plans-grid figcaption{color:var(--ink);opacity:.92}

/* the conversion note is the whole point of the compare — sage was a
   dark-ground colour here */
#v-details .bd-total .v small{color:var(--terra) !important;opacity:1}

/* the dimmed label family carries its alpha in the colour, so the kit's
   opacity compensation cannot reach it — restate at a legible weight */
#v-details .schips-note,#v-details .pgtsub,#v-details .pgtl,#v-details .pnlhead,
#v-details .rt-hero-over,#v-details .rt-hero-sub,#v-details .pnl-val.pnl-cost,
#v-details .vgridnote,#v-details .pl-s{color:color-mix(in srgb,var(--ink) 86%, transparent)}

/* ==========================================================================
   v33 — one cause: the brass token and the kit's dim-label idiom were both
   calibrated on the dark ground, where a 62% alpha and a .7 opacity still
   read. On paper they land at 3.0–4.1:1. Fix the token and the floor once,
   inside the light-ground scope, rather than element by element.
   ========================================================================== */
body.on-bone #v-included,body.on-bone #v-materials,body.on-bone #v-journey,
body.on-bone #v-details{--sun:#6F5410;--sun-soft:color-mix(in srgb,#6F5410 22%, transparent)}

/* the dim family: one floor, both mechanisms */
#v-details .facts .sub,#v-details .fld span,#v-details .bd-name .d,#v-details .vspec,
#v-details .smallprint,#v-details .fnoncap,#v-details .frnote,#v-details .qt-note{
  color:color-mix(in srgb,var(--ink) 86%, transparent);opacity:1}
#v-details .vcard .vmeta div small,#v-details .pnl-lbl small,
#v-details .miles .amt,#v-details .facts .v small,#v-details .facts .k,
#v-details .vcard .vmeta,#v-details .ytag small{
  opacity:1;color:color-mix(in srgb,var(--ink) 86%, transparent)}

/* text sitting ON brass takes the page ground, which is lighter than cream */
#v-details .gd-btn,#v-details .schip.active,#v-details .curchip button.active{
  color:#FBFAF4}

/* v34 — the hero no longer dissolves into paper: it darkens into the green
   chapter bar, so the render and the menu read as one head to the page. */
#v-details .dhero-veil{background:linear-gradient(to bottom,
  rgba(17,21,12,.55) 0%,rgba(17,21,12,.06) 30%,rgba(17,21,12,.62) 68%,
  rgba(29,36,21,.92) 90%,#1D2415 100%)}
#v-details .dhead.dhero + .chbar{position:sticky;top:var(--nav-h);bottom:auto;
  background:#1D2415;border-top:0;
  border-bottom:1px solid rgba(230,225,209,.14)}
#v-details .dhead.dhero ~ .sec{padding-bottom:clamp(56px,8vh,90px)}

/* v34.1 — the nav is position:relative on this page (a later .nav rule wins),
   so it scrolls away and a var(--nav-h) offset leaves an uncovered band with
   content sliding through it. The bar is the page's green head: pin it flush. */
#v-details .dhead.dhero + .chbar{top:0}

/* v35 — nothing washes the top of the render: the veil starts clear and only
   darkens where the type needs it. */
#v-details .dhero-veil{background:linear-gradient(to bottom,
  transparent 0%,transparent 34%,rgba(17,21,12,.58) 68%,
  rgba(29,36,21,.92) 90%,#1D2415 100%)}
#v-details .dhero::before,#v-details .dhead.dhero::before{display:none}#v-details .dhead.dhero .nav::before{opacity:0}

/* ==========================================================================
   v36 — four passes over the sand design.
   ========================================================================== */

/* 3 — one dark landmark in a very long sand scroll. The founders' note is
   the argument the page turns on, so it is the moment that goes forest. */
#v-details .fnote{background:#1D2415;color:#E6E1D1;border-left:0;
  border-radius:6px;padding:clamp(30px,3.6vw,52px) clamp(24px,3.2vw,46px);
  max-width:none}
#v-details .fnote .over{color:#D8B24A}
#v-details .fnote h3{color:#E6E1D1;font-size:clamp(26px,3.2vw,38px);
  font-weight:300;line-height:1.15;margin-bottom:.5em;max-width:20ch}
#v-details .fnote p{color:rgba(230,225,209,.86);max-width:56ch}
#v-details .fnote em{color:#D8B24A;font-style:italic}
#v-details .fnote .fnote-close{color:#E6E1D1;font-style:italic;
  border-top:1px solid rgba(230,225,209,.18);padding-top:20px;margin-top:24px}
#v-details .fnote .fnote-sign{color:rgba(230,225,209,.6);margin-top:18px}

/* 4 — the two numbers people actually compare on carry the row */
#v-details .facts.spectiles{grid-template-columns:repeat(6,1fr)}
#v-details .facts.spectiles>div{grid-column:span 2}
#v-details .facts.spectiles>div:nth-child(3),
#v-details .facts.spectiles>div:nth-child(4){grid-column:span 3;
  background:color-mix(in srgb,#FFFFFF 70%, transparent)}
#v-details .facts.spectiles>div:nth-child(3) .v,
#v-details .facts.spectiles>div:nth-child(4) .v{font-size:clamp(30px,3.4vw,42px)}
@media(max-width:820px){
  #v-details .facts.spectiles{grid-template-columns:repeat(2,1fr)}
  #v-details .facts.spectiles>div,
  #v-details .facts.spectiles>div:nth-child(3),
  #v-details .facts.spectiles>div:nth-child(4){grid-column:span 1}
  #v-details .facts.spectiles>div:nth-child(3),
  #v-details .facts.spectiles>div:nth-child(4){grid-column:span 2}
}

/* 5 — the gallery and the plans are really openable now */#v-details .plans-grid figure{cursor:zoom-in}
#lightbox .lb-cap{max-width:70ch;margin:0 auto;padding:0 24px}

/* v36.1 — Plot and Built span 3 of 6, so they only tile without holes if
   they lead the block. Order them first; the six equal tiles fill two
   clean rows of three behind them. */
#v-details .facts.spectiles>div:nth-child(3),
#v-details .facts.spectiles>div:nth-child(4){order:-1}

/* ==========================================================================
   v37 — one top bar, not two.
   The white nav is gone: the render carries the mark, and everything the nav
   held moves behind a two-rule burger. The green chapter bar is the only
   fixed chrome on the page.
   ========================================================================== */
#v-details .dhead.dhero{padding-top:0}
#v-details .dhero-top{padding-top:22px}
.dhero-burger{display:flex;flex-direction:column;justify-content:center;gap:6px;
  width:44px;height:44px;padding:0 0 0 21px;background:none;border:0;cursor:pointer}
.dhero-burger i{display:block;width:23px;height:1px;background:#E6E1D1;
  transition:transform .35s var(--ease),opacity .25s}
body.vd-open .dhero-burger i:first-child{transform:translateY(3.5px) rotate(45deg)}
body.vd-open .dhero-burger i:last-child{transform:translateY(-3.5px) rotate(-45deg)}

#vd-scrim{position:fixed;inset:0;z-index:640;background:rgba(10,9,6,.5);
  opacity:0;pointer-events:none;transition:opacity .4s var(--ease)}
body.vd-open #vd-scrim{opacity:1;pointer-events:auto}
#vd-menu{position:fixed;inset:0 0 auto 0;z-index:650;background:#1D2415;
  color:#E6E1D1;padding:34px clamp(22px,5vw,56px) 40px;
  display:flex;flex-direction:column;gap:26px;
  border-bottom:1px solid rgba(230,225,209,.14);
  transform:translateY(-101%);transition:transform .5s var(--ease)}
/* The shadow only while open: a box-shadow renders at the element's
   transformed position regardless of what the transform is, so left on the
   base rule it bled a soft dark band under the top of every page from the
   menu sitting just off screen — invisible against the site's usual dark
   grounds, but plainly visible on the v1 start split's near-white ground. */
body.vd-open #vd-menu{transform:none;box-shadow:0 26px 60px rgba(10,9,6,.5)}
.vd-resume{display:flex;flex-direction:column;gap:6px;align-items:flex-start;
  text-align:left;background:none;border:1px solid rgba(216,178,74,.4);
  border-radius:3px;padding:16px 20px 17px;cursor:pointer;transition:.25s}
.vd-resume:hover{border-color:#D8B24A;background:rgba(216,178,74,.08)}
.vd-k{font-family:var(--sans);font-size:9.5px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:#D8B24A}
.vd-v{font-family:var(--serif);font-weight:300;font-size:23px;color:#E6E1D1}
.vd-list{display:flex;flex-direction:column}
.vd-list button{display:flex;align-items:baseline;gap:18px;text-align:left;
  background:none;border:0;border-top:1px solid rgba(230,225,209,.14);
  padding:18px 0 19px;cursor:pointer;color:#E6E1D1;transition:.25s}
.vd-list button:last-child{border-bottom:1px solid rgba(230,225,209,.14)}
.vd-list button:hover{color:#D8B24A}
.vd-list em{font-family:var(--sans);font-style:normal;font-size:9.5px;
  letter-spacing:var(--ls-wide);opacity:.5;min-width:2.4em}
.vd-list span{font-family:var(--serif);font-weight:300;font-size:clamp(26px,3.4vw,38px);line-height:1}
.vd-list button.active span{color:#D8B24A}
.vd-x{position:absolute;top:26px;right:clamp(22px,5vw,56px);background:none;border:0;
  color:rgba(230,225,209,.7);font-size:17px;padding:10px;cursor:pointer;transition:.25s}
.vd-x:hover{color:#E6E1D1}

/* v38 — the currency compare is a question, not a line of copy: a small
   circled mark beside the price that says what it does on hover or tap. */
#v-details .curq{position:relative;width:19px;height:19px;padding:0;flex:none;
  display:inline-flex;align-items:center;justify-content:center;
  font-family:var(--sans);font-size:10.5px;line-height:1;border-radius:50%;
  border:1px solid currentColor;background:none;cursor:pointer;
  opacity:.62;transition:opacity .25s,color .25s,border-color .25s;vertical-align:middle}
#v-details .curq:hover,#v-details .curq:focus-visible{opacity:1;color:var(--sun)}
#v-details .ns-q{color:var(--ink);margin-left:2px;align-self:center}
#v-details .dhero-q{color:#E6E1D1}
#v-details .dhero-q:hover,#v-details .dhero-q:focus-visible{color:#D8B24A}
#v-details .curq-tip{position:absolute;left:50%;bottom:calc(100% + 10px);
  transform:translate(-50%,4px);width:max-content;max-width:min(64vw,25em);
  background:#1D2415;color:#E6E1D1;border:1px solid rgba(216,178,74,.4);
  border-radius:3px;padding:10px 14px 11px;
  font-family:var(--serif);font-style:italic;font-weight:300;font-size:15.5px;
  line-height:1.4;letter-spacing:0;text-transform:none;text-align:left;
  opacity:0;pointer-events:none;transition:opacity .25s var(--ease),transform .25s var(--ease);
  box-shadow:0 16px 40px rgba(10,9,6,.45);z-index:40}
#v-details .curq:hover .curq-tip,#v-details .curq:focus-visible .curq-tip,
#v-details .curq:active .curq-tip{opacity:1;transform:translate(-50%,0)}
#v-details .ns-price{flex-direction:row;align-items:center;gap:9px}
@media(max-width:560px){#v-details .curq-tip{left:0;transform:translate(0,4px)}
  #v-details .curq:hover .curq-tip,#v-details .curq:focus-visible .curq-tip{transform:translate(0,0)}}

/* v38.1 — the marks sit at the right edge of the layout, so a left-anchored
   tip runs off the viewport (and, being laid out at opacity 0, put the page
   into horizontal scroll). Anchor it to the right edge of the button. */
#v-details .curq-tip{left:auto;right:-6px;transform:translate(0,4px)}
#v-details .curq:hover .curq-tip,#v-details .curq:focus-visible .curq-tip,
#v-details .curq:active .curq-tip{transform:translate(0,0)}
@media(max-width:560px){
  #v-details .curq-tip{left:auto;right:-6px;max-width:min(78vw,25em)}
  #v-details .curq:hover .curq-tip,#v-details .curq:focus-visible .curq-tip{transform:translate(0,0)}
}
/* and the light-ground mark carries its dimming in the colour, not opacity */
#v-details .ns-q{opacity:1;color:color-mix(in srgb,var(--ink) 86%, transparent)}
#v-details .ns-q:hover,#v-details .ns-q:focus-visible{color:var(--sun)}

/* v39 — Grace stays put: the brass lockup is fixed to the lower right on
   every chapter, above the chapter bar and clear of the menu. */
#c-fab{position:fixed;right:26px;bottom:26px;z-index:820;opacity:1;pointer-events:auto}
body.vd-open #c-fab{opacity:0;pointer-events:none}
@media(max-width:860px){#c-fab{right:16px;bottom:16px}}

/* ==========================================================================
   v40 — the three places you are asked to type.
   They read as underlines on paper, which on a sand ground is invisible as
   an affordance. Give them a real field: a white well, a ruled edge, a
   label above it and a brass focus.
   ========================================================================== */
#v-details .leadfields{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:16px;margin:22px 0 4px}
#v-details .fld{display:flex;flex-direction:column;gap:8px;cursor:text}
#v-details .fld span{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls-wide);
  text-transform:uppercase;color:color-mix(in srgb,var(--ink) 86%, transparent);opacity:1}
#v-details .fld input{width:100%;min-height:54px;
  background:#FFFFFF;color:var(--ink);
  border:1px solid color-mix(in srgb,var(--ink) 26%, transparent);
  border-radius:3px;padding:15px 16px 16px;
  font-family:var(--serif);font-weight:300;font-size:20px;line-height:1.2;
  outline:none;transition:border-color .2s,box-shadow .2s}
#v-details .fld input::placeholder{color:color-mix(in srgb,var(--ink) 45%, transparent);
  font-style:italic;font-size:17px}
#v-details .fld input:hover{border-color:color-mix(in srgb,var(--ink) 42%, transparent)}
#v-details .fld input:focus{border-color:var(--sun);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--sun) 18%, transparent)}
#v-details .fld-warn{font-family:var(--sans);font-size:11px;letter-spacing:.04em;
  color:#8A3A1E;margin-top:10px}

/* naming the villa is a button, not a whispered link */
#v-details .fnamebar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}

/* v40.1 — naming the villa is the same kind of asking as the name and the
   email below it, so it is the same kind of field. */
#v-details .fnamebar{display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;
  padding:clamp(18px,2.2vw,24px);border-bottom:1px solid color-mix(in srgb,var(--ink) 14%, transparent)}
#v-details .fld-villa{flex:1 1 260px;min-width:0}
#v-details .fnb-save{min-height:54px;flex:none;
  background:var(--sun);border:1px solid var(--sun);color:#FBFAF4;
  font-family:var(--sans);font-size:10.5px;letter-spacing:var(--ls);
  text-transform:uppercase;font-weight:500;padding:0 24px;border-radius:3px;cursor:pointer;transition:.25s}
#v-details .fnb-save:hover{background:var(--ink);border-color:var(--ink)}

/* ==========================================================================
   FACILITIES — the shared ground, as a list you can read.
   The live page crams ten facilities into a translucent two-column panel
   over a render; at a glance nothing is more important than anything else.
   Here they are a numbered editorial list: one rule per entry, the name in
   the serif, one line under it, and the qualifiers as quiet tags.
   ========================================================================== */
/* the list itself */
.faclist{list-style:none;margin:0;padding:0;
  column-count:2;column-gap:clamp(32px,5vw,80px)}
.faclist li{break-inside:avoid;display:flex;gap:clamp(14px,1.6vw,22px);
  padding:22px 0 24px;border-top:1px solid color-mix(in srgb,var(--ink) 14%, transparent)}
.faclist li:first-child{border-top:0;padding-top:6px}
.fac-n{font-family:var(--sans);font-size:10px;letter-spacing:var(--ls-wide);
  color:var(--sun);padding-top:.7em;min-width:2.4em;flex:none}
.fac-b h3{font-family:var(--serif);font-weight:300;font-size:clamp(23px,2.4vw,31px);
  line-height:1.1;margin:0 0 8px;color:var(--ink);
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.fac-b p{font-family:var(--sans);font-size:15px;line-height:1.6;margin:0;
  max-width:34ch;color:color-mix(in srgb,var(--ink) 86%, transparent);text-wrap:pretty}
.fac-tag{font-family:var(--sans);font-style:normal;font-size:9px;
  letter-spacing:var(--ls-wide);text-transform:uppercase;color:var(--sun);
  border:1px solid color-mix(in srgb,var(--sun) 42%, transparent);
  border-radius:3px;padding:5px 8px 6px;white-space:nowrap}
.fac-tag.sacred{color:var(--terra);border-color:color-mix(in srgb,var(--terra) 42%, transparent)}
@media(max-width:820px){
  .faclist{column-count:1}
  .faclist li:first-child{padding-top:14px}
}

/* ==========================================================================
   v41 — one veil, sitewide. The render darkens at the top the way it darkens
   at the bottom: the mark and the burger get a ground to sit on, and no hero
   opens on a bright sky. Same curve on every page.
   ========================================================================== */
#v-details .dhero-veil,.dhero-veil{
  background:linear-gradient(to bottom,
    rgba(17,21,12,.62) 0%,
    rgba(17,21,12,.24) 16%,
    rgba(17,21,12,.04) 34%,
    rgba(17,21,12,.58) 68%,
    rgba(29,36,21,.92) 90%,
    #1D2415 100%)}

/* ==========================================================================
   v41.1 — the "white fade" was never the veil: .dhero::before is a 132px
   CREAM band at z-index 1, painting over it. One dark curve for that band,
   everywhere, matching the bottom of the hero — and the v35 kill rule on
   #v-details is reinstated so all three heroes share it.
   ========================================================================== */
.dhero::before,.dhead.dhero::before,
#v-details .dhero::before,#v-details .dhead.dhero::before{
  content:"";display:block;position:absolute;inset:0 0 auto 0;height:150px;
  z-index:1;pointer-events:none;
  background:linear-gradient(to bottom,
    rgba(17,21,12,.62) 0%,rgba(17,21,12,.3) 46%,transparent 100%)}
/* the veil sits above it now, so the two curves add rather than fight */
#v-details .dhero-veil,.dhero-veil{z-index:2}
#v-details .dhero-top{z-index:5}
#v-details .dhero-in{position:relative;z-index:4}

/* ==========================================================================
   v42 — the masterplan joins the shared shell: mark and two-rule burger
   floating on the map, and the same forest drop-down as every other page.
   Its own .nav sheet is retired.
   ========================================================================== */
.mapnav{position:fixed;top:0;left:0;right:0;z-index:600;display:flex;
  align-items:center;justify-content:space-between;gap:20px;
  padding:20px clamp(16px,2.4vw,30px);pointer-events:none}
.mapnav>*{pointer-events:auto}
.mapnav .dhero-mark{position:static;inset:auto;width:auto;height:32px;
  object-fit:contain;opacity:.95;flex:none}
.mapnav::before{content:"";position:absolute;inset:0 0 auto 0;height:130px;
  z-index:-1;pointer-events:none;
  background:linear-gradient(to bottom,rgba(17,21,12,.62) 0%,rgba(17,21,12,.3) 46%,transparent 100%)}
@media(max-width:860px){.mapnav{padding:14px 14px}.mapnav .dhero-mark{height:26px}}

/* ---- a filtered-out pin never shows its label -----------------------------
   .dot.off dims the whole pin to 16% and the tag inherits it, so sand type
   over the aerial measures 1.42:1. Two separate rules re-show a picked tag —
   one in @media(max-width:860px), one in @media(hover:hover) — so the guard
   sits here, last in the sheet and outside both, where it outranks either by
   source order at equal specificity. This is the LANDING state of #/explore:
   every dot starts .off while one is already .picked, so the first thing on
   the masterplan was an unreadable ghost label. It clears on the first click,
   which is why it survives casual testing. */
html[data-alt="1"] .dot.off .tag{display:none}

/* ---- the cover carries no top bar ----------------------------------------
   #v-cover renders its own .cv1-nav (mark + burger) floating on the picture.
   The shell's persistent #nav would otherwise sit above it and paint a
   second mark and a second burger. */
body.on-cover #nav{display:none}

/* ---- menu ordinals: token, not opacity -----------------------------------
   .vd-list em came in as opacity:.5 over the forest ground, which measures
   2.46:1 — the exact anti-pattern CLAUDE.md rule (b) names. --dim-soft is the
   sanctioned faintest tier and is ground-aware, so it holds the 4.5:1 floor
   here and would still hold if this menu ever opened over bone. Hover hands
   the colour back so the ordinal brightens with its label. */
#vd-menu .vd-list em{opacity:1;color:var(--dim-soft)}
@media(hover:hover){ #vd-menu .vd-list button:hover em{color:inherit} }

/* a view that paints its own mark-and-burger suppresses the shell's top bar */
body.own-chrome #nav{display:none}

/* ---- dark panels on the bone ground: restore the dark-ground palette -------
   The reading chapters run on bone, and body.on-bone flips the tokens so
   --paper (type) becomes deep forest. A panel that paints ITSELF dark then
   draws its text with the same flipped token: dark on dark. Measured on the
   numbers chapter, .rt-proof had 18 of 19 text nodes under 3:1 and .mnband
   14 of 15 — whole panels legible only where an <em> or a brass figure broke
   the pattern. This is the inversion trap the design's own notes warn about
   ("anything painted over a photograph must use dark-ground literals"); a dark
   panel is the same problem as a photograph.

   The background is pinned to a literal FIRST. These panels derive their own
   background from the very tokens being restored, so redefining the tokens
   without pinning turns the panel sand and moves the bug rather than fixing
   it. Everything below the pin is the base (forest) scope again. */
body.on-bone .rt-proof{background:#2E4034}
body.on-bone .fnote{background:#1D2415}
body.on-bone .rt-proof,
body.on-bone .fnote{
  --cream:#1D2415; --paper:#E6E1D1; --ink:#E6E1D1;
  --terra:#949E86; --stone:#314736; --well:#11150C; --midnight:#151A0F;
  --hover:#FFFFFF; --hover-on:#1D2415;
  --dim:color-mix(in srgb,var(--paper) 74%, transparent);
  --dim-soft:color-mix(in srgb,var(--paper) 58%, transparent);
  /* brass has a ground-specific value too: #D8B24A reads ~8.9:1 on forest,
     while the bone scope darkens it to #6F5410. Restoring the palette without
     restoring --sun left the figures in bone-brass on a dark panel — 2.24:1
     for the "5%" and 2.49:1 for "77.5%", the two numbers the panels exist to
     show. */
  --sun:#D8B24A; --sun-soft:color-mix(in srgb,#D8B24A 22%, transparent);
  color:var(--paper);
}
/* the comparison pair inside the founders' note was muted with opacity for a
   light panel; on the dark panel it needs the token tier instead */

/* ---- the last of the opacity-muted text ----------------------------------
   Each of these sets itself back with opacity (.5–.85) or a low-alpha
   color-mix and lands between 3.07:1 and 4.16:1. Same rule (b) as everywhere
   else: --dim / --dim-soft are ground-aware and hold the 4.5:1 floor, and they
   now resolve correctly inside the dark panels too. */
#v-details .qcompare .qc-k,
#v-details .rt-ladder-row span.k,
#v-details .rt-hero-close,
#v-details .splitkey,                 /* the opacity is on the row, not the span */
#v-mine .steps .step .n,
#v-mine .steps .step .t{opacity:1;color:var(--dim)}

/* the shell bar's burger: same mark as the hero one, but it sits on the bar
   rather than on a photograph, so its rules follow the bar's colour */
#nav .dhero-burger{padding:0;width:34px;height:34px;align-items:flex-end}
#nav .dhero-burger i{width:20px;background:currentColor}

/* A chapter bar with ONE chapter was stretching that chapter across the full
   width (.dhead.dhero + .chbar .cbchip{flex:1}), so the founders' villas
   showed what looked like a broken tab bar: a single 1,100px "01 THE VILLA".
   Stretch only when there is a row to share. */
html[data-alt="1"] .chbar.one .cbchip{flex:0 0 auto;justify-content:flex-start}
html[data-alt="1"] .chbar.one{justify-content:flex-start}

/* ==========================================================================
   GN·01 — one capitalisation per kind of thing.
   .over is the eyebrow above a heading and is uppercased by scope
   (.sec .over, .dhead .over, .scrim .over). The facilities sheet is none of
   those, so its "Beyond your walls" was the single eyebrow on the site
   rendering in sentence case. Same element, same job, same treatment.
   The p&l's three total rows are handled in the data, not here: they were
   typed in LITERAL CAPITALS while their eleven siblings were sentence case,
   which is a shout the .pnlbold weight was already making for them.
   ========================================================================== */
#fac-sheet .over{letter-spacing:var(--ls-wide);font-size:12.5px;text-transform:uppercase;
  font-weight:500;color:var(--dim);margin-bottom:16px}

/* the mark is a link now — say so, and give the keyboard somewhere to land */
.brand-mono,.dhero-mark{cursor:pointer}
.brand-mono:focus-visible,.dhero-mark:focus-visible{
  outline:2px solid var(--sun);outline-offset:4px;border-radius:2px}
@media(hover:hover){
  .brand-mono:hover,.dhero-mark:hover{opacity:.78;transition:opacity .2s}
}

/* ============================================================
   Boot cover
   ============================================================
   The server renders the whole village as prose inside #portal-prose so a
   crawler that runs no JavaScript still gets the words. Nothing hid it, so
   every visitor watched that prose paint full size in sand on forest until
   engine.js finished downloading and removed it. That was the first thing
   anyone saw of the village.

   Hidden here rather than in the engine, because the engine arriving is the
   thing being waited for. The <noscript> block in shell.html.j2 puts it back
   for a reader with no JavaScript, which is who it was written for.

   The cover uses the tenant's own tokens, so a village re-skins it by
   changing :root and nothing here. */
#portal-prose{display:none}

#portal-splash{
  position:fixed; inset:0; z-index:9999;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:22px; padding:2rem;
  background:var(--cream); color:var(--paper);
  transition:opacity .45s ease;
}
#portal-splash.is-done{ opacity:0; pointer-events:none }

/* The wordmark, sized off the viewport so it holds the same presence on a
   phone as on a desktop. Height is capped as well as width, because the mark is
   stacked and tall: a width only rule pushes the bar off a short landscape
   screen. */
#portal-splash .ps-mark{
  width:min(340px, 62vw); max-height:38vh;
  height:auto; object-fit:contain;
  /* Alt text, if the image ever fails, should still read as the village. */
  font-family:var(--serif); font-weight:300; font-size:clamp(1.3rem,3vw,2rem);
  color:var(--paper); text-align:center;
}

/* ==========================================================================
   The boot cover's own ink fill and sketch ground.
   ========================================================================== */
#portal-splash.ps-v1{
  gap:0;
  background:var(--swv-neutral-50);
  color:var(--splash-ink);
  /* The released cover's --splash-ink/--splash-ghost (50%/12% of
     --splash-hue) read as barely there over this cover's own sketch layer;
     product asked for it legible, so this overrides both only here, never
     touching the :root pair the released cover still uses unchanged. */
  --splash-ink:var(--swv-accent-950);
  --splash-ghost:color-mix(in srgb, var(--swv-accent-950) 28%, transparent);
}
/* The sketch reads far darker at full opacity than the design draws it, so
   it sits on its own layer at a low opacity rather than as the ground's own
   background-image. --v1-sketch-faint is set inline on #portal-splash by
   render_shell() (shell.html.j2), from ASSETS.CV1_SKETCH_FAINT, so this
   stylesheet names no tenant's file; unset, the shorthand below resolves to
   no image at all rather than Secret Waterfall's. */
#portal-splash.ps-v1::before{
  content:""; position:absolute; inset:0; z-index:-1;
  background:var(--v1-sketch-faint) center/cover no-repeat;
  opacity:.18;
}

/* The wordmark, and the progress bar, in one element.

   A horizontal gradient with both stops at the same position is a hard edge,
   clipped to the glyphs, so the name fills with ink from the left as the
   engine reports its milestones. The bar above sat under the mark and said the
   same thing twice.

   The gradient is twice the element's width and slid by background-position-x,
   rather than being redeclared at a new stop each time. background-position-x
   interpolates and background-image does not, so this is what makes the fill
   ease between milestones instead of jumping; it is the same trick the bar
   uses with transform:scaleX, for the same reason.

   width and max-height are reset, not inherited. The rule above sizes an
   <img>, and this is a <p>: left at min(340px,62vw) the box the fill sweeps
   across is narrower than the name, so the ink stops part way through it.

   Position: the Figma frame puts this box at x16 y428 w1408 h44 on the
   1440x900 desktop frame and x16 y400 w358 h44 on the 390x844 mobile one.
   Both are the box centred exactly between the frame's top and bottom edge
   (428+44+428=900, 400+44+400=844), so top:50% with a translateY holds both
   without a breakpoint; left/right:16px is the same 32px total inset both
   frames give it. */
#portal-splash.ps-v1 .ps-mark{
  position:absolute; left:16px; right:16px; top:50%; transform:translateY(-50%);
  display:block; width:auto; max-width:none; max-height:none;
  font-style:italic; font-weight:400;
  font-size:40px; line-height:44px;
  letter-spacing:-1.2px; text-align:center; text-wrap:balance;
  color:var(--splash-ink);
  /* 48.5%/51.5% rather than a hard 50%/50%: the gradient is rendered at
     200% of the element's width, so a 3-point spread there is 6% of the
     element itself, and the ink visibly sweeps across that width instead
     of jumping from one glyph to the next. */
  background-image:linear-gradient(90deg,var(--splash-ink) 48.5%,var(--splash-ghost) 51.5%);
  background-size:200% 100%; background-repeat:no-repeat;
  background-position-x:calc(100% - var(--ps-p, .06) * 100%);
  transition:background-position-x .5s cubic-bezier(.22,.61,.36,1);
  /* No text-shadow here: the glyphs are transparent and painted by the
     clipped background, so a shadow shows through them and greys the ink. */
}
/* color:transparent only where the clip actually works. Declared the other way
   round, an engine without background-clip:text paints transparent text on the
   near white ground and the cover is blank. */
@supports (background-clip:text) or (-webkit-background-clip:text){
  #portal-splash.ps-v1 .ps-mark{
    -webkit-background-clip:text; background-clip:text; color:transparent;
  }
}

/* Bottom centre, at the design's 16px (836 to the box top, plus its 48px
   height, on a 900 tall frame; the same 16px on the 390x844 mobile frame),
   lifted clear of a phone's home indicator. The shell asks for
   viewport-fit=cover, so on an iPhone the inset is real and 16px from the
   edge of the screen is not 16px from the edge of the safe area. */
#portal-splash.ps-v1 .ps-monogram{
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:max(16px, calc(env(safe-area-inset-bottom, 0px) + 16px));
  width:60px; height:48px;
}

/* The cover reports real milestones, so with motion reduced it still moves,
   it just arrives without easing. */
@media (prefers-reduced-motion:reduce){
  #portal-splash,
  #portal-splash.ps-v1 .ps-mark{ transition:none }
}

/* ---------- v1 two pane landing — Figma "01. Start" ----------
   Ground is --swv-neutral-50, the ps-v1 boot cover's own, so the handover
   from the cover to this page is invisible rather than a second near white
   arriving. --swv-serif/--swv-sans are this file's own stacks, declared
   once near the top. Stacks at 860px, which this file already breaks at in
   dozens of other places, text pane first so the name and the way in are
   above the fold on a phone.

   Nav gets its own 60px grid row on desktop, so the panes below start at
   y60, matching the Figma frame. On a phone the Start frame draws the icons
   directly over the stacked panes instead of pushing them down, so the nav
   there is layered on top rather than given its own row; one .cv1-nav
   element, one rule difference, rather than two markup shapes. */
.cv1{position:relative;display:grid;grid-template-columns:1fr 1fr;
  grid-template-rows:60px 1fr;min-height:100svh;width:100%;
  background:var(--swv-neutral-50)}
.cv1-nav{position:absolute;inset:0 0 auto 0;height:60px;z-index:3;
  display:flex;align-items:center;justify-content:space-between;
  padding:12px 24px 12px 16px}
.cv1-mark{width:45px;height:36px;cursor:pointer}
/* The base .dhero-burger bars are sand, for the dark grounds it is drawn on
   everywhere else. Ink here, the same reason the monogram beside it is the
   ink file: this nav's ground is near white on both breakpoints. */
.cv1-nav .cv1-burger i{background:var(--swv-accent-950)}

/* Neither the Figma start split nor the loader frames carry the chat
   launcher. body.on-cover is the engine's existing "which view is showing"
   flag (see showView in engine.js); :has(.cv1) narrows that to the v1
   cover specifically, since the released cover also sets on-cover and
   still wants the launcher. */
/* !important for the same reason as the panel rule below: openChat/closeChat write #c-fab's display inline on every toggle, anywhere in the product */
body.on-cover:has(.cv1) #c-fab,
html.swv-loading #c-fab{display:none!important}
/* the v1 masterplan's own panel carries the launcher in its own header
   (.swv-panel-grace, engine.js swvPanelInner()), so the floating one would
   just be a second way to open the same chat sitting over the map.
   !important because openChat()/closeChat() (engine.js) write #c-fab's
   display as an inline style on every open and close, anywhere in the
   product, and a plain rule here would only win until the first one of
   those ran — verified by opening and closing chat from the panel and
   watching #c-fab's inline style come back and eclipse .swv-tab clicks. */
body.on-map:has(#map-wrap.mv1) #c-fab{display:none!important}
/* And on a details chapter, for the same reason. The panel is parked inside
   #v-details there (showView's rescue, engine.js), so its launcher is on
   screen beside "The Homes" while the floating one sat over it: two ways to
   open one chat, a few hundred pixels apart. Keyed on the launcher actually
   being in that view rather than on the view existing, because #swv-panel
   stays in the document when it is not shown. */
body:has(#v-details .swv-panel-grace) #c-fab{display:none!important}

.cv1-l,.cv1-r{position:relative;grid-row:2}
.cv1-l{grid-column:1;padding-top:60px}
.cv1-r{grid-column:2;padding-top:60px}
/* No rule between the panes. "01. Start. Stage 02" has none: the cream
   ground and the photograph meet each other directly, and a hairline drawn
   between them reads as a seam the design does not have.

   The card is flush to that meeting point too. The Figma frame puts it at
   x 719 to 1428 in a 1440 wide artboard, so the margin is on the outside
   edge only; a left inset as well left a cream gutter beside the seam. */
.cv1-img{position:absolute;inset:0 12px 12px 0;z-index:0;
  width:calc(100% - 12px);height:calc(100% - 12px);object-fit:cover}

/* ---------- the mask reveal ----------
   One technique, named once, because the cover and the loader had each
   grown their own copy of it and three more are coming.

   A masked layer whose mask-size travels from one size to another. Nothing
   else moves: the layer's box, its background and any drawing inside it all
   stay exactly where they are, so what the viewer sees is a silhouette
   uncovering a picture rather than a picture being scaled. Figma's "Mask
   Image Reveal" is this, and its three morph keyframes are one silhouette
   at three sizes, which is why a single element can do the whole journey.

   mask-size is a paint, not a layout, so this is cheap on a desktop pane
   and is deliberately switched off on phones by the consumers that have a
   phone layout at all.

   To use it: put .wg-mask on the layer, set --wg-mask to the shape and
   --wg-mask-size to the size it starts at. Then let whatever drives the
   sequence set --wg-mask-size to the size it ends at, from a more specific
   selector. The trigger can be anything a selector can see: a data-stage on
   an ancestor, a class, a :hover.

   One knob, moved by ordinary specificity, rather than a from/to pair read
   through nested var() fallbacks. The first cut of this did it that way and
   the size resolved unpredictably between the two consumers, which is a bad
   trade for a primitive whose whole job is to be copied.

     --wg-mask        the mask image. A tenant's file arrives here inline
                      from engine.js, so this stylesheet names none. The
                      default is the same opaque gradient as --wg-grain
                      below, the identity for intersect, so a village that
                      never sets it is left unmasked, not blanked.
     --wg-mask-size   the size, moved by whatever drives the sequence.
                      Unset means 0% 0%, which shows nothing, so a consumer
                      that forgets it reveals nothing rather than
                      everything.
     --wg-grain       an optional second mask, intersected with the first and
                      held at a FIXED size while the first one grows. That
                      fixed size is the whole point: the erosion stays the
                      same number of pixels throughout, so it shreds a small
                      shape into islands and only fringes a large one, which
                      is what makes a growing blot read as watercolour
                      spreading rather than as one shape being scaled. The
                      default is an opaque gradient, the identity for
                      intersect, so a consumer that sets none is unaffected.
     --wg-grain-size  that fixed size                 default 320px
     --wg-mask-dur    how long                        default .64s
     --wg-mask-ease   easing                          default ease-out expo

   The loader (#swv-loader .swv-loader-blot) is the same technique and still
   carries its own copy. It is a full screen sequence that cannot be checked
   without a deploy, so it keeps what works until someone can watch it.

   Reduced motion drops the transition, so setting --wg-mask-size becomes an
   instant jump to the settled state rather than no reveal at all. */
.wg-mask{
  -webkit-mask-image:var(--wg-mask,linear-gradient(#000,#000)),var(--wg-grain,linear-gradient(#000,#000));
  mask-image:var(--wg-mask,linear-gradient(#000,#000)),var(--wg-grain,linear-gradient(#000,#000));
  -webkit-mask-repeat:no-repeat,repeat;mask-repeat:no-repeat,repeat;
  -webkit-mask-position:center,center;mask-position:center,center;
  -webkit-mask-size:var(--wg-mask-size,0% 0%),var(--wg-grain-size,320px 320px);
  mask-size:var(--wg-mask-size,0% 0%),var(--wg-grain-size,320px 320px);
  -webkit-mask-composite:source-in;mask-composite:intersect;
  transition:
    -webkit-mask-size var(--wg-mask-dur,.64s) var(--wg-mask-ease,cubic-bezier(.16,1,.3,1)),
    mask-size var(--wg-mask-dur,.64s) var(--wg-mask-ease,cubic-bezier(.16,1,.3,1))}
@media(prefers-reduced-motion:reduce){
  .wg-mask{transition:none}
}

/* The blot: one watercolour mask (Figma "Mask Image Reveal"), read off
   --v1-splash, set inline by engine.js from ASSETS.CV1_SPLASH_3, so this
   stylesheet names no tenant's file.

   It used to be three separately sized, separately masked copies crossfading
   into each other, on the reasoning that a CSS mask cannot be tweened
   between two different shapes. True, but it does not need to be: the Figma
   morph is one silhouette at three sizes, so growing mask-size on a single
   mask is the same journey with nothing to crossfade. That also removed the
   zoom the crossfade introduced, since stage 1 and stage 3 are different
   drawings at different scales and fading between them moved the drawing as
   well as the mask.

   .cv1-blot stays the shared positioning wrapper, sized to the settled
   stage, because the box must not move: the mask travels across a drawing
   that stays exactly where it is, which is ContentScale.None in the Compose
   source this was ported from. The box clips, it does not scale. */
/* Measured off "01. Start. Stage 02" rather than guessed: in that frame the
   blot is 552 x 742 in a 719 x 840 column, centred on 364, 473. These four
   numbers are what put it there, and the pair of caps stops a very wide
   viewport inflating it past the drawing.

   The frame keeps cream on all four sides of the blot, so the box must stay
   inside the column: cropping this silhouette against the page edge is the
   one thing it must never do. Checked at 900, 1024, 1440 and 1920 wide.

   The mask's own alpha is the silhouette with no padding around it, so the
   box is the blot: these numbers are the frame's numbers, and the small
   offsets are the difference between the column's centre and the frame's. */
.cv1-blot{position:absolute;left:50%;top:50%;
  transform:translate(calc(-50% + 4px),calc(-50% - 7px));
  width:min(76.8%,552px);height:min(88.3%,742px);pointer-events:none;z-index:0}
/* One layer, one mask, and the only thing that moves is how much of that
   mask is showing. SPEC.md for 01. Start: "Content (identical in all three
   stages — only the mask moves)", and the Figma morph is "one silhouette at
   three sizes, not three different shapes".

   mask-size rather than the element's own width: the box has to stay put so
   the drawing inside it stays put, which is the whole of #104's fix. Growing
   the mask instead travels the silhouette across a drawing that never moves.

   Desktop only. The max-width:860px block below drops the mask entirely and
   lets the sketch be a flat background, so this animation never runs on the
   phones where a per-frame mask repaint would actually cost something. */
.cv1-splash{position:absolute;inset:0;
  background-color:var(--swv-neutral-200);
  /* identity when the village publishes no splash, or no grain: an opaque
     layer intersects to a no-op on both, where none would blank the mask
     and vanish a pane that has a sketch to show. */
  --wg-mask:var(--v1-splash,linear-gradient(#000,#000));
  --wg-grain:var(--v1-grain,linear-gradient(#000,#000));
  --wg-grain-size:300px 300px;
  --wg-mask-size:30% 36%;
  --wg-mask-dur:2.2s;
  --wg-mask-ease:cubic-bezier(.22,.42,.30,1)}
/* Stage 1 is the Figma mask small and centred; stage 3 is the same
   silhouette filling its box, which the box above has sized so that filling
   it lands inside the column with cream still showing around it. */
.cv1-blot[data-stage="3"] .cv1-splash{--wg-mask-size:100% 100%}
/* Reduced motion is handled once, on .wg-mask itself. */
/* The drawing is masked, never resized, the same fix #104 made for the
   single-mask version of this element. The layer pins its pair of <img>s at
   the settled size and centres them on the same point it is centred on, so
   the sketch sits at one screen position for the whole reveal while the
   mask uncovers more of it — ContentScale.None in the Compose source this
   is ported from: the box clips, it does not scale. */
.cv1-splash img{position:absolute;left:50%;top:50%;
  width:100%;height:100%;
  /* max-width:none or the global img reset at the top of this file clamps the
     plate to the box, which is exactly the resizing above says this must
     not do. Same reset the swv panel already escapes. */
  max-width:none;
  transform:translate(-50%,-50%);object-fit:cover;
  mix-blend-mode:multiply}
/* Desktop reveals the detailed drawing, B. A is the phone's own flat
   background and is held at zero here. .4 rather than 1 because opacity is
   also what picks between the two, and CSS cannot multiply two opacity
   values on one element, so the faintness lives on these rules rather than
   on .cv1-splash img above. */
.cv1-splash .cv1-blot-a{opacity:0}
.cv1-splash .cv1-blot-b{opacity:.4}

.cv1-mid{position:absolute;left:16px;right:16px;top:347px;z-index:2;
  text-align:center}
.cv1-eyebrow{font-family:var(--swv-serif);font-weight:400;font-size:16px;
  line-height:20.8px;letter-spacing:-.48px;margin:0 0 8px;
  transition:opacity .6s cubic-bezier(.22,1,.36,1)}
/* Hidden while the mask is still small, faded in as the mask begins to grow,
   per the Start frames: no "Interactive map" eyebrow over the small
   silhouette, fading in from the moment data-stage leaves 1, not from the
   moment the growth finishes. Scoped with :has() rather than a sibling
   combinator so a village with
   no blot at all (no CV1_SKETCH_A/B) never hides its own eyebrow waiting for
   a data-stage that will never arrive. */
.cv1-l:has(.cv1-blot[data-stage="1"]) .cv1-eyebrow{opacity:0}
@media(prefers-reduced-motion:reduce){
  .cv1-eyebrow{transition:none}
}
.cv1-title{font-family:var(--swv-serif);font-style:normal;font-weight:400;
  font-size:48px;line-height:52.8px;letter-spacing:-1.44px;margin:0 0 16px;
  text-wrap:balance}
.cv1-l .cv1-eyebrow,.cv1-l .cv1-title{color:var(--swv-accent-950)}
.cv1-r .cv1-eyebrow,.cv1-r .cv1-title{color:var(--swv-neutral-50)}
/* That text is drawn for a photograph. A village with no render gets the
   pane's own ground instead, its darkest accent, so the near white title
   still has something to read against. */
.cv1-r:not(:has(.cv1-img)){background:var(--swv-accent-950)}

@media(max-width:860px){
  .cv1{grid-template-columns:1fr;grid-template-rows:none}
  .cv1-l,.cv1-r{grid-column:1;padding-top:0;min-height:50svh}
  .cv1-l{grid-row:1}
  .cv1-r{grid-row:2}
  .cv1-img{inset:0;width:100%;height:100%}
  /* Figma's mobile Background Image stacks a flat 20% black under the text;
     desktop keeps the jungle photo, which is dark enough there without it.
     The image itself is render-rain on a phone (picture source in
     engine.js swaps it for jungle only from 861px up), not the jungle
     photo, so a phone never downloads that file. */
  .cv1-r::before{content:"";position:absolute;inset:0;z-index:1;background:rgba(0,0,0,.2)}
  .cv1-mid{top:153px}
  .cv1-title{font-size:32px;line-height:35.2px;letter-spacing:-.96px}
  /* The mobile Start frame has no blot at all: the sketch runs full bleed
     behind the text instead, the same faint treatment the preloader uses,
     so the mask, the beige patch fill and the staged reveal are all switched
     off here. The one layer stands in for the whole thing at a fixed faint
     opacity, and the transition is switched off with it so a phone never
     repaints a mask it is not showing. */
  .cv1-blot{left:0;top:0;transform:none;width:100%;height:100%}
  .cv1-blot .cv1-splash{
    inset:0;
    background-color:transparent;
    transition:none;
    -webkit-mask-image:none;mask-image:none}
  .cv1-splash .cv1-blot-a{opacity:.18}
  .cv1-splash .cv1-blot-b{opacity:0}
}

/* Figma Button, Type=Primary. Default, Hover, Disabled are the three states the
   Components frame draws; focus-visible is ours and required. */
.swv-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;
  min-height:36px;padding:6px 20px 8px;border:0;border-radius:999px;
  background:var(--swv-accent-950);color:#FFFFFF;
  font:400 14px/18.2px var(--swv-sans);letter-spacing:-.42px;text-decoration:none;
  cursor:pointer;transition:background-color .2s cubic-bezier(.22,1,.36,1)}
.swv-btn:hover{background:var(--swv-accent-900)}
.swv-btn:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:3px}
.swv-btn--light{background:var(--swv-neutral-50);color:var(--swv-accent-950)}
.swv-btn--light:hover{background:var(--swv-neutral-200)}
/* After every hover rule, and repeating :hover in its own selector list, so
   a disabled button that is also under the pointer stays disabled instead
   of a same-specificity hover rule declared earlier winning the cascade. */
.swv-btn:disabled,.swv-btn:hover:disabled,
.swv-btn[aria-disabled="true"],.swv-btn:hover[aria-disabled="true"]{
  background:var(--swv-disabled);color:var(--swv-neutral-50);cursor:not-allowed}
/* Figma's phone button is the same 36px pill as desktop — min-height:44px
   here made it visibly taller than the design. The tap target still wants
   44px, so it grows on a transparent ::before instead of the box itself. */
@media (max-width:860px){
  .swv-btn::before{content:"";position:absolute;inset:-4px 0}
}

/* ---------- UI Kit, General frame — Input + Button, applied to the Ask
   Grace field/send button (#c-in/#c-send). Not built on .swv-btn: that
   class is shared with cover/hero CTAs this brief does not cover, and its
   disabled state fails the contrast floor (see the note below) — folding
   that fix in here would have silently changed those too. ---------- */

/* Input — Default/Active/Typing/Filled all draw the same box in the kit;
   only the message text differs, so one rule plus a focus rule covers
   every state. #c-in sits on the panel's dark --midnight ground, so the
   kit's ink-on-paper pair is inverted here — a light 8% fill, light
   stroke, light text — to keep the same Input legible on a dark surface
   instead of ink-on-ink. */
#c-in.gv1{height:36px;border-radius:99px;padding:0 17px;
  font:400 14px/18px var(--swv-sans);letter-spacing:-.42px;outline:none;
  background:color-mix(in srgb, var(--swv-neutral-50) 8%, transparent);
  border:1px solid var(--swv-neutral-50);color:var(--swv-neutral-50)}
#c-in.gv1:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
#c-in.gv1::placeholder{color:color-mix(in srgb, var(--swv-neutral-50) 55%, transparent)}

/* Button, Type=Primary, Theme=Light — a light pill reads on the panel's
   dark ground. Disabled isn't reachable on #c-send today (only #cc-send's
   JS ever sets .disabled), kept anyway so the component matches the kit's
   three drawn states. Its swatch there is #D6D5D1 fill with a #FAFAF5
   label — about 1.5:1, under the 4.5:1 floor — so the label is darkened to
   --swv-accent-950 instead, which clears it by a wide margin, and the
   fill is kept exactly as drawn. */
#c-send.gv1{display:inline-flex;align-items:center;justify-content:center;
  min-height:36px;padding:6px 20px 8px;border:0;border-radius:999px;
  font:400 14px/18px var(--swv-sans);letter-spacing:-.42px;text-transform:none;
  cursor:pointer;transition:background-color .2s cubic-bezier(.22,1,.36,1);
  background:var(--swv-neutral-50);color:var(--swv-accent-950)}
#c-send.gv1:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:3px}
#c-send.gv1:hover{background:var(--swv-neutral-100)}
#c-send.gv1:disabled{background:var(--swv-neutral-300);color:var(--swv-accent-950);cursor:not-allowed}

/* ---------- Loader — Figma "02. Loader" ----------
   Reuses the existing z-index scale rather than inventing a new top: 900 is
   the tier #fac-sheet already uses for a full screen fixed overlay, and the
   two never show at once. html.swv-loading lifts .cv1-nav one step above it
   for the run, per the plan's "keep .cv1-nav visible above it" — the nav's
   own z-index:3 is a value local to .cv1's stacking, nowhere near this
   scale, so it would otherwise vanish under the loader.

   The overlay itself fades out at stage 5, handing off to the map already
   built behind it (engine.js sets the hash back at stage 4). Reduced motion
   keeps the old instant cut, since the fade is itself a motion effect. */
#swv-loader{position:fixed;inset:0;z-index:900;overflow:hidden;
  background:var(--swv-neutral-200);
  transition:opacity .4s cubic-bezier(.22,1,.36,1)}
#swv-loader[data-stage="5"]{opacity:0}
/* The way in, matching the way out. Both ends of the loader dissolve now:
   the cover fades to the sketch over the same .4s the sketch fades to the
   map. Removed a frame after mount, so the transition has a start state to
   run from. */
#swv-loader.swv-loader-enter{opacity:0}
@media(prefers-reduced-motion:reduce){
  #swv-loader{transition:none}
}
html.swv-loading .cv1-nav{z-index:901}
#swv-loader .swv-loader-a,#swv-loader .swv-loader-b{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* The real footage, when it replaces the sequence below outright: full bleed,
   no multiply, the clip already carries its own ground. */
#swv-loader .swv-loader-video{position:absolute;inset:0;width:100%;height:100%;
  max-width:none;object-fit:cover}
/* Both sketches paint their lines on an opaque white ground, same as the
   start split's blot images above, so multiply is what lets --swv-neutral-200
   show through as the ground instead of the sketch sitting on white. */
#swv-loader .swv-loader-a,#swv-loader .swv-loader-b{mix-blend-mode:multiply}
#swv-loader .swv-loader-a{z-index:0}
#swv-loader .swv-loader-b{z-index:1;opacity:0;
  transition:opacity .5s cubic-bezier(.22,1,.36,1)}
#swv-loader[data-stage="2"] .swv-loader-b,
#swv-loader[data-stage="3"] .swv-loader-b,
#swv-loader[data-stage="4"] .swv-loader-b,
#swv-loader[data-stage="5"] .swv-loader-b{opacity:1}

/* Five ink-blot windows onto the masterplan render, each a generated mask:
   one large centre and four smaller corners, all torn into the SAME
   cover-fit frame so neighbouring blots show neighbouring parts of one
   picture rather than five small copies of the whole thing. Each blot's own
   box is fixed at its drawn size from the start, nothing here ever changes
   its width, height or position, so background-attachment:fixed can align
   the shared picture to the viewport on its own; no per-frame JS measurement
   is needed the way an animated box would have required. Growth is
   mask-size alone, ease-out expo, staggered 90ms leader to trailer.

   The background and every mask below is a custom property, set inline by
   engine.js's swvLoader() from ASSETS.MASTERPLAN_V1 and ASSETS.LOADER_BLOT_*,
   so this stylesheet names no tenant's file; swvLoader() only ever reaches
   this branch once it has confirmed every one of them exists. */
#swv-loader .swv-loader-blot{position:absolute;z-index:2;pointer-events:none;
  background-image:var(--v1-loader-bg);
  background-repeat:no-repeat;
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:0% 0%;mask-size:0% 0%;
  transition:mask-size .64s cubic-bezier(.16,1,.3,1),-webkit-mask-size .64s cubic-bezier(.16,1,.3,1)}
.swv-loader-blot-c1{left:50%;top:48%;width:42vw;height:48vh;transform:translate(-50%,-50%);transition-delay:0s;
  -webkit-mask-image:var(--v1-loader-blot-b);
  mask-image:var(--v1-loader-blot-b)}
.swv-loader-blot-tl{left:5%;top:8%;width:22vw;height:26vh;transition-delay:.09s;
  -webkit-mask-image:var(--v1-loader-blot-a);
  mask-image:var(--v1-loader-blot-a)}
.swv-loader-blot-tr{right:5%;top:6%;width:22vw;height:26vh;transition-delay:.18s;transform:scaleX(-1);
  -webkit-mask-image:var(--v1-loader-blot-c);
  mask-image:var(--v1-loader-blot-c)}
.swv-loader-blot-bl{left:5%;bottom:6%;width:22vw;height:26vh;transition-delay:.27s;
  -webkit-mask-image:var(--v1-loader-blot-c);
  mask-image:var(--v1-loader-blot-c)}
.swv-loader-blot-br{right:5%;bottom:8%;width:22vw;height:26vh;transition-delay:.36s;transform:scaleX(-1);
  -webkit-mask-image:var(--v1-loader-blot-a);
  mask-image:var(--v1-loader-blot-a)}
#swv-loader[data-stage="3"] .swv-loader-blot,
#swv-loader[data-stage="4"] .swv-loader-blot,
#swv-loader[data-stage="5"] .swv-loader-blot{mask-size:100% 100%;-webkit-mask-size:100% 100%}

/* render-rain itself: opacity only ever steps between 0 and 1 (never a
   translucent double exposure over the sketch) — the reveal is carried
   entirely by mask-size, growing the torn edge outward from the centre
   until it passes every side of the viewport, so the ground shows only
   through the tear and never as a flat fade underneath the whole frame.
   left/top/width/height start at the viewport cover and are rewritten
   inline by engine.js's swvSyncRainToMap at stage 4, to the exact rect
   #map-cam img already occupies, so this layer arrives at the same crop
   the map opens on instead of handing off to a jump cut. */
#swv-loader .swv-loader-rain{position:absolute;left:0;top:0;width:100%;height:100%;
  /* the global img{max-width:100%} reset would otherwise clamp this at the
     viewport width the moment swvSyncRainToMap grows it past that, to match
     a #map-cam img wider than the viewport (mapZoom's extra-than-cover
     scale) — the exact width/height mismatch that used to jump at handoff. */
  max-width:none;object-fit:cover;z-index:3;opacity:0;
  -webkit-mask-image:var(--v1-loader-blot-edge);
  mask-image:var(--v1-loader-blot-edge);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-position:center;mask-position:center;
  -webkit-mask-size:0% 0%;mask-size:0% 0%;
  /* A gentler curve than the blots' ease-out expo on purpose: that curve
     front-loads almost the entire size change into its first fifth, fine
     for a small blot but on a mask running from 0 to 150% of the viewport
     it reads as a snap-then-crawl rather than a reveal. easeOutCubic
     spreads the growth across the full 700ms instead. */
  transition:left .7s cubic-bezier(.33,1,.68,1),top .7s cubic-bezier(.33,1,.68,1),
    width .7s cubic-bezier(.33,1,.68,1),height .7s cubic-bezier(.33,1,.68,1),
    mask-size .7s cubic-bezier(.33,1,.68,1),-webkit-mask-size .7s cubic-bezier(.33,1,.68,1)}
#swv-loader[data-stage="3"] .swv-loader-rain,
#swv-loader[data-stage="4"] .swv-loader-rain,
#swv-loader[data-stage="5"] .swv-loader-rain{opacity:1}
#swv-loader[data-stage="4"] .swv-loader-rain,
#swv-loader[data-stage="5"] .swv-loader-rain{mask-size:150% 150%;-webkit-mask-size:150% 150%}

/* The map's own chrome, hidden the moment #map-wrap turns visible (stage 4,
   when engine.js sets the hash early) and faded back in once the loader
   starts fading out at stage 5 — swv-handoff-in is the more specific rule,
   so adding it is what starts the transition rather than the class order. */
body.swv-handoff .mapnav,body.swv-handoff #swv-panel,body.swv-handoff #hotspots{opacity:0}
body.swv-handoff.swv-handoff-in .mapnav{opacity:1;transition:opacity .4s cubic-bezier(.22,1,.36,1)}
body.swv-handoff.swv-handoff-in #swv-panel{opacity:1;transition:opacity .4s cubic-bezier(.22,1,.36,1) .08s}
body.swv-handoff.swv-handoff-in #hotspots{opacity:1;transition:opacity .4s cubic-bezier(.22,1,.36,1) .16s}

@media (prefers-reduced-motion:reduce){
  #swv-loader{transition:none}
  #swv-loader .swv-loader-b{transition:opacity .2s linear}
  #swv-loader .swv-loader-blot{transition:none;mask-size:0!important;-webkit-mask-size:0!important}
  #swv-loader .swv-loader-rain{transition:opacity .2s linear;
    mask-size:150% 150%!important;-webkit-mask-size:150% 150%!important}
}

/* ---------- Interactive map — Figma "03. Map" ----------
   #map-wrap carries .mv1 only when the flag is on (see mainMapAsset() and the
   #map-wrap markup), so every rule here is scoped under it and the released
   map — the golden render, the plot-footprint dots, the angle switcher —
   never sees them. The dot and tag stay the same elements the released map
   uses (.dot > .pt/.tag/.nm); only their skin and geometry change. */
/* the permanent html[data-alt="1"] design hides #sel-head outright on the map
   (its old .hint text is gone); v1 puts a real headline back inside it, so
   this undoes that hide only where .mv1 marks the map as v1's */
#map-wrap.mv1 #sel-head{display:flex}
/* Arrival only: played once by playSwvMapWelcome() from whichever moment
   actually uncovers the map, then set data-state="done" and hidden for the
   rest of the page load. data-state carries the whole timeline, so nothing
   here brings the line back after a dot hover or a panel expand — those
   call swvWelcomeDismiss() straight to "out", never back to visible. */
#swv-map-welcome{
  position:fixed;left:50%;top:50%;width:509px;max-width:calc(100vw - 32px);
  transform:translate(-50%,-50%);text-align:center;
  font-family:var(--swv-serif);font-style:italic;font-weight:300;
  font-size:40px;line-height:44px;letter-spacing:-1.2px;color:var(--swv-neutral-50);
  z-index:15;pointer-events:none;
  opacity:0;transition:opacity .4s cubic-bezier(.22,1,.36,1);
  /* A filter, not text-shadow: .swv-mw-name is painted by a clipped
     background over transparent glyphs, and a text-shadow would show through
     them. drop-shadow sits behind the finished line, so both parts stay light
     over the bright sky and water. */
  filter:drop-shadow(0 1px 8px rgba(0,0,0,.45))}
#swv-map-welcome[data-state="in"],
#swv-map-welcome[data-state="filling"]{opacity:1}
#swv-map-welcome[data-state="out"]{opacity:0;transition-duration:.8s}
#swv-map-welcome[data-state="done"]{display:none}
/* body.panning is the map drag's own class, shared with the released
   masterplan's grab cursor above — while the turn is past its threshold the
   headline goes, on its own timeline once the gesture ends rather than
   restarting one. */
body.panning #swv-map-welcome{opacity:0!important}
/* the name's ink fill, lighter than #portal-splash.ps-v1 .ps-mark's own
   because the render underneath is dark, not this cover's near white ground:
   filled reads in the same neutral-50 the lead line already carries, unfilled
   holds enough of it that the full name is legible before the fill reaches
   it, and 48.5%/51.5% softens the edge the same way the preloader's does. */
#swv-map-welcome .swv-mw-name{
  background-image:linear-gradient(90deg,var(--swv-neutral-50) 48.5%,color-mix(in srgb, var(--swv-neutral-50) 35%, transparent) 51.5%);
  background-size:200% 100%;background-repeat:no-repeat;
  background-position-x:100%;
  transition:background-position-x 2s cubic-bezier(.22,.61,.36,1)}
@supports (background-clip:text) or (-webkit-background-clip:text){
  #swv-map-welcome .swv-mw-name{-webkit-background-clip:text;background-clip:text;color:transparent}
}
#swv-map-welcome[data-state="filling"] .swv-mw-name,
#swv-map-welcome[data-state="out"] .swv-mw-name{background-position-x:0%}
@media (max-width:860px){
  #swv-map-welcome{left:17px;width:357px;max-width:calc(100vw - 34px);
    transform:translateY(-50%);text-align:left}
}
/* Reduced motion arrives at the same "filling" milestone the full timeline
   ends up holding at, it just never eases there — same reasoning as the
   splash's own reduced-motion rule above. */
@media (prefers-reduced-motion:reduce){
  #swv-map-welcome,#swv-map-welcome .swv-mw-name{transition:none}
}

#map-wrap.mv1 #hotspots .dot{
  width:24px;height:24px;transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;cursor:pointer}
#map-wrap.mv1 #hotspots .dot .pt{
  position:static;transform:none;width:20px;height:20px;border-radius:50%;
  background:#F2F2F2;box-shadow:0 0 0 2px rgba(255,255,255,.16);
  transition:box-shadow .2s}
#map-wrap.mv1 #hotspots .dot .pt::after{display:none}
#map-wrap.mv1 #hotspots .dot:hover .pt,
#map-wrap.mv1 #hotspots .dot:focus-visible .pt{box-shadow:0 0 0 2px #FFFFFF}
/* sold (and reserved, kept for any tenant still using it) read at 64%, and
   stay pressable — sold homes remain viewable. The dot class is shared with
   the released map, whose permanent html[data-alt="1"] rules (see "Only what
   you can buy earns a pin" further up this file) remove a reserved/sold pin
   entirely; v1 wants the opposite, a dimmed pin you can still open. */
#map-wrap.mv1 #hotspots .dot.reserved{opacity:.64;cursor:not-allowed;pointer-events:auto}
#map-wrap.mv1 #hotspots .dot.reserved .pt{opacity:1;transform:none}
#map-wrap.mv1 #hotspots .dot.amen{cursor:pointer}
#map-wrap.mv1 #hotspots .dot.amen .pt{width:20px;height:20px;background:#F2F2F2;
  box-shadow:0 0 0 2px rgba(255,255,255,.16);transform:none}
#map-wrap.mv1 #hotspots .dot.amen:hover .pt{transform:none}

/* the Panel tooltip: 40x40 thumbnail, name, and a plain meta line, beside the
   dot rather than above it — clampTags() still nudges --tagdx to keep it on
   screen, so the transform keeps that term */
#map-wrap.mv1 #hotspots .dot .tag{
  position:absolute;left:20px;top:50%;bottom:auto;
  transform:translate(var(--tagdx,0px),-50%);
  display:flex;align-items:center;gap:10px;
  min-width:187px;min-height:50px;padding:5px 12px 5px 5px;
  border-radius:6px;background:var(--swv-neutral-50);border:1px solid var(--swv-neutral-50);
  backdrop-filter:none;color:var(--swv-accent-950);text-align:left;
  white-space:normal;width:max-content;max-width:min(280px,calc(100vw - 24px))}
/* #map-cam img{position:absolute;left:0;top:0;...} reaches this element too,
   since it is a plain descendant img — position:static pulls it back into
   the tag's flex row instead of pinning it to the tag's own corner */
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-thumb{
  position:static;width:40px;height:40px;border-radius:4px;object-fit:cover;flex:none}
#map-wrap.mv1 #hotspots .dot .tag .nm{
  font-family:var(--swv-sans);font-weight:500;font-size:14px;line-height:1.2;
  letter-spacing:0;text-transform:none;font-style:normal;color:var(--swv-accent-950)}
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta{
  font-family:var(--swv-sans);font-weight:400;font-size:12px;opacity:.64;margin-top:2px}
#map-wrap.mv1 #hotspots .dot:hover .tag,
#map-wrap.mv1 #hotspots .dot.picked .tag,
#map-wrap.mv1 #hotspots .dot:focus-visible .tag{opacity:1}
/* the active state — this hotspot is the one currently picked/focused */
#map-wrap.mv1 #hotspots .dot.picked .tag{border-color:var(--swv-accent-950)}
#map-wrap.mv1 #hotspots .dot.picked .tag .swv-tag-thumb,
#map-wrap.mv1 #hotspots .dot.picked .tag .nm,
#map-wrap.mv1 #hotspots .dot.picked .tag .swv-tag-meta{opacity:.64}

/* ============================================================
   THE PANEL BACKDROP — while #swv-panel is expanded, the villa or facility
   its hero card names fills the map's own full bleed slot, Figma frames
   d_1-2584/2590/2602. #swv-backdrop sits above #map-cam rather than inside
   it, so the aerial's own pan transform and scale never reach a close up
   photo built for a plain cover fit. swv-bd-base is whatever is currently
   settled; swv-bd-fade is a transient top layer swvSyncBackdrop uses to
   crossfade to a different close up without ever showing two translucent
   photos at once, see the function for the sequencing. ============ */
#swv-backdrop{position:absolute;inset:0;z-index:1;opacity:0;pointer-events:none;
  transition:opacity .5s var(--ease)}
#swv-backdrop.on{opacity:1}
#swv-backdrop .swv-bd-l{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
#swv-backdrop .swv-bd-fade{opacity:0;transition:opacity .5s var(--ease)}
#swv-backdrop .swv-bd-fade.on{opacity:1}
/* The pins read as furniture over an aerial the visitor is meant to drag;
   over a close up they would float on top of a stranger's photo. restCam()
   and diveCam() already own #hotspots's opacity as an inline style for the
   same reason during a dive, so swvSyncBackdrop sets it the same way rather
   than fighting that inline style from here with a losing stylesheet rule.
   The welcome line's own swvWelcomeDismiss sends it away for good on first
   expand, which is the only path that reaches an expanded panel, so it needs
   no separate rule here. */
@media(prefers-reduced-motion:reduce){
  #swv-backdrop,#swv-backdrop .swv-bd-fade{transition:none}
}

/* ============================================================
   THE HOMES PANEL — #swv-panel, the design's Panel/Panel Item/Status/
   Tab/Image Preview components assembled from engine data. Docked right,
   replacing the old villa rail rather than restyling it. */
#swv-panel{
  position:fixed;right:16px;bottom:16px;width:400px;z-index:40;
  display:flex;flex-direction:column;overflow:hidden;
  /* Collapsed is Figma's slim single bar: just the 44px head row, no padding
     of its own, the chat button filling the bar's own height edge to edge.
     The count/dots/tabs row and the list only exist in the DOM once
     swvPanelInner() sees swvPanel.expanded, so there is no second row to
     leave room for here the way the old two-row 116px card needed. */
  height:44px;padding:0;box-sizing:border-box;
  background:var(--swv-neutral-50);border-radius:4px;
  box-shadow:0 12px 32px rgba(12,11,9,.16);
  font-family:var(--swv-sans);color:var(--swv-accent-950);
  transition:height .4s cubic-bezier(.22,1,.36,1), border-radius .4s cubic-bezier(.22,1,.36,1)}
#swv-panel[data-state="expanded"]{height:calc(100vh - 76px);border-radius:8px}
@media(prefers-reduced-motion:reduce){#swv-panel{transition:none}}

#swv-panel .swv-panel-head{
  display:flex;align-items:center;gap:12px;flex:none;
  padding:14px 14px 0}
/* Collapsed: the head row IS the whole bar, edge to edge — the chat button
   fills its own corner of the bar rather than sitting inset on a card, and
   the title/toggle pick up their own inset instead of the head's gap/pad. */
#swv-panel[data-state="collapsed"] .swv-panel-head{height:44px;padding:0;gap:0}
#swv-panel[data-state="collapsed"] .swv-panel-title{padding-left:14px}
#swv-panel[data-state="collapsed"] .swv-panel-toggle{margin-right:16px}
#swv-panel .swv-panel-grace{
  width:44px;height:44px;flex:none;border:0;border-radius:8px;
  background:var(--swv-accent-950);color:var(--swv-neutral-50);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  transition:opacity .15s var(--ease)}
/* flush against the bar's own left corner, square on the side that joins
   the near white strip so no notch shows between the two */
#swv-panel[data-state="collapsed"] .swv-panel-grace{border-radius:4px 0 0 4px}
#swv-panel .swv-panel-grace:hover{opacity:.82}
#swv-panel .swv-panel-grace:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
#swv-panel .swv-panel-title{
  flex:1;text-align:left;border:0;background:none;padding:0;cursor:pointer;border-radius:4px;
  font-family:var(--swv-serif);font-weight:500;font-size:22px;line-height:24.2px;
  color:var(--swv-accent-950);transition:opacity .15s var(--ease)}
#swv-panel .swv-panel-title:hover{opacity:.72}
#swv-panel .swv-panel-title:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
#swv-panel .swv-panel-toggle{
  width:24px;height:24px;flex:none;border:0;background:none;padding:0;border-radius:6px;
  display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--swv-accent-950);
  transition:opacity .15s var(--ease)}
#swv-panel .swv-panel-toggle:hover{opacity:.6}
#swv-panel .swv-panel-toggle:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
#swv-panel .swv-panel-toggle svg,
#swv-panel .swv-group-head .swv-chev svg{transition:transform .3s var(--ease)}
#swv-panel[data-state="expanded"] .swv-panel-toggle svg{transform:rotate(180deg)}
#swv-panel .swv-group-head[aria-expanded="true"] .swv-chev svg{transform:rotate(180deg)}
@media(prefers-reduced-motion:reduce){
  #swv-panel .swv-panel-toggle svg,#swv-panel .swv-group-head .swv-chev svg{transition:none}
}

#swv-panel .swv-panel-sub{
  display:flex;align-items:center;gap:12px;flex:none;
  padding:8px 14px 14px;
  font:400 14px/18px var(--swv-sans)}
/* the tabs pill is the one thing at the Figma spec'd 166x36 that must never
   move; the count text must never clip either ("N of M available" read
   right, or not at all), so the dot pager is the one thing left to give up
   its room — its own pitch first (10px here, down from 14px), and past that
   swvPanelInner() renders fewer dots outright on a narrow screen. */
#swv-panel .swv-panel-count{white-space:nowrap;flex:none}
#swv-panel .swv-dots{display:flex;align-items:center;gap:4px;flex:0 1 auto;min-width:0;overflow:hidden}
#swv-panel .swv-dots i{width:6px;height:6px;border-radius:50%;background:var(--swv-accent-950);opacity:.16;flex:none}
#swv-panel .swv-dots i.on{opacity:1}

#swv-panel .swv-tabs{
  margin-left:auto;display:flex;align-items:center;gap:0;flex:none;
  background:color-mix(in srgb,var(--swv-accent-950) 8%, transparent);
  border-radius:999px;padding:2px}
#swv-panel .swv-tab{
  min-width:80px;height:32px;padding:0 14px;border:0;border-radius:999px;background:none;
  font:400 13px/1 var(--swv-sans);color:var(--swv-accent-950);cursor:pointer;
  transition:background-color .15s var(--ease)}
#swv-panel .swv-tab[aria-selected="true"]{background:var(--swv-accent-950);color:#FFFFFF}
#swv-panel .swv-tab:not([aria-selected="true"]):hover{
  background:color-mix(in srgb,var(--swv-accent-950) 12%, transparent)}
#swv-panel .swv-tab:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}

#swv-panel .swv-panel-scroll{
  flex:1 1 auto;min-height:0;overflow:hidden;opacity:0;transition:opacity .4s cubic-bezier(.22,1,.36,1)}
#swv-panel[data-state="expanded"] .swv-panel-scroll{overflow-y:auto;opacity:1}
#swv-panel .swv-panel-scroll-in{padding:0 14px 16px}
@media(prefers-reduced-motion:reduce){#swv-panel .swv-panel-scroll{transition:none}}

/* ---------- the hero: Figma's Product card ---------- */
#swv-panel .swv-hero{padding-top:4px}
#swv-panel .swv-hero-media{position:relative;border-radius:8px;overflow:hidden;
  aspect-ratio:376/240;background:var(--swv-neutral-200)}
#swv-panel .swv-hero-img{width:100%;height:100%;object-fit:cover;display:block}
#swv-panel .swv-thumbs{position:absolute;right:8px;bottom:8px;display:flex;gap:4px}
#swv-panel .swv-thumb{
  width:51px;height:32px;padding:0;border-radius:4px;overflow:hidden;cursor:pointer;
  border:1px solid transparent;opacity:.8;background:none}
#swv-panel .swv-thumb img{width:100%;height:100%;object-fit:cover;display:block}
#swv-panel .swv-thumb[aria-current="true"]{opacity:1;border-color:var(--swv-neutral-50)}
#swv-panel .swv-thumb:hover:not([aria-current="true"]){
  opacity:1;border-color:rgba(247,248,243,.5)}
#swv-panel .swv-thumb:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
#swv-panel .swv-hero-body{padding:14px 2px 0}
#swv-panel .swv-hero-title{font-family:var(--swv-serif);font-weight:500;font-size:22px;line-height:1.2;
  color:var(--swv-accent-950);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#swv-panel .swv-hero-meta{margin-top:6px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  font:400 14px/1.3 var(--swv-sans);color:var(--swv-accent-950);opacity:.7}
#swv-panel .swv-hero-meta .swv-ic{display:inline-flex;color:var(--swv-accent-950)}
#swv-panel .swv-hero-desc{opacity:.64;display:block}
#swv-panel .swv-hero-cta{margin-top:12px;width:auto}

/* ---------- the accordion groups ---------- */
#swv-panel .swv-groups{margin-top:16px}
#swv-panel .swv-groups--flat{display:flex;flex-direction:column;gap:10px}
#swv-panel .swv-group{border-top:1px solid var(--swv-divider)}
#swv-panel .swv-group-head{
  width:calc(100% + 8px);height:45px;display:flex;align-items:center;justify-content:space-between;
  border:0;background:none;padding:0 4px;margin:0 -4px;box-sizing:border-box;
  cursor:pointer;text-align:left;border-radius:6px;
  font:500 16px/1 var(--swv-sans);color:var(--swv-accent-950);
  transition:background-color .15s var(--ease)}
#swv-panel .swv-group-head:hover{background:color-mix(in srgb,var(--swv-accent-950) 6%, transparent)}
#swv-panel .swv-group-head:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:-2px}
#swv-panel .swv-group-head .swv-chev{display:flex;color:var(--swv-accent-950)}
#swv-panel .swv-group-list{display:flex;flex-direction:column;gap:10px;padding-bottom:12px}
#swv-panel .swv-group-list[hidden]{display:none}

/* ---------- rows: Figma's Panel Item ---------- */
#swv-panel .swv-item{
  display:flex;align-items:center;gap:12px;width:calc(100% + 16px);border:0;background:none;
  padding:0 8px;margin:0 -8px;box-sizing:border-box;border-radius:10px;
  cursor:pointer;text-align:left;transition:background-color .15s var(--ease)}
#swv-panel .swv-item:hover{background:color-mix(in srgb,var(--swv-accent-950) 6%, transparent)}
#swv-panel .swv-item:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:-2px}
#swv-panel .swv-item-thumb{position:relative;flex:none;width:160px;height:90px;
  border-radius:8px;overflow:hidden;background:var(--swv-neutral-200)}
#swv-panel .swv-item-thumb img{width:100%;height:100%;object-fit:cover;display:block}
#swv-panel .swv-item-body{min-width:0}
#swv-panel .swv-item-title{display:block;font-family:var(--swv-serif);font-weight:500;font-size:18px;
  line-height:1.25;color:var(--swv-accent-950);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#swv-panel .swv-item-meta{display:block;margin-top:4px;font:500 16px/1.2 var(--swv-sans);
  color:var(--swv-accent-950)}
#swv-panel .swv-item[data-status="sold"],
#swv-panel .swv-item[data-status="reserved"]{cursor:not-allowed}
#swv-panel .swv-item[data-status="sold"] img,
#swv-panel .swv-item[data-status="reserved"] img,
#swv-panel .swv-item[data-status="sold"] .swv-item-body,
#swv-panel .swv-item[data-status="reserved"] .swv-item-body{opacity:.64}

#swv-panel .swv-status{position:absolute;left:8px;top:8px;padding:3px 10px;border-radius:999px;
  font:500 12px/1.4 var(--swv-sans);white-space:nowrap}
#swv-panel .swv-status--available{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
#swv-panel .swv-status--reserved,#swv-panel .swv-status--sold,#swv-panel .swv-status--show{
  background:color-mix(in srgb,var(--swv-accent-950) 8%, transparent);
  color:color-mix(in srgb,var(--swv-accent-950) 64%, transparent)}

/* facility rows carry no Status badge and a two line description instead
   of a price/meta line */
#swv-panel .swv-item--facility .swv-item-desc{
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  margin-top:4px;font:400 14px/1.4 var(--swv-sans);color:var(--swv-accent-950);opacity:.7}

@media(max-width:860px){
  /* Figma draws the collapsed bar the same on a phone as on desktop: the
     same 44px slim strip, just the panel's own 16px side gutters instead of
     a fixed 400px card. Collapsed height and radius already come from the
     base #swv-panel rule above, unchanged at this width. */
  #swv-panel{left:16px;right:16px;width:auto;bottom:16px;
    transition:height .4s cubic-bezier(.22,1,.36,1), bottom .4s cubic-bezier(.22,1,.36,1), border-radius .4s cubic-bezier(.22,1,.36,1)}
  #swv-panel[data-state="expanded"]{bottom:0;height:calc(100vh - 60px)}
  /* the 358px phone panel has less room than the count text, the dot pager
     and the tabs pill together want at desktop's own spacing — trim the
     gaps and the tab size; the pager's own pitch and dot count already give
     up room first, above. */
  #swv-panel .swv-panel-sub{gap:4px}
  #swv-panel .swv-tab{min-width:54px;padding:0 8px;font-size:12px}
}
@media(max-width:860px) and (prefers-reduced-motion:reduce){
  #swv-panel{transition:none}
}
@media(max-width:860px){
  /* Fault 6: an expanded drawer is a modal surface, so the page chrome has
     to go under it — but #swv-panel's own z-index only ever wins locally.
     #map-wrap is position:fixed, and a fixed element always opens its own
     stacking context regardless of its z-index (unlike position:relative/
     absolute, which need an explicit one) — so #swv-panel's 40 (or the 85
     body.swv-ov-open carries) only outranks its own siblings inside
     #map-wrap. #map-wrap itself carries no z-index of its own (see the
     comment above the MAP_FLYIN rule near #story: "#map-wrap carries no
     z-index of its own"), so it sits at auto/0 in #app's stacking order.
     Any chrome that escapes straight up to that same #app level with an
     explicit number then wins regardless of how large a number is trapped
     inside #map-wrap: .cv1-nav (the cover's own nav) is 3, .dv1-nav (the
     villa chapter's, doubling as .dhero-top) is 5 — neither sits inside a
     stacking context of its own between it and #app. Reusing body.rail-open,
     the same class setExpanded() already toggles for the panel's own state,
     lifts #map-wrap itself above them without renumbering either.

     6 was that lift, and it was too small. It clears .cv1-nav (3) and
     .dv1-nav (5), which is what this was written against, but the chrome a
     visitor actually sees over an expanded drawer is the global one:
     .mapnav at 600 and .nav at 601, both position:fixed at the #app level.
     The monogram and the burger sat on top of the drawer, reported from a
     phone. 602 is the smallest value that clears both, and still far under
     the next real tier this scale uses (.chbar's 560 is below it; nothing
     between 602 and the loader's own 900 exists).

     The nav goes under the map while the drawer is expanded, which is the
     point: the comment above already calls an expanded drawer a modal
     surface, and the drawer carries its own header and its own way out.
     Scoped to phone width: desktop already carries this correctly
     (body.swv-ov-open lifts #swv-panel itself to 85 there, and no escaping
     chrome sits between it and #app on that layout). */
  body.rail-open #map-wrap{z-index:602}
}

/* ============================================================
   THE VILLA OVERVIEW — #swv-ov, the flagged reskin of the story
   overlay. It reuses .slayer, .scap, #flight and #story-exit exactly
   as the flag off story does; every rule below only ever matches a
   swv-ov-* class or #swv-ov itself, so the flag off render never
   meets them. */
#swv-ov{position:relative;height:100%}

/* the caption layer keeps fading exactly as .scap already does; only
   its own background and what sits inside it change */
.scap.swv-ov-section{padding:0;
  background:linear-gradient(to top,rgba(0,0,0,.55) 0%,rgba(0,0,0,.12) 45%,rgba(0,0,0,.12) 100%);
  color:var(--swv-neutral-50)}
.swv-ov-kicker{position:absolute;left:16px;bottom:189px;width:640px;max-width:calc(100% - 32px);
  font:500 22px/24.2px var(--swv-sans);color:var(--swv-neutral-50)}
.swv-ov-title{position:absolute;left:16px;bottom:75px;width:640px;max-width:calc(100% - 32px);
  font:400 48px/52.8px var(--swv-serif);letter-spacing:-1.44px;color:var(--swv-neutral-50)}
.swv-ov-chips{position:absolute;left:16px;bottom:35px;display:flex;align-items:center;
  font:400 16px/20.8px var(--swv-sans);color:var(--swv-neutral-50)}
.swv-ov-chip{display:flex;align-items:center;gap:4px}  /* the label and its value are separate flex items, so the space between them in the markup is dropped */
.swv-ov-chip b{font-weight:500}
.swv-ov-chip:not(:first-child){margin-left:8px}
.swv-ov-chip:not(:first-child)::before{content:"";width:2px;height:2px;border-radius:50%;
  background:var(--swv-neutral-50);margin-right:8px;display:inline-block}
.swv-ov-hint{position:absolute;left:50%;bottom:35px;transform:translateX(-50%);
  display:flex;align-items:center;gap:8px;font:400 16px/20.8px var(--swv-serif);color:var(--swv-neutral-50)}
.swv-ov-hint-ic svg{display:block;width:16px;height:16px;transform:rotate(180deg)}

/* ---------- back pill: #story-exit, reused, not a second button ---------- */
#story .exit.swv-ov-back{position:fixed;left:16px;top:68px;width:128px;height:36px;padding:0;
  display:flex;align-items:center;justify-content:center;gap:6px;border:0;border-radius:999px;
  background:rgba(247,248,243,.16);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  font:400 14px/18px var(--swv-sans);letter-spacing:-.42px;text-transform:none;white-space:nowrap;
  color:var(--swv-neutral-50);cursor:pointer}
/* kit Hover fill is #F7F8F3 @32%, not the @24% this carried before */
@media(hover:hover){#story .exit.swv-ov-back:hover{background:rgba(247,248,243,.32)}}
.swv-ov-back-ic svg{display:block;width:20px;height:20px;transform:rotate(-90deg)}
#story .exit.swv-ov-back:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}

/* ---------- thumbnails, the P2 Image Preview reused for beats rather than villas ---------- */
.swv-ov-thumbs{position:absolute;right:17px;bottom:16px;display:flex;gap:11px;z-index:70}
.swv-ov-thumbs .swv-thumb{width:77px;height:48px;padding:0;border-radius:1px;overflow:hidden;
  cursor:pointer;border:1px solid transparent;opacity:.8;background:none}
.swv-ov-thumbs .swv-thumb img{width:100%;height:100%;object-fit:cover;display:block}
.swv-ov-thumbs .swv-thumb[aria-current="true"]{opacity:1;border-color:var(--swv-neutral-50)}
@media(hover:hover){
  .swv-ov-thumbs .swv-thumb:hover:not([aria-current="true"]){opacity:1;border-color:rgba(247,248,243,.5)}
}
.swv-ov-thumbs .swv-thumb:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}

/* ---------- the progress slider ---------- */
.swv-ov-slider{position:fixed;right:16px;top:50%;transform:translateY(-50%);z-index:70;
  width:24px;padding:5px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;
  gap:8px;background:rgba(247,248,243,.16);border-radius:99px;transition:opacity .3s}
.swv-ov-slider i{width:8px;height:8px;padding:0;border-radius:99px;flex:none;display:block;
  background:var(--swv-neutral-50);transition:height .4s cubic-bezier(.22,1,.36,1);cursor:pointer}
.swv-ov-slider i.on{height:72px}
.swv-ov-slider i:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}
#swv-ov.swv-ov-last .swv-ov-slider{opacity:0;pointer-events:none}
@media(prefers-reduced-motion:reduce){
  .swv-ov-slider i{transition:none}
  .swv-ov-slider{transition:none}
}

/* ---------- the CTA card, replacing the panel on the last beat ---------- */
.swv-ov-cta{position:fixed;right:16px;top:68px;width:540px;height:816px;box-sizing:border-box;
  border-radius:4px;background:var(--swv-neutral-50);color:var(--swv-accent-950);
  opacity:0;pointer-events:none;transition:opacity .3s;z-index:75}
/* swv-ov-landed, not swv-ov-last: the last beat's own clip opens on the
   exterior and flies into the room, so the last beat starting is the start
   of that flight, not its end. Gating on swv-ov-last alone showed the card
   over footage still arriving while its caption already named the room, for
   about 90px before swv-ov-past hid it again. Measured on production at
   1440x900, the card appeared at scroll 2473 of 5936. paintStoryChrome
   (engine.js) sets swv-ov-landed once WGFlight.landingPoint says the footage
   has actually reached its final frame; a story with no flight lands the
   instant its last beat is current, the same point swv-ov-last already
   marks. */
#swv-ov.swv-ov-landed .swv-ov-cta{opacity:1;pointer-events:auto}
/* and away again once the flight has left the screen — same specificity as
   the rule above, so source order is what decides it, and past beats last. */
#swv-ov.swv-ov-past .swv-ov-cta{opacity:0;pointer-events:none}
.swv-ov-cta-eyebrow{position:absolute;left:24px;top:24px;border:0;background:none;padding:0;cursor:pointer;
  font:300 italic 16px var(--swv-serif);color:var(--swv-accent-950)}
.swv-ov-cta-eyebrow:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:3px}
.swv-ov-cta-img{position:absolute;right:16px;top:16px;width:300px;height:400px;border-radius:2px;
  overflow:hidden;background:var(--swv-neutral-200)}
.swv-ov-cta-img img{width:100%;height:100%;object-fit:cover;display:block}
.swv-ov-cta-go{position:absolute;left:24px;bottom:87px;display:flex;align-items:center;gap:12px;
  text-decoration:none;color:var(--swv-accent-950)}
.swv-ov-cta-title{font:400 48px/52.8px var(--swv-serif);letter-spacing:-1.44px}
.swv-ov-cta-arrow{display:flex;align-items:center;justify-content:center;width:44px;height:44px;flex:none}
.swv-ov-cta-arrow svg{width:24px;height:24px;transition:transform .3s var(--ease)}
@media(hover:hover){.swv-ov-cta-go:hover .swv-ov-cta-arrow svg{transform:translateX(4px)}}
.swv-ov-cta-go:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:3px;border-radius:2px}
.swv-ov-cta-caption{position:absolute;left:24px;bottom:24px;font:400 16px var(--swv-sans)}
@media(prefers-reduced-motion:reduce){.swv-ov-cta-arrow svg{transition:none}.swv-ov-cta{transition:none}}

/* ---------- the homes panel, docked over the overview instead of the map ---------- */
body.swv-ov-open #swv-panel{z-index:85}
#swv-panel.swv-ov-hide{display:none}
@media(min-width:861px){body.swv-ov-open #swv-panel{width:360px}}

@media(max-width:860px){
  /* one column: a fixed 390px media band at the top of the section, the
     text on plain ground underneath it rather than lit over the photo —
     the photo has no room left under it to stay legible at this width.
     .slayer and .flight-stage both carry their own inline inset:0, so
     only an !important wins against them. */
  #swv-ov .slayer,#swv-ov .flight-stage{top:0 !important;bottom:auto !important;height:390px !important;overflow:hidden}
  .scap.swv-ov-section{position:absolute;top:390px;bottom:0;left:0;right:0;
    background:var(--swv-neutral-50);color:var(--swv-accent-950);
    display:flex;flex-direction:column;gap:8px;padding:20px 16px;overflow:hidden}
  .swv-ov-kicker,.swv-ov-title,.swv-ov-chips,.swv-ov-hint{position:static;left:auto;bottom:auto;transform:none}
  .swv-ov-kicker{width:auto;max-width:none;font-size:18px;line-height:1.2;color:var(--swv-accent-950)}
  .swv-ov-title{width:auto;max-width:none;font-size:32px;line-height:1.15;letter-spacing:normal;color:var(--swv-accent-950)}
  .swv-ov-chips{color:var(--swv-accent-950);flex-wrap:wrap;row-gap:6px}
  .swv-ov-chip:not(:first-child)::before{background:var(--swv-accent-950)}
  .swv-ov-hint{color:var(--swv-accent-950);font-size:14px}
  #story .exit.swv-ov-back{left:12px;top:76px;background:rgba(247,248,243,.08)}
  .swv-ov-thumbs,.swv-ov-slider{display:none}
  /* Fault 2/4: at desktop the card is a fixed overlay that only appears on
     the last beat, painted over the still-scrolling image. A phone has no
     room to spare for an overlay, and #swv-footer sits right after it in
     the document (see openStory in engine.js, which renders the card as
     .flight's sibling, the last thing before #swv-ov-foot), so here the card
     drops into that same normal flow, the last thing before the footer,
     instead of staying pinned over it. */
  /* the release above is for the desktop overlay only. Here the card is part
     of the scroll, the last thing before the footer, so hiding it once the
     flight is behind us would take the only route to the details with it. */
  #swv-ov.swv-ov-past .swv-ov-cta{opacity:1;pointer-events:auto}
  /* Figma 13:11950: the phone card is its own fixed 238x354 size, right
     aligned with an 8px gutter, not the desktop column stretched to full
     width. Children go back to the same absolute-inside-a-positioned-card
     shape the desktop rule already uses, just at these smaller coordinates. */
  /* position:relative still honours the base rule's own right:16px;top:68px
     as offsets from the card's normal-flow position (position:relative,
     unlike static, never ignores them), so both must be cancelled here or
     the card silently drifts left and down from where its margin puts it. */
  .swv-ov-cta{position:relative;right:auto;top:auto;width:238px;height:354px;
    margin:16px 8px 16px auto;padding:0;display:block}
  .swv-ov-cta-eyebrow{position:absolute;left:12px;top:8px;line-height:19px}
  .swv-ov-cta-img{position:absolute;left:8px;right:auto;top:35px;width:222px;height:230px;
    aspect-ratio:auto;background:color-mix(in srgb, var(--swv-accent-950) 8%, transparent)}
  .swv-ov-cta-go{position:absolute;left:16px;top:289px;bottom:auto}
  .swv-ov-cta-title{font-size:28px;line-height:31px;letter-spacing:normal}
  .swv-ov-cta-arrow{width:28px;height:28px}
  .swv-ov-cta-arrow svg{width:20px;height:20px}
  /* the icon stays Figma's own 28x28, but that leaves the anchor's real hit
     area — title plus icon, ~31px tall in a flex row — short of the 44px tap
     target the only route to the details chapters on a phone needs. An
     invisible ::after enlarges the hit box without moving anything in it. */
  .swv-ov-cta-go::after{content:"";position:absolute;inset:-8px}
  .swv-ov-cta-caption{position:absolute;left:16px;top:324px;bottom:auto;
    font-size:12px;line-height:14px}
  /* The back-to-map button is otherwise fixed for the whole scroll (see the
     base rule above), so once the flight ends it keeps floating over
     Footer 2.0 too. swv-ov-last already means "the flight is behind us, the
     card and the footer are what's left" (paintStoryChrome in engine.js
     clamps p at 1 and never unsets the class past that point) — reusing it
     here needs no new scroll math, just hiding chrome that no longer has a
     map underneath it to return to. */
  #swv-ov.swv-ov-last .exit.swv-ov-back{opacity:0;pointer-events:none}
}
@media(max-width:860px) and (hover:hover){
  #story .exit.swv-ov-back:hover{background:rgba(247,248,243,.16)}
}

/* ==========================================================================
   FOOTER 2.0 — Figma node 8:8865. Two 80px row lists on the accent ground,
   then a 70px bar. It ends the villa overview scroll, right after Main.
   ========================================================================== */
/* full bleed: the released #foot carries a 100px side padding that would
   inset every row, and Figma's rows run the whole 1440 so the hover fill
   reaches both edges. The empty ground the frame opens on is the first
   list's own padding instead, below. */
.swv-footer{background:var(--swv-accent-950);color:var(--swv-neutral-50);padding:0}
.swv-foot-list{display:flex;flex-direction:column}
/* Figma opens the frame on empty ground and sets the two lists apart. Its own
   numbers, 235px above and 40px between, overflow the 900px frame they are
   drawn in, so the gap above is taken proportionally rather than literally. */
.swv-foot-list-a{padding-top:96px;margin-bottom:40px}
.swv-foot-row{position:relative;display:block;height:80px;text-decoration:none;color:inherit;
  background-color:rgba(255,255,255,0);transition:background-color .2s var(--ease)}
.swv-foot-row:hover,.swv-foot-row:focus-visible{background-color:rgba(255,255,255,.04);outline:none}
.swv-foot-row-in{max-width:800px;height:100%;margin:0 auto;padding:0 16px;
  display:flex;align-items:center;gap:24px}
/* No thumbnail column. Figma's own footer rows each carry a 128x72 image
   frame, but the product owner asked for them gone, so this is a deliberate
   departure from the file rather than a match to it. Removing the slot as
   well as the picture is what keeps the labels aligned: an empty 128px
   reservation would indent every row against nothing. */
.swv-foot-label{font:300 72px/72px var(--swv-serif);letter-spacing:0;white-space:nowrap}
.swv-foot-idx{display:inline-block;font:700 18px/21.6px var(--swv-sans);
  vertical-align:top;position:relative;top:4px;margin-left:8px}
.swv-foot-cap{font:400 16px/19.2px var(--swv-sans);margin-left:auto;flex:none;padding-left:16px}
.swv-foot-arrow{position:absolute;right:16px;top:50%;transform:translateY(-50%);
  width:24px;height:24px;display:flex;align-items:center;justify-content:center;
  color:var(--swv-neutral-50);opacity:0;transition:opacity .2s var(--ease)}
.swv-foot-arrow svg{width:24px;height:24px;display:block}
.swv-foot-row:hover .swv-foot-arrow,.swv-foot-row:focus-visible .swv-foot-arrow{opacity:1}
/* The concierge pill is fixed at the bottom right of every v1 page and the
   legal links are the bottom right of the footer, so on the last screen one
   sat on top of the other. The bar keeps its own height and its own
   alignment, and the footer ends with the pill's worth of room under it. */
.swv-foot-bar{height:70px;padding:0 16px 24px;display:flex;align-items:center;justify-content:space-between}
.swv-foot-bar-name{font:500 16px/19.2px var(--swv-sans)}
.swv-foot-bar-copy{font:400 16px/19.2px var(--swv-sans);opacity:.7}
.swv-foot-bar-r{display:flex;gap:24px;margin-right:246px}
.swv-foot-bar-r a{font:400 16px/19.2px var(--swv-sans);color:inherit;text-decoration:none}
@media(prefers-reduced-motion:reduce){
  .swv-foot-row,.swv-foot-arrow{transition:none}
}
@media(max-width:600px){
  .swv-foot-row{height:auto;min-height:96px;padding:12px 0}
  .swv-foot-row-in{flex-direction:column;align-items:flex-start;gap:2px;padding:0 16px}
  .swv-foot-list-a{padding-top:40px;margin-bottom:24px}
  .swv-foot-label{font:300 40px/44px var(--swv-serif)}
  .swv-foot-cap{margin-left:0;padding-left:0}
  .swv-foot-bar{height:auto;flex-direction:column;align-items:flex-start;gap:12px;padding:16px 16px 96px}
  /* stacked at this width, so the links are at the left and clear the pill
     sideways; the column's own bottom padding is what keeps them off it. */
  .swv-foot-bar-r{margin-right:0}
}

/* ==========================================================================
   THE DETAILS PAGE — THE VILLA CHAPTER (v1), Figma 8:8185. Desktop only —
   no phone breakpoint is drawn here; the flag-off page below 700px width and
   the released .dhead.dhero above it are both untouched. #swv-panel is
   parked inside .dv1 by dv1ParkPanel() (engine.js) and keeps its own fixed
   position/size from the rules above — nothing here targets it.
   ========================================================================== */
.dv1{position:relative;background:var(--swv-neutral-50);color:var(--swv-accent-950)}
/* No font-family reset here. engine.css already gives every button
   font-family:inherit near the top, and a .dv1 button rule outranks the
   .dv1-tab class below it, which is how the tabs came out in the page's own
   Montserrat rather than the Satoshi they ask for. */
.dv1 *:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}

.dv1-nav{position:absolute;top:0;left:0;right:0;height:60px;z-index:5;
  display:flex;align-items:center;justify-content:space-between;padding:12px 16px;box-sizing:border-box}
.dv1-nav .dhero-mark{position:static;height:36px;width:auto;opacity:1;cursor:pointer}
.dv1-nav .dhero-burger{position:static}

.dv1-hero{position:relative;height:900px;max-height:100vh;min-height:520px;overflow:hidden}
.dv1-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.dv1-hero-scrim{position:absolute;inset:0;background:#000;opacity:.12}
.dv1-hero-content{position:absolute;left:16px;right:16px;bottom:16px;max-width:640px;
  display:flex;flex-direction:column;gap:4px;color:var(--swv-neutral-50)}
.dv1-descriptor{font:500 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;opacity:.86}
.dv1-price{font:400 40px/44px var(--swv-serif)}
.dv1-heading{font:400 22px/24px var(--swv-serif);letter-spacing:-0.66px;margin-top:4px}

/* ---------- Section two: Overview + Media ---------- */
.dv1-sec2{display:flex;padding:12px;box-sizing:border-box;gap:0}
.dv1-col{width:704px;flex:none;box-sizing:border-box;padding:0 24px}
.dv1-media{width:712px;flex:none;position:relative;border-radius:4px;overflow:hidden;
  background:var(--swv-neutral-100);min-height:520px}
.dv1-media-stage{position:absolute;inset:0}
.dv1-media-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;
  transition:opacity .18s ease}
.dv1-media-img.on{opacity:1}
@media(prefers-reduced-motion:reduce){.dv1-media-img{transition:none}}

/* Figma draws this 266px wide holding three content sized pills, which only
   adds up because its own labels are short. Ours come from CHAPTER_META and
   are longer, so the bar sizes to its content instead of forcing three equal
   85px cells that wrapped every label onto two lines. */
.dv1-tabs{width:max-content;max-width:100%;margin:16px auto 48px;padding:2px;box-sizing:border-box;
  display:flex;gap:2px;border-radius:999px;background:color-mix(in srgb, var(--swv-accent-950) 8%, transparent)}
/* On a phone the row takes the page's own gutter instead of being centred.
   Centred, its left edge moved with the labels: 41px in from a 390 screen
   against the 36px everything below it lines up on, and 36 left against 59
   right at 320. Left aligned to the same gutter, it is the same whitespace
   at every width, and it scrolls sideways rather than shrinking if the
   labels ever outgrow the screen (the tabs stay on one line each, which is
   what width:max-content above is for). */
@media(max-width:860px){
  .dv1-tabs{margin-left:0;margin-right:auto;overflow-x:auto;scrollbar-width:none}
  .dv1-tabs::-webkit-scrollbar{display:none}
}
.dv1-tab{flex:none;min-height:32px;padding:0 16px;white-space:nowrap;border:none;border-radius:999px;background:transparent;
  color:var(--swv-accent-950);font:400 14px/18px var(--swv-sans);letter-spacing:-.42px;cursor:pointer;
  transition:background-color .2s var(--ease)}
.dv1-tab:hover{background:color-mix(in srgb, var(--swv-accent-950) 12%, transparent)}
.dv1-tab.active{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
.dv1-tab.active:hover{background:var(--swv-accent-950)}

.dv1-overview-h{font:400 48px/53px var(--swv-serif);letter-spacing:-1.44px;margin:0 0 24px}
.dv1-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px 24px;margin-bottom:28px;
  border-top:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);padding-top:20px}
.dv1-fact-k{font:400 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;opacity:.6}
.dv1-fact-v{font:400 20px/26px var(--swv-serif);margin-top:4px}
.dv1-fact-v--rs{color:color-mix(in srgb, var(--swv-accent-950) 70%, #B5673A)}
.dv1-pill-btn{min-height:44px;padding:0 24px;border:none;border-radius:999px;
  background:var(--swv-accent-950);color:var(--swv-neutral-50);font:500 14px/1 var(--swv-sans);cursor:pointer}
.dv1-pill-btn:hover{opacity:.88}
.dv1-note{font:300 16px/18px var(--swv-serif);letter-spacing:-.48px;max-width:34em;margin:20px 0 0}

.dv1-media-expand{position:absolute;left:16px;top:50%;transform:translateY(-50%);
  width:44px;height:44px;border:none;border-radius:100px;background:var(--swv-neutral-50);
  display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--swv-accent-950)}
.dv1-media-expand .dv1-ic svg{width:20px;height:20px;transform:rotate(45deg)}
.dv1-dots{position:absolute;right:16px;bottom:16px;height:24px;padding:0 10px;border-radius:100px;
  background:color-mix(in srgb, var(--swv-neutral-50) 16%, transparent);
  display:flex;align-items:center;gap:6px}
.dv1-dots i{width:8px;height:8px;border-radius:999px;background:var(--swv-neutral-50);opacity:.4;cursor:pointer}
.dv1-dots i.on{opacity:1}
.dv1-arrows{position:absolute;left:16px;bottom:16px;display:flex;align-items:center;gap:8px}
.dv1-arrow-btn{width:44px;height:44px;border:none;border-radius:4px;background:var(--swv-neutral-50);
  color:var(--swv-accent-950);display:flex;align-items:center;justify-content:center;cursor:pointer}
.dv1-arrow-btn .dv1-ic svg{width:20px;height:20px}
.dv1-arrow-l .dv1-ic svg{transform:scaleX(-1)}
.dv1-arrow-idx{min-width:70px;height:40px;padding:0 12px;border-radius:4px;background:var(--swv-neutral-50);
  color:var(--swv-accent-950);font:400 13px/40px var(--swv-sans);text-align:center}

/* ---------- Section three: the journey ---------- */
/* The dark half runs the whole height of the steps beside it, and the title
   inside it is what sticks. Figma draws one 900px section because its own
   mock carries six short steps; the village's real journey copy is longer,
   and a fixed 900px block ended halfway down, leaving the rest of the steps
   on bare cream. A stretched column cannot end early whatever the copy does. */
.dv1-sec3{display:flex;align-items:stretch}
.dv1-sticky{width:720px;flex:none;min-height:900px;box-sizing:border-box;
  position:relative;
  background:var(--swv-accent-950);color:var(--swv-neutral-50);padding:0 16px}
/* The faint masterplan sketch, ASSETS.JOURNEY_SKETCH_V1 — --dv1-journey-sketch
   is set inline by dv1JourneySectionHTML() only when a village has the key,
   the same "no property, no image" rule engine.css keeps everywhere else so
   this file names no tenant's file. The source art is dark line work on a
   transparent ground (drawn for the light boot splash); invert(1) turns it
   pale so the same file reads correctly on this dark ground instead.
   No overflow:hidden here — that would make .dv1-sticky itself a scroll
   container, which is what a sticky descendant sticks within, and this one
   would then have nowhere to scroll and never appear to stick at all. inset:0
   already keeps the layer inside the box without it. */
.dv1-sticky::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:var(--dv1-journey-sketch, none) center/cover no-repeat;
  filter:invert(1);opacity:.18}
.dv1-sticky-in{position:sticky;top:0;height:100vh;z-index:1;
  display:flex;flex-direction:column;justify-content:center}
@media(prefers-reduced-motion:reduce){
  .dv1-sticky-in{position:static;height:auto;min-height:900px}
}
.dv1-sticky-h{font:400 48px/53px var(--swv-serif);letter-spacing:-1.44px;margin:0 0 16px;text-align:center}
/* Figma 22:7344 centres both on the panel: the title across it, the lead as
   a 320px block on the same axis. */
.dv1-sticky-p{font:400 16px/21px var(--swv-serif);letter-spacing:-.48px;max-width:320px;margin:0 auto;text-align:center}
.dv1-journey{width:720px;flex:none;position:relative;box-sizing:border-box;padding:120px 96px 200px}
@media(min-width:1440px){
  /* Figma 22:7344/22:9479 both hold each step to a 360px measure inside this
     720px column — 180px either side. Left at the mobile-safe 96px below
     1440px, that measure would narrow to almost nothing on a phone. */
  .dv1-journey{padding-left:180px;padding-right:180px}
}
/* No divider between steps in either Figma render, just whitespace — the
   80px gap is the distance Figma's own step instances sit apart (22:9479). */
.dv1-jstep{padding:0 0 80px}
.dv1-jstep:last-child{padding-bottom:0}
.dv1-jk{font:500 12px/16px var(--swv-sans);letter-spacing:-.36px;color:var(--swv-neutral-600);margin:0 0 16px}
.dv1-jt{font:400 26px/29px var(--swv-serif);letter-spacing:-.78px;color:var(--swv-neutral-950);margin:0 0 12px}
.dv1-jp{font:400 16px/21px var(--swv-serif);letter-spacing:-.48px;color:var(--swv-neutral-600);margin:0 0 12px}
/* The hint line — "40% on signing", Figma's own Hint text — plain sans, per
   the full list at 22:9479; see dv1JourneyHTML's own comment for why. */
.dv1-jpay{font:400 12px/16px var(--swv-sans);letter-spacing:-.36px;color:var(--swv-neutral-600);margin:0}
.dv1-fade{position:absolute;left:0;right:0;bottom:0;height:240px;pointer-events:none;
  background:linear-gradient(to bottom, transparent, var(--swv-neutral-50))}

@media(max-width:1439px){
  .dv1-sec2{flex-wrap:wrap}
  .dv1-col,.dv1-media{width:100%}
  .dv1-sec3{flex-wrap:wrap}
  .dv1-sticky,.dv1-journey{width:100%;position:static;min-height:0}
}

/* ==========================================================================
   THE DETAILS PAGE — THE NUMBERS CHAPTER (v1), Figma 8:5699. Desktop only,
   same caveat as the villa chapter above: no phone breakpoint is drawn here,
   reflow below 1439px comes from .dv1-sec2/.dv1-col/.dv1-media's own rule.
   Everything below extends the villa chapter's own .dv1/.dv1-col/.dv1-media/
   .dv1-tabs/.dv1-fact-k/.dv1-fact-v — only what is different about this
   chapter (the accordion rows and the ring) gets a new class. */
.dv1n-media{padding:40px;display:flex;flex-direction:column;align-items:center;text-align:center}
.dv1n-media-title{font:400 clamp(26px,2.6vw,36px)/1.2 var(--swv-serif);letter-spacing:-.02em}
.dv1n-media-desc{font:400 16px/21px var(--swv-serif);letter-spacing:-.48px;color:var(--swv-neutral-600);margin:8px 0 0}

/* 378px — the spec's own Diagram frame (8:5700), which the panel's 712px
   width and 40px padding leave comfortable room for */
.dv1n-ring-wrap{position:relative;width:378px;height:378px;margin:32px auto 8px}
.dv1n-ring{width:100%;height:100%;display:block}
.dv1n-ring-fill{transition:stroke-dasharray .3s var(--ease)}
/* pointer-events:none — this box is caption text only, but its inset:0
   spans over the flip button's right half (the button sits at left:-12px so
   only half of it pokes outside the wrap), and a div painted after a
   button in source order wins the hit test unless it opts out. */
.dv1n-ring-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 24px;pointer-events:none}
.dv1n-ring-cap{font:400 14px/18px var(--swv-sans);letter-spacing:-.42px}
.dv1n-ring-fig{font:400 32px/35px var(--swv-sans);letter-spacing:-.96px;margin:4px 0}
.dv1n-ring-flip{position:absolute;left:-12px;top:50%;transform:translateY(-50%);
  width:40px;height:40px;border:none;border-radius:100px;background:var(--swv-neutral-50);
  display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--swv-accent-950)}
.dv1n-ring-flip svg{width:20px;height:20px;transform:rotate(180deg)}
.dv1n-ring-flip:hover{opacity:.88}

.dv1n-legend{width:100%;max-width:420px;margin-top:16px}
.dv1n-lrow{display:flex;align-items:center;gap:10px;padding:14px 0;
  border-bottom:1px solid var(--swv-divider);font:400 12px/16px var(--swv-sans);letter-spacing:.02em}
.dv1n-sw{width:8px;height:8px;border-radius:2px;flex:none}
.dv1n-lk{flex:1;text-align:left;color:var(--swv-neutral-600)}
.dv1n-lv{color:var(--swv-neutral-600)}
.dv1n-ltotal{border-bottom:none;font-weight:500}

/* accordion rows — native <details>, no marker, a chevron that turns with
   [open] rather than a second script-driven class */
.dv1n-row{border-top:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent)}
.dv1n-row:first-of-type{border-top:none}
.dv1n-row summary{list-style:none;cursor:pointer}
.dv1n-row summary::-webkit-details-marker{display:none}
.dv1n-row-h{display:flex;align-items:center;gap:12px;padding:20px 0}
.dv1n-row-t{font:400 22px/26px var(--swv-serif);letter-spacing:-.02em}
.dv1n-row-s{flex:1;text-align:right;font:400 14px/18px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-chev svg{width:18px;height:18px;transform:rotate(90deg);transition:transform .2s var(--ease)}
.dv1n-row[open] .dv1n-chev svg{transform:rotate(-90deg)}
.dv1n-row-b{padding:0 0 24px}

.dv1n-brow{display:flex;align-items:baseline;gap:16px;padding:8px 0;
  font:400 14px/20px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-bname{flex:1}
.dv1n-bval{color:var(--swv-accent-950)}
.dv1n-btotal{border-top:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  margin-top:6px;padding-top:14px;font-weight:500}
.dv1n-btotal .dv1n-bname,.dv1n-btotal .dv1n-bval{color:var(--swv-accent-950)}

.dv1n-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:16px 20px;margin-bottom:8px}
.dv1n-ts{font:400 12px/16px var(--swv-sans);opacity:.6;margin-top:2px}

.dv1n-stages{display:flex;flex-wrap:wrap;gap:20px 24px;margin-bottom:8px}
.dv1n-stages>div{flex:1 1 96px;min-width:96px}

.dv1n-note{font:400 13px/18px var(--swv-sans);color:var(--swv-neutral-600);margin:4px 0 0}

/* gated: chapterTeaserHTML alone, no media column beside it. Let the one
   remaining column take the row rather than sit 704px wide in a 1416px row.
   Shared by all three v1 chapters now (the villa and furnishing chapters
   gate the same way numbers already did), so the name dropped its "n". */
.dv1-gated .dv1-col{width:100%}

/* ==========================================================================
   THE DETAILS PAGE — THE FURNISHING CHAPTER (v1), Figma 8:7590. Desktop
   only, same caveat as the two chapters above.

   Reuses stylerConfiguratorHTML's own sub-pieces (stylerVbarHTML,
   stylerTabsHTML, stylerPanelHTML, stylerStageHTML) verbatim, so their
   layout rules below the old `.styler` prefix are reused unchanged too —
   only their colour needs to move onto this chapter's own palette.
   Redefining --ink/--paper/--cream/--terra/--well/--dim inside .dv1 is the
   same move body.on-bone makes for the bone ground, elsewhere in this file:
   every rule under `.styler`/`.leadwrap`/`.fld`/`.btn` already reads those
   custom properties, so one remap re-skins the whole configurator and lead
   form without a second copy of their layout. Only the handful of rules
   that paint a literal rgba(230,225,209,…) sand rather than var(--paper) —
   borders, mostly — get a line of their own underneath it. */
.dv1{
  --ink:var(--swv-accent-950); --paper:var(--swv-neutral-50); --cream:var(--swv-neutral-50);
  --terra:var(--swv-accent-500); --well:var(--swv-neutral-100);
  /* neutral-600, not 500. Inside this scope --dim reaches the styler's own
     labels at 10.5px and 11px (.styler .fvtot .k, .styler .fvbar .fvlab),
     and 500 measures 4.40:1 on neutral-50 and 3.99:1 on the --well ground
     this scope also sets. Both fail AA body text, on the two chapters a
     buyer spends the longest in. */
  --dim:var(--swv-neutral-600);
}
.dv1-eyebrow{font:500 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;
  color:var(--swv-neutral-600);margin-bottom:8px}
.dv1-rule{height:1px;background:color-mix(in srgb, var(--swv-accent-950) 16%, transparent);margin:40px 0}

.dv1 .styler{margin-top:0}
.dv1 .styler .fstage{border-color:color-mix(in srgb, var(--swv-accent-950) 20%, transparent)}
.dv1 .styler .ftabs button{border-color:color-mix(in srgb, var(--swv-accent-950) 22%, transparent)}
.dv1 .styler .fpal button{border-color:color-mix(in srgb, var(--swv-accent-950) 22%, transparent)}
.dv1 .styler .fvbar{border-top-color:color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 16%, transparent)}
.dv1 .styler .frp-chip{border-color:color-mix(in srgb, var(--swv-accent-950) 24%, transparent)}
/* the "inversion fallout" rule two chapters up (search .cbchip.active) repaints
   these three .on states as background:var(--paper);color:var(--cream) — sound
   everywhere else, because every other theme in this file keeps --paper and
   --ink equal (both sand at root, both forest on bone). This chapter is the
   first to split them, so that rule now paints an on-state light on light.
   Named directly rather than through --ink/--paper, so it can't drift out of
   step with whichever token a future rule reaches for next. */
.dv1 .styler .ftabs button.on,
.dv1 .styler .fpal button.on,
.dv1 .styler .frp-chip.on{
  background:var(--swv-accent-950);color:#FFFFFF;border-color:var(--swv-accent-950)}

/* .btn's own base colour is var(--paper) — a light theme's own text colour
   everywhere else, so a site-wide "inversion fallout" pass later repaints
   .btn.terra/.btn.solid to background:var(--paper);color:var(--cream) too —
   same paper-equals-ink assumption as above, same fix. "Start designing",
   "Save the name" and "Save & send" all carry one of these two classes. */
.dv1 .btn{color:var(--swv-accent-950)}
.dv1 .btn.terra,.dv1 .btn.solid{
  background:var(--swv-accent-950);border-color:var(--swv-accent-950);color:#FFFFFF}
.dv1 .styler .fdivline{background:color-mix(in srgb, var(--swv-accent-950) 16%, transparent)}
.dv1 .styler .fitem{border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 13%, transparent)}
.dv1 .styler .fitem .ftop .chev{border-right-color:color-mix(in srgb, var(--swv-accent-950) 45%, transparent);
  border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 45%, transparent)}
.dv1 .styler .fopt{border-color:color-mix(in srgb, var(--swv-accent-950) 18%, transparent)}
.dv1 .styler .fopt:hover{border-color:color-mix(in srgb, var(--swv-accent-950) 45%, transparent)}
.dv1 .styler .fopt .fsw.none{background:repeating-linear-gradient(45deg,transparent,transparent 3px,
  color-mix(in srgb, var(--swv-accent-950) 24%, transparent) 3px,color-mix(in srgb, var(--swv-accent-950) 24%, transparent) 4px)}
.dv1 .styler .fopt .tick{border-color:color-mix(in srgb, var(--swv-accent-950) 24%, transparent)}
.dv1 .styler .fname-prompt{border-top-color:color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 16%, transparent)}
.dv1 .styler .fnp-row input{border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 28%, transparent)}
.dv1 .fld input{border-bottom-color:color-mix(in srgb, var(--swv-accent-950) 28%, transparent)}

/* the right column: the villa's gallery slider before a name is given (the
   released chapter's own dv1-media, reused as-is, height and all — the
   gate's own left column is short). Once a name is given the left column
   holds the whole configurator and the lead form, easily several screens
   tall, and .dv1-sec2's flex row stretches .dv1-media by default to match
   whatever that is — Figma's own "Media Sticky" frame is one viewport tall
   (8:7850, 708x900) rather than stretched the full 2703px of its own
   section, so align-self overrides the stretch here and the sticky render
   gets that same one-viewport band to hold, not a well of empty ground
   underneath it for as long as the copy runs on. */
#dv1f-media.dv1-media{align-self:flex-start;min-height:min(720px, calc(100vh - 32px))}
.dv1f-stagewrap{position:sticky;top:16px}
.dv1f-stagewrap .fnoncap{padding:0 4px}

@media(max-width:1439px){
  #dv1f-media.dv1-media{align-self:auto;min-height:520px}
  .dv1f-stagewrap{position:static}
}

/* ==========================================================================
   ABOUT, Figma 8:8873 "08. About." — the idea and the village on one
   editorial page. #v-about carries its own ground and ink (--swv-neutral-50 /
   --swv-accent-950) the same way .dv1 does, rather than relying on body's
   on-bone class.

   The anchors card is sticky WITHOUT a scroll handler: .abt-anchors-col
   stretches to .abt-sections' own height (flex's default align-items:
   stretch on .abt-main), so .abt-anchors' containing block ends exactly
   where the sections do, before Footer 2.0 — the same fix .dv1-sticky /
   .dv1-sticky-in used for the journey column, not the swv-ov-past scroll
   listener the villa overview needed for its FIXED overlay card. */
#v-about{background:var(--swv-neutral-50);color:var(--swv-accent-950)}
.abt{position:relative}
.abt-nav.dv1-nav{background:linear-gradient(180deg, rgba(12,11,9,.32), transparent)}
.abt-nav .dhero-burger i{background:var(--swv-neutral-50)}

.abt-hero{position:relative;height:900px;max-height:100vh;min-height:560px;overflow:hidden}
.abt-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.abt-hero-veil{position:absolute;inset:0;background:
  /* Figma draws an ELLIPSE named Shadow behind the title (8:8873, 400x223 at
     the heading) over a hazy sunrise plate. On this render the plate is not
     hazy: it is a lit village in full colour, and a 55% wash left #1A2412
     heading and #57554E body under 2:1 against it, which is unreadable rather
     than airy. The ellipse is therefore close to opaque at the title and gone
     by the edge of the frame, so the type keeps the design's ink colours and
     still clears 4.5:1 wherever the photo happens to be bright. */
  radial-gradient(46% 34% at 50% 52%, rgba(250,250,245,.97), rgba(250,250,245,.80) 48%, rgba(250,250,245,0) 82%),
  linear-gradient(180deg, rgba(250,250,245,.30), rgba(250,250,245,0) 26%, rgba(250,250,245,0) 72%, rgba(250,250,245,.42))}
.abt-hero-mid{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;text-align:center;gap:8px;padding:0 24px}
.abt-hero-desc{font:500 12px/16px var(--swv-sans);letter-spacing:.02em;color:var(--swv-neutral-600)}
.abt-hero-h{font:400 48px/53px var(--swv-serif);letter-spacing:-1.44px;color:var(--swv-accent-950);margin:8px 0}
.abt-hero-p{font:400 16px/21px var(--swv-serif);letter-spacing:-.48px;color:var(--swv-neutral-600);max-width:360px}

.abt-chipbar{display:none}

/* The gap has to clear .abt-sec--dark's bleed, not just separate two columns.
   That band carries margin:0 -40px so its ground runs past the text and its
   own 40px padding puts the words back where every light section's words are.
   At gap:16px the bleed therefore crossed 24px into the anchors column and
   the sticky card, which owns a stacking context, painted on top of it.
   Measured at 1440 and at 1920: 24.0px of overlap before, 24.0px of clearance
   after. 64 is 40 of bleed plus the 24 that is actually visible as a gap. */
.abt-main{max-width:1440px;margin:0 auto;padding:80px 236px;display:flex;gap:64px;align-items:stretch;box-sizing:border-box}
.abt-anchors-col{width:320px;flex:none}
.abt-anchors{position:sticky;top:96px;width:100%;box-sizing:border-box;background:var(--swv-neutral-50);
  border:1px solid var(--swv-neutral-200);border-radius:4px;padding:5px;display:flex;flex-direction:column}
.abt-anchor{display:flex;align-items:center;width:100%;box-sizing:border-box;text-align:left;
  padding:16px;border-radius:2px;border:none;font:500 12px/16px var(--swv-sans);color:var(--swv-accent-950);position:relative}
.abt-anchor:hover{background:var(--swv-neutral-100)}
.abt-anchor.active{background:var(--swv-neutral-100)}
.abt-anchor.active::after{content:"";position:absolute;right:16px;top:50%;transform:translateY(-50%);
  width:3px;height:3px;border-radius:99px;background:var(--swv-accent-950)}

.abt-sections{flex:1;min-width:0}
.abt-sec{padding:64px 0;border-top:1px solid var(--swv-neutral-200)}
.abt-sec:first-child{border-top:none;padding-top:0}
.abt-sec--dark{background:var(--swv-accent-950);color:var(--swv-neutral-50);padding:64px 40px;
  margin:0 -40px;border-top:none}
.abt-eyebrow{font:500 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;
  color:var(--swv-neutral-600);margin-bottom:12px}
.abt-sec--dark .abt-eyebrow{color:var(--swv-neutral-300)}
.abt-h{font:400 32px/36px var(--swv-serif);letter-spacing:-.5px;margin:0 0 16px}
.abt-lead{font:400 18px/24px var(--swv-serif);letter-spacing:-.54px;color:var(--swv-neutral-600);margin:0 0 20px}
.abt-sec--dark .abt-lead{color:var(--swv-neutral-300)}
.abt-body{font:300 16px/24px var(--swv-serif);letter-spacing:-.48px;color:var(--swv-neutral-700);margin:0 0 16px}
.abt-sec--dark .abt-body{color:var(--swv-neutral-100)}
.abt-quote{font:400 20px/26px var(--swv-serif);font-style:italic;color:var(--swv-neutral-50);margin-top:20px}

.abt-acc-list{margin-top:16px}
.abt-acc{border-top:1px solid var(--swv-neutral-200);padding:16px 0}
.abt-acc:last-child{border-bottom:1px solid var(--swv-neutral-200)}
.abt-acc summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center;
  gap:16px;font:500 16px/22px var(--swv-sans);color:var(--swv-accent-950)}
.abt-acc summary::-webkit-details-marker{display:none}
.abt-acc summary::after{content:"+";flex:none;font:300 22px/22px var(--swv-sans);color:var(--swv-neutral-500)}
.abt-acc[open] summary::after{content:"\2013"}
.abt-acc summary em{font-style:normal;color:var(--swv-neutral-600)}
.abt-acc p{font:300 15px/22px var(--swv-serif);color:var(--swv-neutral-600);margin-top:10px}

.abt-video{position:relative;height:355px;border-radius:4px;overflow:hidden;
  background:var(--swv-neutral-950);margin:64px 0}
.abt-video-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.68}
.abt-video-veil{position:absolute;inset:0;background:rgba(12,11,9,.32)}
.abt-video-play{position:absolute;left:50%;top:50%;transform:translate(-50%,-64px);width:80px;height:80px;
  border-radius:99px;background:rgba(250,250,245,.04);border:1px solid rgba(250,250,245,.16);
  display:flex;align-items:center;justify-content:center}
.abt-video:not(.ready) .abt-video-play{opacity:.6;cursor:default}
.abt-video.ready{cursor:pointer}
.abt-video-play i{width:0;height:0;border-style:solid;border-width:10px 0 10px 16px;
  border-color:transparent transparent transparent var(--swv-neutral-50);margin-left:4px}
.abt-video-cap{position:absolute;left:50%;bottom:40px;transform:translateX(-50%);width:220px;text-align:center;
  font:500 18px/20px var(--swv-serif);color:var(--swv-neutral-50)}

.abt-list{list-style:none;padding:0;margin:16px 0 0;display:flex;flex-direction:column;gap:10px}
.abt-list li{font:300 16px/24px var(--swv-serif);letter-spacing:-.48px;color:var(--swv-neutral-700);
  padding-left:20px;position:relative}
.abt-list li::before{content:"";position:absolute;left:0;top:10px;width:5px;height:5px;
  border-radius:99px;background:var(--swv-accent-500)}

.abt-quote-block{border-left:2px solid var(--swv-neutral-200);padding-left:24px;margin:16px 0 32px}
.abt-quote-block p{font:400 22px/30px var(--swv-serif);font-style:italic;color:var(--swv-accent-950);margin:0 0 12px}
.abt-quote-block cite{font:500 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;
  color:var(--swv-neutral-600);font-style:normal}
.abt-stats{display:flex;gap:40px;flex-wrap:wrap}
.abt-stat{display:flex;flex-direction:column;gap:4px}
.abt-stat b{font:400 40px/44px var(--swv-serif);color:var(--swv-accent-950)}
.abt-stat span{font:500 12px/16px var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;color:var(--swv-neutral-600)}

@media(prefers-reduced-motion:reduce){
  .abt-video-play{transition:none}
}

@media(max-width:860px){
  .abt-hero{height:auto;min-height:100vh}
  /* A phone frame is tall and narrow, so the desktop ellipse covers a strip
     far too small for three centred lines and the type lands on whatever the
     crop happens to show, which here is dark foliage at about 1.2:1. A band
     across the middle of the frame is the same idea at this shape: the plate
     still reads at the top and bottom, and the text sits on near-solid ground. */
  .abt-hero-veil{background:linear-gradient(180deg,
    rgba(250,250,245,0) 0%, rgba(250,250,245,.55) 22%, rgba(250,250,245,.96) 38%,
    rgba(250,250,245,.96) 68%, rgba(250,250,245,.6) 84%, rgba(250,250,245,.35) 100%)}
  .abt-hero-h{font-size:34px;line-height:38px}
  .abt-main{flex-direction:column;padding:40px 16px;gap:0}
  .abt-anchors-col{display:none}
  /* the anchor card's phone answer: the same horizontal, sticky, scrollable
     chip row .chbar-scroll already uses for chapter nav elsewhere in this
     file, rebuilt on swv tokens rather than the bone theme's --ink/--cream —
     a fixed sidebar has nowhere to sit at this width, and a chip a reader has
     already scrolled past staying reachable by a swipe reads better here
     than the card collapsing into an accordion above the content. */
  /* .abt-chipbar sits outside .abt-main, so it has no padded ancestor to
     bleed against — margin:0 -16px here (the trick .abt-sec--dark uses below,
     correctly, against .abt-main's own padding) would just add 32px to an
     auto width computed from the full unpadded viewport instead of cancelling
     anything, and did exactly that until this said margin:0 instead. */
  .abt-chipbar{display:flex;position:sticky;top:0;z-index:34;gap:8px;overflow-x:auto;
    box-sizing:border-box;padding:12px 16px;margin:0 0 24px;background:var(--swv-neutral-50);
    border-bottom:1px solid var(--swv-neutral-200);scrollbar-width:none}
  .abt-chipbar::-webkit-scrollbar{display:none}
  .abt-chipbar .abt-anchor{white-space:nowrap;width:auto;flex:none;min-height:44px;box-sizing:border-box;
    border:1px solid var(--swv-neutral-200);border-radius:99px;padding:12px 18px}
  .abt-chipbar .abt-anchor.active::after{content:none}
  .abt-chipbar .abt-anchor.active{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
  .abt-sec--dark{margin:0 -16px;padding:48px 16px}
  .abt-video{margin:40px 0}
}

/* ==========================================================================
   ask Grace, wherever #swv-panel is off screen — #/about among them. This is
   now the ONE paint and geometry definition for #c-fab, phone override
   included: the old pill, the brass "ask <name>" lockup with its three dots,
   and two separate round-icon mobile variants each painted it in their own
   place, and every one of them is gone from here now, along with the
   .fab-lock/.fab-ask/.fab-name/.fab-dots/.pt rules that only ever styled
   their now deleted markup. What is left elsewhere is behaviour, not paint:
   position:fixed/z-index and the vd-open hide (further up), the hide rules
   keyed on the panel's own chat button and the loading/cover guards (below),
   and the .c-fab-away scroll transition (above, "v23 A2") — this rule does
   not touch `transition` itself so that one stays in force. Same button as
   .swv-panel-grace: the accent 950 square and the light chat outline. Last
   in the sheet, on purpose, so it wins every remaining #c-fab paint rule at
   the same #c-fab specificity by source order alone. */
#c-fab{
  display:flex;align-items:center;justify-content:center;gap:0;
  width:44px;height:44px;padding:0;
  background:var(--swv-accent-950);color:var(--swv-neutral-50);
  border:0;border-radius:8px;
  box-shadow:0 12px 32px rgba(12,11,9,.16);
  right:16px;bottom:16px}
#c-fab:hover{transform:none;opacity:.82;background:var(--swv-accent-950)}
#c-fab:active{transform:none;background:var(--swv-accent-950);box-shadow:0 12px 32px rgba(12,11,9,.16)}
@media(max-width:860px){
  #c-fab{width:44px;height:44px;padding:0;border-radius:8px;right:16px;bottom:16px}
}

