/* ============================================================
   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{
  /* Type floor, Dave 30 Sep 2026: the kit scale is Secret Waterfall's v1 surfaces only, but
     the shared shell labels (menu, chat head, facility sheet, lightbox hint) take caption 14px
     on every village, one floor for the whole portal. Legacy views keep master's sizes. */
  /* 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) 70%, transparent);  /* 6.7:1 on forest, 4.7:1 on the raised moss ground */
  /* TEXT ROLES. One name per job, per ground, so a heading, a paragraph or a
     caption never picks its own colour or opacity. The floor is WCAG 2.2 AA
     measured against the pixels actually behind the type: 4.5:1 for anything
     under 24px (or 18.66px bold), 3:1 for large type and for icons and
     control outlines. Raising a value is always allowed; lowering one needs a
     measurement. apps/web/e2e/portal-contrast.spec.ts asserts every row.

     role         dark ground (default)      light kit ground      on imagery
     heading      --paper         3:1 large   accent-950            neutral-50
     body         --paper         4.5:1       neutral-700           neutral-50
     muted        --dim           4.5:1       neutral-600           neutral-100
     caption      --dim-soft      4.5:1       neutral-600           neutral-100
     link         --paper         4.5:1       accent-950            neutral-50

     Dark ground values flow from the tenant's own --paper, so a village keeps
     its palette. Light kit values come from the --swv-* ramps, which a
     tenant's DESIGN.md may replace. On imagery the type only passes with a
     scrim behind it: --tx-scrim is the one fill every such block uses, strong
     enough that the lightest tenth of the pixels behind the type still holds
     the ratio. */
  --tx-dark-heading:var(--paper);   --tx-dark-body:var(--paper);
  --tx-dark-muted:var(--dim);       --tx-dark-caption:var(--dim-soft);
  --tx-dark-link:var(--paper);
  --tx-light-heading:var(--swv-accent-950);  --tx-light-body:var(--swv-neutral-700);
  --tx-light-muted:var(--swv-neutral-600);   --tx-light-caption:var(--swv-neutral-600);
  --tx-light-link:var(--swv-accent-950);
  --tx-image-heading:var(--swv-neutral-50);  --tx-image-body:var(--swv-neutral-50);
  --tx-image-muted:var(--swv-neutral-100);   --tx-image-caption:var(--swv-neutral-100);
  --tx-image-link:var(--swv-neutral-50);
  /* The lowest opacity scroll scrubbed type may rest at: muted type at this
     opacity holds 4.5 to 1 on neutral-50 (5.25), accent-100 (4.75), neutral-200
     (4.73) and the deep green card (8.78). */
  --tx-rest-opacity:.88;
  --tx-scrim:rgba(12,11,9,.66);
  --tx-scrim-large:rgba(12,11,9,.52);   /* for 24px and larger only */
  /* Focus ring: a white outline over a near black halo, so one of the two
     holds 3:1 on forest, on bone and on any photo. Applied once, at the foot
     of this file, below the last :focus-visible rule. */
  --tx-focus:#FFFFFF; --tx-focus-halo:#0C0B09;
  /* 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-error:#8A3A1E; /* inline form errors */
  --swv-success:#3F5E28; /* the Available status: a green on every village whatever its accent, white on it 7 to 1 */
  --swv-gold:#DFB02F;   /* the kit's highlight, and the chat launcher's ground */
  --swv-panel-w:360px;  /* the homes panel's width */
  /* The morph: a panel opens out of the button that opened it and folds back into it (Dave, 28 Oct 2026:
     "morphing like Mac app when hide/expand, ensure it's elegant and smooth and reusable for swv and tv").
     One duration and one curve for the Homes panel and the chat, in both villages. .45s because the chat
     travels from a 44px button to a 540px by 800px window, and .4s read hurried at that distance; the Homes
     panel moves less and gains the extra .05s. The curve is the Homes panel's own, a long ease out, no bounce. */
  --wg-morph-dur:.45s;--wg-morph-ease:cubic-bezier(.22,1,.36,1);
  /* The one scrim under white type on a photograph: dark at the foot, held for
     the first fifth, then eased out with a smoothstep so both ends are flat and
     there is no band and no veil. The gradient's own 100% is where it reaches
     nothing; a rule picks how tall that is (background-size) for its frame: the
     whole of the details hero (the story's captions use --swv-scrim-story below). Seventeen stops, because a
     browser interpolates linearly between them and fewer would show a kink. */
  --swv-scrim:linear-gradient(to top, rgba(10,9,6,.86) 0%, rgba(10,9,6,.86) 6.3%, rgba(10,9,6,.86) 12.5%, rgba(10,9,6,.86) 18.8%, rgba(10,9,6,.842) 25%, rgba(10,9,6,.8) 31.3%, rgba(10,9,6,.737) 37.5%, rgba(10,9,6,.659) 43.8%, rgba(10,9,6,.569) 50%, rgba(10,9,6,.473) 56.3%, rgba(10,9,6,.375) 62.5%, rgba(10,9,6,.28) 68.8%, rgba(10,9,6,.191) 75%, rgba(10,9,6,.114) 81.3%, rgba(10,9,6,.054) 87.5%, rgba(10,9,6,.014) 93.8%, rgba(10,9,6,0) 100%);
  /* The story's caption shade is lighter than the one above, on purpose: it
     darkens only the foot of the frame, where the words are, so the room above
     still glows. Two layers in one box, the box being the caption (min-height 44%
     of the frame, background-size 100% 100%): the same smoothstep ease as above,
     held for the first quarter and gone at the top of the box, and under it a
     radial from the bottom left, where the type sits, that adds up to .4 and is
     gone by the box's edge. Neither layer has an edge to see. The caption's
     text-shadow carries the rest of the contrast. The details hero keeps
     --swv-scrim, which has its own contrast test. */
  --swv-scrim-story:radial-gradient(ellipse 85% 100% at 0 100%, rgba(10,9,6,0.4) 0%, rgba(10,9,6,0.392) 8.3%, rgba(10,9,6,0.37) 16.7%, rgba(10,9,6,0.338) 25%, rgba(10,9,6,0.296) 33.3%, rgba(10,9,6,0.25) 41.7%, rgba(10,9,6,0.2) 50%, rgba(10,9,6,0.15) 58.3%, rgba(10,9,6,0.104) 66.7%, rgba(10,9,6,0.062) 75%, rgba(10,9,6,0.03) 83.3%, rgba(10,9,6,0.008) 91.7%, rgba(10,9,6,0) 100%), linear-gradient(to top, rgba(10,9,6,0.9) 0%, rgba(10,9,6,0.9) 4.2%, rgba(10,9,6,0.9) 8.3%, rgba(10,9,6,0.9) 12.5%, rgba(10,9,6,0.9) 16.7%, rgba(10,9,6,0.9) 20.8%, rgba(10,9,6,0.9) 25%, rgba(10,9,6,0.895) 29.2%, rgba(10,9,6,0.875) 33.3%, rgba(10,9,6,0.842) 37.5%, rgba(10,9,6,0.796) 41.7%, rgba(10,9,6,0.741) 45.8%, rgba(10,9,6,0.677) 50%, rgba(10,9,6,0.608) 54.2%, rgba(10,9,6,0.535) 58.3%, rgba(10,9,6,0.459) 62.5%, rgba(10,9,6,0.383) 66.7%, rgba(10,9,6,0.309) 70.8%, rgba(10,9,6,0.239) 75%, rgba(10,9,6,0.174) 79.2%, rgba(10,9,6,0.116) 83.3%, rgba(10,9,6,0.068) 87.5%, rgba(10,9,6,0.032) 91.7%, rgba(10,9,6,0.008) 95.8%, rgba(10,9,6,0) 100%);
  /* The two looks of a villa story caption's text. Unwashed is the default, with no shade behind the
     words: the company site's hero shadow (apps/studio/src/lib/statement.css, --unwashed-shadow), in em so
     it scales with the type. Shaded is the pixel shadow the caption carried over --swv-scrim-story, kept so
     the Caption shade look can bring the old picture back exactly. */
  --swv-story-text-unwashed:0 .01em .01em rgba(22, 20, 16, .9), 0 .02em .06em rgba(22, 20, 16, .85), 0 .04em .19em rgba(22, 20, 16, .7), 0 0 .375em rgba(22, 20, 16, .55);
  --swv-story-text-shaded:0 0 3px rgba(10,9,6,.95),0 0 8px rgba(10,9,6,.8),0 1px 18px rgba(10,9,6,.5),0 2px 40px rgba(10,9,6,.35);
  /* What a caption wears when no look sets --swv-story-shade or --swv-story-text: no shade and the
     unwashed shadow, on every village's villa, facility and apartment stories (RN70, R212), so a new
     tenant needs nothing set. The Caption shade look brings --swv-scrim-story and the shaded text back. */
  --swv-story-shade-default:none;
  --swv-story-text-default:var(--swv-story-text-unwashed);
  --swv-serif:'Cormorant Garamond', Georgia, serif;
  /* The face of the small text this design sets in the serif: form fields, the details
     paragraphs, the About copy. The serif, unless a village's DESIGN.md says otherwise. A
     village whose serif is for titles only (Temple Village, A211) sets it to its sans. */
  --copy-face:var(--swv-serif);
  /* Whether the few small lines this design sets italic slant. They do, unless a village's
     copy face has no italic of its own (Manrope has none), which sets this to normal
     so the browser never fakes one by slanting the upright. */
  --copy-slant:italic;
  --swv-sans:'Satoshi', system-ui, sans-serif;
  /* The brand kit's type scale (swv-tokens.css), phone first; the 861px block
     below is its desktop half, matching the portal's layout breakpoint so the
     phone layouts (which run to 860px) get phone sizes. Only Secret Waterfall's
     v1 surfaces read these: the cover, the explore panel, the footer, About and
     the v1 details page. The older theme (Temple Village's story and the
     included, materials and mine views) keeps its own sizes.
     Floors: About never under body, the rest never under caption. hint is
     defined for the kit's sake and used nowhere. */
  --swv-text-footer-link:54px;
  --swv-text-h1:40px; --swv-text-h2:32px; --swv-text-h3:28px;
  --swv-text-h4:23px; --swv-text-h5:21px; --swv-text-h6:18px; --swv-text-body-serif:16px;
  --swv-text-numerals-lg:40px; --swv-text-numerals-md:28px; --swv-text-numerals-sm:23px;
  --swv-text-body-lg:19px; --swv-text-body:16px; --swv-text-caption:14px; --swv-text-hint:12px;
  /* The kit's spacing steps, corner radii and phone control size, for the same
     v1 surfaces as the type scale above (they hold the values those rules
     already used, so nothing moves). Space is named by its pixels. Radius:
     pill for buttons and tabs, card for contact cards and the map block,
     panel for the Homes drawer, input for form fields. Control is the 44px
     tap target; a pill is drawn pill-h tall and grows its hit area to the
     control height on a transparent pseudo element; pill-pad-x is the
     inline padding every pill shares. */
  --swv-space-4:4px; --swv-space-8:8px; --swv-space-12:12px; --swv-space-16:16px;
  --swv-space-24:24px; --swv-space-32:32px; --swv-space-48:48px;
  --swv-radius-pill:999px; --swv-radius-card:12px; --swv-radius-panel:8px; --swv-radius-input:12px;
  --swv-control-h:44px; --swv-pill-h:36px; --swv-pill-pad-x:20px;
  /* --pin, the map's own villa/amenity marker colour, is deliberately not
     declared here: a village whose plot map is a light plan sets it through
     its design tokens, and every rule that reads it falls back to the exact
     value it had before (--paper, or #F2F2F2 on the .mv1 pins), so a village
     that sets nothing renders the dot it always did. */
}
@media (min-width:861px){
  :root{
    --swv-text-footer-link:72px; --swv-text-h1:48px; --swv-text-h2:40px;
    --swv-text-h3:32px; --swv-text-h4:26px; --swv-text-h5:22px; --swv-text-numerals-lg:48px;
    --swv-text-numerals-md:32px; --swv-text-numerals-sm:26px; --swv-text-body-lg:20px;
  }
}
/* Montserrat and Lato are the older theme's families. Secret Waterfall's phone
   screens have two, Cormorant and Satoshi, so the two old tokens point at the
   kit's sans here and every rule still written with them (the details page's
   eyebrows and forms, the facilities sheet) renders in it. body[data-project-slug]
   is set for no other village, and 861px and up is untouched. */
@media(max-width:860px){body[data-project-slug]{--sans:var(--swv-sans);--label:var(--swv-sans)}}
*{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:12px;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}
/* The Explore surface is a picture to look at and steer, never text to select. #map-wrap holds every layer of it:
   the flat map and the Views photographs, the 3D scene, pins, area names, cards and the rail. A double click (how
   people try to zoom a map), a drag or a long press used to paint them in the browser's blue highlight, and select
   all painted the whole view. One rule on the container covers every child, because user-select passes down to
   anything left on auto. Typing still works: fields keep their own selection. Scoped to #map-wrap, so the villa
   pages, About and the legal pages keep normal selection, and the chat panel sits outside it. #swv-panel, the rail of
   homes, is the one part of the surface that is moved out of #map-wrap while a villa page is open (swvOvParkPanel),
   so it is named too. */
#map-wrap,#swv-panel{-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}
#map-wrap input,#map-wrap textarea,#map-wrap select,#map-wrap [contenteditable],
#swv-panel input,#swv-panel textarea,#swv-panel select,#swv-panel [contenteditable]{-webkit-user-select:text;user-select:text}
#map-wrap img,#swv-panel img{-webkit-user-drag: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}body.on-map.turn-wait:not(.story-open) #map-wrap{cursor:progress}}
#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%}
/* masterplan3d.js's mount point (docs/requirements/masterplan-3d.md):
   fills #map-wrap on its own, independent of #map-cam's pan/zoom transform,
   since the 3D scene carries its own camera. display:none until
   ensureMasterplan3d() (engine.js) confirms the gate, WebGL2 and a plan
   image all hold and actually mounts something into it. background is the
   brand cream (--paper), never black, for the instant between this div
   appearing and the first WebGL frame painting over it (phase 1 review,
   fix A) — the scene's own sky/fog/pale-ground take over the moment that
   frame lands, see masterplan3d.js. */
#map3d{position:absolute;inset:0;background:var(--paper)}
#map3d canvas{display:block;width:100%;height:100%}
/* The 3D map loads under the boot cover (engine.js ensureMasterplan3d). It is
   shown but unseen: opacity, not display or visibility, so the scene keeps
   drawing and the first thing seen is a finished picture, and no pointer
   reaches it. revealMasterplan3d takes is-waiting off and puts is-open on, at
   once and under the cover, so there is no transition to style: nobody sees
   the swap. */
#map3d.is-waiting{opacity:0;pointer-events:none;clip-path:circle(0% at 50% 50%)}
/* What a visitor who opens the map before the 3D map is drawn sees in place of
   the flat drawing (engine.js mp3dSyncWait): the splash's own cream ground with
   the shared progress ring, centred. z-index 15 sits over the map (#hotspots 10
   and #spot-desat 8 live inside #map-cam) and under everything the visitor
   works with: #sel-chrome's filters 20, #fac-cta 32, the Homes panel 40, the top
   bar 600 and 601. It takes the pointer so nothing is pressed on the plan behind
   it. Reduced motion is the ring's own rule (.swv-spin stops turning). */
#map3d-wait{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;
  background:var(--ps-ground,var(--cream));color:var(--ps-ink,var(--ink));--spin:44px}
#map3d-wait[hidden]{display:none}
/* 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}
/* the ambient loop: unseen until swvAmbientSync shows it, never a target for a drag or a pin press */
#map-ambient{opacity:0;pointer-events:none}
#map-ambient.amb-fade{transition:opacity .5s ease}
#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(--pin,var(--paper));
  transform:translate(-50%,-50%);
  box-shadow:0 0 0 7px color-mix(in srgb,var(--pin,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(--pin,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:12px;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}
/* a pin of the other tab: still dim, but it answers the pointer and the keyboard, and
   comes up to full while it is being read so its card can be. inherit, not auto: the
   pin layer is switched off with pointer-events none while the camera flies, turns or
   is dragged, and a child's auto would keep an unseen pin pressable through that */
.dot.off.away{opacity:.32;pointer-events:inherit;transition:opacity .2s var(--ease)}
.dot.off.away:hover,.dot.off.away:focus-visible{opacity:1}
/* Getting ready. Entering a villa waits until its fly in, its clips and its
   pictures have all arrived, and the ring fills with them: --p is the share
   that has, 0 to 1, written by engine.js (swvRing). It fills rather than
   turns, so a visitor on a slow connection can see it is still moving. */
/* above the pin's own card, which opens beside it and would cover half the ring */
.dot.warming .pt{z-index:2}
.dot.warming .pt::after{animation:none;opacity:0}
.dot.warming .pt::before{content:"";position:absolute;inset:-13px;border-radius:50%;
  /* the plots are photographed in daylight, so the part still to come is a
     dark track rather than a clear one: a paper coloured arc on a pale roof
     reads as nothing without something dark beside it */
  background:conic-gradient(var(--pin,var(--paper)) calc(var(--p,0) * 1turn), rgba(0,0,0,.55) 0);
  -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px));
  mask:radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3.5px))}

.dot.reserved .pt{background:color-mix(in srgb,var(--pin,var(--paper)) 40%, transparent);box-shadow:0 0 0 1px var(--pin,var(--paper)) inset,0 1px 8px rgba(0,0,0,.45)}
.dot.reserved .pt::after{display:none}
/* The Sold / Reserved badge under a locked pin (engine.js buildDots, only when settings.statusLabels is
   set): the map label's own dark ground and light type, 12px. neutral-50 on accent-950 is 15.37 to 1. */
.dot .pt-badge{position:absolute;left:0;top:14px;transform:translateX(-50%);padding:3px 8px;border-radius:10px;
  white-space:nowrap;pointer-events:none;background:var(--swv-accent-950);color:var(--swv-neutral-50);
  font:500 var(--swv-text-hint)/1 var(--swv-sans)}
.dot.show-villa .pt{background:var(--terra)}
.dot.amen{cursor:default}
.dot.amen .pt{width:8px;height:8px;background:color-mix(in srgb,var(--pin,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(--cream);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;visibility:hidden;transition:.45s var(--ease)}
#fac-cta.on{opacity:1;transform:translate(-50%,0);pointer-events:auto;visibility:visible}
#fac-cta:hover{background:var(--cream)}
@media(max-width:700px){#fac-cta{bottom:190px}}

/* club mode: the festival evening control, docs/features/swv-club-mode.md.
   Its look is the shared Sound control's (soundtoggle.css, the home page's
   too); here is only where the masterplan puts it. */
.club-toggle{position:fixed;left:16px;bottom:calc(8px + env(safe-area-inset-bottom,0px));z-index:33}
/* On a phone the bottom edge belongs to The Homes bar, which spans it and
   sits above this control, so every tap down there landed on the bar. The
   control moves to the top right instead, under the burger. */
@media(max-width:860px){
  .club-toggle{left:auto;right:16px;bottom:auto;top:calc(58px + env(safe-area-inset-top,0px))}
  /* The Homes sheet opens 60px down from the top and covers most of the control
     (its z-index is 601, this one's 33), so while it is open the control is gone
     rather than half under it, and out of the tab order. */
  body.rail-open .club-toggle{visibility:hidden;pointer-events:none}
}

/* The same box and the same fill as #map-cam img, which is what keeps the
   village registered under the pins. No object-fit on purpose: the day
   render stretches to this box too, so stretching the night twin to it
   puts the two in the same place to the pixel. */
#club-mode{position:absolute;left:0;top:0;width:100%;height:100%;z-index:2;pointer-events:none}
#club-mode canvas{position:absolute;left:0;top:0;width:100%;height:100%;
  opacity:0;transition-property:opacity;transition-timing-function:ease}
/* Duration is set by club.js from XFADE_MS, the one value the sound shares. */


/* The artist's impression caption (RN110): one fixed element, #swv-impression, built by engine.js only
   when the village sets BRAND.disclaimer.map or .image. These rules, not script, choose which of its two
   spans shows and where. Plain type, no chip (Dave, 6 Nov 2026: "no need chip background, keep it simple"):
   --paper with a soft dark shadow, so it still reads over a bright picture (the page cannot know what is
   behind it). z-index 160 is over the fly in (150) and the story (80). It is
   on body while #app is a stacking layer of its own (z 1), so it would paint over the chat (810) and the
   expanded Homes panel (602), both inside #app: every rule below leaves it out while body has chat-open or
   rail-open (swvSetExpanded sets rail-open on desktop and phone alike). The menu (2000) sits on body
   outside #app and covers it. */
#swv-impression{position:fixed;display:none;z-index:160;pointer-events:none;user-select:none;-webkit-user-select:none}
#swv-impression span{display:none;width:fit-content;color:var(--paper);
  font:400 var(--swv-text-caption)/1.3 var(--swv-sans);text-align:left;letter-spacing:.01em;
  text-shadow:0 1px 2px rgba(0,0,0,.8),0 0 6px rgba(0,0,0,.6),0 0 16px rgba(0,0,0,.45)}
/* The masterplan: bottom left, in the sound control's line (it is 48px tall at 8px, so its centre is 32px up),
   12px to its right, and no wider than the room before the chat button and the Homes bar (the bar, 8px, the
   44px button and the 16px edge, then its own 12px). */
body.on-map:not(.swv-ov-open):not(.story-open):not(.swv-flyin-on):not(.chat-open):not(.rail-open) #swv-impression{display:block;
  left:124px;bottom:calc(18px + env(safe-area-inset-bottom,0px));
  max-width:calc(100vw - 124px - var(--swv-panel-w) - 8px - var(--swv-control-h) - 16px - 12px)}
body.on-map:not(.swv-ov-open):not(.story-open):not(.swv-flyin-on):not(.chat-open):not(.rail-open) #swv-impression .swv-imp-map{display:block}
/* A villa or facility story and the fly in: top right, level with Back to map (top 68px, 36px tall), under the burger. */
body:is(.swv-ov-open,.swv-flyin-on):not(.chat-open):not(.rail-open) #swv-impression{display:block;top:72px;right:max(16px,env(safe-area-inset-right,0px))}
body:is(.swv-ov-open,.swv-flyin-on):not(.chat-open):not(.rail-open) #swv-impression .swv-imp-story{display:block}
/* A phone: the foot belongs to the Homes bar (16px up, 44px tall), so the caption stands 8px above it, over its
   left end, where the chat button sits beside the bar and not above it. It is left out while the sheet is open. */
@media(max-width:860px){
  body.on-map:not(.swv-ov-open):not(.story-open):not(.swv-flyin-on):not(.chat-open):not(.rail-open) #swv-impression{left:16px;
    bottom:calc(var(--swv-space-16) + var(--swv-control-h) + 8px);max-width:calc(100vw - 32px)}
}
/* R342: a desktop map with the Views top view button (Temple Village) has that button where the caption stands, beside
   the sound control (left 128px, 12px up, 44px tall): the caption stands on the line above them instead, at the
   corner's left edge, so it covers no control. A phone keeps its place above the Homes bar. */
@media(min-width:861px){
  body.on-map:not(.swv-ov-open):not(.story-open):not(.swv-flyin-on):not(.chat-open):not(.rail-open):has(#map-wrap > .views-top-btn:not([hidden])) #swv-impression{
    left:16px;bottom:calc(12px + 44px + 20px + env(safe-area-inset-bottom,0px))}
}

/* 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)}
/* Over the 3D aerial (engine.js puts map-3d on #map-wrap when its reveal ends)
   the two bands take a strength a village's DESIGN.md can set (--map-veil-top,
   --map-veil-foot); unset, they are what they always were. A daylight aerial
   needs far less of them than a night render does. Not on the flat plan,
   which is near white: there the white mark needs the ground it has. */
#map-wrap.map-3d #sel-chrome::before,#map-wrap.map-views #sel-chrome::before{background:linear-gradient(to bottom,rgba(10,9,6,var(--map-veil-top,.55)),transparent)}
#map-wrap.map-3d #sel-chrome::after,#map-wrap.map-views #sel-chrome::after{background:linear-gradient(to top,rgba(10,9,6,var(--map-veil-foot,.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:12px;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(--pin,var(--paper));
  box-shadow:0 0 0 7px color-mix(in srgb,var(--pin,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(--pin,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:12px;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(--pin,var(--paper)) 70%, transparent) inset,0 1px 6px rgba(0,0,0,.4)}
  .dot.show-villa .pt{width:21px;height:21px}
  /* 3 — one row of controls, not two: the bedroom filters go behind Villas */
  #chips-sub.open{max-height:120px}

  #sel-head .hint{font-size:12px;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. Her idle wave (RN87) points at nothing but
   her own button, so it alone is let through. */
html[data-alt="1"] body.on-map #sel-head{display:none}
html[data-alt="1"] body.on-map #fab-bubble:not([data-key="idle"]){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:12px;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:12px}
#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:12px;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(--copy-face);font-style:var(--copy-slant);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:12px;
  letter-spacing:var(--ls);text-transform:uppercase}
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:12px;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 .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:12px;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:12px;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:12px}
.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:12px;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:12px;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:12px;
  text-transform:uppercase;font-weight:500;color:var(--dim);font-family:var(--copy-face)}
.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:12px;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:12px;text-transform:uppercase;color:var(--paper);align-self:flex-start;transition:.3s}
.pack .pick:hover,.pack .pick.picked{background:var(--paper);color:var(--cream)}

/* 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:12px;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;visibility:hidden;transition:opacity .45s,visibility 0s linear .45s;display:flex;justify-content:center;align-items:flex-end}
#fac-sheet.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity .45s,visibility 0s}
#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:var(--swv-text-caption)}
#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:12px;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:var(--swv-text-caption);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}

/* ============================================================
   CONCIERGE
   ============================================================ */
/* the note Grace leaves by her icon — persistent, tappable (opens the chat) */
.fab-bubble{position:fixed;right:calc(26px + var(--fab-shift,0px));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(--bubble-face,var(--swv-sans));font-size:16px; /* the note belongs to the chat, so it is set in the chat's Satoshi, upright since Satoshi has no italic (RN103, Dave 2 Nov); a village that names its own bubble face (Temple Village) keeps it */
  font-style:normal;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}
/* The idle note (RN87) waves once as it lands, a hand lifted twice from its corner. It
   turns with `rotate`, so the entrance transition on `transform` is left alone, and the
   reduced motion block below takes it away with every other animation. */
.fab-bubble.wave.show{animation:wg-wave 1.2s cubic-bezier(.36,.07,.19,.97) .45s 1}
.fab-bubble.from-left{transform-origin:bottom left;border-radius:14px 14px 14px 5px}
@keyframes wg-wave{0%,100%{rotate:0deg}18%{rotate:-5deg}36%{rotate:4deg}54%{rotate:-3deg}72%{rotate:2deg}}
.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, set in the same face as the
   bubble around it. font-style stays upright, as the bubble is now. */
.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}
/* Master fallback: the dark panel's structure. The light panel further down now dresses every village over it
   (R84), so the dark colours here only show where the light block is silent. */
#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-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{display:block;margin-top:2px;font-size:var(--swv-text-caption);letter-spacing:0;text-transform:none;opacity:.8}
#c-head .t .nm{font-size:19px}
#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}
/* The header's avatar (the tenant's mark in a circle) and live dot, as on wgrace.co's chat
   (apps/studio/src/components/GraceChat.css): an 8px dot that breathes to .35 over 2.4s, a steady
   dot under reduced motion. Fixed sizes, so nothing shifts when the image arrives. */
#c-head .c-id{display:flex;align-items:center;gap:12px;min-width:0}
#c-head .c-av{flex:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:44px;height:44px;border-radius:50%;
  overflow:hidden;background:color-mix(in srgb,var(--paper) 10%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--paper) 22%,transparent)}
#c-head .c-av-img{display:block;width:62%;height:auto}
#c-head .c-live{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:10px;vertical-align:middle;flex:none;
  background:var(--terra);animation:c-pulse 2.4s infinite}
@keyframes c-pulse{50%{opacity:.35}}
@media(prefers-reduced-motion:reduce){#c-head .c-live{animation:none}}
/* a message being typed in: the whole of it, for a screen reader, once, out of sight */
.c-vh{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
#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:var(--swv-text-hint);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:var(--swv-text-caption);text-transform:uppercase;transition:.3s;cursor:pointer}
.msg .act:hover{background:var(--paper);color:var(--midnight)}
/* Gracie's option buttons under a question (renderChatChoices): wrapping pills, 44px tall,
   in the chat's own look like .act above. Closed once answered. */
.msg .c-choices{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.msg .c-choice{min-height:44px;padding:0 16px;border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:99px;
  background:transparent;color:inherit;font:inherit;font-size:var(--swv-text-caption);cursor:pointer;transition:.2s}
.msg .c-choice:hover:not(:disabled){background:var(--paper);color:var(--midnight)}
.msg .c-choice[aria-pressed=true]{background:var(--paper);color:var(--midnight)}
.msg .c-choices.c-done .c-choice:not([aria-pressed=true]){opacity:.55}
.msg .c-choice:disabled{cursor:default}
:root body #c-panel .msg .c-choice{border-color:var(--swv-accent-950);color:var(--swv-accent-950);font:400 var(--swv-text-caption)/18px var(--swv-sans)}
:root body #c-panel .msg .c-choice:hover:not(:disabled),:root body #c-panel .msg .c-choice[aria-pressed=true]{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
/* The two contact cards (agentAct's act.contactCards): block
   rows the full width of the message, not a pill sized to its own label like
   .act above, so the tap target is the whole card rather than a short line
   of text (Dave, 13 Oct: "make those cards wider so that easier to click"). */
.msg .chat-contact-cards{display:flex;flex-direction:column;gap:8px;width:100%;margin-top:8px}
.msg .chat-contact-card{display:flex;align-items:center;gap:12px;width:100%;margin:0;border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:14px;padding:12px 16px;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;transition:.3s}
.msg .chat-contact-card:hover{background:var(--paper);color:var(--midnight)}
.msg .chat-cc-ico{flex:none;display:flex}
.msg .chat-cc-txt{flex:1;overflow-wrap:anywhere;font-size:var(--swv-text-caption)}
.msg .chat-cc-ico-r{opacity:.7}
/* The contact form card (agentAct's act.contactForm, contactFormEl): a short form the width of
   the message. Fields are 16px so a phone does not zoom on focus; errors sit beside their field. */
.msg .chat-contact-form{width:100%;margin-top:8px}
.msg .ccf{display:flex;flex-direction:column;gap:12px;border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:14px;padding:16px}
.msg .ccf-title{margin:0;font-weight:600}
.msg .ccf-field{display:flex;flex-direction:column;gap:4px;margin:0;padding:0;border:0;min-width:0}
.msg .ccf-field label,.msg .ccf-field legend{padding:0;font-size:var(--swv-text-caption)}
.msg .ccf-opt{opacity:.75}
.msg .ccf input[type=text],.msg .ccf input[type=email],.msg .ccf textarea{box-sizing:border-box;width:100%;font:inherit;font-size:16px;color:inherit;background:transparent;border:1px solid currentColor;border-radius:10px;padding:10px 12px;min-height:44px}
.msg .ccf textarea{resize:vertical;min-height:84px}
.msg .ccf [aria-invalid=true]{border-width:2px}
.msg .ccf-checks{display:flex;flex-wrap:wrap;gap:8px 16px}
.msg .ccf-check{display:inline-flex;align-items:center;gap:8px;min-height:44px;cursor:pointer}
.msg .ccf-check input{width:20px;height:20px;margin:0;accent-color:currentColor}
.msg .ccf-err,.msg .ccf-fail{margin:0;font-size:var(--swv-text-caption);font-weight:600}
.msg .ccf-trap{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.msg .ccf-send{align-self:flex-start;min-height:44px;padding:8px 20px;border:1px solid currentColor;border-radius:99px;background:none;font:inherit;color:inherit;cursor:pointer;transition:.3s}
.msg .ccf-send:hover:not(:disabled){background:var(--paper);color:var(--midnight)}
.msg .ccf-send:disabled{opacity:.6;cursor:default}
.msg .ccf :focus-visible{outline:2px solid currentColor;outline-offset:2px}
.msg .ccf-sent{border:1px solid color-mix(in srgb,var(--paper) 55%, transparent);border-radius:14px;padding:16px}
.msg .ccf-sent p{margin:0 0 4px}
:root body #c-panel .msg .ccf,:root body #c-panel .msg .ccf-sent{border-color:var(--swv-accent-950);color:var(--swv-accent-950);font:400 var(--swv-text-caption)/18px var(--swv-sans)}
:root body #c-panel .msg .ccf input[type=text],:root body #c-panel .msg .ccf input[type=email],:root body #c-panel .msg .ccf textarea{border-color:var(--swv-accent-950);font-size:16px}
:root body #c-panel .msg .ccf-send{border-color:var(--swv-accent-950);color:var(--swv-accent-950)}
:root body #c-panel .msg .ccf-send:hover:not(:disabled){background:var(--swv-accent-950);color:var(--swv-neutral-50)}
.msg .gline{font-style:italic;font-size:16px;opacity:.72;margin-top:8px}
/* the first message's closing offer: a footer under it, smaller than the message and at full strength, so it reads on the cream and the dark panel alike */
.msg .c-foot{display:block;margin-top:14px}
/* The map card Gracie draws for "where is it" (engine.js locationMapHTML):
   a picture, the place and its address, and the Open in Google Maps button.
   Everything takes its colour from the message's own text, so one rule set
   serves the dark panel and the cream Secret Waterfall panel; the cream panel
   only swaps the muted address colour and the button's hover below. */
.msg .chat-map{margin-top:12px;max-width:100%;border:1px solid color-mix(in srgb,currentColor 24%,transparent);border-radius:8px;overflow:hidden;
  background:color-mix(in srgb,currentColor 5%,transparent)}
.msg .chat-map-pic{display:block;width:100%;height:112px;border:0}
.msg .chat-map-art svg{display:block;width:100%;height:100%}
.msg .chat-map-txt{display:flex;flex-direction:column;gap:2px;padding:12px 14px 0}
.msg .chat-map-name{font-weight:500;font-size:var(--swv-text-body);line-height:1.3}
.msg .chat-map-addr{font-size:var(--swv-text-caption);line-height:1.4;opacity:.82}
.msg.ag a.chat-map-open{display:inline-flex;align-items:center;gap:8px;margin:12px 14px 14px;padding:8px 16px 9px;border:1px solid currentColor;
  border-radius:99px;font-size:var(--swv-text-caption);line-height:18px;transition:background-color .2s,color .2s}
.msg .chat-map-open svg{width:16px;height:16px;flex:none}
.msg .chat-map-open:hover{background:var(--paper);color:var(--midnight)}
/* 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:var(--swv-text-hint);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:16px}
/* 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:var(--swv-text-hint);opacity:.7;letter-spacing:var(--ls);text-transform:uppercase}
/* [[pics:ID]]: a short strip of a home's pictures under her message; a press opens the lightbox */
.msg .c-pics{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.msg .c-pic{flex:1 1 0;min-width:56px;max-width:25%;min-height:44px;aspect-ratio:4/3;padding:0;overflow:hidden;cursor:pointer;
  border:1px solid color-mix(in srgb,var(--paper) 20%, transparent);border-radius:6px;background:none;transition:border-color .25s}
.msg .c-pic:hover{border-color:color-mix(in srgb,var(--paper) 55%, transparent)}
.msg .c-pic:focus-visible{outline:2px solid var(--paper);outline-offset:2px}
.msg .c-pic img{width:100%;height:100%;object-fit:cover;display:block}
.msg .c-pics-cap{flex:0 0 100%;font-size:var(--swv-text-hint);opacity:.85}
:root body #c-panel .msg .c-pic:focus-visible{outline-color:var(--swv-accent-950)}
#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:var(--swv-text-caption);text-transform:uppercase;color:var(--paper);transition:.3s}
#c-send:hover{background:var(--paper);color:var(--midnight)}
/* A question Grace could not answer: a note and a Retry inside the visitor's
   own bubble, never a line from her. Full --paper on the panel, the same
   pairing as #c-send, so nothing here is light grey on dark. */
.msg.you .unanswered{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px 14px;margin:10px 0 0;
  font:400 16px/1.35 var(--swv-sans);letter-spacing:0;color:var(--paper);animation:unanswered .48s cubic-bezier(.22,1,.36,1) backwards}
.msg.you .retry{min-height:40px;padding:0 18px;border:1px solid var(--paper);border-radius:99px;background:none;
  color:var(--paper);font:500 15px/1 var(--swv-sans);cursor:pointer;transition:background-color .3s,color .3s}
.msg.you .retry:hover{background:var(--paper);color:var(--midnight)}
@keyframes unanswered{from{opacity:0;transform:translateY(-4px)}}
@media (prefers-reduced-motion: reduce){.msg.you .unanswered{animation:none}}
#c-note{padding:0 22px 14px;font-size:16px;font-style:italic;opacity:.82}
#c-note:empty{display:none}
/* A phone: a full height sheet that follows the visual viewport, as on SWV
   below, so the keyboard pushes the input row up rather than covering it. */
@media(max-width:860px){#c-panel{top:var(--c-vvt,0px);right:0;bottom:auto;left:0;width:100%;height:100vh;height:var(--c-vvh,100dvh);border-radius:0}
  #c-panel.half{top:auto;bottom:0}}

/* ---------- Ask Grace, Figma "05. Ask Grace" ----------
   One panel for every opener: docked right over the page on a desktop, a full
   height sheet on a phone. Cream ground, ink type, the kit's greys for labels.
   Every child rule is keyed under #c-panel so it outranks the older shell
   rules further down (the 3px radius list, the uppercase .caps, the .msg .act
   button metrics) without touching them, since those still style other
   controls. Small grey type is --swv-neutral-600: the design's lighter grey
   is --swv-neutral-500 or lighter, which fails 4.5:1 on this ground (see
   contrast.test.js).

   Every village wears this panel (R84, Dave: "consistent light mode, not
   green for dark mode"). The rules are keyed :root body, the same weight as
   the Secret Waterfall slug key they replaced. Villages with a light kit of
   their own hand it to the ramp steps the panel paints with, the way .dv1
   does for the details page, so Temple Village's chat is its cream and
   green. Secret Waterfall sets no dv1 tokens and keeps its own ramp: its chat
   ink is a neutral where its details ink is the accent. The send button is
   left out on purpose, it stays brass (see #c-send.gv1 below). */
body:not([data-project-slug]) #c-panel{--swv-neutral-50:var(--dv1-ground);--swv-neutral-100:var(--dv1-surface);
  --swv-neutral-300:var(--dv1-rule);--swv-neutral-600:var(--dv1-hint);--swv-neutral-950:var(--dv1-ink);
  --swv-accent-950:var(--dv1-ink);--swv-accent-900:var(--dv1-control);--swv-accent-500:var(--dv1-focus)}
:root body #c-panel{position:fixed;top:68px;right:16px;bottom:16px;height:auto;z-index:810;width:min(540px,calc(100vw - 32px));
  background:var(--swv-neutral-50);color:var(--swv-neutral-950);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;
  visibility:hidden;transition:.45s var(--ease),visibility 0s linear .45s}
:root body #c-panel.open{transform:none;opacity:1;pointer-events:auto;
  visibility:visible;transition:.45s var(--ease),visibility 0s}
/* RN67, R173 (Dave, 28 Oct 2026: "morphing like Mac app when hide/expand"): the chat opens out of the launcher
   that opened it and folds back into it. The panel is laid out at its final size the whole time; only a clip
   grows. engine.js (chatMorph) measures #c-fab, writes where it sits as four insets from the panel's own edges
   (--m-t, --m-r, --m-b, --m-l), and sets data-morph for the length of the move. The shut clip is the launcher's
   box, 8px corners; the open clip is a little past the panel on every side so its shadow is not cut, and it
   rounds at the panel's own radius. Two layers ride above the content: the launcher's gold, which fills the clip
   at first so the button becomes the window, and the panel's ground, which gives way after it so the content
   follows the shape. Only clip-path, opacity and colour move; nothing is laid out again. Without a launcher
   to start from (no data-morph) the panel fades as before. A press mid way turns the clip around from where
   it is, since the state alone changes and a transition reverses itself. */
:root body #c-panel{--c-rad:6px}
:root body #c-panel[data-morph]{transform:none;opacity:1;visibility:hidden;pointer-events:none;
  clip-path:inset(var(--m-t,0px) var(--m-r,0px) var(--m-b,0px) var(--m-l,0px) round 8px);
  transition:clip-path var(--wg-morph-dur) var(--wg-morph-ease),visibility 0s linear var(--wg-morph-dur)}
:root body #c-panel[data-morph].open{visibility:visible;pointer-events:auto;
  clip-path:inset(-140px round var(--c-rad));
  transition:clip-path var(--wg-morph-dur) var(--wg-morph-ease),visibility 0s}
:root body #c-panel[data-morph]::before,:root body #c-panel[data-morph]::after{
  content:"";position:absolute;inset:0;z-index:50;pointer-events:none;border-radius:inherit;opacity:1}
:root body #c-panel[data-morph]::before{background:inherit;
  transition:opacity calc(var(--wg-morph-dur) * .4) linear}
:root body #c-panel[data-morph]::after{background:var(--wg-gracie,var(--swv-gold));
  transition:opacity calc(var(--wg-morph-dur) * .4) linear calc(var(--wg-morph-dur) * .55)}
:root body #c-panel[data-morph].open::before{opacity:0;
  transition:opacity calc(var(--wg-morph-dur) * .45) linear calc(var(--wg-morph-dur) * .15)}
/* the gold is the button's, so it is gone while the shape is still small: the curve has covered half the way by
   a sixth of the time, and a window sized slab of gold would be the chrome shouting over the photographs */
:root body #c-panel[data-morph].open::after{opacity:0;
  transition:opacity calc(var(--wg-morph-dur) * .2) linear}
/* Reduced motion (M5): no morph for the chat. The panel keeps its place and crossfades in a moment, with none of
   the 24px rise; the script does not set data-morph here, and this keeps it off should it be set. */
@media(prefers-reduced-motion:reduce){
  :root body #c-panel{transform:none;transition:opacity .15s linear,visibility 0s linear .15s}
  :root body #c-panel.open{transition:opacity .15s linear,visibility 0s}
  :root body #c-panel[data-morph],:root body #c-panel[data-morph].open{clip-path:none;transition:none}
  :root body #c-panel[data-morph]::before,:root body #c-panel[data-morph]::after{display:none}
}
/* 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}
}
:root body #c-head{flex:none;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;
  padding:24px 16px 26px 24px;border-bottom:1px solid var(--swv-neutral-300);cursor:pointer}
:root body #c-panel #c-head .t .nm{display:block;font:400 var(--swv-text-h2)/1.15 var(--swv-serif);color:var(--swv-neutral-950)}
:root body #c-head .t .caps{display:block;margin-top:2px;font:400 var(--swv-text-body)/20px var(--swv-serif);
  letter-spacing:0;text-transform:none;opacity:1;color:var(--swv-neutral-600)}
:root body #c-head .ops{display:flex;gap:6px}
:root body #c-head .c-id{align-items:flex-start}
:root body #c-head .c-av{background:var(--swv-neutral-100);box-shadow:inset 0 0 0 1px var(--swv-neutral-300)}
:root body #c-head .c-live{background:var(--swv-accent-900)}
:root body #c-head button{display:flex;align-items:center;justify-content:center;width:44px;height:44px;padding:0;
  font-size:16px;line-height:1;color:var(--swv-neutral-600);border-radius:99px;transition:color .2s}
:root body #c-head button:hover{color:var(--swv-neutral-950)}
:root body #c-msgs{flex:1;overflow-y:auto;padding:32px 24px 24px;display:flex;flex-direction:column;gap:28px;scrollbar-width:thin}
/* The message itself: serif on the cream ground, no bubble. Each one has a
   small sans label above it, the agent's name (the .who span agentSay writes)
   or "You", drawn here so the visitor's own line keeps its text alone. */
:root body #c-panel .msg{max-width:400px;font:400 18px/28px var(--swv-sans);letter-spacing:0;color:var(--swv-neutral-950)}
:root body #c-panel .msg.you{align-self:flex-end;max-width:88%;text-align:right;background:none;padding:0;border-radius:0}
:root body #c-panel .msg.you::before{content:"You";display:block;margin-bottom:8px}
:root body #c-panel .msg.ag{align-self:flex-start;text-shadow:none}
:root body #c-panel .msg.ag .who, :root body #c-panel .msg.you::before{font:400 var(--swv-text-body)/20px var(--swv-sans);
  letter-spacing:0;text-transform:none;opacity:1;color:var(--swv-neutral-600)}
:root body #c-panel .msg.ag .who{display:block;margin-bottom:8px}
/* the visitor's "Request sent", the one line the design boxes */
:root body #c-panel .msg .c-sent{display:inline-block;padding:6px 12px 8px;border-radius:4px;
  background:var(--swv-neutral-100);font-style:italic}
:root body #c-panel .msg.ag a{border-bottom:1px solid currentColor}
/* 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. */
:root body #c-panel .msg .act{display:block;width:fit-content;max-width:100%;overflow-wrap:anywhere;margin-top:12px;
  padding:8px 16px 9px;border:1px solid var(--swv-accent-950);border-radius:99px;
  font:400 var(--swv-text-caption)/18px var(--swv-sans);letter-spacing:0;text-transform:none;
  color:var(--swv-accent-950);transition:background-color .2s,color .2s;cursor:pointer}
:root body #c-panel .msg .act:hover{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
:root body #c-panel .msg .swv-next{display:flex;flex-wrap:wrap;gap:0 10px}
:root body #c-panel .msg .swv-next .act{display:inline-flex;align-items:center;min-height:44px;box-sizing:border-box;text-decoration:none}
:root body #c-panel .msg button.act{background:transparent;cursor:pointer}
:root body #c-panel .msg button.act:hover{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
:root body #c-panel .msg .chat-contact-card{border:1px solid var(--swv-accent-950);border-radius:14px;
  font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-accent-950);transition:background-color .2s,color .2s}
:root body #c-panel .msg .chat-contact-card:hover{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
:root body #c-panel .msg .gline{margin-top:8px;font:italic 400 var(--swv-text-body)/1.3 var(--swv-sans);color:var(--swv-neutral-600)}
:root body #c-panel .msg .chat-map{font-family:var(--swv-sans);color:var(--swv-accent-950)}
:root body #c-panel .msg .chat-map-addr{opacity:1;color:var(--swv-neutral-600)}
:root body #c-panel .msg.ag a.chat-map-open{border:1px solid var(--swv-accent-950);color:var(--swv-accent-950)}
:root body #c-panel .msg .chat-map-open:hover{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
/* the access form inside the agent's message: two pill fields and the dark
   pill under them, 240 wide, 12px apart */
:root body #c-panel .c-capture{display:flex;flex-direction:column;gap:12px;width:240px;max-width:100%;margin-top:24px}
:root body #c-panel .c-capture input{box-sizing:border-box;height:36px;padding:0 16px;border-radius:99px;outline:none;
  background:var(--swv-neutral-100);border:1px solid var(--swv-neutral-300);color:var(--swv-neutral-950);
  font:400 var(--swv-text-body)/18px var(--swv-sans);letter-spacing:0}
:root body #c-panel .c-capture input::placeholder{color:var(--swv-neutral-600);font-style:normal}
:root body #c-panel .c-capture input:focus-visible{border-color:var(--swv-accent-950)}
/* the message to the team, when Gracie finds a proposal already open: the fields' look, in a taller box */
:root body #c-panel .c-followup textarea{box-sizing:border-box;min-height:96px;padding:10px 16px;border-radius:18px;resize:vertical;outline:none;
  background:var(--swv-neutral-100);border:1px solid var(--swv-neutral-300);color:var(--swv-neutral-950);
  font:400 var(--swv-text-body)/20px var(--swv-sans);letter-spacing:0}
:root body #c-panel .c-followup textarea::placeholder{color:var(--swv-neutral-600);font-style:normal}
:root body #c-panel .c-followup textarea:focus-visible{border-color:var(--swv-accent-950)}
:root body #c-panel .c-capture button{height:36px;padding:0 16px;border:0;border-radius:99px;
  background:var(--swv-accent-950);color:var(--swv-neutral-50);
  font:400 var(--swv-text-caption)/18px var(--swv-sans);letter-spacing:0;text-transform:none;
  cursor:pointer;transition:background-color .2s}
:root body #c-panel .c-capture button:hover{background:var(--swv-accent-900)}
/* still disabled until the name and email are valid, but the design keeps it
   the same dark pill throughout; only the cursor says it cannot be pressed yet */
:root body #c-panel .c-capture button[disabled]{background:var(--swv-accent-950);color:var(--swv-neutral-50);cursor:not-allowed}
:root body #c-panel .c-capture .c-capture-note{margin:0;font:400 var(--swv-text-body)/1.35 var(--swv-sans);color:var(--swv-neutral-600)}
:root body #c-panel .c-capture .ok{font-style:italic;font-size:16px}
/* Grace shares an image in-chat — tap to open the fullscreen lightbox */
:root body .c-img{display:block;width:100%;margin-top:8px;border-radius:6px;overflow:hidden;
  border:1px solid var(--swv-neutral-300);text-align:left;transition:.25s;background:none}
:root body .c-img:hover{border-color:var(--swv-neutral-600)}
:root body .c-img img{width:100%;display:block}
:root body .c-img .c-img-cap{display:block;padding:8px 12px 9px;font:400 var(--swv-text-hint)/1.3 var(--swv-sans);color:var(--swv-neutral-600)}
:root body #c-panel.half{top:auto;height:min(46vh,420px)}
:root body #c-panel.half #c-chips, :root body #c-panel.half #c-note{display:none}
:root body .typing{display:flex;gap:5px;padding:6px 0}
:root body .typing i{width:6px;height:6px;border-radius:50%;background:var(--swv-neutral-600);opacity:.4;animation:ty 1.2s infinite}
:root body .typing i:nth-child(2){animation-delay:.18s}
:root body .typing i:nth-child(3){animation-delay:.36s}
@keyframes ty{0%,100%{opacity:.25;transform:none}50%{opacity:1;transform:translateY(-3px)}}
:root body #c-chips{flex:none;display:flex;gap:8px;padding:0 24px 12px;flex-wrap:wrap}
:root body #c-panel #c-chips button{padding:7px 14px 8px;border:1px solid var(--swv-neutral-300);border-radius:99px;
  font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-950);transition:background-color .2s}
:root body #c-panel #c-chips button:hover{background:var(--swv-neutral-100)}
:root body #c-note{flex:none;padding:0 24px 12px;font:400 var(--swv-text-body)/20px var(--swv-sans);color:var(--swv-neutral-600)}
:root body #c-inrow{flex:none;display:flex;align-items:center;gap:12px;padding:24px;border-top:1px solid var(--swv-neutral-300)}
:root body #c-in{flex:1;min-width:0}
/* A question Grace could not answer: a note and a Retry inside the visitor's
   own bubble, never a line from her. Ink on the cream panel, the same pairing
   as the message itself, so nothing here is light grey. */
:root body #c-panel .msg.you .unanswered{display:flex;align-items:center;justify-content:flex-end;flex-wrap:wrap;gap:8px 14px;margin:10px 0 0;
  font:400 16px/1.35 var(--swv-sans);letter-spacing:0;color:var(--swv-neutral-950);animation:unanswered .48s cubic-bezier(.22,1,.36,1) backwards}
:root body #c-panel .msg.you .retry{min-height:40px;padding:0 18px;border:1px solid var(--swv-accent-950);border-radius:99px;background:none;
  color:var(--swv-accent-950);font:500 15px/1 var(--swv-sans);cursor:pointer;transition:background-color .3s,color .3s}
:root body #c-panel .msg.you .retry:hover{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
@keyframes unanswered{from{opacity:0;transform:translateY(-4px)}}
@media (prefers-reduced-motion: reduce){:root body .msg.you .unanswered{animation:none}}
/* The chat is set in Satoshi, the face wgrace.co uses, on every village: the
   header, the messages, the chips, the note, the field and the cards drawn
   inside it. It reads --swv-sans, so a village whose design file names its
   own sans keeps it. Satoshi has no italic, so the older panel's italic chips,
   note and placeholders are drawn straight rather than slanted by the
   browser. */
#c-panel, #c-panel #c-head .t .nm, #c-panel .msg, #c-panel input, #c-panel button, #c-panel textarea{font-family:var(--swv-sans)}
#c-panel #c-chips button, #c-panel #c-note, #c-panel .c-capture .ok, #c-panel .c-capture input::placeholder, #c-panel #c-in::placeholder{font-style:normal}
/* "Powered by" and the With Grace wordmark, the last line of the panel on every
   village, one link to wgrace.co. The wordmark is a mask, so it is drawn in
   the line's own colour on the dark panel and the cream one alike. The mark's
   own baseline is the bottom of its box, so the two sit on one line: "Powered
   by" and the mark's "with" read straight across. */
#c-powered{flex:none;align-self:center;display:flex;align-items:baseline;gap:6px;min-height:28px;padding:0 14px 10px;
  font:400 var(--swv-text-hint)/1 var(--swv-sans);letter-spacing:0;text-decoration:none;color:var(--dim)}
#c-powered:hover{color:var(--paper)}
#c-powered i{display:block;height:13px;width:79px;background:currentColor;
  -webkit-mask:url("assets/brand/wordmark.svg") center/contain no-repeat;mask:url("assets/brand/wordmark.svg") center/contain no-repeat}
:root body #c-powered{color:var(--swv-neutral-600)}
:root body #c-powered:hover{color:var(--swv-neutral-950)}
@media(max-height:520px){#c-powered{display:none}}
/* A phone: the same panel as a full height sheet. It follows the visual
   viewport (engine.js sets both values), so the on screen keyboard pushes
   the input row up rather than covering it; dvh is the fallback. */
@media(max-width:860px){
  :root body #c-panel{top:var(--c-vvt,0px);right:0;bottom:auto;left:0;width:100%;
    height:100vh;height:var(--c-vvh,100dvh);border-radius:0;--c-rad:0px}
  :root body #c-panel.half{top:auto;bottom:0;height:min(46vh,420px)}
  :root body #c-head{padding:16px 8px 16px 16px}
  :root body #c-msgs{padding:24px 16px 16px}
  :root body #c-chips, :root body #c-note{padding-left:16px;padding-right:16px}
  :root body #c-inrow{padding:16px}
  :root body #c-panel .msg.you .retry{min-height:var(--swv-control-h);font-size:var(--swv-text-caption)}
}
/* Typing on a phone: the keyboard leaves half a screen, so the head gets smaller (the line under her name
   steps aside, the padding halves) and the conversation takes its room, its top edge fading under the head.
   The head stays, because Close lives in it (R215: it used to fold to nothing and take Close with it). The
   suggestions and Powered by step aside. All of it returns when the keyboard closes. */
@media(max-width:860px){
  #c-panel #c-head{transition:padding .32s cubic-bezier(.25,1,.5,1)}
  #c-panel.c-typing #c-head{padding-top:8px;padding-bottom:8px}
  #c-panel.c-typing #c-head .t .caps{display:none}
  #c-panel.c-typing #c-chips,#c-panel.c-typing #c-powered{display:none}
  /* Filling the contact card (c-filling, chat-keys.js): the message row has no use, and its room is the card's. */
  #c-panel.c-filling #c-inrow{display:none}
  #c-panel.c-typing #c-msgs{padding-top:16px;-webkit-mask-image:linear-gradient(to bottom,transparent,#000 28px);mask-image:linear-gradient(to bottom,transparent,#000 28px)}
}
@media(max-width:860px) and (prefers-reduced-motion:reduce){#c-panel #c-head,#c-panel.c-typing #c-head{transition:none}}
/* 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:12px;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;
  visibility:hidden;transition:opacity .35s,visibility 0s linear .35s}
#lightbox.open{opacity:1;pointer-events:auto;visibility:visible;transition:opacity .35s,visibility 0s}
#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:var(--swv-text-caption);letter-spacing:var(--ls-wide);opacity:.5;pointer-events:none;transition:opacity .3s}
#lightbox.zoomed .lb-hint{opacity:0}
/* On a phone the hint ran under the close button and wrapped across it. It
   keeps clear of the button's side and tracks tighter, so it sits on one or
   two centred lines. */
@media(max-width:600px){#lightbox .lb-hint{padding:0 60px;letter-spacing:.14em}}
#lightbox .lb-cap{position:absolute;bottom:26px;left:0;right:0;text-align:center;color:var(--paper);font-size:12px;opacity:.85;pointer-events:none}
/* Opacity only: a transition on everything also eased the visibility these
   buttons inherit, which left them hidden for the first frame the viewer was
   open and refused the focus openLightbox gives the close button. */
#lightbox button{position:absolute;color:var(--paper);font-size:26px;padding:14px;opacity:.75;transition:opacity .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:16px;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:var(--swv-text-hint);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:12px;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(--copy-face);font-size:15px;white-space:nowrap}
.vsw-price.on-request{font-style:var(--copy-slant);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:12px;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(--copy-face);font-size:12px;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:12px;text-transform:uppercase;opacity:.6}
.vcard .vtag{display:inline-block;margin-bottom:16px;letter-spacing:var(--ls);font-size:12px;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:16px;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(--copy-face);font-size:18px;line-height:1}
.vcard .vmeta div small{letter-spacing:.14em;font-size:12px;text-transform:uppercase;opacity:.62}
.vgridnote{font-style:italic;font-size:16px;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:12px;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:12px;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(--copy-face);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:12px;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:12px;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:12px;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:16px;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:12px;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:16px;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}
/* The access teaser on Secret Waterfall's details view is a v1 surface, so its label meets the 14px floor. */
#v-details .dv1 .chtease .over{font-size:var(--swv-text-caption);color:var(--swv-accent-700)}
.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:12px;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:12px;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:12px;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:12px;text-transform:uppercase;font-weight:600;color:var(--terra);width:100%}
.qtuned .qt-v{font-size:16px}.qtuned .qt-v b{font-weight:600}
.qtuned .qt-note{width:100%;font-style:italic;opacity:.7;font-size:16px;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:12px;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(--cream);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:12px;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:12px;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:16px;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 .acc-chev{display:flex;align-self:center}

/* ---------- 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}
/* furnishing layers (Task 8.3): swapped in place, not crossfaded, so they
   carry no "on" class of their own; the base pair above keeps its fade,
   this stays visible the moment it is added. Same box as the base, so every
   layer shares its aspect ratio exactly. */
.styler .fcanvas img.flayer{opacity:1;transition:none}
.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:12px;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:12px;text-transform:uppercase;font-weight:500}
.styler .fcap .r{opacity:.85}
.styler .fnoncap{font-size:16px;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:12px;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:12px;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:12px;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:12px;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:12px;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:12px;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:10px 20px;min-height:36px;letter-spacing:0;font-size:14px;text-transform:none;font-weight:400;font-family:var(--swv-sans);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:12px;text-transform:uppercase;font-weight:500;opacity:.55;text-align:right}
.styler .fitem .ftop .chev{display:flex;flex:none;color:var(--paper)}
.styler .foptwrap{overflow:hidden;max-height:400px;transition:max-height .5s var(--ease)}
.styler .fitem.open .foptwrap{max-height:400px}
.styler .fitem:not(.open) .foptwrap{max-height:0}
.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(--copy-face);font-size:18px;line-height:1.25}
.styler .fopt .pr{letter-spacing:var(--ls);font-size:12px;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:12px;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:16px;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(32px,4vw,44px);line-height:1.1;margin:10px 0 14px}
/* The save form: two stacked pill inputs and one dark pill, all one width.
   Named under #v-details because the older .fld rules below are an id deep.
   Greys are the kit's own neutrals: 600 on 100 and on 50 clears 4.5 to 1,
   which apps/portal/test/contrast.test.js holds the line on. */
.leadsub{margin:0;max-width:30em;font-family:var(--swv-sans);font-size:var(--swv-text-body-lg);
  line-height:1.5;color:var(--swv-neutral-600)}
.leadform{display:flex;flex-direction:column;gap:12px;margin-top:24px}
#v-details .leadform{max-width:290px}
#v-details .leadform .fld{display:block;min-width:0;flex:none}
#v-details .leadform .fld input{width:100%;box-sizing:border-box;min-height:52px;
  background:var(--swv-neutral-100);color:var(--swv-neutral-950);
  border:1px solid transparent;border-radius:999px;padding:0 22px;
  font-family:var(--swv-sans);font-style:normal;font-weight:400;font-size:var(--swv-text-body)}
#v-details .leadform .fld input::placeholder{color:var(--swv-neutral-600);font-style:normal;font-size:var(--swv-text-body)}
#v-details .leadform .fld input:hover{border-color:var(--swv-neutral-300)}
#v-details .leadform .fld input:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:2px;
  border-color:transparent;box-shadow:none}
#v-details .leadform .fld.err input{border-color:var(--swv-error)}
#v-details .leadform .fld-warn{margin:0;min-height:0;font-size:var(--swv-text-caption);letter-spacing:0;
  font-style:normal;color:var(--swv-error)}
#v-details .leadform .fld-warn:not(.show){display:none}
#v-details .leadform .leadsave{width:100%;min-height:52px;margin-top:4px;border-radius:var(--wg-radius,999px);
  display:inline-flex;align-items:center;justify-content:center;padding:0 22px;letter-spacing:0;
  text-transform:none;font-size:var(--swv-text-body);font-family:var(--swv-sans)}
.leadconfirm:focus{outline:none}
#v-details .leadform .leadsave:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:2px}
#v-details .dv1-col>#lead-block{padding-bottom:clamp(48px,6vw,96px)}
#v-details .leadform .leadsave:disabled{opacity:.6;cursor:progress}
.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:12px;text-transform:uppercase;color:color-mix(in srgb,var(--paper) 62%, transparent)}
.fld input{font-family:var(--copy-face);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:16px;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}
  /* the save form's pills take the whole column on a phone */
  #v-details .leadform{max-width:none}
}
@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,
.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 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 -> 16px (A7, then the 16px body floor).  "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:16px}
.styler .fnoncap{font-size:16px}

/* 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, because the label
   sat at the left and the icon at the far edge of a wide summary, and the
   hairlines were indistinguishable from the decorative .rule hairlines above
   it.  Three moves:
     1. cap the interactive row at the same 520px discipline as FIX F2 and let
        flex put the icon (.acc-chev, the shared expand icon) immediately after
        the label instead of floating it to the far edge;
     2. the icon is the control, so it turns 180deg when the row is open and
        the label takes the terracotta 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), and
   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:hover{color:var(--terracotta-deep)}

/* 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,#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(16px,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}

/* ---- 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:12px;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-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) 82%, 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{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:12px;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 */

.palcard-shots figcaption{font-size:12px}

/* ---- 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:12px}
}
/* 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, the face of the Secret Waterfall v1 surfaces and of the chat panel
   on every village: 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");}
/* Manrope, Temple Village's sans (the brand review's pairing for its
   serif): see docs/decisions.md. Self hosted, SIL Open Font License, one
   variable file for every weight. */
@font-face{font-family:'Manrope';font-style:normal;font-weight:200 800;font-display:swap;src:url("assets/fonts/Manrope-200_800-normal.woff2") format("woff2");}
/* Butler, by Fabian De Smet (fabiandesmet.com): a village's second choice for titles, after a face
   the device may have and before Cormorant. Light and Roman only, as the designer ships them
   (assets/fonts/Butler-EULA-v2-00.pdf). Declared, never preloaded: a village that does not name
   it never fetches it. */
@font-face{font-family:'Butler';font-style:normal;font-weight:300;font-display:swap;src:url("assets/fonts/Butler-Free-Lgt.woff2") format("woff2");}
@font-face{font-family:'Butler';font-style:normal;font-weight:400;font-display:swap;src:url("assets/fonts/Butler-Free-Rmn.woff2") format("woff2");}
/* Butler's free cut has no italic, and a browser asked for one slants the upright. So the
   italic of this family is Cormorant Garamond's own, the face every italic line here was
   drawn in before Butler arrived. */
@font-face{font-family:'Butler';font-style:italic;font-weight:300;font-display:swap;src:url("assets/fonts/CormorantGaramond-300-italic.woff2") format("woff2");}
@font-face{font-family:'Butler';font-style:italic;font-weight:400;font-display:swap;src:url("assets/fonts/CormorantGaramond-400-italic.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:12px;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(--copy-face);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:12px;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:12px;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:12px;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:12px;
  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:12px;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:12px;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:12px;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:12px;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(--copy-face);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:12px;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:12px;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:12px;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:max(12px,.52em)}
.pl-v.brass{color:var(--sun)}
.pl-s{font-family:var(--sans);font-size:12px;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:12px;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:12px;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-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}
/* Hover answers without moving the target, the way wgrace.co's does: the
   lower line draws in from the right. Not while the menu is open, when the
   two lines are the close mark and turn about their own middles. */
@media (hover:hover){
  body[data-project-slug]:not(.vd-open) .dhero-burger:not([aria-expanded="true"]) i:last-child{transform-origin:right}
  body[data-project-slug]:not(.vd-open) .dhero-burger:not([aria-expanded="true"]):hover i:last-child{transform:scaleX(.6)}
}
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}
/* display:flex below would otherwise outrank the hidden attribute */
#vd-menu[hidden],#vd-scrim[hidden]{display:none}
#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:var(--swv-text-caption);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:var(--swv-text-caption);
  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:12px;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(--copy-face);font-style:var(--copy-slant);font-weight:300;font-size:16px;
  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)}

/* ==========================================================================
   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:12px;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(--copy-face);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:var(--copy-slant);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:12px;letter-spacing:.04em;
  color:var(--swv-error);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:12px;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)}

/* the name bar's two states (Task 8.6): "ask" (no name yet) always shows the
   field; "closed"/"open" (named) show the text or the field, never both.
   Structural, not v-details specific, since the same bar renders wherever
   stylerNamebarHTML is used. */
.fnamebar .fnb-text{display:none;width:100%;min-height:54px;
  font-family:var(--copy-face);font-size:20px;color:var(--ink);text-align:left;
  background:transparent;border:none;border-bottom:1px solid transparent;
  padding:9px 2px;cursor:pointer}
#v-details .fnamebar .fnb-text{font-family:var(--copy-face);font-size:20px}
.fnamebar[data-state="closed"] .fnb-text{display:block}
.fnamebar[data-state="closed"] input#f-name-input,
.fnamebar[data-state="closed"] .fnb-save{display:none}
.fnamebar[data-state="open"] input#f-name-input:focus{border-color:var(--sun)}
.fnamebar input#f-name-input.err{border-color:var(--terra)}

/* ==========================================================================
   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:12px;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:16px;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:12px;
  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;filter:var(--map-nav-lift,none)}
/* A village with no veil over the map lifts its mark and burger instead (--map-nav-lift, R389) */
.mapnav .dhero-burger{filter:var(--map-nav-lift,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%)}
/* Over the 3D aerial the bar's veil takes a strength a village's DESIGN.md
   can set (--map-nav-veil, a multiplier, and --map-nav-ink, its colour;
   unset they are 1 and the near black green of the veil above). Only
   there: on a villa or facility page the same bar is moved into #story, over
   a render, and on the flat plan it stands on near white, and in both it
   keeps the veil it has always had. */
#map-wrap.map-3d > .mapnav::before,#map-wrap.map-views > .mapnav::before{
  background:linear-gradient(to bottom,color-mix(in srgb,var(--map-nav-ink,#11150C) calc(62%*var(--map-nav-veil,1)),transparent) 0%,color-mix(in srgb,var(--map-nav-ink,#11150C) calc(30%*var(--map-nav-veil,1)),transparent) 46%,transparent 100%)}
/* The names the 3D map puts beside lit facility pins and the plot numbers beside
   lit homes (masterplan3d.js places them, from the pin's drawn edge, so there is
   no margin here; the look is here). A small card on the map, never under the
   pointer. */
#map3d .mp3d-label{padding:7px 10px;border-radius:4px;white-space:nowrap;
  background:var(--swv-neutral-50);color:var(--swv-accent-950);font:500 var(--swv-text-caption)/1 var(--swv-sans);
  box-shadow:0 4px 14px rgba(12,11,9,.22);opacity:0;transition:opacity .3s ease}
#map3d .mp3d-label.is-on{opacity:1}
/* A home's number: a small round chip of the same ground and ink. Its minimum
   width is its height, so one digit is a circle and two make a pill. The size is
   the caption token (14px), never under 12px. */
#map3d .mp3d-label--villa{box-sizing:border-box;display:flex;align-items:center;justify-content:center;
  min-width:28px;height:28px;padding:0 7px;border-radius:14px;text-align:center}
/* The pin the panel is showing a card for: filled dark, with light type. The
   panel's own ink and ground swapped: neutral-50 on accent-950 is 15.37 to 1 in
   the engine's ramp (#FAFAF5 on #1A2412) and 14.46 to 1 in Temple Village's
   (#F2F2F2 on #262011). */
#map3d .mp3d-label.is-picked{background:var(--swv-accent-950);color:var(--swv-neutral-50)}
/* A home in an area with a colour of its own: its number's chip is that colour
   (masterplan3d.js sets --mp3d-area on the label), with the same dark ink, which
   clears 7 to 1 on each of Temple Village's four. The picked home's stays dark. */
#map3d .mp3d-label--villa:not(.is-picked){background:var(--mp3d-area,var(--swv-neutral-50))}
/* A village that sets bedColors (engine.js swvBedVars) gives its homes' numbers the photographs' pin:
   the bedroom colour for the chip and its ink for the number (masterplan3d.js sets --bed and
   --bed-ink on the label). A chosen or hovered coloured area still tints it, and the picked one stays dark. */
#map3d .mp3d-label--bed:not(.is-picked):not([style*="--mp3d-area"]){background:var(--bed);color:var(--bed-ink,var(--tv-cream,#F0E9D9))}
/* a reserved or sold home's label is its padlock (R164), the number's chip with the icon in its ink */
#map3d .mp3d-label--lock svg{display:block;width:12px;height:12px}
/* A locked home's Sold / Reserved chip (settings.statusLabels): the picked pin's dark ground and light
   type, 12px, which clears 4.5 to 1 with room (15.37 to 1 in the engine's ramp). */
#map3d .mp3d-label--badge{background:var(--swv-accent-950);color:var(--swv-neutral-50);
  font:500 var(--swv-text-hint)/1 var(--swv-sans);padding:5px 9px;border-radius:12px}
@media(prefers-reduced-motion:reduce){#map3d .mp3d-label{transition:none}}
/* The names of the village's areas over the photograph (masterplan3d.js makes a
   button for each and places it, and so does the Views look's viewsAreas inside #hotspots; the look is here): quiet serif type with no
   chip, the company site's four layer shadow written out in em so it scales with
   the line (a tight dark edge and a soft wide one), and a press target of at
   least 44px. A name lies on roofs and paths as often as on trees, so it brings
   a little dusk of its own, soft at every edge, and a small chevron after it
   says it goes somewhere. Hidden, not displayed, until its area is shown, so a
   name for an area that is not shown can be neither pressed nor focused; it
   fades in over .3s and, once hidden, only goes after the fade. */
:is(#map3d,#hotspots) .mp3d-area{display:flex;align-items:center;gap:10px;min-height:44px;padding:10px 20px;border:0;cursor:pointer;white-space:nowrap;
  background:radial-gradient(closest-side,rgba(22,20,16,.46),rgba(22,20,16,.3) 55%,rgba(22,20,16,0));
  font:500 18px/1.2 var(--swv-sans);letter-spacing:.01em;color:var(--swv-neutral-50);
  text-shadow:0 .01em .01em rgba(22,20,16,.9),0 .02em .06em rgba(22,20,16,.85),0 .04em .19em rgba(22,20,16,.7),0 0 .375em rgba(22,20,16,.55);
  visibility:hidden;opacity:0;transition:opacity .3s ease,visibility 0s linear .3s}
:is(#map3d,#hotspots) .mp3d-area.is-on{visibility:visible;opacity:1;transition:opacity .3s ease,visibility 0s}
/* A name the layout has no room for (is-muted) is not hidden: an area's name is the keyboard's
   way into and out of an area, so it stays a button Tab reaches, drawn at no opacity and not
   pressable by pointer, and it comes up where it sits when it takes focus. */
:is(#map3d,#hotspots) .mp3d-area.is-muted{visibility:visible;opacity:0;pointer-events:none}
:is(#map3d,#hotspots) .mp3d-area.is-muted:focus-visible{opacity:1}
/* and where the scene has given the name its area's colour, a dot of it before the name */
:is(#map3d,#hotspots) .mp3d-area[style*="--mp3d-area"]::before{content:"";flex:none;width:10px;height:10px;border-radius:50%;background:var(--mp3d-area);
  box-shadow:0 0 0 1.5px rgba(250,250,245,.85),0 0 6px rgba(22,20,16,.5)}
/* The one area that is chosen: its name is the way out of it, and its chevron
   is a close mark (masterplan3d.js puts is-alone on the one area shown alone). */
:is(#map3d,#hotspots) .mp3d-area.is-alone::after{width:12px;height:12px;margin-top:0;border:0;transform:rotate(45deg);
  background:linear-gradient(currentColor,currentColor) center/100% 1.5px no-repeat,linear-gradient(currentColor,currentColor) center/1.5px 100% no-repeat}
:is(#map3d,#hotspots) .mp3d-area::after{content:"";flex:none;width:7px;height:7px;margin-top:.1em;border:solid currentColor;border-width:1.5px 1.5px 0 0;transform:rotate(45deg);
  filter:drop-shadow(0 0 2px rgba(22,20,16,.8))}
/* R240 (Dave, 2 Nov 2026: "Sans Serif satoshi as usual ... On hover, just put a nice underline animation"): the
   name is set in the village's sans, as the chat is (Satoshi, or the sans a village's design file names), at
   18px where the serif was 24, the same height of small letters. On hover or keyboard focus a line draws in
   under the words alone (the dot and the chevron sit outside the span) from the left, on the curve of
   wgrace.co's LineLabel, and goes back quickly; at rest there is none, so the photograph stays quiet. */
:is(#map3d,#hotspots) .mp3d-area-name{background:linear-gradient(currentColor,currentColor) 0 100%/0 1.5px no-repeat;transition:background-size .25s ease}
:is(#map3d,#hotspots) .mp3d-area:is(:hover,:focus-visible) .mp3d-area-name{background-size:100% 1.5px;transition:background-size .6s cubic-bezier(.65,0,.35,1)}
@media(prefers-reduced-motion:reduce){:is(#map3d,#hotspots) .mp3d-area-name,:is(#map3d,#hotspots) .mp3d-area:is(:hover,:focus-visible) .mp3d-area-name{transition:none}}
/* Focus shows on any ground: a 2px outline in the light ink with a dark halo outside it. */
:is(#map3d,#hotspots) .mp3d-area:focus-visible{border-radius:6px;outline:2px solid var(--swv-neutral-50);outline-offset:0;box-shadow:0 0 0 4px rgba(22,20,16,.6)}
@media(prefers-reduced-motion:reduce){:is(#map3d,#hotspots) .mp3d-area,:is(#map3d,#hotspots) .mp3d-area.is-on{transition:none}}
@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:not(.away:hover):not(.away:focus-visible) .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) 70%, 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:var(--swv-text-caption);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 }
/* RN99: a plot cannot be scrolled before it can be seen. The page holds still behind a story's cover, or a
   visitor who kept scrolling meets the story at its end when the cover lifts, and while a whole film comes up
   (film.js sets data-film-whole only once every byte is in, and from there the film goes live or gives up for
   stills by itself, within its own budgets, so this cannot trap anyone). The cover going frees it too, whatever the download is
   doing, so a stalled clip still leaves the stills to scroll. Going means gone from the page, not merely fading (is-done):
   a wheel in the fade moved the story a notch while the cover still showed. On body, as html.abt-covered does: body has
   overflow-x:hidden, so a hold on html makes body a scroller of its own and the wheel moves the story unseen.
   The gutter keeps the picture one size when the scroll bar comes back, and a drag on the cover moves nothing
   where a phone ignores overflow on the root. */
html:has(#flight):has(#portal-splash) body,
html:has(#flight[data-film="loading"][data-film-whole]) body{overflow:hidden !important}
html:has(#flight):has(#portal-splash),
html:has(#flight[data-film="loading"][data-film-whole]){scrollbar-gutter:stable}
#portal-splash:not(.is-done){touch-action:none}
/* The press path raises a cover over that wait (film.js coverFor, engine.js swvFilmCover). This quiet ring is only
   what is left if no cover could be raised, so the held page does not read as a frozen one. */
#flight[data-film="loading"][data-film-whole] .fstage::after{content:"";position:absolute;left:50%;top:50%;z-index:5;
  width:30px;height:30px;margin:-15px 0 0 -15px;border-radius:50%;box-sizing:border-box;pointer-events:none;
  border:2px solid rgba(255,255,255,.28);border-top-color:rgba(255,255,255,.9);opacity:0;
  animation:swvHoldRingIn .3s .8s forwards,swvHoldRingSpin 1s linear infinite}
@keyframes swvHoldRingIn{to{opacity:1}}
@keyframes swvHoldRingSpin{to{transform:rotate(360deg)}}
/* The About film's cover (R75; shell.html.j2 sets ps-film on the film's own
   address): the opening's dark and gold wordmark from the first paint, the
   light cover's name, monogram and sketch kept out of sight (both classes
   named, since .ps-v1 sets its ground later in this file). The mark sits
   where .film-open-mark does, at its size, so the engine's opening takes over
   underneath without a seam when this fades. */
#portal-splash.ps-film,#portal-splash.ps-v1.ps-film{background:var(--cover-ground,#000)}
#portal-splash.ps-v1.ps-film::before,#portal-splash.ps-v1.ps-film .ps-mark,#portal-splash.ps-v1.ps-film .ps-monogram{opacity:0}
#portal-splash .ps-film-mark{position:absolute;left:50%;top:50%;width:min(300px,56vw);height:auto;transform:translate(-50%,-50%);opacity:.28;
  animation:psFilmMark 1.4s cubic-bezier(.16,1,.3,1) .15s both}
@keyframes psFilmMark{from{opacity:0;transform:translate(-50%,calc(-50% + 10px))}}
@media(prefers-reduced-motion:reduce){ #portal-splash .ps-film-mark{animation:none} }

/* The Explore intro's clouds (explore-clouds.js, docs/features/explore-cloud-intro.md). The shell keeps
   the pictures in an inert template and turns the boot cover into this from the first paint on the
   reveal address (ps-clouds); the in page button clones the same elements into #explore-clouds.
   The village and the clouds are drawn on two screen sized canvases, the veil between them; the
   layers' table and the ground's geometry are explore-clouds.js's alone. Nothing here is bigger
   than the screen, and nothing is turned, scaled or masked. */
#portal-splash.ps-clouds,#portal-splash.ps-v1.ps-clouds{background:transparent;padding:0}
#portal-splash.ps-v1.ps-clouds::before{display:none}
#portal-splash.ps-clouds > :not(.pcl){display:none}
#explore-clouds{position:fixed; inset:0; z-index:9999; pointer-events:auto}
.pcl{position:absolute; inset:0; overflow:hidden; background:rgb(219,226,223)}
.pcl-veil{position:absolute; inset:0; background:rgba(236,240,237,.16); pointer-events:none}
/* both canvases are drawn from the one clock (explore-clouds.js frame), not animated */
.pcl-ground, .pcl-sky{position:absolute; inset:0; width:100%; height:100%; pointer-events:none}
/* hidden until every picture has decoded (explore-clouds.js adds is-drawn), so nothing half loaded
   shows: until then the cover is its own haze, which reads as being inside the cloud */
.pcl-ground, .pcl-sky{opacity:0; transition:opacity .4s ease}
.pcl.is-drawn .pcl-ground, .pcl.is-drawn .pcl-sky{opacity:1}
.pcl-pics{display: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(--ps-ground, 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:var(--swv-text-h1); line-height:1.1;
  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,
#portal-splash.ps-v1 .ps-monogram-light{
  position:absolute; left:50%; transform:translateX(-50%);
  bottom:max(16px, calc(env(safe-area-inset-bottom, 0px) + 16px));
  width:60px; height:48px; object-fit:contain;
}

/* A village's logo in place of the text name (NAME_MARK_INK, shell.html.j2).
   Two copies of the picture: the ghost sets the box by height, so the logo
   keeps its own shape, and the ink one lies over it and is revealed from the
   left by the same --ps-p and the same timing the text fill uses. */
#portal-splash.ps-v1 .ps-mark.ps-mark-logo{
  left:50%; right:auto; transform:translate(-50%,-50%);
  width:max-content; max-width:calc(100vw - 32px); line-height:0;
  background-image:none;
}
#portal-splash.ps-v1 .ps-mark-logo .ps-logo-ghost{
  display:block; height:clamp(72px, 12vh, 120px); width:auto; max-width:100%; opacity:.28;
}
#portal-splash.ps-v1 .ps-mark-logo .ps-logo-ink{
  position:absolute; left:0; top:0; width:100%; height:100%;
  clip-path:inset(0 calc(100% - var(--ps-p, .06) * 100%) 0 0);
  transition:clip-path .5s cubic-bezier(.22,.61,.36,1);
}

/* About's wait for its text film (WG-545) on a direct landing; raised from the
   site it is the hero's own photo darkened instead (.ps-photo below, RN51). The
   same cover on the hero's own green, --swv-accent-950, the film's first and
   last frame, so it
   hands over with no seam. Light ink, no sketch. The ink monogram is drawn for
   a light ground, so the village's light mark (.ps-monogram-light, shell.html.j2,
   rendered only for a village with a text film) takes its place; a village with
   none shows no monogram on the green. .ps-enter is the fade in when the cover
   is raised again from inside the site. */
#portal-splash.ps-v1.ps-about{
  background:var(--swv-accent-950);
  --splash-ink:var(--swv-neutral-50);
  --splash-ghost:color-mix(in srgb, var(--swv-neutral-50) 28%, transparent);
}
#portal-splash.ps-v1.ps-about::before{ opacity:0 }
#portal-splash.ps-v1.ps-about .ps-monogram{ display:none }
#portal-splash .ps-monogram-light{ display:none }
#portal-splash.ps-v1.ps-about .ps-monogram-light{
  display:block;
}
#portal-splash.ps-enter{ opacity:0 }
/* RN77: over the start page's curtain the cover is there at full strength at once (abtCoverShow) */
#portal-splash.ps-enter.ps-take{ opacity:1 }
/* RN52: under an About cover the page neither scrolls nor shows a scrollbar (abtScrollHold in
   engine.js); body's own overflow is set inline by showView, hence the !important. */
html.abt-covered,
html.abt-covered body{ overflow:hidden !important }
/* RN53: the boot's failsafe lifting a page landing before its page is routed uncovers this instead of
   the start page: the village's green, with nothing on it, until the page is in place and raises its own cover. */
html.boot-blank,
html.boot-blank body{ background:var(--swv-accent-950); overflow:hidden !important }
html.boot-blank body > *{ visibility:hidden }
/* Raised from the site, the cover keeps the hero's own photo instead (RN51,
   abtCoverPhoto in engine.js): the photo pushes in to the whole screen while
   it darkens, and the name and monogram come up once it is dark.
   While the film is still coming the photo drifts, slower than anyone would
   call movement. On the way out the name goes first and the photo dissolves
   into the film, whose opening is the same river from above. */
#portal-splash.ps-v1.ps-about.ps-photo{ background:transparent; overflow:hidden }
#portal-splash .ps-photo-pic{
  position:absolute; z-index:0; overflow:hidden; pointer-events:none;
  transform-origin:50% 50%; will-change:transform;
}
#portal-splash .ps-photo-drift{ position:absolute; inset:0; animation:psPhotoDrift 18s cubic-bezier(.37,0,.63,1) 1.4s both }
#portal-splash .ps-photo-drift img{
  position:absolute; inset:0; display:block; width:100%; height:100%; max-width:none; object-fit:cover;
}
#portal-splash .ps-photo-veil{ position:absolute; inset:0 }
#portal-splash .ps-photo-dim{
  position:absolute; inset:0; z-index:1; opacity:0; pointer-events:none;
  /* darkened, not recoloured: near black with a trace of the hero's green */
  background:rgb(8 12 6 / .56);
}
@keyframes psPhotoDrift{ from{ transform:scale(1) } to{ transform:scale(1.04) } }
#portal-splash.ps-photo .ps-mark,
#portal-splash.ps-photo .ps-monogram-light{
  z-index:2;
  transition-property:opacity, background-position-x, clip-path;
  transition-duration:.9s, .5s, .5s;
  transition-timing-function:cubic-bezier(.16,1,.3,1), cubic-bezier(.22,.61,.36,1), cubic-bezier(.22,.61,.36,1);
  transition-delay:.55s, 0s, 0s;
}
#portal-splash.ps-photo.ps-enter .ps-mark,
#portal-splash.ps-photo.ps-enter .ps-monogram-light{ opacity:0 }
/* .12s plus .7s, inside the 900 ms abtCoverEnd waits before it removes the cover */
#portal-splash.ps-photo.is-done{ transition:opacity .7s cubic-bezier(.33,0,.2,1) .12s }
#portal-splash.ps-photo.is-done .ps-mark,
#portal-splash.ps-photo.is-done .ps-monogram-light{ opacity:0; transition-duration:.25s, .5s, .5s; transition-delay:0s }

/* 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,
  #portal-splash.ps-v1 .ps-mark-logo .ps-logo-ink{ 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.

   The nav floats on the picture at every width now (Dave, 12 Oct: the
   dedicated 60px row above both panes left a bare strip of ground above the
   right pane's photo, since .cv1-nav is position:absolute and never actually
   sat in that row). One .cv1-nav element, one rule, the same on a phone and
   on desktop. */
.cv1{position:relative;display:grid;grid-template-columns:1fr 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: on a phone the whole nav sits on the cream pane, which is near
   white. From 861px the mark stays over that same cream left pane, but the
   burger is over the right pane, always dark now that the nav floats on
   the picture (a photo, or its solid dark fallback when there is none:
   .cv1-r:not(:has(.cv1-img)) below) rather than sitting on a dedicated
   cream row above it. The burger keeps the base sand colour there instead. */
.cv1-nav .cv1-burger i{background:var(--swv-accent-950)}
@media(min-width:861px){
  .cv1-nav .cv1-burger i{background:#E6E1D1}
}

.cv1-l,.cv1-r{position:relative}
.cv1-l{grid-column:1}
.cv1-r{grid-column:2}
/* 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.

   Full bleed on every side (Dave, 13 Oct: the Figma artboard's own 719 to
   1428 of 1440 left a 12px gap on the outside edge, which read as unfilled
   ground once the pane ran the full viewport height rather than the
   artboard's fixed one). A phone already drew this full bleed; this is now
   the one rule for every width instead of a second, narrower copy of it. */
.cv1-img{position:absolute;inset:0;z-index:0;width:100%;height:100%;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:calc(50% - 6px);transform:translateY(-50%);z-index:2;
  text-align:center}
/* The 1440x900 frame's 347 is the pane's centre less 6, so this is the same
   pixel at 900 tall and the centre of the pane at any other height (4K). */
.cv1-eyebrow{font-family:var(--copy-face);font-weight:400;font-size:var(--swv-text-body-serif);
  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:var(--swv-text-h1);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}
  .cv1-l,.cv1-r{grid-column:1;min-height:50svh}
  .cv1-l{grid-row:1}
  .cv1-r{grid-row:2}
  /* 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;transform:none}
  .cv1-title{font-size:var(--swv-text-h2);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.
   --wg-radius: a village still choosing its corners (the reviewer's look panel,
   lookpanel.js) or one that has chosen them in its DESIGN.md. This button, the lead
   form's save and the details page's button read it, each with the radius it always
   had as the fallback, so a village that sets none is unchanged. */
.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:var(--wg-radius,999px);
  background:var(--swv-accent-950);color:#FFFFFF;
  font:400 var(--swv-text-caption)/18.2px var(--swv-sans);letter-spacing:-.42px;text-decoration:none;
  cursor:pointer;transition:background-color .2s cubic-bezier(.22,1,.36,1),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{min-height:var(--swv-pill-h);padding-inline:var(--swv-pill-pad-x)}
  .swv-btn::before{content:"";position:absolute;inset:calc((var(--swv-pill-h) - var(--swv-control-h))/2) 0}
}
/* ---------- getting ready, on a control ----------
   The ring a pin wears while its villa downloads (.dot.warming), small enough
   to sit in a button. --swv-busy is the share that has arrived, 0 to 1, written
   by engine.js (swvBusy). It fills with the bytes and turns slowly, so a slow
   connection still reads as moving. Drawn in the control's own colour. */
@property --swv-busy{syntax:"<number>";inherits:true;initial-value:0}
.swv-spin{flex:none;display:block;width:var(--spin,16px);height:var(--spin,16px);border-radius:50%;
  background:conic-gradient(currentColor calc(var(--swv-busy) * 1turn), color-mix(in srgb,currentColor 34%,transparent) 0);
  -webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
  mask:radial-gradient(farthest-side, transparent calc(100% - 2px), #000 calc(100% - 1.5px));
  transition:--swv-busy .3s linear;
  animation:swv-spin 1.4s linear infinite, swv-spin-in .2s cubic-bezier(.22,1,.36,1) backwards}
@keyframes swv-spin{to{transform:rotate(1turn)}}
@keyframes swv-spin-in{from{opacity:0}}
@media (prefers-reduced-motion:reduce){.swv-spin{animation:none;transition:none}}
/* A button that is getting ready keeps its width: the label stays where it
   was, unseen, and the ring sits over its centre. */
.swv-btn.is-loading,.swv-btn.is-loading:hover{background:var(--swv-accent-950);color:transparent;cursor:progress}
.swv-btn.is-loading .swv-spin{position:absolute;inset:0;margin:auto;color:#FFFFFF}
/* Forced colours paint the label whatever colour it is given, so there the ring sits beside it rather than over it. */
@media (forced-colors:active){.swv-btn.is-loading .swv-spin{position:static;margin:0}}
/* The ring a pressed pin wears while the ground it flies to is loading: the shared
   .swv-spin, filled by --swv-busy, in a box that masterplan3d.js moves onto the pin
   every frame (the ring's own turn is a transform, so it cannot carry the position). */
#map3d .mp3d-pin-wait{position:absolute;left:0;top:0;width:36px;height:36px;margin:-18px 0 0 -18px;pointer-events:none;
  color:#FFFFFF;filter:drop-shadow(0 0 2px rgba(0,0,0,.65))}
#map3d .mp3d-pin-wait .swv-spin{--spin:36px}

/* The 3D map's pins reach and breathe as Secret Waterfall's do (Dave, 19 October;
   the pinReach option of masterplan3d.js builds the layer, so no class is needed
   here). One ring per pin, a 24px box that masterplan3d.js moves and scales onto
   the pin's drawn disc by transform alone, whose ::after is the flat map's own
   ring, in white where the flat map's is the pin's colour (the dark pin colour
   was lost on the canopy; the pin here is a white disc too): the keyframes swv-pin-rest-far and swv-pin-ring-far, driven by the same
   custom properties, with the same numbers (the rules that set them on #hotspots
   name .mp3d-rings too, so there is one copy, the white rest as bright as the Views
   look's on a busy photograph), staggered
   the same way. --pin-reach is also what masterplan3d.js reads for the press
   reach. It is on only when the pin shows no card of its own (is-on), is on the
   screen (is-out), and the camera is still (is-moving), and a sold or reserved
   home's is dimmer, as there. The wave (.arrive, --arrive, --arrive-ring) is
   written by masterplan3d.js (playArrive). */
#map3d .mp3d-rings{position:absolute;inset:0;overflow:hidden;pointer-events:none}
#map3d .mp3d-ring{position:absolute;left:0;top:0;width:24px;height:24px;visibility:hidden;--ring-peak:var(--rest-peak)}
#map3d .mp3d-ring.is-on:not(.is-out){visibility:visible}
#map3d .mp3d-ring.is-dim{--ring-peak:calc(var(--rest-peak) * .4)}
#map3d .mp3d-ring:nth-child(5n+2){--rest-at:1300ms}
#map3d .mp3d-ring:nth-child(5n+3){--rest-at:2600ms}
#map3d .mp3d-ring:nth-child(5n+4){--rest-at:650ms}
#map3d .mp3d-ring:nth-child(5n+5){--rest-at:1950ms}
#map3d .mp3d-ring::after{content:"";position:absolute;inset:-2px;border-radius:50%;opacity:0;
  border:1.5px solid #fff;box-shadow:0 0 0 1px rgba(20,30,25,.3);
  background:radial-gradient(circle,transparent 45%,rgba(255,255,255,.55) 100%);
  animation:swv-pin-rest-far var(--rest-period) cubic-bezier(.3,.1,.7,.8) var(--rest-at,0ms) infinite}
#map3d .mp3d-ring.arrive::after{
  animation:swv-pin-ring-far var(--arrive-ring) cubic-bezier(.3,.1,.7,.8) var(--arrive,0ms) both}
#map3d .mp3d-ring:not(.is-on)::after,#map3d .mp3d-ring.is-out::after{animation:none}
#map3d .mp3d-rings.is-moving .mp3d-ring::after{animation-play-state:paused;visibility:hidden}
@media(prefers-reduced-motion:reduce){#map3d .mp3d-ring::after{content:none;animation:none}}

/* ---------- a small picture that comes in blurred ----------
   The picture of a pin's card or a row of The Homes is a 320px thumbnail, and
   what shows first is a 16px copy of it, blown up and blurred, so a slow
   connection sees the colour and light of the real picture and never one drawn
   from the top down. engine.js (swvBlurHTML) puts the copy in --swv-ph and the
   picture inside the wrapper; the picture stays unseen until its load event,
   when the wrapper gets .is-in and it fades up over the blur. A picture with no
   copy waits on the quiet ground instead. The blur reaches past the wrapper's
   edges so its soft rim is clipped away, and it is dropped once the fade is done:
   a faded picture (a sold row) is not opaque and would show it through. A
   picture that fails to load keeps the blur. --swv-blur-rest is how opaque the
   picture comes to rest, and --swv-blur-tone a filter for the blur alone. */
.swv-blur{position:relative;display:block;overflow:hidden;background:var(--swv-neutral-200)}
.swv-blur::before{content:"";position:absolute;inset:-12px;
  background-image:var(--swv-ph,none);background-size:cover;background-position:center;
  filter:var(--swv-blur-tone,grayscale(0)) blur(6px)}
.swv-blur img{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity .4s cubic-bezier(.22,1,.36,1)}
.swv-blur.is-in img{opacity:var(--swv-blur-rest,1)}
.swv-blur.is-in::before{opacity:0;transition:opacity 0s .4s}
@media (prefers-reduced-motion:reduce){.swv-blur img{transition:none}.swv-blur.is-in::before{transition:none}}
/* RN75, R209 (Dave, 29 Oct 2026: "the gallery loading effect is weirdly from top to bottom, default always
   breathing or at least blur"): a gallery picture is never on screen half drawn. engine.js marks the Homes
   hero and its thumbnails, the details slider's slides and the full screen viewer's picture .wg-pic, and each
   stays unseen until it is whole (.is-whole) while its frame breathes: a tint of the frame's own text colour
   that rises and falls, so it suits a light panel and the dark viewer alike. A picture replacing one already
   showing waits off screen, and the one showing stays, blurred and breathing (.wg-hold), until the swap. The
   small loader above breathes the same way when it has no preview to blur. */
@keyframes wg-ground{0%,100%{background-color:color-mix(in srgb,currentColor 6%,transparent)}
  50%{background-color:color-mix(in srgb,currentColor 14%,transparent)}}
@keyframes wg-breathe{50%{opacity:.62}}
img.wg-pic:not(.is-whole){opacity:0}
img.wg-pic.wg-hold{filter:blur(14px);animation:wg-breathe 2.4s ease-in-out infinite}
#swv-panel .swv-hero-media:has(> .swv-hero-img:not(.is-whole)),#swv-panel .swv-thumb:has(> img:not(.is-whole)),
.dv1-media-stage:has(> .dv1-media-img.on:not(.is-whole)),.swv-blur:not(.is-in):not(.is-lost){animation:wg-ground 2.4s ease-in-out infinite}
#lightbox .lb-stage:has(> img:not(.is-whole))::before{content:"";position:absolute;inset:0;margin:auto;
  width:min(92vw,126vh);max-height:84vh;aspect-ratio:3/2;color:var(--paper);animation:wg-ground 2.4s ease-in-out infinite}
/* Less motion needs nothing here: the reduced motion block near the top of this file takes every animation and
   transition away, so nothing breathes or fades, and a held picture keeps its blur, which is not motion. */
#swv-panel .swv-hero-img,#swv-panel .swv-thumb img{transition:opacity .4s cubic-bezier(.22,1,.36,1)}

/* ---------- 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. The panel is cream now (Figma "05. Ask Grace"), so this is
   the kit's own light pill: a grey fill, a hairline, ink text. It stays at
   body size where the design draws 14px: the village type floor set it to
   16px, and a phone zooms the page into any field smaller than that. */
#c-in.gv1{height:36px;border-radius:99px;padding:0 17px;
  font:400 var(--swv-text-body)/18px var(--swv-sans);letter-spacing:-.42px;outline:none;
  background:var(--swv-neutral-100);
  border:1px solid var(--swv-neutral-300);color:var(--swv-neutral-950);text-overflow:ellipsis}
#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-950) 30%,transparent)}

/* Send: the design's 36px round dark button with an up arrow (the shared
   arrow icon, turned). 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 mark, about 1.5:1, so the mark is darkened to --swv-accent-950
   instead and the fill is kept exactly as drawn. */
#c-send.gv1{display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:36px;height:36px;min-height:36px;padding:0;border:0;border-radius:999px;
  font:400 var(--swv-text-caption)/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-accent-950);color:var(--swv-neutral-50)}
#c-send.gv1 svg{display:block;width:20px;height:20px;transform:rotate(-90deg)}
#c-send.gv1:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:3px}
#c-send.gv1:hover{background:var(--swv-accent-900)}
#c-send.gv1:disabled{background:var(--swv-neutral-300);color:var(--swv-accent-950);cursor:not-allowed}
/* Every village but Secret Waterfall sends with brass (body has no data-project-slug there): the
   gold Dave kept when the panel went light (R84). Brass with its own ink, 4.7:1 for the arrow and
   about 3:1 for the button on the cream panel, lifted 2px on hover. The ::after widens the target
   to 44px for a thumb. */
body:not([data-project-slug]) #c-send.gv1{position:relative;background:var(--sun);color:var(--sun-ink);
  border:0;letter-spacing:0;transition:transform .2s cubic-bezier(.22,1,.36,1)}
body:not([data-project-slug]) #c-send.gv1::after{content:"";position:absolute;inset:-4px}
body:not([data-project-slug]) #c-send.gv1:hover{background:var(--sun);color:var(--sun-ink);transform:translateY(-2px)}
/* On the light panel (R84) --paper is near white: the ring takes the village's focus gold, and the
   arrow on a soft gold stays dark. */
body:not([data-project-slug]) #c-send.gv1:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:3px}
body:not([data-project-slug]) #c-send.gv1:disabled{background:var(--sun-soft);color:var(--sun-ink);transform:none}
@media(prefers-reduced-motion:reduce){body:not([data-project-slug]) #c-send.gv1{transition:none}}

/* ---------- 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.

   Two things play in it. A village that publishes a film (LOADER_SKETCH) gets
   the film alone; a village with none gets the drawn sequence of stages 1 to 5
   below (second sketch, ink blots, the render's torn edge). Both end the same
   way: the overlay fades out at stage 5, handing off to the map already built
   behind it (engine.js sets the hash back at stage 4 of the sequence, or once
   the film is three quarters played). 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}
/* A village with BRAND.loaderFit (engine.js loaderFilmBox) draws the film and
   the still it opens on in one box, the one where the film's frame is the
   masterplan's own picture. engine.js sets that box inline, so inset gives way
   to it and the picture is stretched by the box (object-fit:fill) rather than
   cropped inside it. Nothing moves, so there is no transform and no mask. */
#swv-loader .swv-loader-video--fit,
#swv-loader .swv-loader-a--fit{inset:auto;max-width:none;object-fit:fill}
/* Its own layer, as the curtain's picture has: painted in place the still is snapped to whole pixels, half a pixel off
   the video above it (measured at 1440, where the box starts at -106.48). */
#swv-loader .swv-loader-a--fit{will-change:transform}
/* 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:var(--swv-text-h1);line-height:1.1;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 follows the letters only, with no pool behind the line
     (Dave, 5 Oct): a tight shadow keeps the edges of the white type clear over
     bright sky and water, a soft one lifts the line off the render. */
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.5)) drop-shadow(0 2px 14px rgba(0,0,0,.4))}
#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%}
/* A village's logo in place of the text name (NAME_MARK_LIGHT, engine.js
   mapWelcomeName). The lead sits on its own line with the logo centred under
   it at every width. Two copies again: the ghost holds the unfilled share (.35,
   as the gradient's) and sizes the box by height in the headline's own em; the
   ink one is revealed left to right over the same 2s. The drop-shadow filter on
   #swv-map-welcome follows the picture's alpha, so it needs nothing here. */
#swv-map-welcome.mw-logo .swv-mw-lead{display:block}
#swv-map-welcome.mw-logo .swv-mw-name{
  display:block;position:relative;width:fit-content;margin:.15em auto 0;line-height:0;
  background:none;transition:none}
#swv-map-welcome .swv-mw-ghost{display:block;height:2em;width:auto;max-width:100%;opacity:.35}
#swv-map-welcome .swv-mw-ink{
  position:absolute;left:0;top:0;width:100%;height:100%;
  clip-path:inset(0 100% 0 0);
  transition:clip-path 2s cubic-bezier(.22,.61,.36,1)}
#swv-map-welcome[data-state="filling"] .swv-mw-ink,
#swv-map-welcome[data-state="out"] .swv-mw-ink{clip-path:inset(0 0 0 0)}
@media (max-width:860px){
  /* The design's phone welcome: centred, two lines ("Welcome to the Secret" over
     "Waterfall Village"), at the h3 size. 10em is the measure that breaks it
     there, wider than the first line and narrower than it with "Waterfall"
     added; it scales with the type, where a pixel width would not. */
  #swv-map-welcome{width:10em;max-width:calc(100vw - 2*var(--swv-space-16));
    font-size:var(--swv-text-h3);line-height:1.2;letter-spacing:-.03em}
  /* On a phone the camera opens on the village heart, so its name lands under
     the centred headline. The names wait while it shows and fade back in through
     .is-on's own transition as it leaves (data-state "out"). This beats .is-on and
     .is-muted alike, so Tab skips the names for those few seconds. */
  body:has(#swv-map-welcome[data-state="in"]) #map3d .mp3d-area,
  body:has(#swv-map-welcome[data-state="filling"]) #map3d .mp3d-area{visibility:hidden;opacity:0}
}
/* 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,#swv-map-welcome .swv-mw-ink{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}
/* #F2F2F2 was --paper's own literal value, hardcoded rather than read as a
   token — on Temple Village's daylight plan (a light ground, not the night
   render every other village's map is) that painted the dot the same colour
   as the ground it sits on. --pin carries the same "paper on a dark render"
   default every other village already has (declared in :root above), so
   this is the same fix as the plain .dot .pt rule further up the file, on
   the selector that actually wins here: #map-wrap always carries .mv1
   (engine.js's own map markup), so this is the rule every village's pin is
   drawn by, not the older one above it. */
#map-wrap.mv1 #hotspots .dot .pt{
  position:static;transform:none;width:20px;height:20px;border-radius:50%;
  background:var(--pin,#F2F2F2);box-shadow:0 0 0 2px color-mix(in srgb,var(--pin,#fff) 16%, transparent);
  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) pins 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{cursor:not-allowed;pointer-events:auto}
/* A pin for sale carries its plot number ("01"); a reserved or sold pin is
   the padlock on a faint dark disc, so the homes you can buy read first. The
   quiet is the pin's own, not its hover card's: the card names the status,
   and a faded label is the one thing a buyer must not have to squint at.
   flex:none because the pin is a flex item of its 24px dot: left to shrink it
   came out 24 wide and 28 tall, an oval. */
#map-wrap.mv1 #hotspots .dot .pt:has(.pt-n),
#map-wrap.mv1 #hotspots .dot.reserved .pt{
  flex:none;width:28px;height:28px;display:flex;align-items:center;justify-content:center;transition:box-shadow .2s,opacity .2s}
#map-wrap.mv1 #hotspots .dot .pt-n{
  font:600 13px/1 var(--copy-face);font-variant-numeric:lining-nums tabular-nums;color:var(--swv-accent-950)}
/* Plot labels (R246): a longer label such as B-10 or 2BR-10 makes the pin a pill, as wide as its words and
   never under the disc, centred where the disc was. The dot (its hit area) keeps its size. max-content and
   nowrap, since an auto width is the 24px dot's and the words broke at their hyphen (seen live, 3 Nov); and
   .mv1, so it outranks the Views look's own disc size (body.explore-views ... .dot:not(.amen) .pt, 26px). */
#map-wrap.mv1[data-pin-label]:not([data-pin-label=""]) #hotspots .dot .pt:has(.pt-n){width:max-content;min-width:28px;box-sizing:border-box;padding:0 6px;border-radius:14px;white-space:nowrap}
/* On a phone the pin keeps its look and its 44px hit area grows around it on a
   transparent pseudo element; the number moves onto the scale (13px is off it). */
@media(max-width:860px){
  #map-wrap.mv1 #hotspots .dot::before{content:"";position:absolute;
    inset:calc((var(--swv-control-h) - 24px)/-2)}
  #map-wrap.mv1 #hotspots .dot .pt-n{font-size:var(--swv-text-caption)}
}
#map-wrap.mv1 #hotspots .dot.reserved .pt{
  opacity:1;transform:none;background:color-mix(in srgb,var(--swv-accent-950) 38%, transparent);
  box-shadow:0 0 0 1px color-mix(in srgb,#fff 22%, transparent);color:var(--swv-neutral-50)}
#map-wrap.mv1 #hotspots .dot.reserved .pt svg{width:16px;height:16px;opacity:.85}
#map-wrap.mv1 #hotspots .dot.reserved:hover .pt,
#map-wrap.mv1 #hotspots .dot.reserved:focus-visible .pt{box-shadow:0 0 0 1px color-mix(in srgb,#fff 60%, transparent)}
#map-wrap.mv1 #hotspots .dot.amen{cursor:pointer}
#map-wrap.mv1 #hotspots .dot.amen.wayfinding{cursor:default}
#map-wrap.mv1 #hotspots .dot.amen .pt{width:20px;height:20px;background:var(--pin,#F2F2F2);
  box-shadow:0 0 0 2px color-mix(in srgb,var(--pin,#fff) 16%, transparent);transform:none}
#map-wrap.mv1 #hotspots .dot.amen:hover .pt{transform:none}
/* A village that sets bedColors (engine.js swvPaintPin) fills each pin with its
   bedroom colour, the facilities' light and the homes' dark. --pin is the one
   thing the fill, both rings and the resting ring read, so the pin's own
   colour is all that changes. A padlock pin keeps its look. The number is
   cream, and --bed-ink where cream would be too faint on the fill. */
#map-wrap.mv1 #hotspots .dot.bed:not(.reserved){--pin:var(--bed)}
#map-wrap.mv1 #hotspots .dot.bed:not(.reserved) .pt-n{color:var(--bed-ink,var(--tv-cream,#F0E9D9))}

/* The pins of the tab in view breathe while a tab or filter lights them: one thin ring leaves each pin,
   widens a little and fades, every 3.6s, and the next pin a beat later, so the
   map shimmers rather than blinks. It lives on the dot's own ::after, a
   transparent circle the size of the 28px pin, so it never touches the pin's
   hit area, the card, or anything clampTags measures. Only transform and
   opacity move. The pins of the tab that is not showing (.off) carry no ring
   at all, so nothing runs for them. The numbers are here and nowhere else:
   the period, the peak opacity and how far the ring grows. A padlock pin is
   dimmed to 38%, so its ring is 40% of the others, never brighter than it.
   The ring starts at nothing and its base style is opacity 0, so there is no
   frame in which it shows whole. */
#map-wrap.mv1 #hotspots,#map3d .mp3d-rings{--rest-period:3600ms;--rest-peak:.4;--rest-grow:.75}
#map-wrap.mv1 #hotspots .dot{--ring-peak:var(--rest-peak)}
/* a pin showing its card stands over its neighbours, so their rings pass
   under the card and never across it */
#map-wrap.mv1 #hotspots .dot:hover,
#map-wrap.mv1 #hotspots .dot:focus-visible,
#map-wrap.mv1 #hotspots .dot.picked,
#map-wrap.mv1 #hotspots .dot.lit{z-index:2}
#map-wrap.mv1 #hotspots .dot.reserved,
#map-wrap.mv1 #hotspots .dot.sold{--ring-peak:calc(var(--rest-peak) * .4)}
#map-wrap.mv1 #hotspots .dot:nth-child(5n+2){--rest-at:1300ms}
#map-wrap.mv1 #hotspots .dot:nth-child(5n+3){--rest-at:2600ms}
#map-wrap.mv1 #hotspots .dot:nth-child(5n+4){--rest-at:650ms}
#map-wrap.mv1 #hotspots .dot:nth-child(5n+5){--rest-at:1950ms}
@keyframes swv-pin-rest{
  from{transform:scale(1);opacity:0}
  14%{opacity:var(--ring-peak)}
  to{transform:scale(calc(1 + var(--rest-grow)));opacity:0}
}
#map-wrap.mv1 #hotspots .dot:not(.off)::after{content:"";position:absolute;inset:-2px;border-radius:50%;pointer-events:none;opacity:0;
  /* a soft halo, brightest at its rim: a hairline alone is lost on the render */
  border:1.5px solid var(--ring-ink,var(--pin,#fff));
  background:radial-gradient(circle,transparent 45%,color-mix(in srgb,var(--ring-ink,var(--pin,#fff)) 55%,transparent) 100%);
  animation:swv-pin-rest var(--rest-period) cubic-bezier(.3,.5,.4,1) var(--rest-at,0ms) infinite}

/* A map with .pin-reach (engine.js pinReachOn: Secret Waterfall, and any village that sets
   settings.pinReach) has pins that reach further (Dave, 10 October). The 28px pin was
   the whole target, too small to find. A transparent disc three times as wide
   (--pin-reach, 84px) now takes the hover, the press and the tap. The resting
   ring ends at 2.1 times the plain ring's size (--ring-reach: three times was
   too big, so it is 70% of that, 103px across at its end). Up to its old edge it is
   the ring it was; past it, it carries on at a quarter of its peak (--ring-far,
   about 10%), there to be seen and no more. A tab's arrival ring was already
   big, so it ends at 1.4 times the plain one (--arrive-reach, 70% of double, 98px across). The
   scale runs on one curve from start to end, so the ring never changes pace at
   the old edge: 35% and 61% are where that curve crosses it, and the peak comes
   just after the ring clears the pin. On Secret Waterfall the closest two pins are 71px apart
   and a disc only covers another pin's own body under 56px. Where two discs overlap the pointer
   goes to the nearer pin (engine.js pinReachWire). Other villages keep the plain pin.
   :where() keeps these at the weight of the rules they follow, so the rules
   further down (the arrival, and no ring on a hovered or picked pin) still win. */
#map-wrap.mv1:where(.pin-reach) #hotspots,#map3d .mp3d-rings{--pin-reach:3;--ring-reach:2.1;--arrive-reach:1.4;--ring-far:.25}
#map-wrap.mv1:where(.pin-reach) #hotspots .dot::before{content:"";position:absolute;border-radius:50%;
  inset:calc((24px - 28px * var(--pin-reach)) / 2)}
/* R241 (Dave, 2 Nov 2026: "big enough so clicking around the house will easily open the hover card ... 2-3x
   bigger"): a home on the Views photographs is drawn far larger than a pin. Its house, deck and round
   pavilion reach 66 to 113px from its pin at whole (the outline the masterplan repaint draws round a
   villa), and three pins (42px from the centre) covered the roof alone. Seven (98px) take
   the house round it. Where two reach, the nearer pin still wins (pinReachAt), so a neighbour's house is
   its own. The 3D top view keeps three: its homes are drawn small. */
body.explore-views #map-wrap.mv1.pin-reach #hotspots{--pin-reach:7}
@keyframes swv-pin-rest-far{
  from{transform:scale(1);opacity:0}
  16%{opacity:var(--ring-peak)}
  35%{opacity:calc(var(--ring-peak) * var(--ring-far))}
  85%{opacity:calc(var(--ring-peak) * var(--ring-far) * .8)}
  to{transform:scale(calc((1 + var(--rest-grow)) * var(--ring-reach)));opacity:0}
}
#map-wrap.mv1:where(.pin-reach) #hotspots .dot:not(.off)::after{animation-name:swv-pin-rest-far;animation-timing-function:cubic-bezier(.3,.1,.7,.8)}
/* R191, Dave: "ensure ripple only when filtered". With no tab or filter lighting a set the pins
   rest still; a tab's own arrival ring still plays. */
#map-wrap.mv1:not(.pins-filtered) #hotspots .dot:not(.arrive)::after{animation:none}
/* R213: a village that turns the ripple off (settings.pinRipple false) has no resting ring at all,
   lit set or not, on the photographs and on the 3D top view; a tab's arrival wave is left to play. */
#map-wrap.no-pin-ripple #hotspots .dot:not(.arrive)::after,
#map-wrap.no-pin-ripple #map3d .mp3d-ring:not(.arrive)::after{animation:none}

/* A tab press brings that tab's pins in (swvPinsArrive), as one slow pulse,
   like a drop landing in still water: the pin rises into place without
   passing its size, and a single ring leaves it at the same moment and widens
   as it fades; it starts from nothing, so a pin waiting its turn in the wave
   shows no ring before it. One long ease out for both, so nothing bounces.
   The ring is the resting ring made twice as big and twice as bright (the
   same box, twice the growth, twice the peak: .8, under .85), so it reads as
   the same pulse, louder, for the length of one ring. --arrive is the pin's
   turn in the wave, outward from the middle. Only transform and opacity move,
   and the dot is never touched, so the pin stays on its building.
   Hand over: while .arrive is on, this rule replaces the resting animation
   (one animation on the ::after, not two stacked), and the class comes off at
   the last ring's animationend. The ring ends at opacity 0 and the resting
   ring begins at opacity 0 behind its own delay, with a base style of opacity
   0, so no frame between them shows a ring: no flash, no jump, no second
   pulse. The ring holds its last keyframe (both, not backwards) until then.
   The two durations are not here: engine.js writes them, with the stagger and
   the clean up, to --arrive-pin and --arrive-ring (SWV_ARRIVE_*). */
@keyframes swv-pin-arrive{
  from{transform:scale(.8);opacity:0}
  to{transform:scale(1);opacity:1}
}
@keyframes swv-pin-ring{
  from{transform:scale(1);opacity:0}
  12%{opacity:calc(var(--ring-peak) * 2)}
  to{transform:scale(calc(1 + var(--rest-grow) * 2));opacity:0}
}
#map-wrap.mv1 #hotspots .dot.arrive .pt{animation:swv-pin-arrive var(--arrive-pin) cubic-bezier(.22,1,.36,1) var(--arrive,0ms) backwards}
#map-wrap.mv1 #hotspots .dot.arrive::after{
  animation:swv-pin-ring var(--arrive-ring) cubic-bezier(.3,.5,.4,1) var(--arrive,0ms) both}
@keyframes swv-pin-ring-far{
  from{transform:scale(1);opacity:0}
  19%{opacity:calc(var(--ring-peak) * 2)}
  61%{opacity:calc(var(--ring-peak) * var(--ring-far))}
  88%{opacity:calc(var(--ring-peak) * var(--ring-far) * .8)}
  to{transform:scale(calc((1 + var(--rest-grow) * 2) * var(--arrive-reach)));opacity:0}
}
#map-wrap.mv1:where(.pin-reach) #hotspots .dot.arrive::after{animation-name:swv-pin-ring-far;animation-timing-function:cubic-bezier(.3,.1,.7,.8)}
/* No ring where the pin steps aside for its card (hovered, picked, focused, lit
   by its row), or while it is getting ready (its own progress ring is there). */
#map-wrap.mv1 #hotspots .dot:hover::after,
#map-wrap.mv1 #hotspots .dot:focus-visible::after,
#map-wrap.mv1 #hotspots .dot.picked::after,
#map-wrap.mv1 #hotspots .dot.lit::after,
#map-wrap.mv1 #hotspots .dot.warming::after{animation:none}
/* A drag pauses every ring where it is and hides it, so the map moves without
   a trail of rings, and they carry on from the same point when it is let go. */
body.panning #map-wrap.mv1 #hotspots .dot::after{animation-play-state:paused;visibility:hidden}
@media(prefers-reduced-motion:reduce){
  #map-wrap.mv1 #hotspots .dot.arrive .pt{animation:none}
  #map-wrap.mv1 #hotspots .dot:not(.off)::after,
  #map-wrap.mv1 #hotspots .dot.arrive::after{content:none;animation:none}
}

/* the Panel tooltip: a thumbnail as tall as the text beside it (cropped, never
   stretched: contain:size keeps its own height out of the row's), 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:block;padding:0;border:0;border-radius:0;background:none;
  backdrop-filter:none;color:var(--swv-accent-950);text-align:left;
  white-space:normal;width:max-content;max-width:min(299px,calc(100vw - 5px))}
/* The card is the .tag's one child. The .tag keeps the position, the fade and
   the nudge clampTags measures and never tilts; the child is what you see, so a
   tilt (the finish further down) cannot move what clampTags reads. 299px is the
   old 280px of content plus the card's padding and border. */
#map3d .mp3d-card .swv-tag-card,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-card{
  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)}
/* the arrow only a picked, openable card carries (see the finish below) */
.swv-tag-go,.swv-tag-lock{display:none}
/* The thumbnail is a .swv-blur wrapper (engine.js swvThumbHTML), which is the
   positioned box its picture and its blur are laid in. Its least height is the
   villa card's three lines (pill, name, area): a facility's card has the name
   alone, and its picture came out 38px tall beside a villa's 67px. With the
   least height a facility's card is as tall as a villa's and its picture the
   same size, the one line centred against it by the card's align-items. A
   villa's text is already that tall, so its card does not change. The picture
   is a square (Dave, 18 Oct: 58px wide read too narrow), centred against the
   text, so a taller text grows the card and never the picture. */
#map3d .mp3d-card .swv-tag-thumb,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-thumb{
  width:67px;height:67px;min-height:67px;align-self:center;contain:size;border-radius:4px;flex:none}
/* #map-cam img{position:absolute;left:0;top:0;width:100%;height:100%} reaches
   the picture inside it too, since it is a plain descendant img. It would fill
   the wrapper by luck of those values; this says so, with more weight. */
#map3d .mp3d-card .swv-tag-thumb img,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-thumb img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
#map3d .mp3d-card .swv-status,
#map-wrap.mv1 #hotspots .dot .tag .swv-status{margin-bottom:4px;letter-spacing:0;text-transform:none}
#map3d .mp3d-card .nm,
#map-wrap.mv1 #hotspots .dot .tag .nm{
  font-family:var(--swv-sans);font-weight:500;font-size:var(--swv-text-caption);line-height:1.2;
  letter-spacing:0;text-transform:none;font-style:normal;color:var(--swv-accent-950)}
#map3d .mp3d-card .swv-tag-meta,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta{
  font-family:var(--swv-sans);font-weight:400;font-size:var(--swv-text-caption);opacity:.64;margin-top:2px;
  display:flex;flex-wrap:wrap;align-items:center;gap:2px 8px}
/* rooms, then sizes: a picked card's open arrow takes width from the row, so it wraps between the
   two groups. One fact is the line icon beside its number, the icon the height of the text. */
#map3d .mp3d-card .swv-tag-meta .swv-facts,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta .swv-facts{display:inline-flex;align-items:center;gap:8px;white-space:nowrap}
#map3d .mp3d-card .swv-tag-meta .swv-fact,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta .swv-fact{display:inline-flex;align-items:center;gap:3px}
#map3d .mp3d-card .swv-tag-meta .swv-ic,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta .swv-ic{display:inline-flex;flex:none}
#map3d .mp3d-card .swv-tag-meta svg,
#map-wrap.mv1 #hotspots .dot .tag .swv-tag-meta svg{width:14px;height:14px}
#map-wrap.mv1 #hotspots .dot:hover .tag,
#map-wrap.mv1 #hotspots .dot.picked .tag,
#map-wrap.mv1 #hotspots .dot:focus-visible .tag{opacity:1}
/* on the card the pill is a plain label, icon then word, and a home off the
   market shows its picture in greyscale, the same as its Homes row */
#map3d .mp3d-card .swv-tag-txt .swv-status,
#map-wrap.mv1 #hotspots .dot .tag .swv-status{height:auto;padding:0;background:none;color:var(--swv-neutral-600)}
#map3d .mp3d-card .swv-tag-txt .swv-status--available,
#map-wrap.mv1 #hotspots .dot .tag .swv-status--available{color:var(--swv-success);font-weight:500}
#map3d .mp3d-card .reserved .swv-tag-thumb,
#map-wrap.mv1 #hotspots .dot.reserved .tag .swv-tag-thumb{filter:grayscale(1)}
/* the card stands in for the pin it came from: the hovered villa pin steps
   aside so the house under it stays in view, and so does the picked one,
   whose card stays up with no pointer on it. Desktop only, since a phone
   shows no card (below). */
@media(min-width:861px){
  /* The card is part of the pin's hover: while the pin is hovered the card takes
     the pointer too (it is the pin's child, so :hover holds on it), and a strip
     on its pin side carries the pointer across the 20px gap a flipped card (see
     clampTags) stands from its pin. Only while the card is showing, hovered or
     picked: a hidden card takes no pointer, so it never covers another pin or
     the map's drag, which still starts on a shown card as it does on a pin. A click on it is a click on its pin: the card
     is the dot's child, so the click bubbles to wireDots' own handler and the
     pin's press flow (swvPressOpen) runs, with the pin's cursor, and the text
     cannot be selected, so a press on it never starts a selection. */
  #map-wrap.mv1 #hotspots .dot:hover .tag,
  #map-wrap.mv1 #hotspots .dot.picked .tag{pointer-events:auto;user-select:none}
  #map-wrap.mv1 #hotspots .dot .tag::before{content:"";position:absolute;top:0;bottom:0;left:-24px;width:24px}
  #map-wrap.mv1 #hotspots .dot .tag.flip::before{left:auto;right:-24px}
  #map-wrap.mv1 #hotspots .dot[data-id]:hover .pt,
  #map-wrap.mv1 #hotspots .dot[data-id].picked .pt,
  #map-wrap.mv1 #hotspots .dot[data-id]:focus-visible .pt{opacity:0}
}
/* the active state: this hotspot is the one picked. Its card reads at full
   strength, the same as a hovered one; the darker border is what marks it. */
#map3d .mp3d-card .swv-tag-card.picked,
#map-wrap.mv1 #hotspots .dot.picked .tag .swv-tag-card{border-color:var(--swv-accent-950)}
/* The 3D map's card: the flat card above, put in a button (masterplan3d.js,
   the pinCard option) that the scene stands beside the picked pin. The button is a bare frame: the card
   inside is what you see, at the flat map's own size, on phones as well. Above
   the area names (it is a later layer) and below the page's own overlays. The
   scene shows and places it; there is no animation, so reduced motion needs no
   rule of its own. */
#map3d .mp3d-card{display:block;margin:0;padding:0;border:0;border-radius:6px;background:none;font:inherit;text-align:left;
  color:var(--swv-accent-950);cursor:pointer;width:max-content;max-width:min(299px,calc(100vw - 16px));
  box-shadow:0 4px 14px rgba(12,11,9,.22)}
#map3d .mp3d-card[aria-disabled="true"]{cursor:default}
#map3d .mp3d-card:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:2px}
/* the arrow, or the padlock in its place, as the flat picked card has them */
#map3d .mp3d-card .go .swv-tag-go,#map3d .mp3d-card .lock .swv-tag-go{display:flex;align-items:center;justify-content:center;width:22px;height:22px;flex:none;margin-left:auto;color:var(--swv-accent-950)}
#map3d .mp3d-card .go .swv-tag-go svg{width:20px;height:20px}
#map3d .mp3d-card .lock .swv-tag-go>svg{display:none}
#map3d .mp3d-card .lock .swv-tag-lock{display:flex}
#map3d .mp3d-card .lock .swv-tag-lock svg{width:20px;height:20px}
/* The hover card keeps the plain card's thin body and outline (no gilt edge,
   9 October) and takes two things from the Explore details card
   (.swv-ov-cta-card): a slight 3D tilt toward the cursor while it is hovered,
   and its glare, here a soft sheen that follows the pointer. wireTagTilt()
   writes --tx and --ty (the tilt) and --tag-mx and --tag-my (the sheen's centre,
   as percentages of the card) on the card, and the card is the .tag's child, so
   what clampTags measures never moves. It does not spin and has no back. Every
   village's, keyed on :root body, not on Secret Waterfall's slug (R91, Dave
   25 Oct: "the hover card should reuse 3d card as in SWV"). The About page's
   Instagram posts (.abt-close-post, RN105) are the same card: the same tilt
   and the same sheen, drawn by the same rules, with a deeper shadow under
   the hovered post. */
@media(min-width:861px){
  :root body #map-wrap.mv1 #hotspots .dot .tag .swv-tag-card{
    position:relative;
    border-color:color-mix(in srgb,var(--swv-accent-950),transparent 45%);
    box-shadow:0 2px 8px rgba(10,9,6,.16)}
  :root body #map-wrap.mv1 #hotspots .dot .tag .swv-tag-card,.abt-close-post{
    transform:perspective(640px) rotateX(var(--tx,0deg)) rotateY(var(--ty,0deg));
    transition:transform .3s cubic-bezier(.22,1,.36,1),box-shadow .3s cubic-bezier(.22,1,.36,1)}
  /* the picked card's full strength outline stays over the hovered one's */
  :root body #map-wrap.mv1 #hotspots .dot.picked .tag .swv-tag-card{border-color:var(--swv-accent-950)}
  /* the reflection: a light that sits under the pointer and a faint shade
     that gathers away from it, so it reads on a pale card as well as over the
     picture. Clipped to the card's radius, never taking a click. */
  :root body #map-wrap.mv1 #hotspots .dot .tag .swv-tag-card::after,.abt-close-post::after{
    content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:0;
    background:
      radial-gradient(circle at var(--tag-mx,50%) var(--tag-my,50%),rgba(255,255,255,.28),rgba(255,255,255,0) 38%),
      radial-gradient(circle at var(--tag-mx,50%) var(--tag-my,50%),rgba(26,36,18,0) 30%,rgba(26,36,18,.14) 115%);
    transition:opacity .3s}
  :root body #map-wrap.mv1 #hotspots .dot .tag:hover .swv-tag-card::after,.abt-close-posts li:hover .abt-close-post::after{opacity:1}
  :root body #map-wrap.mv1 #hotspots .dot:hover .tag .swv-tag-card,.abt-close-posts li:hover .abt-close-post{will-change:transform}
  .abt-close-posts li:hover .abt-close-post{box-shadow:0 18px 40px rgba(0,0,0,.45)}
  /* the arrow: the Explore details card's own icon and its own hover nudge, and
     while the card is picked a slow cue of the same move, which says a click
     on the card goes in. Transform only. A home that may not be opened (sold,
     founders) never gets .go, so its card has no arrow. */
  #map-wrap.mv1 #hotspots .dot.picked.go .swv-tag-go,#map-wrap.mv1 #hotspots .dot.picked.lock .swv-tag-go{display:flex;align-items:center;justify-content:center;width:22px;height:22px;flex:none;margin-left:auto;color:var(--swv-accent-950)}
  #map-wrap.mv1 #hotspots .dot.picked.go .swv-tag-go svg{width:20px;height:20px;transition:transform .3s var(--ease);
    animation:swv-tag-go-cue 2.6s cubic-bezier(.45,0,.25,1) infinite}
  #map-wrap.mv1 #hotspots .dot.picked.go .tag:hover .swv-tag-go svg{animation:none;transform:translate(3px,-3px)}
  /* signed in but not allowed: a padlock in the arrow's slot, same size, no cue and no nudge */
  #map-wrap.mv1 #hotspots .dot.picked.lock .swv-tag-go>svg{display:none}
  #map-wrap.mv1 #hotspots .dot.picked.lock .swv-tag-lock{display:flex}
  #map-wrap.mv1 #hotspots .dot.picked.lock .swv-tag-lock svg{width:20px;height:20px}
}
@keyframes swv-tag-go-cue{
  0%,55%,100%{transform:translate(0,0)}
  75%{transform:translate(2px,-2px)}
}
@media(prefers-reduced-motion:reduce){
  #map-wrap.mv1 #hotspots .dot .tag .swv-tag-card,.abt-close-post{transform:none !important;transition:none !important}
  #map-wrap.mv1 #hotspots .dot .tag .swv-tag-card::after,.abt-close-post::after{display:none !important}
  #map-wrap.mv1 #hotspots .dot.picked.go .swv-tag-go svg{animation:none;transition:none}
}
/* No hotspot cards on a phone. Touch browsers keep :hover after a tap, so a
   card that should have gone with the tap stayed lit until another dot was
   pressed. Hidden outright at phone width rather than chasing each state
   (hover, .picked, .lit) that could still show it. */
@media(max-width:860px){
  #map-wrap.mv1 #hotspots .dot .tag{display: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:var(--swv-panel-w);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 launcher is #c-fab, which stands beside the bar on its left (--fab-shift).
     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 var(--wg-morph-dur) var(--wg-morph-ease), border-radius var(--wg-morph-dur) var(--wg-morph-ease), box-shadow var(--wg-morph-dur) var(--wg-morph-ease)}
#swv-panel[data-state="expanded"]{height:calc(100vh - 76px);border-radius:var(--swv-radius-panel)}
@media(prefers-reduced-motion:reduce){#swv-panel{transition:none}}

#swv-panel .swv-panel-head{
  display:flex;align-items:center;gap:12px;flex:none;
  padding:15px 16px 0}
/* Collapsed: the head row IS the whole bar, edge to edge, 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-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:var(--swv-text-h5);line-height:1.2;
  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}

/* Two columns under a hairline: the count over the dot pager on the left, the
   tabs pill on the right. */
#swv-panel .swv-panel-sub{
  display:flex;align-items:center;justify-content:space-between;gap:12px;flex:none;
  padding:12px 16px;min-height:64px;box-sizing:border-box;border-bottom:1px solid var(--swv-divider);
  font:400 var(--swv-text-caption)/18px var(--swv-sans)}
#swv-panel .swv-panel-meter{display:flex;flex-direction:column;align-items:flex-start;gap:6px;min-width:0}
/* the count text must never clip ("N of M available" read right, or not at
   all), so the dot pager is the one thing left to give up its room: past its
   own overflow, 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;max-width:100%;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;position:relative;isolation:isolate}
#swv-panel .swv-tab{
  position:relative;min-width:80px;height:26px;padding:0 14px;border:0;border-radius:999px;background:none;
  font:400 var(--swv-text-caption)/1 var(--swv-sans);color:var(--swv-accent-950);cursor:pointer;
  transition:background-color .15s var(--ease)}
/* The pill is the selected tab's own ground, drawn under both labels so it can
   travel from one tab to the other (engine.js, swvTabGlide) without covering
   either word on the way. */
#swv-panel .swv-tab[aria-selected="true"]{color:#FFFFFF}
#swv-panel .swv-tab[aria-selected="true"]::after{content:"";position:absolute;inset:0;z-index:-1;
  border-radius:999px;background:var(--swv-accent-950)}
/* a tab change brings the list in from the side the pill went to, row by row */
#dv1-panel[data-swap] > .dv1-col > :not(.rv),
#dv1-panel[data-swap] > .dv1-media,
#swv-panel[data-swap] :is(.swv-panel-meter,.swv-hero,.swv-group-head,.swv-sub-head,.swv-item){
  animation:swv-list-in var(--swap-ms) cubic-bezier(.22,1,.36,1) backwards;animation-delay:calc(var(--swap-wait) + var(--i,0)*var(--swap-step))}
@keyframes swv-list-in{from{opacity:0;transform:translateX(calc(var(--swap,1)*14px))}}
/* the media column holds the sticky render, so it fades without moving */
#dv1-panel[data-swap] > .dv1-media{--swap:0}
#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}
/* A village with a tab per bedroom count (settings homesTabs, data-tabs="beds"
   on the panel): the tab row IS the bar. It sits in the head beside the toggle,
   the same 44px row shut and open, so no title is needed. The panel opens
   upward from the foot, so the row rises with its top edge. Four equal
   columns on a wide screen; the tab's height, its 44px touch area on a phone
   and its 14px label there are the ones above and below, untouched (a phone
   sizes the columns and the label for itself, below). Shut, none of the tabs is selected, so no pill is drawn. The row
   under it holds the meter alone. */
#swv-panel[data-tabs="beds"][data-state] .swv-panel-head{height:44px;padding:0 12px 0 8px;gap:8px;box-sizing:border-box}
#swv-panel[data-tabs="beds"][data-state] .swv-panel-toggle{margin-right:0}
#swv-panel[data-tabs="beds"] .swv-tabs{flex:1;min-width:0;margin-left:0;display:grid;grid-auto-flow:column;grid-auto-columns:1fr}
#swv-panel[data-tabs="beds"] .swv-tab{min-width:0;padding:0 2px;white-space:nowrap}
#swv-panel[data-tabs="beds"] .swv-panel-sub{min-height:0;padding:10px 16px 12px}
/* Seamless: this row has no grey ground and no grey under the pointer, so the
   tabs sit straight on the panel and the selected one keeps its dark pill. A
   village that sets bedColors (settings, engine.js swvBedVars) gives each tab a
   dot in its colour, before the label; the pins on the map carry the same
   colour. The ring is a hairline of the ink so the cream dot reads on a light
   panel, and on the dark pill it turns light so no dot is lost in it. */
#swv-panel[data-tabs="beds"] .swv-tabs{background:none}
#swv-panel[data-tabs="beds"] .swv-tab:not([aria-selected="true"]):hover{background:none}
#swv-panel[data-tabs="beds"] .swv-tab{display:inline-flex;align-items:center;justify-content:center;gap:6px}
#swv-panel .swv-tab-dot{flex:none;width:20px;height:20px;border-radius:50%;background:var(--bed);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--swv-accent-950) 25%,transparent),0 0 0 2px color-mix(in srgb,var(--bed) 16%,transparent)}
#swv-panel .swv-tab[aria-selected="true"] .swv-tab-dot{box-shadow:0 0 0 1px color-mix(in srgb,#fff 60%,transparent),0 0 0 2px color-mix(in srgb,var(--bed) 16%,transparent)}
/* On a phone this panel steers the map, so open it is a sheet over the lower
   part of the screen only and the map stays in view above it: press an area
   and watch the map go there. The list scrolls inside the sheet. */
@media(max-width:860px){
  /* Open, the sheet is as tall as what it holds, up to 56% of the screen, the
     list scrolling inside when it is longer: a tab with two rows is not a tall
     sheet with a blank under them. The height eases where the browser can
     animate to an automatic height, and opens at once where it cannot. */
  #swv-panel[data-tabs="beds"]{interpolate-size:allow-keywords}
  #swv-panel[data-tabs="beds"][data-state="expanded"]{height:auto;max-height:56vh;max-height:56dvh}
  /* a picked home's card is a picture, a name and a button, about 460px down to
     the button's foot: it may take more of the screen than a list does so the
     button is not cut, and no more than that, so the picked pin stays in view
     above it. 480px is the floor for a short phone, and the screen less the top
     bar is the ceiling for a phone held sideways. The homes of the tab stay under
     the card in the same scroll, so a visitor reaches the others of its kind (R201). */
  #swv-panel[data-tabs="beds"][data-state="expanded"]:has(.swv-hero){max-height:min(max(64vh,480px),calc(100vh - 60px));max-height:min(max(64dvh,480px),calc(100dvh - 60px))}
  #swv-panel[data-tabs="beds"][data-state="expanded"] .swv-hero{border-bottom:0}
  /* The tabs share the bar between its left padding and the toggle, and the
     bar stands beside the chat button (R6), so at 360px a fourth of it is 55px:
     "Facilities" with its dot was 70px at the caption size and ran into 1 BR. Here
     the words take the hint size (12px, the floor) and an 11px dot (R245: 12px left 3.1px between words at 360px), and each column
     is as wide as its word with the spare width shared out, so a longer word has
     more room than 1 BR and every word clears its neighbour (R86). Since R147 the
     row is five tabs (AMN, 1 BR, 2 BR, 3 BR, APT). The pill is its column, so a
     word's room is its share of the spare width. */
  #swv-panel[data-tabs="beds"] .swv-tabs{grid-auto-columns:auto}
  #swv-panel[data-tabs="beds"] .swv-tab{padding-inline:0;gap:4px;font-size:var(--swv-text-hint)}
  #swv-panel[data-tabs="beds"] .swv-tab-dot{width:11px;height:11px}
}
/* Under 340px five tabs leave under 4px between two words, so the dot and its gap
   give up a little room, the words keep the 12px floor (R147). */
@media(max-width:340px){
  #swv-panel[data-tabs="beds"] .swv-tab{gap:2px}
  #swv-panel[data-tabs="beds"] .swv-tab-dot{width:5px;height:5px}
}
/* On a wide screen the panel of such a village docks where every village's
   does: the foot right, with the chat launcher beside it on its left (--fab-shift). What belongs to the tab row is its height. Open, it grows up
   from the foot to the height of what it holds, the list scrolling inside when
   it is longer, and never past the top bar: 100vh less the 16px foot gutter and
   the 60px the top bar takes, the same 76px the open panel of any village is
   short of the screen by. The height eases where the
   browser can animate to an automatic height (interpolate-size) and opens at
   once where it cannot. Only on the map: a villa or facility page parks the
   panel inside the story, where it keeps the full height it has everywhere. */
@media(min-width:861px){
  #map-wrap > #swv-panel[data-tabs="beds"]{interpolate-size:allow-keywords}
  #map-wrap > #swv-panel[data-tabs="beds"][data-state="expanded"]{height:auto;max-height:calc(100vh - 76px)}
}

#swv-panel .swv-panel-scroll{
  flex:1 1 auto;min-height:0;overflow:hidden;opacity:0;transition:opacity var(--wg-morph-dur) var(--wg-morph-ease)}
#swv-panel[data-state="expanded"] .swv-panel-scroll{overflow-y:auto;opacity:1}
/* A list that runs out of the sheet fades at its foot instead of ending in a
   row cut in half: 16px, which is the foot padding every list here ends in, so
   a list at its end, or one short enough not to scroll, loses nothing but that
   padding. And a scrolled list fades out under the meter the same way: 14px
   from nothing to full at its top edge, only once it has been scrolled
   (engine.js puts is-scrolled on it). */
#swv-panel[data-tabs="beds"] .swv-panel-scroll{
  -webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 16px),transparent)}
#swv-panel[data-tabs="beds"] .swv-panel-scroll.is-scrolled{
  -webkit-mask-image:linear-gradient(to bottom,transparent,#000 14px,#000 calc(100% - 16px),transparent);mask-image:linear-gradient(to bottom,transparent,#000 14px,#000 calc(100% - 16px),transparent)}
#swv-panel .swv-panel-scroll-in{padding:0 12px 60px}
@media(prefers-reduced-motion:reduce){#swv-panel .swv-panel-scroll{transition:none}}

/* The way back from a picked home or facility, in a village whose tabs are the
   bar: a plain chevron turned to point left, and where it goes. A row of its own
   above the card, at least 44px to touch. */
#swv-panel .swv-hero-backrow{padding:4px 0 0}
#swv-panel .swv-hero-back{display:flex;align-items:center;gap:4px;min-height:44px;margin:0 -4px;padding:0 8px 0 0;
  border:0;border-radius:6px;background:none;color:var(--swv-accent-950);cursor:pointer;
  font:500 var(--swv-text-body)/1.2 var(--swv-sans);transition:background-color .15s var(--ease)}
#swv-panel .swv-hero-back:hover{background:color-mix(in srgb,var(--swv-accent-950) 6%, transparent)}
#swv-panel .swv-hero-back:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:-2px}
#swv-panel .swv-hero-back .swv-chev{display:flex}
#swv-panel .swv-hero-back .swv-chev svg{transform:rotate(-90deg)}

/* ---------- the hero: Figma's Product card ----------
   The home or facility the panel is on (swvHeroHTML): its picture with the
   thumbnail strip laid over the bottom right corner, then the name and its
   facts on the left and View more on the right. */
#swv-panel .swv-hero{padding:12px 0 16px;border-bottom:1px solid var(--swv-divider)}
#swv-panel .swv-hero-media{position:relative;border-radius:4px;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}
/* A thumbnail press (engine.js swvHeroSwipe), in the manner of the gallery
   sliders the award sites show: a clip reveal with the two pictures moving
   against each other. The old picture is wiped away toward the side it
   leaves by, drifting that way and dimming a little as it goes, so it reads
   as the sheet underneath; the new one settles under it from a little
   further along and a little larger. One slow in, slow out curve for both,
   a touch over a second, so it is a single unhurried draw. */
#swv-panel .swv-hero-swipe{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none;
  animation:swvHeroOut 1.1s cubic-bezier(.76,0,.24,1) both}
#swv-panel .swv-hero-img.is-in{animation:swvHeroIn 1.1s cubic-bezier(.76,0,.24,1) both}
@keyframes swvHeroOut{
  from{clip-path:inset(0);transform:none;filter:none}
  to{clip-path:inset(0 calc(50% + 50%*var(--swipe,1)) 0 calc(50% - 50%*var(--swipe,1)));transform:translateX(calc(-8%*var(--swipe,1)));filter:brightness(.78)}
}
@keyframes swvHeroIn{
  from{transform:translateX(calc(10%*var(--swipe,1))) scale(1.1)}
  to{transform:none}
}
#swv-panel .swv-hero-media::after{content:"";position:absolute;inset:auto 0 0 0;height:40%;pointer-events:none;
  background:linear-gradient(to top,rgba(0,0,0,.45),transparent)}
/* The whole picture is one button (engine.js swvHeroZoom) that opens it full
   screen. It lies over the image and under the thumbnail strip (z-index 1
   below), so a thumbnail press still only picks. */
#swv-panel .swv-hero-zoom{position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:0;
  background:none;cursor:zoom-in}
/* flags.homes_hero_villa: the picture goes into the villa, so it reads as a link, not a zoom */
#swv-panel .swv-hero-zoom.swv-hero-go{cursor:pointer}
/* The picture clips whatever is outside it, and the global ring draws its dark
   halo outside the box, so here the ring sits inside and the halo is drawn
   inside too. White and dark together hold against any render. */
#swv-panel .swv-hero-zoom:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:-4px;
  box-shadow:inset 0 0 0 2px var(--tx-focus-halo)!important}
/* A hint for a finger that the picture opens, in the top right corner. The
   target is the whole picture; the glyph does not take a press of its own. */
#swv-panel .swv-hero-zoom-ic{position:absolute;top:8px;right:8px;width:28px;height:28px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;pointer-events:none;
  color:var(--swv-neutral-50);background:color-mix(in srgb,var(--swv-neutral-950) 60%,transparent);
  transition:background-color .4s cubic-bezier(.22,1,.36,1)}
#swv-panel .swv-hero-zoom:hover .swv-hero-zoom-ic{background:color-mix(in srgb,var(--swv-neutral-950) 80%,transparent)}
@media(prefers-reduced-motion:reduce){#swv-panel .swv-hero-zoom-ic{transition:none}}
/* Walk through: a pill over the picture's top left, above the full screen button (z-index 2), for a
   home that has a 3D walk. White on rgba(12,10,8,.62) over any render is well past 4.5 to 1 (the
   worst case, a white picture under the fill, still gives about 6 to 1). */
#swv-panel .swv-hero-walk{position:absolute;top:10px;left:10px;z-index:2;display:inline-flex;align-items:center;gap:8px;
  min-height:40px;padding:0 14px 0 10px;border:1px solid rgba(255,255,255,.18);border-radius:999px;
  background:rgba(12,10,8,.62);color:#fff;font:500 14px/1 var(--swv-sans);cursor:pointer;
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  transition:border-color .3s cubic-bezier(.22,1,.36,1)}
#swv-panel .swv-hero-walk::before{content:"";position:absolute;inset:-2px}
#swv-panel .swv-hero-walk-ic{display:flex}
#swv-panel .swv-hero-walk:hover,#swv-panel .swv-hero-walk:focus-visible{border-color:var(--tv-gold,#A58A5A)}
#swv-panel .swv-hero-walk:focus-visible{outline:2px solid var(--tv-gold,#A58A5A);outline-offset:2px}
#swv-panel .swv-hero-walk[aria-busy="true"]{cursor:progress}
@media(prefers-reduced-motion:reduce){#swv-panel .swv-hero-walk{transition:none}}
/* Every picture of the home is a thumbnail, so the strip scrolls sideways
   when there are more than fit (renderSwvPanel keeps the current one in
   view). The padding is room for the focus ring, which the scroll box would
   otherwise cut. The 6px above and below is room for a thumbnail's own hit
   area to reach past its edge; bottom is 3px lower to keep the look. */
#swv-panel .swv-thumbs{position:absolute;z-index:1;right:5px;bottom:2px;max-width:calc(100% - 10px);padding:6px 3px;
  display:flex;gap:4px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
#swv-panel .swv-thumbs::-webkit-scrollbar{display:none}
#swv-panel .swv-thumb{
  flex:none;width:51px;height:32px;padding:0;border-radius:2px;overflow:hidden;cursor:pointer;
  border:1px solid transparent;opacity:.8;background:none;transition:opacity .4s cubic-bezier(.22,1,.36,1),border-color .4s cubic-bezier(.22,1,.36,1)}
#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)}
/* a locked home's pictures: greyscale, a padlock on each, and not pressable (R205) */
#swv-panel .swv-thumbs.is-locked .swv-thumb{position:relative;cursor:default}
#swv-panel .swv-thumbs.is-locked .swv-thumb img{filter:grayscale(1)}
#swv-panel .swv-thumb-lock{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--swv-neutral-50);filter:drop-shadow(0 0 2px rgba(0,0,0,.6))}
#swv-panel .swv-thumb-lock svg{width:12px;height:12px}
#swv-panel .swv-thumb:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:1px}
#swv-panel .swv-hero-body{display:flex;align-items:center;gap:12px;padding:14px 2px 0}
#swv-panel .swv-hero-txt{flex:1;min-width:0}
/* the picked home's status, the same pill its row carries, above its name */
#swv-panel .swv-hero-txt .swv-status{margin-bottom:8px}
#swv-panel .swv-hero-title{font-family:var(--swv-serif);font-weight:500;font-size:var(--swv-text-h5);line-height:1.2;
  color:var(--swv-accent-950)}
#swv-panel .swv-hero-pin{display:none}
/* R192: on a phone, where the pin cards are hidden, the hero title starts with the home's pin, drawn as on the map */
@media(max-width:860px){
  #swv-panel .swv-hero-pin{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;margin-right:8px;vertical-align:middle;background:var(--bed,#F2F2F2);color:var(--bed-ink,var(--tv-cream,#F0E9D9));font:600 var(--swv-text-caption)/1 var(--copy-face);font-variant-numeric:lining-nums tabular-nums}
  #swv-panel .swv-hero-pin:not(.bed){color:var(--swv-accent-950)}
  #swv-panel .swv-hero-pin.reserved{background:color-mix(in srgb,var(--swv-accent-950) 38%, transparent);color:var(--swv-neutral-50)}
  #swv-panel .swv-hero-pin svg{width:16px;height:16px}
  #swv-panel .swv-hero-pin .pt-n{font:inherit;color:inherit}
}
#swv-panel .swv-hero-meta{margin-top:6px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  font:400 var(--swv-text-caption)/1.3 var(--swv-sans);color:var(--swv-neutral-600)}
#swv-panel .swv-hero-area{margin-top:2px}
#swv-panel .swv-hero-fact{display:inline-flex;align-items:center;gap:5px}
#swv-panel .swv-hero-meta .swv-ic{display:inline-flex}
#swv-panel .swv-hero-desc{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
#swv-panel .swv-hero-cta{flex:none;width:auto}
/* R198 (Dave, 28 Oct: "the active tab color and CTA button should be dark green"): on Temple Village's
   Explore page (the Views look) the selected tab's pill, which glides between tabs, and the panel's
   buttons are the village's own dark green; white on it is 11.4 to 1, 9.3 under the pointer. A disabled
   button keeps the shared grey. */
body.explore-views{--tv-green:var(--tv-explore-green,#263F35);--tv-green-hover:var(--tv-explore-green-hover,#2F4D41)}
body.explore-views #swv-panel .swv-tab[aria-selected="true"]::after{background:var(--tv-green)}
body.explore-views #swv-panel .swv-btn:not(.swv-btn--light):not(:disabled):not([aria-disabled="true"]){background:var(--tv-button-fill,var(--tv-green));font-weight:var(--tv-button-weight,400);letter-spacing:var(--tv-button-tracking,-.42px);text-transform:var(--tv-button-case,none)}
body.explore-views #swv-panel .swv-btn:not(.swv-btn--light):not(:disabled):not([aria-disabled="true"]):hover{background:var(--tv-button-fill,var(--tv-green-hover));filter:var(--tv-button-hover-filter,none)}
/* and View more carries a soft light that crosses it once every 5 seconds: a band painted inside the
   button's own shape, so it never spills or takes a press, moved by background-position alone. The
   ::before is a phone's tap target, so the light is the ::after. */
body.explore-views #swv-panel .swv-hero-more::after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(105deg,transparent 38%,rgba(255,255,255,.34) 50%,transparent 62%) no-repeat;background-size:250% 100%;background-position:160% 0;
  animation:swv-shine 5s cubic-bezier(.22,1,.36,1) 1.2s infinite}
@keyframes swv-shine{
  0%{background-position:160% 0}
  22%{background-position:-60% 0}
  100%{background-position:-60% 0}
}
@media(prefers-reduced-motion:reduce){
  body.explore-views #swv-panel .swv-hero-more::after{animation:none;background:none}
}
/* Unavailable: the shared disabled grey is 1.5:1 behind white type, so this one reads on a pale ground */
#swv-panel .swv-hero-cta:disabled,#swv-panel .swv-hero-cta:hover:disabled{background:var(--swv-neutral-200);color:var(--swv-neutral-600)}

/* ---------- the accordion groups ---------- */
#swv-panel .swv-groups--flat{display:flex;flex-direction:column;gap:12px;padding:12px 0 16px}
#swv-panel .swv-group{border-top:1px solid var(--swv-divider)}
#swv-panel .swv-group:first-child{border-top:0}
#swv-panel .swv-group-head{
  width:calc(100% + 8px);height:44px;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 var(--swv-text-body)/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-head > span{font:500 var(--swv-text-h4)/1 var(--swv-serif)}
#swv-panel .swv-group-list{display:flex;flex-direction:column;gap:16px;padding-bottom:16px}
#swv-panel .swv-sub{display:flex;flex-direction:column;gap:12px}
#swv-panel .swv-sub-head{margin:0;font:500 var(--swv-text-body)/1.2 var(--swv-sans);color:var(--swv-accent-950)}
/* A village with a tab per bedroom count (settings homesTabs): the area lines
   under a tab are a stack of these buttons. The heading is a button the width
   of the list, its count and its chevron at the far end. */
#swv-panel .swv-sub-toggle{display:flex;align-items:center;gap:10px;width:calc(100% + 8px);min-height:44px;margin:0 -4px;padding:0 4px;box-sizing:border-box;
  border:0;border-radius:6px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background-color .15s var(--ease)}
/* An area's own colour, where its village gives it one (cluster.color): a small
   round swatch before its name, the colour its land and its pins carry on the
   map. A hairline of the panel's ink keeps a pale swatch from dissolving into a
   pale panel. It shares a line with the name's own words (swv-sub-label: the
   name is a column, its words and then the few words under them), so those
   few words keep the row's whole width. */
#swv-panel .swv-sub-swatch{display:inline-block;width:12px;height:12px;margin-right:8px;border-radius:50%;background:var(--area);
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--swv-accent-950) 22%, transparent)}
#swv-panel .swv-sub-toggle:hover{background:color-mix(in srgb,var(--swv-accent-950) 6%, transparent)}
#swv-panel .swv-sub-toggle:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:-2px}
#swv-panel .swv-sub-count{margin-left:auto;flex:none;white-space:nowrap;font:400 var(--swv-text-caption)/1.2 var(--swv-sans);color:var(--swv-neutral-700)}
#swv-panel .swv-sub-toggle .swv-chev{display:flex}
/* The open line is marked beyond its chevron: a quiet tint on the row, held
   while it is hovered, and its name a step heavier (the blurb sets its own
   weight, so it stays as it was). The small text on the tint, from the tokens:
   neutral-700 on the 8% accent-950 tint over the panel's neutral-50 is 8.22 to 1
   with the engine's own ramp, and 7.46 to 1 with Temple Village's (#444444 on
   #E2E1E0); accent-950 on it is 13.17 and 12.41. All clear 4.5. */
#swv-panel .swv-sub-toggle[aria-expanded="true"]{background:color-mix(in srgb,var(--swv-accent-950) 8%, transparent)}
#swv-panel .swv-sub-toggle[aria-expanded="true"] .swv-sub-name{font-weight:600}
/* A plain chevron, which points down while the line is closed and up while it
   is open: the glyph drawn for the panel's own toggle read as inverted here. */
#swv-panel .swv-sub-toggle .swv-chev svg{transition:transform .2s var(--ease)}
#swv-panel .swv-sub-toggle[aria-expanded="false"] .swv-chev svg{transform:rotate(180deg)}
@media(prefers-reduced-motion:reduce){#swv-panel .swv-sub-toggle .swv-chev svg{transition:none}}
/* An area's line carries a few words on what the area is, under its name. */
#swv-panel .swv-sub-name{display:flex;flex-direction:column;gap:3px;min-width:0;padding:10px 0}
#swv-panel .swv-sub-blurb{font:400 var(--swv-text-caption)/1.35 var(--swv-sans);color:var(--swv-neutral-700)}
#swv-panel .swv-groups--areas{display:flex;flex-direction:column;gap:4px;padding:8px 0 16px}
#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:4px;
  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[aria-current="true"]{background:color-mix(in srgb,var(--swv-accent-950) 8%, transparent)}
#swv-panel .swv-item:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:-2px}
#swv-panel .swv-item-thumb{flex:none;width:160px;height:90px;
  border-radius:4px;overflow:hidden;background:var(--swv-neutral-200)}
#swv-panel .swv-item-thumb img{width:100%;height:100%;object-fit:cover;display:block}
/* the text column is centred against the thumb: pill, name, area */
#swv-panel .swv-item-body{min-width:0;flex:1;display:flex;flex-direction:column;justify-content:center;gap:4px}
/* The name wraps to a second line rather than clipping: the column beside a
   160px thumb is about 164px, and a CMS name ("Two-Bedroom · Plot 1") runs
   longer than the design's placeholder at 22px serif. Two lines still sit
   inside the 90px row; past that it clips. */
#swv-panel .swv-item-title{font-family:var(--swv-serif);font-weight:500;font-size:var(--swv-text-h5);
  line-height:1.2;color:var(--swv-accent-950);overflow:hidden;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
/* the muted role, not neutral-500: 500 is 4.40 to 1 on the panel and 3.85 on the hover wash */
#swv-panel .swv-item-meta{display:block;font:400 var(--swv-text-caption)/1.3 var(--swv-sans);
  color:var(--tx-light-muted)}
/* a sold villa is listed and shows in the hero, and goes in for a signed in
   guest only: the picture goes grey and faded and the name drops to the muted
   grey. The text is never faded: opacity .64 took it to 2.36 to 1. The picture
   rests at .64 through --swv-blur-rest, not an opacity of its own: a rule this
   specific would beat the picture's opacity 0 and show it half loaded. */
#swv-panel .swv-item[data-status="sold"] img{filter:grayscale(1);--swv-blur-rest:.64}
#swv-panel .swv-item[data-status="sold"] .swv-item-title{color:var(--tx-light-muted)}
/* reserved still opens, so it keeps its pointer and full text; only the
   picture goes grey, the same as every home off the market */
#swv-panel .swv-item[data-status="reserved"] img{filter:grayscale(1)}
/* and so is the blur it comes in over */
#swv-panel .swv-item[data-status="sold"] .swv-item-thumb,
#swv-panel .swv-item[data-status="reserved"] .swv-item-thumb{--swv-blur-tone:grayscale(1)}

#map3d .mp3d-card .swv-status,#swv-panel .swv-status,#hotspots .swv-status{align-self:flex-start;display:inline-flex;align-items:center;gap:5px;height:20px;
  padding:0 8px;box-sizing:border-box;border-radius:999px;
  font:500 var(--swv-text-hint)/1 var(--swv-sans);white-space:nowrap}
/* The success green, not the accent: a village remaps the accent scale (Temple Village's is brown) and a
   status is not the brand (R204). 950 was so dark the chip looked black (Dave, 10 Oct).
   White on it is 7 to 1. Regular weight: light text on dark reads heavier than the
   500 the grey chips carry (Dave, 10 Oct: "too thick"). */
#swv-panel .swv-status--available,#hotspots .swv-status--available{background:var(--swv-success);color:var(--swv-neutral-50);font-weight:400}
/* the pill's left icon: a filled dot in the text colour, or the padlock svg */
.swv-status .swv-status-ico{display:inline-flex;flex:none}
.swv-status .swv-status-ico:empty{width:7px;height:7px;border-radius:50%;background:currentColor}
#hotspots .swv-status--available .swv-status-ico{background:var(--swv-success)}
#swv-panel .swv-status--reserved,#swv-panel .swv-status--sold,#swv-panel .swv-status--show,
#hotspots .swv-status--reserved,#hotspots .swv-status--sold,#hotspots .swv-status--show,
#swv-panel .swv-status--facility,#hotspots .swv-status--facility{
  background:var(--swv-neutral-100);
  /* 600, not the design's 500: 500 on 100 is 3.99 to 1, under the 4.5 small
     text floor, and this label is the one a buyer must be able to read */
  color:var(--swv-neutral-600)}

/* facility rows carry no Status pill and a three line description instead
   of an area line */
#swv-panel .swv-item--facility .swv-item-desc{
  display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
  font:400 var(--swv-text-body)/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:var(--swv-space-16);right:var(--swv-space-16);width:auto;bottom:var(--swv-space-16);
    transition:height var(--wg-morph-dur) var(--wg-morph-ease), width var(--wg-morph-dur) var(--wg-morph-ease), bottom var(--wg-morph-dur) var(--wg-morph-ease), left var(--wg-morph-dur) var(--wg-morph-ease), right var(--wg-morph-dur) var(--wg-morph-ease), border-radius var(--wg-morph-dur) var(--wg-morph-ease), translate var(--wg-morph-dur) var(--wg-morph-ease)}
  /* open, it is a bottom sheet: flush with the foot of the screen and 60px
     down from the top, as before, with the design's 8px at either side. In
     dvh: a phone's 100vh is the screen with its address bar put away, so
     with the bar showing the sheet rose about 56px, into the header. */
  #swv-panel[data-state="expanded"]{left:var(--swv-space-8);right:var(--swv-space-8);bottom:0;height:calc(100vh - 60px);height:calc(100dvh - 60px)}
  /* the grab bar: the open sheet can be pulled down to put it away (wireSwvSwipe).
     Above the title, clear of it; a village whose tabs are the head row has no room
     there, and its sheet is short. */
  #swv-panel[data-state="expanded"]::before{content:"";position:absolute;top:6px;left:50%;width:32px;height:4px;margin-left:-16px;
    border-radius:2px;background:var(--swv-accent-950);opacity:.2;pointer-events:none}
  #swv-panel[data-tabs="beds"][data-state="expanded"]::before{content:none}
  /* 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. The head keeps its 12px above, as it did
     when it held the launcher. */
  #swv-panel .swv-panel-head{padding:12px 16px 0}
  #swv-panel .swv-panel-sub{gap:var(--swv-space-4)}
  /* the toggle stays 24px to the eye, its tap target grows around it */
  #swv-panel .swv-panel-toggle{position:relative}
  #swv-panel .swv-panel-toggle::before{content:"";position:absolute;inset:calc((var(--swv-control-h) - 24px)/-2)}
  /* Villas and Facilities are an equal width pair, each a pill-height pill whose hit
     area reaches the control height */
  #swv-panel .swv-tabs{display:grid;grid-auto-flow:column;grid-auto-columns:1fr}
  #swv-panel .swv-tab{min-width:0;height:var(--swv-pill-h);padding:0 var(--swv-pill-pad-x);font-size:var(--swv-text-caption)}
  #swv-panel .swv-tab::before{content:"";position:absolute;inset:calc((var(--swv-pill-h) - var(--swv-control-h))/2) 0}
  /* the thumbnails are 32px tall: same trick, and the clip moves off the button
     (its picture takes the corner radius) so the extension is not cut away */
  #swv-panel .swv-thumb{position:relative;overflow:visible}
  #swv-panel .swv-thumb img{border-radius:inherit}
  #swv-panel .swv-thumb::before{content:"";position:absolute;inset:calc((32px - var(--swv-control-h))/2 - 1px) 0}
  /* the two group titles are one role, the design's Satoshi label */
  #swv-panel .swv-group-head > span{font:inherit}
}
@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 (inside #map-wrap) and .nav at 601 (at the #app level), both position:fixed.
     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).

     Not while a story is open. A villa or facility story is #story at 80 and
     the panel is parked inside it, so lifting #map-wrap to 602 then put the
     map's pin layer over the story: the panel's toggle was painted under
     #hotspots and could not be tapped to close it. The story carries its own
     nav and burger (parked in #swv-ov), so nothing there needs the lift. */
  body.rail-open:not(.story-open) #map-wrap{z-index:602}
  /* Inside #map-wrap the nav (.mapnav, 600) out-ranks the panel (40), so the
     mark and the burger painted over the drawer wherever it reached the top
     of the screen. A villa or facility page parks the panel in #swv-ov, next
     to the story's own .mapnav at the same 600, where it was 85: the burger
     took the tap meant for the drawer's toggle and the drawer could not be
     shut (Dave, 26 Oct 2026, from a phone on a villa page). So the lift holds
     on every page, and 602 clears both navs, .mapnav and the global .nav at
     601, whichever shares the drawer's stacking context. [data-state] makes
     it outrank body.swv-ov-open #swv-panel, which comes later. The menu is a
     body level sheet at 2000 and still covers the lot. */
  body.rail-open #swv-panel[data-state="expanded"]{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 16px 35px;
  display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-end;
  min-height:44%;  /* the box holds the caption at the foot however short the words; a shade, when a look turns it on, fades out by 44% of the way up the frame */
  background:var(--swv-story-shade, var(--swv-story-shade-default)) bottom/100% 100% no-repeat;
  text-shadow:var(--swv-story-text, var(--swv-story-text-default));
  color:var(--swv-neutral-50)}
/* The kicker, the title and the chips were pinned to three fixed distances
   from the bottom edge: 189px, 75px and 35px. That holds only while a title
   wraps to two lines. The title is the one that grows, and it grows upward,
   so a third line put it straight through the kicker and the two printed on
   top of each other. Every interior beat wraps to three lines at this width.
   They stack in flow now, off the same bottom edge, so the column grows
   upward and nothing can land on anything else however long the copy runs or
   however narrow the window gets. The 9px and 19px are the gaps the old fixed
   offsets produced at two lines, kept so the design itself does not move. */
.swv-ov-kicker{width:640px;max-width:100%;margin:0 0 9px;
  font:500 var(--swv-text-h5)/1.1 var(--swv-sans);color:var(--swv-neutral-50)}
.swv-ov-title{width:640px;max-width:100%;margin:0 0 19px;
  font:400 var(--swv-text-h1)/52.8px var(--swv-serif);letter-spacing:-1.44px;color:var(--swv-neutral-50)}
/* An em in a text-shadow resolves where it is declared and is inherited as a fixed length. The caption
   declares the shadow at body size, which the kicker, the chips and the hint inherit; that is a floor on
   purpose, since their own 12px would give a halo of 4px. The title declares it again so its ems resolve
   at the title's size, as wgrace.co puts the shadow on .statement__text. The .45em of room on
   .swv-ov-wm below is sized against that. */
.scap.swv-ov-section .swv-ov-title{text-shadow:var(--swv-story-text, var(--swv-story-text-default))}
/* A chapter stacks its titles in one grid cell: the cell is as tall as the tallest line, so the kicker above never moves. */
.swv-ov-titles{display:grid;width:640px;max-width:100%;margin:0 0 19px}
.swv-ov-titles>.swv-ov-title{grid-area:1/1;margin:0;width:auto;align-self:end}
/* The title's words, each in a mask it rises out of as the caption arrives (paintStoryChrome sets the
   transform, opacity and filter from the scroll; nothing here transitions). The mask's padding keeps
   descenders, the blur and the word's shadow inside it without changing the line's height. As on wgrace.co
   the room is .45em on every side because the soft shadow reaches .375em and the clip would otherwise cut
   it into a hard edge; the bottom keeps its extra .10em for descenders. SWV_OV_WORD_RISE_EM in engine.js
   must stay at least as big as the bottom room, so a word still starts fully hidden. */
.swv-ov-wm{display:inline-block;overflow:hidden;vertical-align:top;padding:.45em .45em .55em;margin:-.45em -.45em -.55em}
.swv-ov-w{display:inline-block}
/* While the story is open the page behind it never moves: no rubber band at either end. */
html:has(body.swv-ov-open){overscroll-behavior-y:none}
.swv-ov-chips{display:flex;align-items:center;
  font:400 var(--swv-text-body)/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}
/* The scroll cue at the foot of each stop, one component for a villa's stops
   and a facility's. A button: the label, then a thin line a light runs down.
   No arrowhead (Dave, 10 Oct: the line is enough). Every 2.5s, one soft in
   out, transform and opacity only; the label stays still. It is 8px off the
   bottom (label 20, gap 4, line 38). Its colour, text shadow and the shade
   under the line are the caption's own, so
   it reads on the photograph the way the words beside it do. The stop out of
   view is hidden after the caption has faded, which rests its animation and
   takes it out of the tab order and the pointer's way (swv-ov-on). */
.swv-ov-hint{--swv-cue-period:2.5s;--swv-cue-line:38px;--swv-cue-shade:rgba(10,9,6,.7);
  position:absolute;left:50%;bottom:8px;transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;gap:4px;
  min-width:44px;min-height:44px;padding:0 12px;margin:0;border:0;background:none;cursor:pointer;
  font:500 var(--swv-text-body)/20px var(--swv-sans);color:var(--swv-neutral-50);text-shadow:inherit;
  -webkit-tap-highlight-color:transparent;
  visibility:hidden;transition:visibility 0s linear .4s}
.swv-ov-on .swv-ov-hint{visibility:visible;transition-delay:0s}
/* a chapter hides its cue on its last stop; display:flex above would beat the hidden attribute */
.swv-ov-hint[hidden]{display:none}
/* the stops' captions are stacked one over another, so the last one, though
   unseen, took every press meant for the stop in view: only that one takes them */
.swv-ov-section:not(.swv-ov-on){pointer-events:none}
.swv-ov-hint:focus-visible{outline:2px solid currentColor;outline-offset:4px;border-radius:2px}
.swv-ov-hint-cue{position:relative;display:block;width:20px;height:var(--swv-cue-line);flex:none}
.swv-ov-hint-line{position:absolute;left:50%;top:0;width:1px;height:var(--swv-cue-line);margin-left:-.5px;overflow:hidden;
  background:color-mix(in srgb,currentColor 40%,transparent);box-shadow:0 0 4px var(--swv-cue-shade)}
.swv-ov-hint-line::after{content:"";position:absolute;left:0;top:0;width:100%;height:16px;
  background:linear-gradient(to bottom,transparent,currentColor);opacity:0;transform:translateY(-16px);
  animation:swv-cue-light var(--swv-cue-period) cubic-bezier(.65,0,.35,1) infinite}
.scap.swv-ov-section:not(.swv-ov-on) .swv-ov-hint-line::after{animation-play-state:paused}
@keyframes swv-cue-light{
  0%{transform:translateY(-16px);opacity:0}
  12%{opacity:1}
  55%{opacity:1}
  72%,100%{transform:translateY(var(--swv-cue-line));opacity:0}}
@media(prefers-reduced-motion:reduce){
  .swv-ov-hint-line::after{animation:none;opacity:0}}

/* ---------- back pill: #story-exit, reused, not a second button ----------
   Was a flat rgba(247,248,243,x) glass, near-white on near-white whenever
   the villa's first beat is a bright sky or a pale wall (measured: Temple
   Village's own villa photos put this pill a few percent of contrast above
   invisible). #story .exit above — the facility story's own "Back to the
   masterplan" pill — already solves the same problem, reading on every
   photo it sits over, with tokens rather than a fixed colour: var(--well)
   tinted glass under var(--paper) type. This pill is the same element
   (engine.js's openStory reuses #story-exit, the comment above is not
   decorative), so it reads the same two tokens instead of inventing a
   third look, and a village with no dark --well loses nothing: the
   stylesheet's own default (#11150C, the same near-black #map-wrap already
   sits on) reads as the same near-black glass every other village's map
   chrome already carries. */
#story .exit.swv-ov-back,.dv1-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:var(--swv-ov-glass);
  font:400 var(--swv-text-caption)/18px var(--swv-sans);letter-spacing:-.42px;text-transform:none;white-space:nowrap;
  color:var(--paper);cursor:pointer}
@media(hover:hover){#story .exit.swv-ov-back:hover,.dv1-back:hover{background:var(--swv-ov-glass-hover)}}
.swv-ov-back-ic svg{display:block;width:20px;height:20px;transform:rotate(-90deg)}
#story .exit.swv-ov-back:focus-visible,.dv1 .dv1-back:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}
/* Back to map getting ready: the ring takes the chevron's place, so the pill does not move. */
#story .exit.swv-ov-back.is-loading{cursor:progress}
#story .exit.swv-ov-back.is-loading .swv-ov-back-ic{display:none}
#story .exit.swv-ov-back .swv-spin{--spin:14px;margin:3px}
/* Walk through beside Back to map on a villa that has one: the same pill, sized to its words. */
#story .exit.swv-ov-back.swv-ov-walk{left:152px;width:auto;padding:0 14px 0 10px}
.swv-ov-walk-ic svg{display:block}
#story .exit.swv-ov-walk[aria-busy="true"]{cursor:progress}
/* The same pill on every details chapter, leading back to the villa's story. Over
   the hero rather than fixed to the window, so it never rides over the chapter
   once the page has scrolled on; under the mark (60px nav) and above the sticky
   tab bar's own row, which starts at the hero's bottom edge. Everything that
   makes it look like Back to map is the shared rule above.

   .dv1 remaps --well and --paper to its own light ground (see its rule further
   down), so read bare the shared rule would paint a pale glass under pale type.
   The story reads the root's two, and so does this: the aliases are taken at
   :root, before any remap, and put back on the pill itself. */
:root{--root-well:var(--well);--root-paper:var(--paper)}
.dv1-back{position:absolute;z-index:6;--well:var(--root-well);--paper:var(--root-paper)}

/* ---------- the progress slider ---------- */
.swv-ov-slider{width:24px;padding:5px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;
  gap:16px;background:rgba(247,248,243,.16);border-radius:99px;transition:opacity .3s}
.swv-ov-slider i{position:relative;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}
/* the marker stays 8px to the eye; its press target is 24px wide and 16px taller
   than it is, and with the 16px gap two neighbouring targets meet and never overlap */
.swv-ov-slider i::before{content:"";position:absolute;inset:-8px}
.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 right edge column: the dots, and previous and next stop under them ----------
   One fixed group, vertically centred, so the buttons sit with the dots rather
   than in a corner the card and the chat button own. Real buttons, always
   shown, so a reader who has asked for reduced motion (or who steers by
   keyboard) steps through the same stops the scroll passes. A solid
   translucent fill and no blur (DESIGN.md: no glassmorphism), the same as the
   back pill. The flight leaving the screen takes the group with it, and the
   dots alone go on the last room. */
#swv-ov,.dv1-back{--swv-ov-glass:color-mix(in srgb,var(--well) 62%, transparent);--swv-ov-glass-hover:color-mix(in srgb,var(--well) 90%, transparent)}
.swv-ov-rail{position:fixed;right:16px;
  /* Centred in the right column, but never lower than 12px above the card that docks in the corner under it (--swv-ov-cta-h tall, 16px up),
     so on a short screen the group rides up rather than under it: 286px of group is 143 either side of its centre. */
  top:min(50%, calc(100% - var(--swv-ov-cta-h) - 16px - 12px - 143px));transform:translateY(-50%);z-index:70;
  display:flex;flex-direction:column;align-items:center;gap:12px;transition:opacity .3s}
/* The card docks in the bottom corner, below the dots' column, with the chat button at its foot on its left, so the group stays in the plain right edge column (no --swv-cta-clear). */
#swv-ov.swv-ov-past .swv-ov-rail{opacity:0;pointer-events:none}
/* A short desktop (under 720px) cannot hold 286px of group between the masthead and the card.
   The dots stay centred, clear of the masthead at the top. */
@media(min-width:861px) and (max-height:719px){
  .swv-ov-rail{top:max(50%, calc(70px + 89px + 8px))}
}
@media(prefers-reduced-motion:reduce){.swv-ov-rail{transition:none}}

/* ---------- the CTA card, replacing the panel on the last beat ---------- */
/* The wrapper holds the card's place and its show and hide; the button inside
   is the card itself, so the tilt and the flip never fight the fade. It floats
   at the bottom right of the screen, 16px from both edges, for as long as the
   story's last room has been reached: through the furnishing, past the flight
   and over the footer (the footer reserves room under its last row for it,
   below). 300 by 335 is the reference's card, Ask Grace, the render, Explore
   details and the chapters it opens. */
:root{--swv-ov-cta-h:335px;--swv-ov-cta-w:300px}
/* What text under the end card keeps clear at its foot: the card and a 32px margin. Gracie's
   launcher stands at the card's foot on its left (R7), inside that band. */
:root{--swv-ov-clear:calc(var(--swv-ov-cta-h) + 32px)}
/* On a phone the card takes the width the launcher leaves (R9, Dave 25 Oct: "take as much width
   until chat icon button sits on the left corner nicely"): the screen less its own 16px gutter, the
   8px gap, the 44px launcher and the left 16px gutter, no wider than 420px for a small tablet. Its
   height follows its width (the picture fills what the label leaves), and is never more than 45%
   of the screen, for a phone held sideways. */
@media(max-width:860px){:root{--swv-ov-cta-h:min(calc(var(--swv-ov-cta-w)*.6 + 36px),45vh);--swv-ov-cta-w:min(calc(100vw - 84px),420px)}}
.swv-ov-cta{position:fixed;right:16px;bottom:16px;width:var(--swv-ov-cta-w);height:var(--swv-ov-cta-h);
  opacity:calc(var(--swv-ov-turn,0)*400);pointer-events:none;z-index:75;
  /* The reader throws it up: engine.js writes --swv-ov-turn, 0 to 1, from the
     scroll through the last stretch before the last room settles. The card
     comes up from under the screen spinning like a card dealt out of a chest,
     two and a half turns that slow as it arrives, and it holds wherever the
     scroll stops. Both the rise and the spin run on the square of what is
     left, so most of the travel is early and the landing is slow. Opacity is
     only a switch at the very start: anything under 1 flattens the two faces.
     The short ease only smooths a wheel's steps; it is not a timed entrance. */
  transform-origin:50% 50%;transform-style:preserve-3d;
  transform:perspective(1100px)
    translateY(calc((1 - var(--swv-ov-turn,0))*(1 - var(--swv-ov-turn,0))*(100% + 64px)))
    rotateZ(calc((1 - var(--swv-ov-turn,0))*-7deg))
    rotateY(calc((1 - var(--swv-ov-turn,0))*(1 - var(--swv-ov-turn,0))*-900deg))
    scale(calc(1 - (1 - var(--swv-ov-turn,0))*.22));
  transition:opacity .01s linear,transform .22s cubic-bezier(.22,1,.36,1);will-change:transform}
/* In the air the card is a thing with a body, not a picture of one. It is
   --swv-ov-thick deep: the face sits half of that toward the reader, the back
   half of it away, and the two long sides between them are gilt, the way the
   edges of a good card are, so each time it comes round edge on there is a
   line of gold where a flat card would vanish. All of it exists only while
   the card is turning: once landed it is the flat button it always was, which
   is what the two flips that follow expect to find. */
.swv-ov-cta{--swv-ov-thick:7px}
/* the back: the village's dark green and one gold line, its own mark in the
   middle (engine.js hands the url over as --swv-ov-mark) and a soft light
   lying across it. Neither face casts a shadow in the air: a large blurred
   shadow on a plane that turns every frame costs frames, and nothing reads it
   while the card is moving. The settled card has its own again. */
.swv-ov-cta::before{content:"";position:absolute;inset:0;border-radius:6px;
  background:
    linear-gradient(112deg,transparent 30%,rgba(255,244,214,.16) 48%,transparent 62%),
    var(--swv-ov-mark,none) 50% 50%/auto 34% no-repeat,
    var(--swv-accent-950);
  outline:1px solid var(--swv-gold);outline-offset:-9px;
  transform:rotateY(180deg) translateZ(calc(var(--swv-ov-thick)/2));backface-visibility:hidden;pointer-events:none}
#swv-ov:not(.swv-ov-landed) .swv-ov-cta-card{backface-visibility:hidden;transform:translateZ(calc(var(--swv-ov-thick)/2));transition:none;box-shadow:none}
/* the two gilt sides: planes standing on the card's left and right edges, as
   deep as the card is thick and as tall as its straight run between corners */
.swv-ov-cta-edge{position:absolute;inset:0;transform-style:preserve-3d;pointer-events:none}
.swv-ov-cta-edge::before,.swv-ov-cta-edge::after{content:"";position:absolute;top:5px;bottom:5px;width:var(--swv-ov-thick);
  background:linear-gradient(180deg,
    color-mix(in srgb,var(--swv-gold),#fff 55%),var(--swv-gold) 22%,
    color-mix(in srgb,var(--swv-gold),#000 28%) 58%,var(--swv-gold) 82%,
    color-mix(in srgb,var(--swv-gold),#fff 40%));
  transform:rotateY(90deg)}
.swv-ov-cta-edge::before{left:calc(var(--swv-ov-thick)/-2)}
.swv-ov-cta-edge::after{right:calc(var(--swv-ov-thick)/-2)}
/* The light on the face is a run of bands a card's width apart, moved by the
   same square that spins the card, so one crosses the face each time the face
   comes round to the reader. Wider than the plain glare so there is band
   enough for every turn. */
#swv-ov:not(.swv-ov-landed) .swv-ov-cta-glare i{left:-300%;width:700%;
  /* on from the first of the turn, and gone again over the last sixth so the settled face is clean */
  opacity:calc(min(1,var(--swv-ov-turn,0)*400)*min(1,(1 - var(--swv-ov-turn,0))*6));
  background:repeating-linear-gradient(100deg,transparent 0,transparent 4.6%,rgba(255,250,235,.62) 7.14%,transparent 9.7%,transparent 14.2857%);
  transform:translateX(calc((1 - var(--swv-ov-turn,0))*(1 - var(--swv-ov-turn,0))*35.7%));
  transition:transform .22s cubic-bezier(.22,1,.36,1)}
/* Landing: one ring of gold light leaves the card as it comes to rest, then
   nothing. It is the only timed thing here, and it answers the reader's own
   arrival rather than playing at them. */
.swv-ov-cta::after{content:"";position:absolute;inset:0;border-radius:6px;pointer-events:none;opacity:0}
#swv-ov.swv-ov-landed .swv-ov-cta::after{animation:swv-ov-land .9s cubic-bezier(.16,1,.3,1) .12s both}
@keyframes swv-ov-land{
  0%{opacity:.9;box-shadow:0 0 0 0 color-mix(in srgb,var(--swv-gold),transparent 20%),0 0 18px 2px color-mix(in srgb,var(--swv-gold),transparent 45%)}
  100%{opacity:0;box-shadow:0 0 0 14px color-mix(in srgb,var(--swv-gold),transparent 100%),0 0 46px 10px color-mix(in srgb,var(--swv-gold),transparent 100%)}}
#swv-ov.swv-ov-landed .swv-ov-cta::before,#swv-ov.swv-ov-landed .swv-ov-cta-edge{display:none}
/* 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 it was gone 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. */
/* Landed is the end of the turn: flat, and only now live to a click. The
   wrapper carries the turn because neither flip transforms it (they work on
   the card inside and on clones of it). */
#swv-ov.swv-ov-landed .swv-ov-cta{opacity:1;pointer-events:auto;transform:none;transform-style:flat}
/* The light crosses the face while it turns: nothing at either end, brightest
   half way, and it travels with the same number. A hover's own glare is set
   inline by swvOvCtaTilt and so still wins once the card is flat. */
#swv-ov .swv-ov-cta-glare i{opacity:calc(var(--swv-ov-turn,0)*(1 - var(--swv-ov-turn,0))*3.6);
  transform:translateX(calc((var(--swv-ov-turn,0) - .5)*44%))}
/* While the chat is open the card is out of the tab order and unclickable,
   whichever way the chat was opened. The extra id and class beat the
   landed and past rules above, which set opacity 1. Visibility is what takes
   it out of the tab order; it waits for the fade, so a reduced motion visitor
   gets a crossfade into the panel. */
body.chat-open #swv-ov .swv-ov-cta{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .2s linear,visibility 0s .2s}
/* On a wide screen the homes bar goes with the card while the
   chat is open, in every village. Secret Waterfall's chat covers that corner
   whole; a shorter chat left a sliver of the bar showing under its foot. Below
   that width the bar runs the width of the foot and keeps what the chat leaves
   free. The panel's own height ease is kept, since opening the chat shuts it. */
@media(min-width:861px){
  body.chat-open #map-wrap > #swv-panel{opacity:0;visibility:hidden;pointer-events:none;
    transition:opacity .2s linear,visibility 0s .2s,height var(--wg-morph-dur) var(--wg-morph-ease),border-radius var(--wg-morph-dur) var(--wg-morph-ease)}
}
body.swv-flipping #swv-ov .swv-ov-cta{transition:none}
/* The card turning into the details page: the real card is hidden the frame its
   clone takes over. */
body.swv-dflipping #swv-ov .swv-ov-cta{transition:none}
body.swv-dflipping #swv-ov .swv-ov-cta-card{visibility:hidden}
.swv-ov-cta-card{position:relative;display:flex;flex-direction:column;gap:10px;width:100%;height:100%;
  box-sizing:border-box;margin:0;padding:16px;border:0;border-radius:6px;overflow:hidden;
  background:var(--swv-neutral-50);color:var(--swv-accent-950);text-align:left;cursor:pointer;
  box-shadow:0 1px 2px rgba(10,9,6,.18),0 18px 44px rgba(10,9,6,.30);
  transition:transform .45s cubic-bezier(.22,1,.36,1)}
/* a ring that reads on the dark footage and on a light ground alike */
.swv-ov-cta-card:focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:3px;
  box-shadow:0 0 0 3px var(--swv-neutral-50)}
.swv-ov-cta-eyebrow{flex:none;font:400 var(--swv-text-caption)/1.2 var(--swv-sans)}
.swv-ov-cta-img{position:relative;flex:1;min-height:0;border-radius:4px;overflow:hidden;background:var(--swv-neutral-200)}
.swv-ov-cta-img img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.swv-ov-cta-title{flex:none;display:flex;align-items:center;gap:8px;
  font:400 34px/36px var(--swv-serif);letter-spacing:-1px}
.swv-ov-cta-title em{font-style:italic;color:inherit}
.swv-ov-cta-arrow{display:flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;margin-left:auto}
.swv-ov-cta-arrow svg{width:26px;height:26px;transition:transform .3s var(--ease)}
@media(hover:hover){.swv-ov-cta-card:hover .swv-ov-cta-arrow svg{transform:translate(3px,-3px)}}
.swv-ov-cta-caption{flex:none;font:400 var(--swv-text-caption)/1.2 var(--swv-sans);color:var(--swv-neutral-600)}
/* the highlight band: wider than the face and clipped by it, so sliding it is
   a transform and never a repaint */
.swv-ov-cta-glare{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none}
.swv-ov-cta-glare i{position:absolute;top:0;bottom:0;left:-100%;width:300%;opacity:0;
  background:linear-gradient(100deg,transparent 40%,var(--glare,rgba(255,255,255,.6)) 50%,transparent 60%);
  transition:transform .45s cubic-bezier(.22,1,.36,1),opacity .3s}
/* reduced motion: no rotation, the card simply fades in */
@media(prefers-reduced-motion:reduce){.swv-ov-cta-arrow svg{transition:none}
  .swv-ov-cta{opacity:0;transform:none;transform-style:flat;transition:opacity .3s}
  .swv-ov-cta::before,.swv-ov-cta::after,.swv-ov-cta-edge{display:none}
  #swv-ov:not(.swv-ov-landed) .swv-ov-cta-card{transform:none}
  #swv-ov .swv-ov-cta-glare i,#swv-ov:not(.swv-ov-landed) .swv-ov-cta-glare i{opacity:0;transform:none}}

/* ---------- the flipper: the card turning into the Grace panel (engine.js,
   swvFlipPlay). Everything here is a position or a face; every motion is a
   Web Animations transform and opacity. will-change lives only as long as
   the flipper does. ---------- */
body.swv-flipping #c-panel:not(.swv-flip-clone){visibility:hidden}
.swv-flip{position:fixed;left:0;top:0;width:0;height:0;z-index:815;pointer-events:none;perspective:1600px}
.swv-flip-card{position:absolute;left:0;top:0;width:0;height:0;transform-style:preserve-3d;will-change:transform}
.swv-flip-card>*{position:absolute;left:0;top:0;will-change:transform}
.swv-flip-front,.swv-flip-back{backface-visibility:hidden;border-radius:6px}
.swv-flip-front>.swv-ov-cta-card{transition:none;pointer-events:none}
/* the thickness: thin slabs stacked between the faces, slightly darker than
   the cream, which is what shows when the card is edge on */
.swv-flip-slab{border-radius:6px;background:color-mix(in srgb,var(--swv-neutral-50) 78%,var(--swv-accent-950))}
.swv-flip-shadow{position:absolute;left:0;top:0;opacity:0;will-change:transform,opacity;
  background:radial-gradient(closest-side,rgba(0,0,0,.55),rgba(0,0,0,.25) 55%,transparent)}
.swv-flip-back #c-panel{position:absolute;inset:0;width:100%;height:100%;transform:none;opacity:1;transition:none;pointer-events:none}
/* no glare colour override on the back face: both faces are cream now, so the default white band reads the same on either */

/* ---------- the details flip: the card, or the chat, turning over into the
   details page (engine.js, swvDetailsFlip). A veil that dims the story, one
   perspective box that travels, and a turning object whose front is the clone
   of what was clicked and whose back is the hero. Every motion is a Web
   Animations transform or opacity; will-change lives as long as the flipper. */
.swv-dflip-root{position:fixed;inset:0;z-index:815;pointer-events:none;overflow:hidden}
.swv-dflip-veil{position:absolute;inset:0;background:rgba(10,9,6,.94);opacity:0;will-change:opacity}
.swv-dflip{position:absolute;left:0;top:0;width:0;height:0;perspective:2000px;will-change:transform}
.swv-dflip-turn{position:absolute;left:0;top:0;width:0;height:0;transform-style:preserve-3d;will-change:transform}
.swv-dflip-turn>*{position:absolute;left:0;top:0;will-change:transform}
.swv-dflip-front{backface-visibility:hidden;border-radius:6px}
.swv-dflip-front>.swv-ov-cta-card{transition:none;pointer-events:none}
.swv-dflip-front #c-panel{position:absolute;inset:0;width:100%;height:100%;transform:none;opacity:1;transition:none;pointer-events:none}
.swv-dflip-slab{border-radius:6px;background:color-mix(in srgb,var(--swv-neutral-50) 78%,var(--swv-accent-950))}
.swv-dflip-shadow{position:absolute;left:0;top:0;opacity:0;will-change:transform,opacity;
  background:radial-gradient(closest-side,rgba(0,0,0,.6),rgba(0,0,0,.28) 55%,transparent)}
/* during the zoom, the real details page animates with fixed positioning,
   clipped to the screen, starting from the midpoint of the card move. Two IDs
   in the selector beat the curtain rule (html.swv-curtain #views>.view.active). */
body.swv-dzoom #views>#v-details{position:fixed;left:0;top:0;width:100%;height:100vh;overflow:hidden;z-index:816;transform-origin:50% 50%;will-change:transform,clip-path;backface-visibility:hidden}
/* #app is its own stacking context (z-index 1), so nothing inside it can rise
   over the veil on its own. For the page half the whole of #app comes up over
   the veil, with everything in it but the turning page hidden, so the veil is
   all that shows around the page. */
body.swv-dzoom #app{z-index:816}
body.swv-dzoom #app>:not(#views),body.swv-dzoom #views>:not(#v-details){visibility:hidden}
/* a confirmation in the chat eases in, and the line that says where she is
   taking them follows it */
@keyframes swv-msg-in{from{opacity:0;transform:translateY(8px)}}
.msg.swv-arrive{animation:swv-msg-in .42s var(--ease) both}
.msg.swv-arrive .gline{animation:swv-msg-in .36s var(--ease) .55s both}

/* ---------- the homes panel, docked over the overview instead of the map ---------- */
body.swv-ov-open #swv-panel{z-index:85}
/* The Homes bar is on screen on every property page. The one accepted
   exception is the end of a villa's story: there the bar sinks below the
   screen by the same number that turns the card in, so the two trade places
   under the reader's own scroll, the card landing where the bar sat. It is
   moved, not cut: 48px past its own height clears its shadow. Once landed it
   is out of the tab order and takes no taps, and it stays away through the
   footer so it never sits on the card. A facility without a card never sets
   the number, so its bar stays docked to the end of the page. */
#swv-ov #swv-panel{transform:translateY(calc(var(--swv-ov-turn,0)*(100% + 48px)));
  transition:height var(--wg-morph-dur) var(--wg-morph-ease), width var(--wg-morph-dur) var(--wg-morph-ease), bottom var(--wg-morph-dur) var(--wg-morph-ease), left var(--wg-morph-dur) var(--wg-morph-ease), right var(--wg-morph-dur) var(--wg-morph-ease), border-radius var(--wg-morph-dur) var(--wg-morph-ease), transform .22s cubic-bezier(.22,1,.36,1), translate var(--wg-morph-dur) var(--wg-morph-ease), box-shadow var(--wg-morph-dur) var(--wg-morph-ease), visibility 0s}
#swv-ov #swv-panel.swv-ov-hide{visibility:hidden;pointer-events:none;transition:transform .22s cubic-bezier(.22,1,.36,1), visibility 0s .22s}
@media(prefers-reduced-motion:reduce){#swv-ov #swv-panel{transform:none;transition:none}#swv-ov #swv-panel.swv-ov-hide{transition:none}}

@media(max-width:860px){
  /* one column: every story is one full screen frame per stop. The picture
     (a still, the flight video, the furnishing layer) fills the stage with the
     inline inset:0 each layer carries, and the caption sits over its foot on
     the same scrim, type colour and shadow as the wide layout. The caption
     takes the height its copy needs and grows upward, with a minimum of 44% so
     the scrim is gone by the same height however short the words. It clears
     the closed Homes bar (16px up, 44px tall), the launcher above it
     (--fab-bar-lift) and the safe area; on the last stop it clears the Explore
     details card and the launcher over it instead (swv-ov-scap-last).

     justify-content is spelled out rather than inherited, so a caption that
     grows keeps its text at the foot. */
  .scap.swv-ov-section{position:absolute;top:auto;bottom:0;left:0;right:0;height:auto;
    min-height:44%;
    background:var(--swv-story-shade, var(--swv-story-shade-default)) bottom/100% 100% no-repeat;color:var(--swv-neutral-50);
    display:flex;flex-direction:column;justify-content:flex-end;
    gap:8px;padding:0 var(--swv-space-16) calc(var(--swv-space-16)*2 + var(--swv-control-h) + var(--fab-bar-lift) + env(safe-area-inset-bottom,0px))}
  .swv-ov-kicker,.swv-ov-title,.swv-ov-chips,.swv-ov-hint{position:static;left:auto;bottom:auto;transform:none}
  /* this block sets its own gap; the desktop margins would add to it */
  .swv-ov-kicker,.swv-ov-title,.swv-ov-titles{margin:0}
  .swv-ov-titles{width:auto;max-width:none}
  .swv-ov-kicker{width:auto;max-width:none;font-size:var(--swv-text-body-lg);line-height:1.2;color:var(--swv-neutral-50)}
  .swv-ov-title{width:auto;max-width:none;font-size:var(--swv-text-h2);line-height:1.15;letter-spacing:normal;color:var(--swv-neutral-50)}
  /* a wrapped line never starts on the separator: the row hides the 22px
     (8 + 2 + 8 + 4) a separator takes at the start of a line, and the first
     chip, which has none, is set in by the same. The other three sides are
     left open so a full screen story's text shadow is not cut */
  .swv-ov-chips{color:var(--swv-neutral-50);flex-wrap:wrap;row-gap:6px;
    margin-left:-22px;clip-path:inset(-24px -24px -24px 22px)}
  .swv-ov-chip:first-child{margin-left:22px}
  .swv-ov-chip:not(:first-child)::before{background:var(--swv-neutral-50)}
  /* a plan drawn on cream (a contain stop, swv-ov-ink) keeps dark ink and no scrim:
     light type would vanish on its ground */
  .scap.swv-ov-section.swv-ov-ink{background:none}
  .swv-ov-ink .swv-ov-kicker,.swv-ov-ink .swv-ov-title,.swv-ov-ink .swv-ov-chips,.swv-ov-ink .swv-ov-hint{color:var(--swv-accent-950);text-shadow:none}
  /* The title declares its own shadow (.scap.swv-ov-section .swv-ov-title), which outranks the line above: ink has none. */
  .scap.swv-ov-section.swv-ov-ink .swv-ov-title{text-shadow:none}
  .swv-ov-ink .swv-ov-chip:not(:first-child)::before{background:var(--swv-accent-950)}
  /* "Scroll to explore" and "Back to map" are the same small label: Satoshi 14.
     Here the cue is a row, the label with the line beside it, so the band takes
     44px of it and not the 66 a column would; it sits at the left of the band as
     the text does, in light type with the caption's shadow behind it. */
  .swv-ov-hint{--swv-cue-line:36px;--swv-cue-shade:rgba(10,9,6,.7);color:var(--swv-neutral-50);font-family:var(--swv-sans);font-size:var(--swv-text-caption);line-height:1.3;letter-spacing:-.42px;
    flex-direction:row;gap:10px;padding:0;text-shadow:inherit}
  #story .exit.swv-ov-back,.dv1-back{left:var(--swv-space-12);top:76px;height:var(--swv-pill-h)}
  #story .exit.swv-ov-back.swv-ov-walk{left:calc(var(--swv-space-12) + 136px)}
  #story .exit.swv-ov-back::before,.dv1-back::before{content:"";position:absolute;
    inset:calc((var(--swv-pill-h) - var(--swv-control-h))/2) 0}
  .swv-ov-slider{display:none}
  .swv-ov-rail,#swv-ov.swv-ov-landed .swv-ov-rail{right:12px;top:334px;transform:none}
  /* The card floats here as it does on a desktop, fixed to the bottom right,
     but compact: the render and Explore details, no eyebrow and no caption.
     It is 16px from both edges, with Gracie's launcher at its foot on its
     left. Under it, the last caption and the footer carry the room the card
     takes (below), so no text sits beneath it. */
  .swv-ov-cta{--swv-ov-thick:5px}
  .swv-ov-cta-card{padding:8px;gap:6px}
  .swv-ov-cta-eyebrow,.swv-ov-cta-caption{display:none}
  .swv-ov-cta-title{font-size:var(--swv-text-h4);line-height:26px;letter-spacing:-.4px;gap:5px}
  .swv-ov-cta-arrow{width:22px;height:22px}
  .swv-ov-cta-arrow svg{width:18px;height:18px}
  .scap.swv-ov-section.swv-ov-scap-last{padding-bottom:calc(var(--swv-ov-clear) + env(safe-area-inset-bottom,0px))}
  /* 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:not([data-facility]).swv-ov-last .exit.swv-ov-back{opacity:0;pointer-events:none}
  /* A facility can be a single room, so its first room is already its last
     and the rule above would hide its only way back. Any facility keeps it,
     so the pill stays until the story itself has left for the footer. */
  #swv-ov[data-facility].swv-ov-past .exit.swv-ov-back{opacity:0;pointer-events:none}
  /* The Homes expanded over a story fills the screen up to the pill's own
     height, and the pill sat on its header, over the chat button and the
     title. The drawer carries its own way out, so the pill steps aside
     until it closes. */
  body.rail-open #swv-ov .exit.swv-ov-back{opacity:0;pointer-events:none}
}
@media(max-width:860px) and (hover:hover){
  #story .exit.swv-ov-back:hover,.dv1-back:hover{background:color-mix(in srgb,var(--well) 45%, transparent)}
}

/* ==========================================================================
   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}
/* A facility's page without an Explore details card floating over the foot of it keeps none of the room the villa footer leaves for one. */
#swv-ov[data-facility]:not(:has(.swv-ov-cta)) #swv-ov-foot{padding-bottom:0}
/* The Explore details card floats over the foot of this page, so the footer
   ends with the card's height (--swv-ov-cta-h, at either width) and a 32px margin (the launcher stands beside the card, R7),
   and the last row and the legal links can scroll clear of both. */
#swv-ov-foot{padding-bottom:var(--swv-ov-clear)}
.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}
/* A village with the shared menu draws its footer rows in the menu's own markup
   and keeps the class swv-foot-row on each link as a hook (.swv-foot-menu, below),
   so the plain list's row design is scoped to the plain list. */
.swv-foot-list .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-list .swv-foot-row:hover,.swv-foot-list .swv-foot-row:focus-visible{background-color:rgba(255,255,255,.04)}
.swv-foot-list .swv-foot-row:focus-visible{outline-offset:-5px}
.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 var(--swv-text-footer-link)/1.0 var(--swv-serif);letter-spacing:0;white-space:nowrap}
.swv-foot-idx{display:inline-block;font:700 var(--swv-text-h6)/21.6px var(--swv-sans);
  vertical-align:top;position:relative;top:4px;margin-left:8px}
.swv-foot-cap{font:400 var(--swv-text-body)/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-list .swv-foot-row:hover .swv-foot-arrow,.swv-foot-list .swv-foot-row:focus-visible .swv-foot-arrow{opacity:1}
/* The footer of a village with the shared menu is a whole screen, like the menu
   it repeats: the rows (menu.css draws them, here and in the panel) centred
   between the top of the window and the bar, and the bar on the last line.
   100vh first for a browser without svh. On a window too short for the rows it
   grows past the screen, and the curtain (syncFootCurtain) stays off then. The
   list's padding keeps the first row's photo off the top edge and the last
   row's off the bar. */
/* The generic footer rule's 1px top border would make a full screen footer one
   pixel taller than the window, and the curtain only reveals a footer that fits. */
.swv-footer:has(> .swv-foot-menu){border-top:0}
.swv-foot-menu{display:flex;flex-direction:column;min-height:100vh;min-height:100svh}
.swv-foot-menu .wg-menu-list{padding:clamp(2rem,8vh,5rem) var(--wg-menu-gutter)}
.swv-foot-menu .swv-foot-bar{flex:none}
/* 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 var(--swv-text-body)/19.2px var(--swv-sans)}
.swv-foot-bar-copy{font:400 var(--swv-text-body)/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 var(--swv-text-body)/19.2px var(--swv-sans);color:inherit;text-decoration:none}
@media(prefers-reduced-motion:reduce){
  .swv-foot-list .swv-foot-row,.swv-foot-arrow{transition:none}
}
@media(max-width:600px){
  .swv-foot-list .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 var(--swv-text-h1)/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. Its colours do follow the .dv1 remap below, so on a village
   that sets the dv1 tokens it wears the details page's ground and ink there (Temple Village: cream and green).
   ========================================================================== */
/* The details page's own colours, one token each, so a village's DESIGN.md can dress it without
   a rule of its own. Every default is what the page painted before the tokens existed (a ramp
   step or the literal), so a village that sets none renders exactly as it did. .dv1 hands the
   ramp steps the page paints with to these, so the ~140 rules below read them unchanged. They
   are declared here on :root, so each default is resolved there before .dv1 reassigns the step
   it names: no cycle. */
:root{
  --dv1-ground:var(--swv-neutral-50); --dv1-surface:var(--swv-neutral-100); --dv1-field:var(--swv-neutral-200);
  --dv1-ink:var(--swv-accent-950); --dv1-ink-2:#44423C; --dv1-hint:var(--swv-neutral-600);
  --dv1-icon:var(--swv-neutral-500); --dv1-guide:var(--swv-neutral-400);
  --dv1-rule:#E5E5E5; --dv1-hairline:#D6D5D1; --dv1-edge:#1C1A17; --dv1-card:#fff;
  --dv1-field-hover:#E4E3DA; --dv1-rs:#B5673A;
  --dv1-focus:var(--swv-accent-500); --dv1-bar:var(--swv-accent-400); --dv1-control:var(--swv-accent-900);
}
.dv1{--swv-neutral-50:var(--dv1-ground); --swv-neutral-100:var(--dv1-surface); --swv-neutral-200:var(--dv1-field);
  --swv-neutral-400:var(--dv1-guide); --swv-neutral-500:var(--dv1-icon); --swv-neutral-600:var(--dv1-hint);
  --swv-accent-950:var(--dv1-ink); --swv-divider:var(--dv1-rule)}
.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(--dv1-focus);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}

/* Half the page's old hero, so the tab bar is in the first screen at 1440x900 and
   1280x720 and the chapter is a scroll away, not three. The floor is what the
   descriptor, price and heading need above the nav (60px) and below the back pill
   (68 + 36): taller than 50vh on a phone held sideways, where it wins. */
.dv1-hero{position:relative;height:450px;max-height:50vh;min-height:280px;overflow:hidden}
.dv1-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* Dark from the foot, where the descriptor, price and line sit, eased out to
   nothing at the top of the frame (a short hero puts the descriptor higher up it, so the shade spans all of it to hold 4.5:1 there).
   The same --swv-scrim the story's captions use, so a render keeps one shadow
   from the story into the page it opens. */
.dv1-hero-scrim{position:absolute;inset:0;background:var(--swv-scrim) bottom/100% 100% no-repeat}
.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 var(--swv-text-caption)/1.33 var(--swv-sans);letter-spacing:.04em;text-transform:uppercase}
.dv1-title{font:400 var(--swv-text-numerals-lg)/1.1 var(--swv-serif);text-wrap:wrap}   /* an h1 now: keeps the wrapping the div had */
/* "Three-Bedroom · Plot 1" is the longest villa name; at the 40px ramp step it
   broke onto a second line at 390px with "1" alone. Scale with the viewport on
   phones so the name holds one line. */
@media (max-width:600px){.dv1-title{font-size:var(--swv-text-h2)}}
/* the chapter line is one small sentence above the villa name, not a serif headline */
.dv1-heading{font:400 14px/1.4 var(--swv-sans);letter-spacing:0;color:rgba(255,255,255,.85);margin:0 0 2px}

/* ---------- Section two: Overview + Media ---------- */
.dv1-sec2{display:flex;padding:0;box-sizing:border-box;gap:0;width:100%}
.dv1-col{width:50%;flex:0 0 50%;min-width:0;box-sizing:border-box;padding:36px 0 0;padding-inline:clamp(24px,4.2vw,60px);position:relative}
.dv1-media{width:50%;flex:none;min-width:0;box-sizing:border-box;position:relative;border-radius:0;overflow:hidden;
  background:var(--swv-neutral-100);min-height:520px;padding:0}
.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:0 auto;padding:2px;box-sizing:border-box;position:relative;isolation:isolate;
  display:flex;gap:2px;border-radius:999px}
/* The track under the tabs, and the currency switch beside them, are one raised capsule of the
   page's own ground on the frosted bar (R82, Dave: "luxury glass UI background", one rule for both
   villages): solid, so a label never sits on anything see through, with a hairline and a soft
   shadow to lift it off the cream. They were the ink at 8% over nothing, so a price scrolling
   under the sticky bar ran through the labels on a phone. */
.dv1-tabs,.dv1n-cur{background:var(--swv-neutral-50);
  box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--swv-accent-950) 10%, transparent),
    0 6px 20px -8px color-mix(in srgb, var(--swv-accent-950) 35%, transparent)}
/* The bar sticks to the top of the window under the hero, on the page's own
   ground so the chapter passes beneath it, and it is a child of .dv1 so it holds
   for the whole page. Nothing else on this page is fixed at the top (the mark and
   burger live in the hero and go with it), so top:0 is the window's edge, not a
   guess at a masthead. Its height is measured by syncDetailsBar into
   --dv1-tabbar-h, for what pins beneath it. The same 12px gutter and 50% column as
   .dv1-sec2, so the pills sit centred over the left column where they always did
   (and 36px in from a phone's edge, as they did). The 48px the tabs used to carry
   under them is now .dv1-sec2's own top padding. */
.dv1-tabbar{position:sticky;top:0;z-index:6;box-sizing:border-box;padding:16px 0;
  border-bottom:none;width:100%;display:flex;pointer-events:none}
/* On a phone the bar is frosted glass of the page's own ground (R82): over the cream at rest it
   cannot be seen, and once it sticks whatever scrolls beneath it blurs into the cream instead of
   showing round the tabs. At 82% the ground keeps anything beneath from competing with the bar
   (ink on it would hold 4.5:1 even over black; details-tabs-glass test). It looks solid, so it
   takes its own taps rather than passing them to a blurred link beneath. Solid where blur is
   missing or the visitor asks for less transparency. Not on a wide screen, where the photo column
   runs to the window's top beside the bar and a full width band would frost its top edge. */
@media(max-width:860px){
  .dv1-tabbar{background:color-mix(in srgb, var(--swv-neutral-50) 82%, transparent);pointer-events:auto;
    -webkit-backdrop-filter:blur(18px) saturate(1.4);backdrop-filter:blur(18px) saturate(1.4)}
  @supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.dv1-tabbar{background:var(--swv-neutral-50)}}
  @media (prefers-reduced-transparency: reduce){.dv1-tabbar{background:var(--swv-neutral-50)}}
}
.dv1-tabbar .dv1-tabs{pointer-events:auto}
/* A village with no footer has nothing under a short chapter, and a page
   that ends before the hero has scrolled away never brings the bar to the
   top: switching chapter from further down then loses the reader's place.
   The chapter holds a window's height, so the bar can always reach the top. */
body:has(#foot:empty) #dv1-panel{min-height:100vh;min-height:100svh}
.dv1-tabbar-in{width:50%;box-sizing:border-box;padding:0;padding-inline:clamp(24px,4.2vw,60px);display:flex;justify-content:center;margin:0;position:relative;left:0}
/* A control brought into view by focus or a script lands below the bar, not under
   it. The furnishing chapter has its own, larger, margin (the pinned render too). */
.dv1-sec2 :is(input,button,select,textarea,summary,a[href],[tabindex]){scroll-margin-top:calc(var(--dv1-tabbar-h,0px) + 12px)}
/* 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-tabbar-in{width:100%;display:block}
  .dv1-tabs{margin-left:0;margin-right:auto;overflow-x:auto;scrollbar-width:none}
  .dv1-tabs::-webkit-scrollbar{display:none}
}
.dv1-tab{position:relative;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 var(--swv-text-caption)/18px var(--swv-sans);letter-spacing:-.42px;cursor:pointer;
  transition:background-color .2s var(--ease)}
.dv1-tab:not(.active):hover{background:color-mix(in srgb, var(--swv-accent-950) 12%, transparent)}
/* The dark ground is the active tab's own ::after, drawn under both labels, so a
   tab press can play it from where the old tab stood (engine.js, dv1TabGlide)
   without covering either word on the way. */
.dv1-tab.active{color:var(--swv-neutral-50)}
.dv1-tab.active::after{content:"";position:absolute;inset:0;z-index:-1;border-radius:999px;background:var(--swv-accent-950)}
/* the tab row scrolls sideways, which would clip a transparent hit area, so on
   a phone the tab itself is the control height */
@media(max-width:860px){.dv1-tab{min-height:var(--swv-control-h)}}

.dv1-overview-h{font:400 34px/1.15 var(--swv-serif);letter-spacing:-1.02px;margin:0 0 24px;text-align:left;color:var(--swv-accent-950)}
.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 var(--swv-text-caption)/1.33 var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;opacity:.6}
.dv1-fact-v{font:400 var(--swv-text-numerals-sm)/1.3 var(--swv-serif);margin-top:4px}
/* Each fact takes two rows of the grid, so the labels in a row share one track and
   the values the next: a label that wraps on a phone ("Construction starts" was one at 390)
   no longer pushes its value below the one beside it. Without subgrid a fact
   stacks as before. */
.dv1-fact{display:grid;grid-row:span 2;grid-template-rows:subgrid;row-gap:0}
.dv1-fact-v--rs{color:color-mix(in srgb, var(--swv-accent-950) 70%, var(--dv1-rs))}
.dv1-pill-btn{min-height:44px;padding:0 24px;border:none;border-radius:var(--wg-radius,999px);
  background:var(--swv-accent-950);color:var(--swv-neutral-50);font:500 var(--swv-text-caption)/1 var(--swv-sans);cursor:pointer}
.dv1-pill-btn:hover{opacity:.88}
.dv1-note{font:400 12px/1.5 var(--swv-sans);letter-spacing:0;color:var(--swv-neutral-600);max-width:34em;margin:12px 0 28px}
/* The disclaimer's caveat (RN110) is the one note a buyer must be able to read, so it takes the caption size,
   not the hint's 12px. In the numbers chapter .dv1n2 .dv1-note, later in this file, raises it to body size. */
.dv1-note.dv1-disclaimer{font-size:var(--swv-text-caption)}

/* The villa chapter after access: the price above the facts, then the compare
   panel under both columns. Sizes follow Anders's redesign. */
.dv1-price{margin:0 0 20px}
.dv1-price-usd{font:400 var(--swv-text-numerals-lg)/1.1 var(--swv-serif);margin-top:4px}
.dv1-price-idr{font:400 var(--swv-text-caption)/1.4 var(--swv-sans);color:var(--swv-neutral-600);margin-top:2px}
.dv1-price-note{font:400 12px/1.5 var(--swv-sans);color:var(--swv-neutral-600);margin-top:6px}
/* Before access the overview sits inside the teaser copy: it brings its own
   heading, so the teaser's own spacing steps aside and the ask sits under the facts. */
.dv1-locked .chtease{max-width:none;padding:0 0 40px}
.dv1-locked .chtease .teaser-body{margin-bottom:28px}
.dv1-locked .dv1-overview .dv1-grid{margin-bottom:28px}
.dv1-locked .teaser-actions{margin:0}
@media(max-width:860px){
  body[data-project-slug] .dv1-locked .dv1-col{padding-bottom:calc(var(--swv-space-16)*2 + var(--swv-control-h) + var(--fab-bar-lift) + env(safe-area-inset-bottom,0px))}
}

/* Compare villas. Closed by default; the reveal is a grid row going from 0fr to
   1fr, with the contents hidden from the tab order until it is open. */
.dv1-compare{display:grid;grid-template-rows:0fr;opacity:0;visibility:hidden;
  transition:grid-template-rows .3s var(--ease),opacity .3s var(--ease),visibility 0s linear .3s}
.dv1-compare[data-open="true"]{grid-template-rows:1fr;opacity:1;visibility:visible;transition-delay:0s}
.dv1-compare-in{min-height:0;overflow:hidden;box-sizing:border-box;padding-inline:clamp(24px,4.2vw,60px)}
.dv1-compare[data-open="true"] .dv1-compare-in{padding-top:12px;padding-bottom:8px}
@media(prefers-reduced-motion:reduce){.dv1-compare,.dv1-compare[data-open="true"]{transition:none}}
.dv1-compare-h{font:400 28px/1.3 var(--swv-serif);letter-spacing:-.6px;margin:0 0 20px}
.dv1-compare-scroll{overflow-x:auto;max-width:100%}
.dv1-cmp{width:100%;border-collapse:collapse;min-width:640px}
.dv1-cmp th{font:400 var(--swv-text-caption)/1.33 var(--swv-sans);letter-spacing:.04em;text-transform:uppercase;opacity:.6;
  text-align:right;padding:0 12px 12px 0;border-bottom:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);font-variant-numeric:tabular-nums}
.dv1-cmp td{font:400 14px/1.4 var(--swv-sans);padding:14px 12px 14px 0;text-align:right;font-variant-numeric:tabular-nums;
  border-bottom:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent)}
.dv1-cmp th:first-child,.dv1-cmp td:first-child{text-align:left}
.dv1-cmp td a{color:inherit;text-decoration:none}
.dv1-cmp td a:hover{text-decoration:underline}
.dv1-cmp-here{background:var(--swv-neutral-100)}
.dv1-cmp-here td:first-child{padding-left:8px}
.dv1-cmp-badge{display:inline-block;margin-left:8px;padding:2px 8px;border-radius:999px;background:var(--swv-accent-950);color:var(--swv-neutral-50);
  font:500 var(--swv-text-hint)/1.4 var(--swv-sans);letter-spacing:.08em;text-transform:uppercase;vertical-align:middle}
.dv1-compare-note{font:400 12px/1.5 var(--swv-sans);color:var(--swv-neutral-600);max-width:34em;margin:12px 0 0}
@media(max-width:860px){
  .dv1-compare[data-open="true"] .dv1-compare-in{padding-bottom:calc(var(--swv-space-16)*2 + var(--swv-control-h) + var(--fab-bar-lift) + env(safe-area-inset-bottom,0px))}
  .dv1-compare-h{font-size:var(--swv-text-h3,24px)}
}

.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 var(--swv-text-caption)/40px var(--swv-sans);text-align:center}

@media(max-width:860px){
  .dv1-sec2{flex-wrap:wrap}
  .dv1-col,.dv1-media{width:100%;flex:none}
}

/* a figure draws its digits at one width while it counts (countUp), and rests in the design's own */
[data-count].is-counting{font-variant-numeric:tabular-nums}
.dv1n-ring-cap{font:400 var(--swv-text-caption)/18px var(--swv-sans);letter-spacing:-.42px}
.dv1n-row-s{flex:1;text-align:right;font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600)}

.dv1n-brow{display:flex;align-items:baseline;gap:16px;padding:8px 0;
  font:400 var(--swv-text-caption)/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,minmax(0,1fr));gap:16px 20px;margin-bottom:8px}
.dv1n-ts{font:400 var(--swv-text-caption)/1.33 var(--swv-sans);opacity:.6;margin-top:2px}

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

/* ==========================================================================
   THE NUMBERS TAB v2 (Phase C), the summary sections, then the deep sections
   6 to 10. The open chapter's own layout is the .dv1n2 block further down. */
.dv1n-s6,.dv1n-s7,.dv1n-s8,.dv1n-s9,.dv1n-s10{margin-top:0}
/* Sections 7-10 have dividers above them with proper spacing */
.dv1n-s7,.dv1n-s8,.dv1n-s9,.dv1n-s10{border-top:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);padding-top:50px;margin-top:40px}
.dv1n-sub-h{font:500 var(--swv-text-body)/20px var(--swv-sans);letter-spacing:.02em;margin:28px 0 12px}
/* the deep chapter's own section headings (7 to 10), visually identical to
   .dv1-overview-h (section 2's own "Price, returns and the path" keeps that
   class), but its own name: portal-swv-access.spec.ts's "an access link…"
   test counts .dv1-overview-h and expects exactly one on this chapter, a
   count that held when the chapter had one heading and stopped holding the
   moment this one grew four more. A shared rule, not a shared class. */
.dv1n-head-h{font:400 30px var(--swv-serif);letter-spacing:-0.9px;margin:0 0 20px;line-height:1.3}

/* section 6 (the deep recap) and section 8's own top-of-model cards share
   this fully bordered card shape (item 8: a border-top-only line read as
   plain text, not a card). .dv1n-cards--2 is the two-light-card row item 10
   puts ahead of the dark NOI card; always two columns, narrow screens
   included. The design keeps these two side by side even at 390. Written
   as the compound .dv1n-cards.dv1n-cards--2 (both classes sit on the same
   element) so its specificity beats the narrower-screen column collapses
   below, which also answer to plain .dv1n-cards and sit later in the file. */
.dv1n-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
.dv1n-cards.dv1n-cards--2{grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:16px;gap:8px}
.dv1n-card{border:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  border-radius:4px;padding:12px;min-height:120px;display:flex;flex-direction:column;justify-content:space-between;background:var(--swv-neutral-50)}
.dv1n-card-v{font:400 26px var(--swv-sans);line-height:1.1;white-space:nowrap;color:var(--swv-accent-950);font-weight:400}
.dv1n-card-k{font:400 12px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-card-s{font:400 12px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-card-dark{background:var(--swv-accent-950);border-color:var(--swv-accent-950)}
.dv1n-card-dark .dv1n-card-k{color:color-mix(in srgb, var(--swv-neutral-50) 70%, transparent)}
.dv1n-card-dark .dv1n-card-v{color:var(--swv-neutral-50)}
.dv1n-card-dark .dv1n-card-s{color:color-mix(in srgb, var(--swv-neutral-50) 55%, transparent)}

/* section 7, "01. Make it yours": a dark card for the live playground's own
   headline figure (item 9), underline scenario tabs below it (not pills),
   three sliders in a light bordered card, and the accordion. */
#dv1n-pg-live{background:var(--swv-accent-950);color:var(--swv-neutral-50);
  border-radius:4px;padding:24px;margin-top:20px}
/* the underline tab bar sits on its own light strip directly under the dark
   card, three equal segments, the active one underlined rather than pilled. */
.dv1n-stabs{display:flex;background:var(--swv-neutral-100);
  border:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  border-radius:4px 4px 0 0;margin:0;box-sizing:border-box}
.dv1n-stab{flex:1;letter-spacing:0;text-transform:none;font:500 var(--swv-text-caption)/1 var(--swv-sans);
  padding:14px 8px;border:none;border-bottom:2px solid transparent;
  background:none;color:var(--swv-neutral-600);cursor:pointer;transition:.2s}
.dv1n-stab:hover{color:var(--swv-accent-950)}
.dv1n-stab.active{color:var(--swv-accent-950);border-bottom-color:var(--swv-accent-950);font-weight:600}
.dv1n-sliders{margin-top:0;display:flex;flex-direction:column;gap:18px;
  border:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);border-top:none;
  border-radius:0 0 4px 4px;padding:20px 20px 24px;box-sizing:border-box}
/* item 6: label and value on one line above the track, the min and max
   labels on their own line under it, both ends of the track. */
.dv1n-slider{display:flex;flex-direction:column;gap:8px}
.dv1n-slider-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.dv1n-slider-k{font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-slider-v{font:500 var(--swv-text-caption)/18px var(--swv-sans);text-align:right;min-width:3.5em}
.dv1n-slider input[type="range"]{width:100%;accent-color:var(--dv1-control);
  height:22px;margin:0;box-sizing:border-box}
.dv1n-slider-range{display:flex;justify-content:space-between;
  font:400 var(--swv-text-caption)/14px var(--swv-sans);color:var(--swv-neutral-600)}
/* "Reset to Base case" as a centred outlined pill (item 9), scoped to this
   card so the shared .quietlink look elsewhere in the file is untouched. */
.dv1n-sliders .quietlink{display:block;width:fit-content;margin:4px auto 0;
  padding:12px 22px;border:1px solid color-mix(in srgb, var(--swv-accent-950) 30%, transparent);
  border-radius:999px;font:400 var(--swv-text-caption)/1 var(--swv-sans);color:var(--swv-accent-950);
  text-transform:none;letter-spacing:0;text-align:center}

/* section 8, the rental model: season cards, then the revenue and cost
   tables, reusing the villa breakdown's own row shape (.dv1n-brow/-bname/
   -bval/-btotal, see the invest breakdown above) rather than a second one. */
/* two columns from 521px up, not three on desktop: the price per night at the
   kit's numerals size (26px from 861px up) does not fit a third of the 680px
   column without breaking after "IDR". At 520px and below the max-width 520px
   rule further down draws one column. */
.dv1n-seasons{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin:24px 0}
.dv1n-season{border:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);
  border-radius:4px;padding:16px 18px}
.dv1n-season-k{font:500 var(--swv-text-caption)/16px var(--swv-sans);letter-spacing:.02em;text-transform:uppercase;color:var(--swv-neutral-600)}
.dv1n-season-v{font:400 var(--swv-text-numerals-sm)/28px var(--swv-sans);margin-top:6px}
.dv1n-season-v small{font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-season-sub{font:400 var(--swv-text-caption)/16px var(--swv-sans);color:var(--swv-neutral-600);margin-top:2px}
.dv1n-table{margin-top:4px}
/* item 8: the reserve itself as one large figure, its refundable line
   under it, ahead of the stage cards above. */
.dv1n-reserve{margin-bottom:8px}
.dv1n-reserve-k{font:500 var(--swv-text-caption)/16px var(--swv-sans);letter-spacing:.02em;color:var(--swv-neutral-600)}
.dv1n-reserve-v{font:300 var(--swv-text-numerals-lg)/1.1 var(--swv-sans);color:var(--swv-accent-950);margin-top:6px}
.dv1n-reserve-sub{font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600);margin-top:4px}

/* item 7: section 10's own dark card, the photo behind it dimmed by a
   scrim so the light eyebrow, heading and pills stay readable over
   whatever the villa's own exterior render puts behind them. */
.dv1n-cta-card{position:relative;border-radius:8px;overflow:hidden;
  background:var(--swv-accent-950) center/cover no-repeat;padding:32px 20px}
.dv1n-cta-scrim{position:absolute;inset:0;
  background:linear-gradient(180deg, color-mix(in srgb, var(--swv-accent-950) 55%, transparent), color-mix(in srgb, var(--swv-accent-950) 88%, transparent))}
.dv1n-cta-body{position:relative}
.dv1n-cta-card .dv1-eyebrow{color:color-mix(in srgb, var(--swv-neutral-50) 70%, transparent)}
.dv1n-cta-card .dv1n-head-h{color:var(--swv-neutral-50)}
.dv1n-cta-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
/* an <a> pill does not centre its label the way a <button> does */
.dv1n-cta-row .dv1-pill-btn{display:inline-flex;align-items:center;justify-content:center;padding:0 12px;font-size:var(--swv-text-caption);text-decoration:none}
.dv1n-cta-fill{background:var(--swv-neutral-50);color:var(--swv-accent-950)}
.dv1n-cta-outline{background:transparent;color:var(--swv-neutral-50);
  border:1px solid color-mix(in srgb, var(--swv-neutral-50) 55%, transparent)}
/* the revealed lead form (or confirmation) sits on its own light panel.
   leadBlockHTML is unchanged, still built for a light ground, so it keeps
   reading right rather than needing a dark variant of its own. */
.dv1n-cta-lead{background:var(--swv-neutral-50);border-radius:4px;
  padding:24px;margin-top:24px;max-width:640px}
.dv1n-cta-lead[hidden]{display:none}

/* "01. Make it yours" opens the numbered part, so it takes a wider break
   than the 40px between the summary's own sections */
.dv1n-deep .dv1n-s7{margin-top:64px}

@media(max-width:860px){
  /* Adjust layout for narrower viewports. Stack columns and reorder. */
  .dv1-col{width:100%;padding-top:24px;padding-inline:24px}
  .dv1-tabbar{width:100%}
  .dv1-tabbar-in{width:100%;padding-inline:24px;position:relative;left:auto}
  /* item 2: the scenario tiles stay a row of three at every width, the
     design's own shape, so they keep out of this narrowing rule while
     the recap cards drop to two columns (the season cards are already two
     above 520px, and one at 520px and below, in the rule after this one). */
  .dv1n-cards{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media(max-width:520px){
  .dv1n-cards,.dv1n-seasons{grid-template-columns:1fr}
}

/* 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%}
/* On a phone the closed Homes bar (44px, 16px up) is fixed over the foot of the
   page, and the gated teaser's button is the last thing in it: the column ends
   with room for the bar and the safe area, so the button always scrolls clear. */
@media(max-width:860px){
  body[data-project-slug] .dv1-gated .dv1-col{padding-bottom:calc(var(--swv-space-16)*2 + var(--swv-control-h) + var(--fab-bar-lift) + env(safe-area-inset-bottom,0px))}
}
.dv1n-h1{font:400 48px/1.1 var(--swv-serif);letter-spacing:-1.44px;text-align:center;margin:0 0 40px}
.dv1n-card{justify-content:flex-start}
.dv1n-card-v{margin-top:auto;padding-top:20px}
.dv1n-cards.dv1n-cards--2{grid-template-columns:repeat(2,minmax(0,1fr));margin-bottom:8px}
.dv1n-card-dark{min-height:120px;box-sizing:border-box}
.dv1n-card-dark .dv1n-card-v{font-size:36px}
.dv1n-card-dark .dv1n-card-s{color:color-mix(in srgb, var(--swv-neutral-50) 70%, transparent)}
.dv1n-s6{margin-bottom:0}
.dv1n-s7,.dv1n-s8,.dv1n-s9,.dv1n-s10{border-top:1px solid var(--dv1-rule);padding-top:50px;margin-top:40px}
.dv1n-s7 > .dv1-eyebrow,.dv1n-s8 > .dv1-eyebrow,.dv1n-s9 > .dv1-eyebrow{font:400 12px/1.3 var(--swv-sans);color:var(--swv-neutral-600);margin:0 0 20px}
.dv1n-s7 > .dv1n-head-h,.dv1n-s8 > .dv1n-head-h,.dv1n-s9 > .dv1n-head-h{font:400 30px/1.15 var(--swv-serif);letter-spacing:-.9px;max-width:320px;margin:0 0 20px}
/* playground */
#dv1n-pg-live{margin-top:0}
#dv1n-reset[style*="hidden"]{display:none}
.dv1n-sliders{padding:20px 20px 20px}
.dv1n-sliders .quietlink{margin:8px auto 0}
.dv1n-brow{align-items:center;gap:16px;min-height:33px;box-sizing:border-box;padding:0;border-bottom:1px solid var(--dv1-rule);
  font:400 13px/20px var(--swv-sans);color:var(--dv1-ink-2)}
.dv1n-bval{text-align:right}
.dv1n-btotal{border-top:none;border-bottom:none;margin-top:0;padding-top:0;font-weight:500}
.dv1n-brow:has(+ .dv1n-btotal){border-bottom:1px solid var(--dv1-rule)}
/* season cards: one row of three, stat-card shape */
.dv1n-seasons{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:8px 0 16px}
.dv1n-season{display:flex;flex-direction:column;min-height:120px;box-sizing:border-box;padding:12px;background:var(--swv-neutral-50)}
.dv1n-season-k{font:400 var(--swv-text-hint)/1.3 var(--swv-sans);letter-spacing:0;text-transform:none;color:var(--swv-neutral-600)}
.dv1n-season-v{font:400 22px/1.1 var(--swv-sans);white-space:nowrap;margin-top:auto;padding-top:20px}
.dv1n-season-v small{font:400 var(--swv-text-hint) var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-season-sub{font:400 var(--swv-text-hint)/1.3 var(--swv-sans);color:var(--swv-neutral-600);margin-top:4px}
/* closing card */
.dv1n-s10{margin-bottom:96px}
.dv1n-cta-card{min-height:194px;box-sizing:border-box;padding:28px 24px;display:flex;flex-direction:column;justify-content:center}
.dv1n-s10 .dv1n-head-h{font:400 28px/1.15 var(--swv-serif);letter-spacing:-.84px;max-width:340px;margin:0 0 20px}
.dv1n-s10 .dv1-eyebrow{margin:0 0 12px}

@media(max-width:860px){
  .dv1n-cards{grid-template-columns:repeat(2,minmax(0,1fr))}
  .dv1n-s6 > .dv1n-card:first-child{grid-column:1 / -1}
  .dv1n-seasons{grid-template-columns:1fr}
  .dv1n-season{min-height:96px}
  .dv1n-card-v{font-size:var(--swv-text-numerals-sm);line-height:1.1}
  .dv1n-card-dark .dv1n-card-v{font-size:var(--swv-text-h2)}
  .dv1n-head-h{font-size:var(--swv-text-h3);line-height:1.15}
  .dv1n-s7 > .dv1n-head-h,.dv1n-s8 > .dv1n-head-h,.dv1n-s9 > .dv1n-head-h{font-size:var(--swv-text-h3);line-height:1.15}
  .dv1n-brow{font-size:var(--swv-text-caption);line-height:1.4}
  .dv1n-season-k,.dv1n-season-sub,.dv1n-season-v small{font-size:var(--swv-text-hint)}
  .dv1n-season-v{font-size:var(--swv-text-numerals-sm)}
}

/* ==========================================================================
   THE NUMBERS CHAPTER, Redesign-Numbers (D5). One wide column of three
   sections; each is a row of figures on the left and ONE sticky picture on the
   right (position: sticky, no scroll script). Hover and focus only add detail:
   the rows and their segments light together (dnHighlight). Below 861px the
   picture stacks under its section.
   ========================================================================== */
.dv1n2 .dv1-col{width:100%;flex:none;max-width:1320px;margin:0 auto;padding:64px clamp(24px,4.2vw,60px) 0}
.dv1n2 .dv1n-s7{border-top:none;padding-top:0;margin-top:8px}
.dv1n2 .dv1n-s10{margin-bottom:96px}
/* Body notes in the chapter are 16px; the notes sit inside .dv1n2-l and .dv1n2-viz, so they are set here rather than by section */
.dv1n2 .dv1-note{font:400 var(--swv-text-body)/1.5 var(--swv-sans)}
.dv1n2-row{display:flex;gap:40px;align-items:stretch}
.dv1n2-row.dv1n2-row--cont{border-top:none;padding-top:0;margin-top:0}
.dv1n2-l{flex:1 1 56%;min-width:0}
.dv1n2-r{flex:1 1 40%;min-width:0}
.dv1n2-sticky{position:sticky;top:calc(var(--dv1-tabbar-h,66px) + 24px);display:flex;flex-direction:column;gap:28px}
.dv1n2 .dv1n-head-h{margin:0 0 20px;max-width:none}

/* currency switch, in the tab bar, drawn like the chapter tabs beside it */
.dv1-tabbar-in:has(.dv1n-cur){width:100%;max-width:1320px;margin:0 auto;justify-content:space-between;align-items:center;gap:12px}
.dv1-tabbar-in:has(.dv1n-cur) .dv1-tabs{margin:0;min-width:0}
.dv1n-cur{display:flex;flex:none;gap:2px;padding:2px;border-radius:999px;pointer-events:auto}
.dv1n-cur-b{min-height:32px;padding:0 14px;border:none;border-radius:999px;background:transparent;color:var(--swv-accent-950);
  font:400 var(--swv-text-caption)/18px var(--swv-sans);cursor:pointer;transition:background-color .2s var(--ease),color .2s var(--ease)}
.dv1n-cur-b:not(.active):hover{background:color-mix(in srgb, var(--swv-accent-950) 12%, transparent)}
.dv1n-cur-b.active{background:var(--swv-accent-950);color:var(--swv-neutral-50)}

/* the price, and the disclosure that opens its five lines */
.dv1n2-price{display:block;margin:8px 0 0;padding:0;border:0;background:none;color:inherit;text-align:left;font:inherit}
button.dv1n2-price{cursor:pointer}
.dv1n2-price-v{display:block;font:400 var(--swv-text-numerals-lg)/1.1 var(--swv-serif);letter-spacing:-.02em;color:var(--swv-accent-950)}
.dv1n2-price-t{display:inline-flex;align-items:center;gap:6px;margin-top:8px;font:500 var(--swv-text-caption)/1.3 var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n2-chev{display:inline-flex}
.dv1n2-ptable{margin-top:16px}
.dv1n2-furn{margin-top:12px;padding-top:12px;border-top:1px dashed color-mix(in srgb, var(--swv-accent-950) 20%, transparent)}
.dv1n2-furn .dv1n-brow{border-bottom:none;color:var(--swv-neutral-600)}
.dv1n2-furn .dv1-note{margin:4px 0 0}
.dv1n2-recover{margin-top:16px}
.dv1n2-pct{color:var(--swv-neutral-600)}
.dv1n2-withtip{display:inline-flex;align-items:center;gap:6px}
.dv1n2-label{font:600 var(--swv-text-hint)/1.3 var(--swv-sans);text-transform:uppercase;letter-spacing:.06em;color:var(--swv-neutral-600);margin:28px 0 12px}
.dv1n2-tblh{font:400 var(--swv-text-h5)/1.18 var(--swv-serif);letter-spacing:-.02em;margin:24px 0 8px;padding-top:20px;border-top:1px solid var(--swv-divider)}
.dv1n2-row--cont .dv1n2-tblh{margin-top:32px;padding-top:0;border-top:none}
.dv1n2-lede{max-width:420px;margin:0 0 4px}

/* the three headline cards: the profit card full width, then yield and payback.
   #dv1n-pg-live is the old dark playground panel's id; here it only holds the cards. */
#dv1n-pg-live.dv1n2-live{background:none;color:inherit;padding:0;border-radius:0;margin-top:24px}
.dv1n2-cards1{grid-template-columns:1fr;margin-bottom:8px}
.dv1n2 .dv1n-card-k{display:inline-flex;align-items:center;gap:6px;font-size:var(--swv-text-hint)}
.dv1n2 .dv1n-card-s{font-size:var(--swv-text-hint)}
.dv1n2 .dv1n-card-dark .dv1n2-i{color:color-mix(in srgb, var(--swv-neutral-50) 70%, transparent)}

/* an (i) and its tip: CSS only, shown on hover and on keyboard focus */
.dv1n2-info{position:relative;display:inline-flex;vertical-align:middle}
.dv1n2-i{position:relative;display:inline-flex;padding:0;border:0;background:none;cursor:pointer;color:var(--swv-neutral-500)}
.dv1n2-i::after{content:"";position:absolute;inset:-13px}
.dv1n2-tip{position:absolute;bottom:calc(100% + 6px);left:0;z-index:6;display:none;width:240px;max-width:calc(100vw - 48px);box-sizing:border-box;
  padding:10px 14px;border-radius:6px;background:var(--swv-accent-950);color:var(--swv-neutral-50);
  font:400 var(--swv-text-hint)/1.4 var(--swv-sans);letter-spacing:0;text-align:left;text-transform:none;font-weight:400;box-shadow:0 8px 20px rgba(0,0,0,.2)}
.dv1n2-i:hover + .dv1n2-tip,.dv1n2-i:focus-visible + .dv1n2-tip{display:block}
.dv1n2-info.is-light .dv1n2-tip{background:var(--swv-neutral-50);color:var(--swv-accent-950)}
.dv1n2-info.is-above .dv1n2-tip{left:50%;transform:translateX(-50%);width:260px}

/* a row that is also a chart segment lights with it, and the other way round */
.dv1n2-hrow{position:relative;border-radius:4px;transition:background-color .15s var(--ease)}
.dv1n2-hrow.is-hl{background:color-mix(in srgb, var(--swv-accent-950) 8%, transparent)}
.dv1n2-hrow.dv1n-btotal{border-radius:4px}

/* the pictures */
.dv1n2-viz{display:flex;flex-direction:column;align-items:center;text-align:center}
.dv1n2-vt{font:400 var(--swv-text-h5)/1.2 var(--swv-serif);letter-spacing:-.02em}
.dv1n2-ringwrap{position:relative;width:100%;max-width:220px;aspect-ratio:1/1;margin:16px auto 8px}
.dv1n2-ring{width:100%;height:100%;display:block;overflow:visible}
.dv1n2-ring circle{fill:none}
.dv1n2-hit{stroke:transparent;stroke-width:36;pointer-events:stroke;cursor:pointer;transition:stroke .15s var(--ease)}
.dv1n2-arc{stroke-width:14;pointer-events:none;transition:stroke-width .15s var(--ease),stroke-opacity .15s var(--ease)}
.dv1n2-seg{outline:none}
.dv1n2-seg.is-hl .dv1n2-arc,.dv1n2-seg:focus-visible .dv1n2-arc{stroke-width:18}
.dv1n2-seg.is-dim .dv1n2-arc{stroke-opacity:.4}
.dv1n2-seg:focus-visible .dv1n2-hit{stroke:color-mix(in srgb, var(--swv-accent-950) 14%, transparent)}
.dv1n2-ctr{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 36px;text-align:center;pointer-events:none}
.dv1n2-fig{font:400 var(--swv-text-h6)/1.2 var(--swv-sans);letter-spacing:-.02em;margin:2px 0;white-space:nowrap}
.dv1n2-fig.is-sm{font-size:var(--swv-text-caption)}
.dv1n2-hint{text-align:center;margin:8px auto 0}
.dv1n2-hline{min-height:20px;margin:6px 0 8px;text-align:center;font:500 var(--swv-text-caption)/1.4 var(--swv-sans);color:var(--swv-accent-950)}
.dv1n2-leasefig{position:relative;display:flex;align-items:baseline;justify-content:center;gap:8px;margin-top:12px}
.dv1n2-leasev{font:400 var(--swv-text-numerals-lg)/1.1 var(--swv-sans);letter-spacing:-.02em;color:var(--swv-accent-950)}
.dv1n2-lease{display:block;width:100%;max-width:360px;height:auto;margin:0 auto}
.dv1n2-bar,.dv1n2-pline{outline:none}
.dv1n2-bhit,.dv1n2-phit{fill:transparent;cursor:pointer}
.dv1n2-b{fill:var(--dv1-bar);pointer-events:none;transition:fill .15s var(--ease)}
/* a bar grows from the baseline (engine.js, dnGrow and dnMorph scale it) */
.dv1n2-b{transform-box:fill-box;transform-origin:50% 100%}
.dv1n2-bar.is-hl .dv1n2-b,.dv1n2-bar:focus-visible .dv1n2-b{fill:var(--swv-accent-950)}
.dv1n2-pline line{stroke:var(--swv-neutral-400);stroke-width:1.5;stroke-dasharray:4 4;pointer-events:none}
.dv1n2-pline.is-hl line,.dv1n2-pline:focus-visible line{stroke:var(--swv-accent-950)}

/* reservation: the stages as cards, and as one bar */
.dv1n-stages{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:20px 0 12px}
.dv1n-stage{border:1px solid color-mix(in srgb, var(--swv-accent-950) 16%, transparent);border-radius:4px;padding:12px;background:var(--swv-neutral-50);
  transition:border-color .15s var(--ease),background-color .15s var(--ease)}
.dv1n-stage.is-hl{border-color:var(--swv-accent-950);background:color-mix(in srgb, var(--swv-accent-950) 6%, transparent)}
.dv1n-stage-k{font:400 var(--swv-text-caption)/1.3 var(--swv-sans);color:var(--swv-neutral-600)}
.dv1n-stage-pct{font:400 var(--swv-text-numerals-sm)/1.1 var(--swv-sans);margin-top:12px;color:var(--swv-accent-950)}
.dv1n-stage-amt{font:400 var(--swv-text-caption)/1.3 var(--swv-sans);color:var(--swv-neutral-600);margin-top:4px}
.dv1n2-sbar{display:flex;width:100%;height:28px;border-radius:6px;overflow:hidden}
.dv1n2-sseg{cursor:pointer;outline:none;transition:opacity .15s var(--ease)}
.dv1n2-sseg.is-dim{opacity:.45}
.dv1n2-sseg:focus-visible{box-shadow:inset 0 0 0 3px var(--swv-neutral-50)}

@media(prefers-reduced-motion:reduce){
  .dv1n2-arc,.dv1n2-hit,.dv1n2-b,.dv1n2-sseg,.dv1n2-hrow,.dv1n-stage,.dv1n-cur-b{transition:none}
}

/* ==========================================================================
   A tab press between two chapters of one villa is a View Transition on this
   document (engine.js, dv1ChapterSwitch). The names exist only while it runs,
   so no stacking context is left on the page afterwards. The hero and the tab
   bar hold still (the bar shows the live new one at once; the pill glides on
   it), the chapter body leaves toward the side it came from and the new one
   arrives from the side of travel, and the rest of the page is not cross faded.
   Only transform and opacity move. */
:root[data-dv1-move=fwd]{--dv1-dx:32px}
:root[data-dv1-move=back]{--dv1-dx:-32px}
:root[data-dv1-move] .dv1-hero{view-transition-name:dv1-hero}
:root[data-dv1-move] .dv1-tabbar{view-transition-name:dv1-bar}
:root[data-dv1-move] #dv1-panel{view-transition-name:dv1-body}
:root[data-dv1-move]::view-transition-old(root){display:none}
/* the hero never thins: the old picture stays whole under the new one while it fades in, so a photo
   still decoding shows the old one rather than the page ground */
:root[data-dv1-move]::view-transition-old(dv1-hero){animation:none;mix-blend-mode:normal}
:root[data-dv1-move]::view-transition-new(dv1-hero){animation:dv1-fade 320ms cubic-bezier(.16,1,.3,1) both;mix-blend-mode:normal}
@keyframes dv1-fade{from{opacity:0}}
:root[data-dv1-move]::view-transition-new(root){animation:none}
:root[data-dv1-move]::view-transition-old(dv1-bar){display:none}
:root[data-dv1-move]::view-transition-new(dv1-bar){animation:none}
:root[data-dv1-move]::view-transition-group(dv1-bar),
:root[data-dv1-move]::view-transition-group(dv1-body){animation:none}
/* the old chapter is nearly gone before the new one starts, so the two never read on top of each other */
:root[data-dv1-move]::view-transition-old(dv1-body){animation:dv1-leave 140ms cubic-bezier(.4,0,1,1) both}
:root[data-dv1-move]::view-transition-new(dv1-body){animation:dv1-arrive 460ms cubic-bezier(.16,1,.3,1) 130ms both}
@keyframes dv1-leave{to{opacity:0;transform:translateX(calc(var(--dv1-dx) * -1))}}
@keyframes dv1-arrive{from{opacity:0;transform:translateX(var(--dv1-dx))}}
@media(prefers-reduced-motion:reduce){
  :root[data-dv1-move]::view-transition-group(*),
  :root[data-dv1-move]::view-transition-old(*),
  :root[data-dv1-move]::view-transition-new(*){animation:none}
}
@media(max-width:860px){
  /* the switch drops onto its own line under the tabs, so the three tabs keep the
     whole row; both lines share the bar's 24px inset */
  .dv1-tabbar-in:has(.dv1n-cur){display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;gap:8px}
  .dv1-tabbar-in:has(.dv1n-cur) .dv1-tabs{max-width:100%}
  .dv1n-cur-b{min-height:var(--swv-control-h)}
  .dv1n2 .dv1-col{padding-top:44px}
  .dv1n2 .dv1n-h1{margin:0 0 28px;font-size:var(--swv-text-h1);line-height:1.1;letter-spacing:-1.2px}
  .dv1n2-row{flex-direction:column;gap:32px}
  .dv1n2-l,.dv1n2-r{flex:none;width:100%}
  .dv1n2-sticky{position:static}
  .dv1n2-price-v{font-size:var(--swv-text-numerals-lg)}
}
/* at 390 and 360 the three labels need the tighter padding to sit whole in the row */
@media(max-width:420px){
  .dv1-tab{padding:0 10px}
}

/* ==========================================================================
   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:400 12px var(--swv-sans);letter-spacing:0;
  color:var(--swv-neutral-600);margin-bottom:25px}
.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:var(--dv1-card);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:var(--dv1-card)}
/* the Furnishing tab's own buttons and lead form labels, which the older
   rules size at 10 to 13px. The kit's caption is the floor on the v1 surfaces
   outside About. #v-details is named because its own .fld rules are an id
   deep. */
.dv1 .btn.terra,.dv1 .btn.ghost,.dv1 .btn.solid{font-size:var(--swv-text-caption)}
#v-details .dv1 .fld span{font-size:var(--swv-text-caption)}
#v-details .dv1 .fld-warn{font-size:var(--swv-text-body)}
/* The configurator's labels, which the older .styler rules size at 10 to 11px, rise
   to the kit's hint (12) and its tabs and option prices to caption (14). Its two
   paragraphs, the room note and the small print, rise to body (16). Named under
   #v-details .dv1 so the legacy base rules stay as they are. */
#v-details .dv1 .styler :is(.flive,.fcap span,.fvbar .fvlab,.fvtot .k,.fvtot .tier,.fpanel .rhead .rn,
  .fpanel .frp .lab,.frp-chip,.fitem .ftop .cur,.fopt .tick){font-size:var(--swv-text-hint)}
#v-details .dv1 .styler :is(.ftabs button,.fopt .pr){font-size:var(--swv-text-caption)}
#v-details .dv1 .styler .frnote,#v-details .dv1 p.fnoncap.smallprint{font-size:var(--swv-text-body)}
/* The Talk tab's hero labels, raised to the 12px floor on every village's Talk tab (Dave, 30 Sep). */
#v-details .dhero-k,#v-details .dhero-price .k,#v-details .curq{font-size:var(--swv-text-hint)}
.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{color:var(--swv-accent-950)}
.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))}
/* Once named, the render must stay in view down the whole picker list, so the
   column is as tall as the pickers (the sticky render's containing block) and
   lets it stick: overflow:hidden would make the column its own scroll box.
   The column's grey ground would then run on as an empty panel under the
   render, so the stretched column drops it and the render carries its own. */
#dv1f-media.dv1-media:has(.dv1f-stagewrap){align-self:stretch;overflow:visible;background:none}
.dv1f-stagewrap{position:sticky;top:calc(var(--dv1-tabbar-h,0px) + 16px)}
.dv1f-stagewrap .fnoncap{padding:0 4px}

.dv1f-name-block{display:flex;flex-direction:column;align-items:center;gap:14px;max-width:320px;margin:40px auto;padding:0}
.dv1f-name-label{font-size:13px;font-weight:400;letter-spacing:0;text-transform:none;color:var(--swv-accent-950);text-align:center}
.dv1f-name-input{width:100%;height:40px;border-radius:20px;background:var(--swv-neutral-200);border:1px solid var(--swv-neutral-200);padding:0 20px;font-family:var(--copy-face);font-size:16px;color:var(--swv-accent-950);box-sizing:border-box;transition:0.2s}
.dv1f-name-input::placeholder{color:color-mix(in srgb, var(--swv-accent-950) 50%, transparent);font-style:normal}
.dv1f-name-input:focus{outline:none;border-color:var(--swv-accent-950);background:var(--swv-neutral-50)}
.dv1f-name-input.err{border-color:#d42a0d}
.dv1f-name-submit{width:100%;height:36px;border-radius:20px;background:var(--swv-accent-950);color:var(--swv-neutral-50);border:none;font-family:var(--swv-sans);font-size:13px;font-weight:500;letter-spacing:0;text-transform:none;cursor:pointer;transition:0.2s}
.dv1f-name-submit:hover{opacity:0.9}
.dv1f-name-submit:active{opacity:0.8}

/* ---- Furnishing chapter layout (reference: S/ref-furn-1.png) ----------------
   Everything is scoped to .dv1f-sec2 / .dv1f-* so the shared .dv1-* rules the
   other chapters use are left alone. Desktop (>=900px) is two halves: the
   left content column and, once the villa is named, a sticky render card that
   fills the right half with a 12px inset. Below 900px it is one column with
   the render card first. */
.dv1f-sec2:not(.dv1-gated){padding-bottom:96px}
.dv1f-sec2 .dv1-overview-h{font-size:48px;line-height:1.1;letter-spacing:-1.44px;text-align:center;margin:14px 0 0}
.dv1f-sec2 .dv1-note{font:400 16px/1.5 var(--swv-sans);color:var(--swv-neutral-600);text-align:center;max-width:400px;margin:16px auto 0}
.dv1f-sec2 .dv1-rule{height:1px;background:var(--dv1-rule);margin:40px 0 0}
.dv1f-sec2 .leadwrap{padding-top:56px}
.dv1f-sec2 .leadh{font-size:30px;font-weight:400;line-height:1.15;margin:0 0 12px}
.dv1f-sec2 .leadsub{font-size:16px;line-height:1.5;color:var(--swv-neutral-600);max-width:400px}
.dv1f-sec2 .leadform{margin-top:24px}
#v-details .dv1f-sec2 .leadform{max-width:220px}
#v-details .dv1f-sec2 .leadform .fld input,#v-details .dv1f-sec2 .leadform .leadsave{min-height:44px;height:44px;font-size:16px}
#v-details .dv1f-sec2 .leadform .fld input::placeholder{font-size:16px}

.dv1f-name-block{margin:40px auto 40px}
.dv1f-name-input{height:36px;font-family:var(--swv-sans);font-size:13px}

/* the unnamed chapter: the dark name card, and the looks gallery that stands in for the story slider */
.dv1f-name-block.dv1f-name-card{gap:12px;width:100%;max-width:420px;box-sizing:border-box;padding:32px 20px;background:var(--swv-accent-950);color:var(--swv-neutral-50);border-radius:var(--swv-radius-card);text-align:center}
.dv1f-name-h{margin:0;font:400 var(--swv-text-h4)/1.15 var(--swv-serif);color:var(--swv-neutral-50)}
.dv1f-name-sub{margin:0;font:400 var(--swv-text-caption)/1.5 var(--swv-sans);color:color-mix(in srgb, var(--swv-neutral-50) 78%, transparent)}
.dv1f-name-card .dv1f-name-input{height:40px;margin-top:8px;font:400 var(--swv-text-body) var(--swv-sans);color:var(--swv-neutral-50);background:color-mix(in srgb, var(--swv-neutral-50) 14%, transparent);border-color:color-mix(in srgb, var(--swv-neutral-50) 50%, transparent)}
.dv1f-name-card .dv1f-name-input::placeholder{color:color-mix(in srgb, var(--swv-neutral-50) 75%, transparent)}
.dv1f-name-card .dv1f-name-input:focus{outline:2px solid var(--swv-neutral-50);outline-offset:2px;border-color:var(--swv-neutral-50);background:color-mix(in srgb, var(--swv-neutral-50) 20%, transparent)}
.dv1f-name-card .dv1f-name-input.err{border-color:#ffb4a6}
.dv1f-name-card .dv1f-name-submit{height:40px;background:var(--swv-neutral-50);color:var(--swv-accent-950);border-radius:var(--swv-radius-pill);font-size:var(--swv-text-caption)}
.dv1f-name-card .dv1f-name-submit:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}
.dv1f-looks{display:flex;flex-direction:column;height:100%}
.dv1f-rooms{display:flex;gap:8px;padding:12px 16px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
.dv1f-rooms::-webkit-scrollbar{display:none}
.dv1f-room{position:relative;flex:none;height:var(--swv-pill-h);padding:0 var(--swv-pill-pad-x);border-radius:var(--swv-radius-pill);border:1px solid color-mix(in srgb, var(--swv-accent-950) 24%, transparent);background:none;color:var(--swv-accent-950);font:400 var(--swv-text-caption)/1 var(--swv-sans);white-space:nowrap;cursor:pointer}
.dv1f-room[aria-pressed="true"]{background:var(--swv-accent-950);color:var(--swv-neutral-50);border-color:var(--swv-accent-950)}
.dv1f-room:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
.dv1f-looks-view{position:relative;flex:1;min-height:min(640px, calc(100vh - 96px))}
.dv1f-cap{position:absolute;left:16px;right:16px;bottom:76px;font:400 var(--swv-text-caption)/1.4 var(--swv-sans);color:var(--swv-neutral-50);text-shadow:0 1px 8px rgba(0,0,0,.7);opacity:0;transition:opacity .18s ease;pointer-events:none}
.dv1-media-img.on + .dv1f-cap{opacity:1}
@media(prefers-reduced-motion:reduce){.dv1f-cap{transition:none}}

/* sections: hairline, 56px, content, 40px */
#v-details .dv1 .dv1f-sec2 .styler .fvbar{display:block;margin:0;padding:56px 0 40px;border-top:1px solid var(--dv1-rule);border-bottom:0}
#v-details .dv1 .dv1f-sec2 .styler .fvbar .fvgrp{display:flex;flex-direction:column;align-items:flex-start;gap:28px}
#v-details .dv1 .dv1f-sec2 .styler .fvbar .fvgrp .fvlab,
#v-details .dv1 .dv1f-sec2 .styler .fsec-h{font:400 34px/1.15 var(--swv-serif);letter-spacing:-1.02px;text-transform:none;color:var(--swv-accent-950);margin:0}
.dv1f-sec2 .styler .fsec-h{flex:0 0 100%;margin-bottom:16px}
.dv1f-sec2 .styler .ftabs{margin:0;padding:56px 0 40px;border-top:1px solid var(--dv1-rule);gap:12px}
.dv1f-sec2 .styler .fpanel{padding-top:56px;border-top:1px solid var(--dv1-rule)}
.dv1f-sec2 .styler .fpanel .rhead{margin:0}
.dv1f-sec2 .styler .fpanel .rhead h3{font-size:34px;font-weight:400;line-height:1.15;letter-spacing:-1.02px}
.dv1f-sec2 .styler .fpanel .rhead .rn{display:none}
.dv1f-sec2 .styler .fpanel .frp{margin:20px 0 0}
#v-details .dv1 .dv1f-sec2 .styler .fpanel .frp .lab{font-size:12px;text-transform:none;letter-spacing:0;font-weight:400;margin-bottom:20px}
.dv1f-sec2 .styler .fdivline{display:none}
.dv1f-sec2 .styler .fvbar .fpal{margin:0}
.dv1f-sec2 .styler .frp-set{gap:12px}
#v-details .dv1 .dv1f-sec2 .styler .frp-set button.frp-chip{font-size:14px}

/* chips: villa palette, room, per-room palette */
#v-details .dv1 .dv1f-sec2 .styler :is(.fpal button,.ftabs button,.frp-chip){
  display:inline-flex;align-items:center;gap:8px;height:36px;min-height:36px;padding:0 14px;border:1px solid var(--dv1-hairline);border-radius:999px;
  background:transparent;color:var(--swv-accent-950);font:400 14px/1 var(--swv-sans);letter-spacing:0;text-transform:none;opacity:1}
#v-details .dv1 .dv1f-sec2 .styler :is(.fpal button,.ftabs button,.frp-chip):has(.fsw){padding-left:6px}
#v-details .dv1 .dv1f-sec2 .styler :is(.fpal button,.ftabs button,.frp-chip):hover{border-color:var(--swv-accent-950)}
#v-details .dv1 .dv1f-sec2 .styler :is(.fpal button,.ftabs button,.frp-chip).on{background:var(--dv1-card);color:var(--swv-accent-950);border-color:var(--dv1-edge)}
#v-details .dv1 .dv1f-sec2 .styler :is(.fpal,.frp-set) .fsw{width:24px;height:24px}
.dv1f-sec2 .styler .ftabs button .ricon{display:block;flex:none}
.dv1f-sec2 .styler .ftabs button.override::after{display:none}

/* piece groups: every one open, H3 left, current choice + chevron right */
.dv1f-sec2 .styler .fitem{padding:0;margin-top:56px;border-bottom:0}
.dv1f-sec2 .styler .fpanel .frp + .fitem{margin-top:40px}
.dv1f-sec2 .styler .fitem .ftop{min-height:44px}
#v-details .dv1 .dv1f-sec2 .styler .fitem .ftop .l{font-size:26px;font-weight:400;line-height:1.2;letter-spacing:-.52px}
#v-details .dv1 .dv1f-sec2 .styler .fitem .ftop .cur{font-size:13px;text-transform:none;letter-spacing:0;font-weight:400;opacity:1;color:var(--swv-neutral-600)}
.dv1f-sec2 .styler .fitem .ftop .tr{gap:12px}
.dv1f-sec2 .styler .fitem .ftop .chev svg{display:block;transform:rotate(0deg)}
.dv1f-sec2 .styler .fitem.open .ftop .chev svg{transform:rotate(180deg)}
.dv1f-sec2 .styler .foptwrap{max-height:none}
.dv1f-sec2 .styler .fitem:not(.open) .foptwrap{max-height:0}
.dv1f-sec2 .styler .fopts{gap:12px;padding-top:16px}
#v-details .dv1 .dv1f-sec2 .styler .fopt{height:54px;padding:0 16px 0 12px;gap:12px;border:0;border-radius:27px;background:var(--dv1-field);color:var(--swv-accent-950)}
#v-details .dv1 .dv1f-sec2 .styler .fopt:hover{background:var(--dv1-field-hover)}
#v-details .dv1 .dv1f-sec2 .styler .fopt.on{background:var(--dv1-card);border:1.5px solid var(--dv1-edge)}
#v-details .dv1 .dv1f-sec2 .styler .fopt:has(.fsw.none){background:transparent;border:1px solid var(--dv1-hairline)}
#v-details .dv1 .dv1f-sec2 .styler .fopt:has(.fsw.none).on{border:1.5px solid var(--dv1-edge);background:var(--dv1-card)}
#v-details .dv1 .dv1f-sec2 .styler .fopt .fsw{width:24px;height:24px}
#v-details .dv1 .dv1f-sec2 .styler .fopt .fsw.none{background:var(--dv1-card);box-shadow:inset 0 0 0 1px var(--dv1-rule)}
#v-details .dv1 .dv1f-sec2 .styler .fopt .nm,
#v-details .dv1 .dv1f-sec2 .styler .fopt.on .nm{font-family:var(--swv-sans);font-size:15px;line-height:1.2;color:var(--swv-accent-950)}
/* no price of any kind in the furnishing chapter, a piece's or the total, preview included (Dave, 5 October) */
.dv1f-sec2 .styler .fopt .tick,.dv1f-sec2 .styler .fopt .pr,.dv1f-sec2 .styler .fvtot{display:none}
#v-details .dv1 .dv1f-sec2 .styler .frnote{font:400 12px/1.5 var(--swv-sans);font-style:normal;color:var(--swv-neutral-600);margin:32px 0 40px}

/* the render card */
#dv1f-media.dv1-media:has(.dv1f-stagewrap){align-self:stretch;overflow:visible;background:none;padding:12px 12px 12px 0;min-height:0}
/* sticks below the chapter's sticky tab bar, never under it */
.dv1f-stagewrap{position:sticky;top:calc(var(--dv1-tabbar-h,0px) + 12px);height:min(900px,calc(100vh - var(--dv1-tabbar-h,0px) - 24px))}
.dv1f-stagewrap .fstage{height:100%;border:0;border-radius:4px}
.dv1f-stagewrap .fcanvas{height:100%;aspect-ratio:auto}
.dv1f-stagewrap .flive{display:none}
.dv1f-stagewrap .fcap{padding:120px 20px 18px;background:linear-gradient(to top,rgba(10,9,6,.7),transparent)}
.dv1f-stagewrap .fcap span{font-size:11px;letter-spacing:.04em;font-weight:400}
.dv1f-stagewrap .fnoncap,#v-details .dv1 .dv1f-stagewrap p.fnoncap.smallprint{position:absolute;left:20px;right:20px;bottom:46px;margin:0;padding:0;z-index:5;font:400 16px/1.4 var(--swv-sans);color:#fff;opacity:.8;max-width:420px;font-size:16px;text-shadow:0 1px 8px rgba(0,0,0,.7)}

@media(max-width:899px){
  .dv1f-sec2:has(.dv1f-stagewrap){flex-direction:column;flex-wrap:nowrap}
  .dv1f-sec2:has(.dv1f-stagewrap) .dv1-col{width:100%;flex:none;padding-inline:20px}
  /* On a phone the render sits above the pickers, so its own column is only
     as tall as the render and a sticky child could not move. The column
     itself sticks instead, inside the chapter, which runs the whole list. */
  #dv1f-media.dv1-media:has(.dv1f-stagewrap){order:-1;width:100%;flex:none;align-self:auto;overflow:visible;
    background:var(--swv-neutral-50);padding:0;min-height:0;
    position:sticky;top:var(--dv1-tabbar-h,0px);z-index:3}
  .dv1f-stagewrap{position:static;height:auto}
  /* a field the keyboard focuses is brought up below the pinned render, not under it */
  #v-details .dv1f-sec2 :is(input,button,select,textarea){scroll-margin-top:calc(var(--dv1-tabbar-h,0px) + 56.25vw + 16px)}
  /* full width and 16:9, its height following its width: sized the other way
     round (by a fixed height) it came out wider than a phone and zoomed the page out */
  .dv1f-stagewrap .fstage{width:100%;height:auto;aspect-ratio:16/9;border-radius:0;border:0}
  .dv1f-sec2 .dv1-overview-h{margin-top:40px}
  #v-details .dv1 .dv1f-sec2 .styler :is(.fpal button,.ftabs button,.frp-chip){min-height:44px}
  .dv1f-sec2 .styler .fitem .ftop .l{white-space:nowrap}
}
/* a phone on its side: too short to stack the render over the pickers, so
   they sit side by side again, the render pinned on the right */
/* the furnishing chapter's phone type, on the scale; the sizes above are the
   kit's desktop half. Each selector repeats its base rule's so it wins. */
@media(max-width:860px){
  .dv1-overview-h{font-size:var(--swv-text-h2);line-height:1.15;letter-spacing:-.96px}
  .dv1f-name-label,.dv1f-name-submit{font-size:var(--swv-text-caption)}
  .dv1f-name-input{font-size:var(--swv-text-body)}
  .dv1f-sec2 .dv1-overview-h{font-size:var(--swv-text-h1);line-height:1.1;letter-spacing:-1.2px}
  .dv1f-sec2 .leadh{font-size:var(--swv-text-h3)}
  #v-details .dv1 .dv1f-sec2 .styler .fvbar .fvgrp .fvlab,
  #v-details .dv1 .dv1f-sec2 .styler .fsec-h{font-size:var(--swv-text-h2);letter-spacing:-.96px}
  .dv1f-sec2 .styler .fpanel .rhead h3{font-size:var(--swv-text-h2);letter-spacing:-.96px}
  #v-details .dv1 .dv1f-sec2 .styler .fitem .ftop .l{font-size:var(--swv-text-h4);letter-spacing:-.46px}
  #v-details .dv1 .dv1f-sec2 .styler .fitem .ftop .cur,
  #v-details .dv1 .dv1f-sec2 .styler .fopt .nm,
  #v-details .dv1 .dv1f-sec2 .styler .fopt.on .nm{font-size:var(--swv-text-caption)}
  .dv1f-stagewrap .fcap span{font-size:var(--swv-text-hint)}
}
/* the looks gallery on a phone: chips, then the tall pictures at their own ratio, full width */
@media(max-width:860px){
  .dv1f-name-card .dv1f-name-input,.dv1f-name-card .dv1f-name-submit{height:var(--swv-control-h)}
  #dv1f-media.dv1-media:has(.dv1f-looks){min-height:0;background:none}
  .dv1f-looks{height:auto}
  .dv1f-room::before{content:"";position:absolute;inset:calc((var(--swv-pill-h) - var(--swv-control-h))/2) 0}
  .dv1f-looks-view{flex:none;width:100%;min-height:0;aspect-ratio:1290/2312;max-height:100svh}
  .dv1f-looks-view .dv1-media-img{object-fit:cover}
  /* the looks follow the name card directly, ahead of the save form: the
     column's children join the section's own flow, as the numbers chapter does */
  .dv1f-sec2:has(.dv1f-name-card) .dv1-col{display:contents}
  .dv1f-sec2:has(.dv1f-name-card) .dv1-col > *{width:calc(100% - 48px);margin-left:24px;margin-right:24px;box-sizing:border-box;flex:none}
  .dv1f-sec2:has(.dv1f-name-card) .dv1-col > .dv1-overview{margin-top:24px}
  .dv1f-sec2:has(.dv1f-name-card) #dv1f-media{order:1}
  .dv1f-sec2:has(.dv1f-name-card) .dv1-col > :is(.dv1-rule,#lead-block){order:2}
}
@media(max-width:899px) and (orientation:landscape) and (max-height:540px){
  .dv1f-sec2:has(.dv1f-stagewrap){flex-direction:row}
  .dv1f-sec2:has(.dv1f-stagewrap) .dv1-col{width:55%;flex:none}
  #dv1f-media.dv1-media:has(.dv1f-stagewrap){order:0;width:45%;align-self:stretch;position:static;padding:8px 8px 8px 0}
  .dv1f-stagewrap{position:sticky;top:8px;height:calc(100vh - 16px)}
}
@media(prefers-reduced-motion:reduce){
  .styler .fcanvas img,.styler .foptwrap{transition:none}
}

/* ==========================================================================
   ABOUT, Figma 8:8873 "08. About." the idea and the village on one editorial
   page. #v-about carries its own ground and ink, the same way .dv1 does.

   Layout rule: a band is either a 640px centred column (.abt-sec is a three
   track grid, its children sit in the middle track) or full bleed (the place
   band, the collage and the closing photo reach both viewport
   edges with no gutter). Nothing here uses 100vw, so nothing can overflow. */
#v-about{background:var(--swv-neutral-50);color:var(--swv-accent-950)}
.abt{position:relative;--abt-hh:clamp(560px,100vh,828px);--abt-ch:clamp(560px,100vh,792px);
  --abt-hh:clamp(560px,100svh,828px);--abt-ch:clamp(560px,100svh,792px)}
/* RN104: under the text film the page is the film. Its hero is the window with
   nothing below it, so the page does not scroll, and the cut is picked by the
   window's own shape (abtTextFilmSrc). aboutHTML sets the class only when the
   film is built. It outranks the phone layout's 800px below. */
.abt.abt--text-film{--abt-hh:100vh;--abt-hh:100svh}
/* about-nav-start */
/* The About header is pinned (H67, Dave 12 Oct), not carried off with the hero.
   Fixed, it is the same 60px row and padding .dv1-nav gives it, and the only
   change is where it sits. Nothing above it in the page has a transform,
   filter, perspective, contain or will-change, so fixed means the window here.
   The view is display:none whenever About is not showing, and so is the
   header. z-index 3 is one above .abt-main (2), the highest layer the sections
   hold in the header's own stacking context; everything the header has to sit
   under is far higher (the menu 2000, Grace 810 and 820, the Motion panel 700),
   and the sign in dialog is in the top layer. The row is click through, so it
   never swallows a tap meant for the page behind it, and only the mark and the
   burger take one. .dv1-nav on the other pages is left as it is. */
#v-about .abt-nav{position:fixed;z-index:3;pointer-events:none}
#v-about .abt-nav :is(.dhero-mark,.dhero-burger){pointer-events:auto}
/* No bar. Over a photo the soft dark gradient that was always here keeps the
   sand mark and bars readable; over the plain page it fades out. It is a
   layer of its own because a gradient cannot be faded, an opacity can. */
.abt-nav::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:linear-gradient(180deg, rgba(12,11,9,.32), transparent)}
.abt-nav[data-ground="dark"]::before{opacity:1}
.abt-nav[data-ground="light"]::before{opacity:0}
/* Two marks, one box. The sand mark (dark ground) sets the box and the ink mark
   (light ground) lies exactly over it, so neither moves when they swap. The one
   not showing is hidden and out of reach, and a village that supplies only one
   of the files keeps it showing on both grounds (:only-of-type). */
.abt-nav-marks{position:relative;display:block;height:36px}
.abt-nav .abt-nav-marks .abt-nav-mark--light{position:absolute;left:0;top:0}
.abt-nav[data-ground="dark"] .abt-nav-mark--light:not(:only-of-type),
.abt-nav[data-ground="light"] .abt-nav-mark--dark:not(:only-of-type){opacity:0;visibility:hidden}
/* Bars: sand over a photo, the ink the page's own headings use over the page. */
.abt-nav[data-ground="dark"] .dhero-burger i{background:var(--swv-neutral-50)}
.abt-nav[data-ground="light"] .dhero-burger i{background:var(--swv-accent-950)}
/* The swap is a cross fade over 400ms on the file's own ease, and it is only
   there for a reader who has not asked for less motion: with the preference
   set nothing here transitions, so the ground changes in one frame. A mark
   going out turns hidden after its fade, which is what takes it out of the
   tab order, and one coming in shows at once. */
@media (prefers-reduced-motion:no-preference){
  .abt-nav-mark{transition:opacity 400ms var(--ease),visibility 0s}
  .abt-nav[data-ground="dark"] .abt-nav-mark--light:not(:only-of-type),
  .abt-nav[data-ground="light"] .abt-nav-mark--dark:not(:only-of-type){transition:opacity 400ms var(--ease),visibility 0s linear 400ms}
  .abt-nav::before{transition:opacity 400ms var(--ease)}
  .abt-nav .dhero-burger i{transition:transform .35s var(--ease),opacity .25s,background-color 400ms var(--ease)}
}
/* about-nav-end */

.abt-hero{position:relative;height:var(--abt-hh);overflow:hidden;background:var(--swv-accent-950)}
.abt-hero-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
/* The photo beneath is the poster; the film fades in only once it is actually
   playing, so there is never a black frame. */
.abt-hero-vid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity .8s ease}
.abt-hero-vid.is-on{opacity:1}
/* The text film says the hero's words itself, larger and in its own place, so
   on its page the hero's words never show (RN77): handed over while the film
   came in, the two headlines crossed in the middle. The h1 stays in the page for
   screen readers, and the words come back if the film fails (abtTextFilmFail
   takes the class away). The veil steps back while the film plays and returns
   with the photo when it stops. */
.abt--text-film .abt-hero-mid{opacity:0}
.abt-hero-vid--text ~ .abt-hero-veil{transition:opacity .8s ease}
.abt-hero-vid--text.is-on ~ .abt-hero-veil{opacity:0}
/* The text film's bar (filmBarHTML, drawn by filmWire) lies over the foot of the film
   in Temple Village's layout (.film-scenes .film-bar below) and its band heights.
   There is no black band under this picture, so a soft dark gradient, the film's own
   dark, holds the title and the controls against a bright frame. It is hidden, and out
   of the tab order, until the film shows, and comes in with it. The
   veil and the words fill the hero, so z-index 1 keeps the bar above them. */
.abt-hero{--film-band:104px}
@media(max-width:700px){ .abt-hero{--film-band:88px} }
@media(max-height:500px){ .abt-hero{--film-band:64px} }
.abt-hero .film-bar{z-index:1;visibility:hidden;opacity:0;transition:opacity .8s ease,visibility 0s .8s;
  background:linear-gradient(0deg,rgba(12,11,9,.7),rgba(12,11,9,.5) 40%,rgba(12,11,9,0))}
/* The Gracie launcher (#c-fab, 44px at 16px from the corner) stands at the hero's foot
   whenever the hero fills the window, so the last segment ends short of it. The id
   outranks the shared bar rule's padding further down. */
#v-about .abt-hero .film-bar{padding-right:max(clamp(16px,4vw,48px),76px)}
.abt-hero-vid--text.is-on ~ .film-bar{visibility:visible;opacity:1;transition:opacity .8s ease}
.abt-hero-veil{position:absolute;inset:0;background:
  /* The design keeps the photo clear and the type light, with only a soft
     dark pool behind the words, not a wash over the whole frame. */
  radial-gradient(46% 34% at 50% 52%, rgba(10,9,6,.5), rgba(10,9,6,.28) 48%, rgba(10,9,6,0) 82%),
  linear-gradient(180deg, rgba(10,9,6,.22), rgba(10,9,6,0) 26%, rgba(10,9,6,0) 72%, rgba(10,9,6,.3))}
.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:400 var(--swv-text-body-serif)/21px var(--copy-face);letter-spacing:-.48px;color:var(--swv-neutral-200);margin:0}
.abt-hero-h{font:400 var(--swv-text-h1)/53px var(--swv-serif);letter-spacing:-1.44px;color:var(--swv-neutral-50);margin:0}
.abt-hero-p{font:400 var(--swv-text-body-serif)/21px var(--copy-face);letter-spacing:-.48px;color:var(--swv-neutral-100);max-width:280px;margin:0}

/* The page ground has to cover the pinned hero when the idea slides over it. */
.abt-main{position:relative;background:var(--swv-neutral-50)}
/* RN62: under the text film the ground around the hero's window is the village's
   green, not the page's cream. The share band, when the page keeps it (the film
   has no share scene yet), stays on the cream. */
.abt--text-film .abt-hero-wrap{background:var(--swv-accent-950)}
.abt--text-film .abt-main{background:var(--swv-accent-950)}
.abt--text-film .abt-share{background:var(--swv-neutral-50)}
/* aboutTocEnabled() (engine.js) only renders this <aside> under ?_preview=true.
   It sits out of the flow so the bands can run edge to edge either way. */
.abt-anchors-col{position:absolute;top:0;bottom:0;left:24px;width:200px;z-index:3;pointer-events:none}
.abt-anchors{pointer-events:auto;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 40px 16px 16px;border-radius:2px;border:none;font:500 var(--swv-text-body)/1.33 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)}
@media(max-width:1199px){ .abt-anchors-col{display:none} }
/* Motion v2 keeps the floating index hidden: it overlaps the hero pullback. */
#v-about[data-mv2="on"] .abt-anchors-col{display:none}

/* The preview only motion panel (about-motion-panel.js). Bottom left, so it
   never sits on Grace's launcher at bottom right; a bottom sheet on phones. */
#mv2-panel{position:fixed;left:16px;bottom:16px;z-index:700;font:500 14px/1.3 var(--swv-sans);color:var(--swv-neutral-900)}
#mv2-panel[hidden]{display:none}
:where(#mv2-panel) :is(button,input){font:inherit;color:inherit}
#mv2-panel :focus-visible{outline:2px solid var(--swv-accent-950);outline-offset:2px}
.mv2-toggle{display:block;min-height:40px;padding:0 18px;border-radius:99px;cursor:pointer;
  background:var(--swv-neutral-50);border:1px solid var(--swv-neutral-500);color:var(--swv-neutral-900)}
.mv2-toggle:hover{background:var(--swv-neutral-100)}
#mv2-panel[data-open="1"] .mv2-toggle{display:none}
.mv2-body{box-sizing:border-box;width:360px;max-width:calc(100vw - 32px);max-height:min(70vh,560px);overflow:auto;
  padding:14px 16px 16px;background:var(--swv-neutral-50);border:1px solid var(--swv-neutral-500);border-radius:4px;
  box-shadow:0 8px 28px rgba(12,11,9,.22)}
.mv2-body[hidden]{display:none}
.mv2-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}
.mv2-title{margin:0;font:500 var(--swv-text-h5)/1.2 var(--swv-serif);color:var(--swv-neutral-900)}
.mv2-close,.mv2-act,.mv2-tabs [role=tab],.mv2-seg button{cursor:pointer;background:transparent;border:1px solid var(--swv-neutral-500);border-radius:2px;min-height:36px;padding:0 12px}
.mv2-close{border-color:transparent;text-decoration:underline}
.mv2-note{margin:0 0 10px;font-size:13px;color:var(--swv-neutral-700)}
.mv2-tabs{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px}
.mv2-tabs [role=tab]{padding:0 10px;min-height:32px;font-size:13px;color:var(--swv-neutral-800)}
.mv2-tabs [role=tab][aria-selected=true]{background:var(--swv-accent-950);border-color:var(--swv-accent-950);color:var(--swv-neutral-50)}
.mv2-pnl[hidden]{display:none}
.mv2-seg{display:flex;gap:4px;margin-bottom:12px}
.mv2-seg button{flex:1}
.mv2-seg button[aria-pressed=true]{background:var(--swv-accent-950);border-color:var(--swv-accent-950);color:var(--swv-neutral-50)}
.mv2-row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:2px 12px;margin-bottom:10px}
.mv2-row label{font-size:14px;color:var(--swv-neutral-900)}
.mv2-out{font-size:13px;font-variant-numeric:lining-nums tabular-nums;color:var(--swv-neutral-700)}
.mv2-row input{grid-column:1/-1;width:100%;min-height:28px;margin:0;accent-color:var(--swv-accent-950)}
.mv2-pnl > .mv2-act,.mv2-foot .mv2-act{background:var(--swv-neutral-100)}
.mv2-foot{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--swv-divider)}
.mv2-status{margin:8px 0 0;min-height:16px;font-size:13px;color:var(--swv-neutral-700)}
@media(max-width:860px){
  #mv2-panel{left:0;right:0;bottom:0;padding:0 0 0 16px;pointer-events:none}
  #mv2-panel > *{pointer-events:auto}
  #mv2-panel[data-open="1"]{padding:0}
  .mv2-toggle{margin-bottom:16px}
  .mv2-body{width:100%;max-width:none;max-height:62vh;border-radius:12px 12px 0 0;border-width:1px 0 0}
}

.abt-wm > i{font-style:normal}
.abt-sec{display:grid;grid-template-columns:minmax(16px,1fr) minmax(0,640px) minmax(16px,1fr);
  padding:90px 0;text-align:center}
.abt-sec > *{grid-column:2}
.abt-sec > .abt-collage{grid-column:1/-1}
#about-people[hidden],.abt-anchor[hidden]{display:none}
#about-idea{padding-top:80px;padding-bottom:120px}

/* 02 The place: a full bleed band. Two page coloured layers (.abt-cut, below)
   sit over the photo, each cut from the design's reference frame, so the photo
   shows through torn top and bottom edges. Without the artwork there are no
   layers and the band is a plain rectangle. The scroll rule below slides the
   layers toward each other with transform. The older ink blot layers
   (.abt-edge, with .abt-seam for the reworked art) are drawn only by place=svg,
   which wraps them in .abt-edges; their full sheet mask overhangs by 32px so
   the layer's own edge leaves no 1px line. */
.abt-sec--torn{display:block;position:relative;padding:0;text-align:center;container-type:inline-size;
  aspect-ratio:1440/800;color:var(--swv-neutral-50);--abt-u:calc(100cqi/1440);--abt-s:var(--abt-u);
  --abt-t0:calc(290*var(--abt-s))}
.abt-place-media{position:absolute;inset:0;z-index:0;overflow:hidden;background:var(--swv-accent-950)}
/* The layer is the blot plus page colour past it. Excluding the artwork from a
   full sheet leaves the page colour wherever the blot is empty. */
.abt-edge{--abt-pad:0;--abt-trim:0;--abt-shift:0;
  position:absolute;left:calc(50% - 720*var(--abt-u));width:calc(1440*var(--abt-u));
  height:calc((626 - var(--abt-trim))*var(--abt-u) + 100%);background:var(--swv-neutral-50);
  -webkit-mask-image:linear-gradient(#000,#000),var(--abt-edge);mask-image:linear-gradient(#000,#000),var(--abt-edge);
  -webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;
  -webkit-mask-composite:xor;mask-composite:exclude;
  -webkit-mask-size:calc(100% + 64px) calc(100% + 64px),calc((1440 + 2*var(--abt-pad))*var(--abt-u)) calc(626*var(--abt-u));
  mask-size:calc(100% + 64px) calc(100% + 64px),calc((1440 + 2*var(--abt-pad))*var(--abt-u)) calc(626*var(--abt-u))}
.abt-seam{--abt-pad:64;--abt-trim:16;--abt-shift:36}
/* The legacy ink blot layers only draw for place=svg, which wraps them in
   .abt-edges; every other variant shows the .abt-cut layers below. */
.abt-edge{display:none}
.abt-edges .abt-edge{display:block}
/* The Place's torn edges, cut from the design's reference frame (1440 by 800,
   see tools/cut-place-edges.sh): each art is 1440 by 184, the torn 180 rows and
   4 solid rows on the outer side. The art is an alpha mask over a page coloured
   layer, with a solid mask above it so the layer reaches past the band; the 2px
   overlap keeps a fractional scale from showing a seam. At rest (the band centred
   in the window) the top art's torn edge ends 180 units below the band's top and
   the bottom art starts 180 units above its bottom, which is the reference's place.
   --abt-s is the scale: one unit is 1/1440 of the band's width on a desktop, a
   fixed .6px on a phone, where the art is cropped at the sides and keeps its height. */
.abt-cut{position:absolute;left:calc(50% - 720*var(--abt-s));width:calc(1440*var(--abt-s));
  height:calc(184*var(--abt-s) + 100%);background:var(--swv-neutral-50);pointer-events:none}
.abt-cut--top{top:calc(-4*var(--abt-s) - 100%);
  -webkit-mask:linear-gradient(#000,#000) top/100% calc(100% - 184*var(--abt-s) + 2px) no-repeat,var(--abt-cut) bottom/100% calc(184*var(--abt-s)) no-repeat;
  mask:linear-gradient(#000,#000) top/100% calc(100% - 184*var(--abt-s) + 2px) no-repeat,var(--abt-cut) bottom/100% calc(184*var(--abt-s)) no-repeat}
.abt-cut--bot{top:calc(100% - 180*var(--abt-s));
  -webkit-mask:linear-gradient(#000,#000) bottom/100% calc(100% - 184*var(--abt-s) + 2px) no-repeat,var(--abt-cut) top/100% calc(184*var(--abt-s)) no-repeat;
  mask:linear-gradient(#000,#000) bottom/100% calc(100% - 184*var(--abt-s) + 2px) no-repeat,var(--abt-cut) top/100% calc(184*var(--abt-s)) no-repeat}
.abt-place-media:has(.abt-edges) .abt-cut,.abt-place--bloom .abt-cut{display:none}
.abt-edge--top{top:calc((var(--abt-shift) - 388)*var(--abt-u) - 100%);
  -webkit-mask-position:-32px -32px,left calc(-1*var(--abt-pad)*var(--abt-u)) bottom calc(-1*var(--abt-trim)*var(--abt-u));
  mask-position:-32px -32px,left calc(-1*var(--abt-pad)*var(--abt-u)) bottom calc(-1*var(--abt-trim)*var(--abt-u))}
.abt-edge--bot{top:calc(100% - (248 + var(--abt-shift) - var(--abt-trim))*var(--abt-u));
  -webkit-mask-position:-32px -32px,left calc(-1*var(--abt-pad)*var(--abt-u)) top calc(-1*var(--abt-trim)*var(--abt-u));
  mask-position:-32px -32px,left calc(-1*var(--abt-pad)*var(--abt-u)) top calc(-1*var(--abt-trim)*var(--abt-u))}
.abt-place-img{position:absolute;left:0;top:-12%;width:100%;height:124%;object-fit:cover}
.abt-place-veil{position:absolute;inset:0;
  background:radial-gradient(ellipse 52% 46% at 50% 50%, rgba(6,10,4,.3), rgba(6,10,4,.12) 62%, rgba(6,10,4,0)),rgba(6,10,4,.42)}
.abt-place-in{position:relative;z-index:1;box-sizing:border-box;height:100%;padding:0 16px;
  display:flex;flex-direction:column;align-items:center;justify-content:center}
.abt-place-in > *{max-width:640px}
#about-place + .abt-sec{padding-top:80px}

.abt-eyebrow{font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;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 var(--swv-text-h1)/1.1 var(--swv-serif);letter-spacing:-1px;margin:0 auto 20px;max-width:560px;text-wrap:balance}
.abt-h--sm{font-size:var(--swv-text-h3);line-height:1.15;letter-spacing:-.5px;max-width:none;margin-bottom:8px}
#about-idea .abt-h{margin-bottom:40px}
#about-people .abt-h{max-width:240px;text-wrap:wrap}
/* A share in the village: the heading's two lines must each stay whole at
   desktop, so it takes a wider box than .abt-h, and wraps on a phone. Two
   paragraphs of prose read better a little wider than the 364px default. */
.abt-share .abt-h{max-width:640px}
.abt-share .abt-body{max-width:440px;text-wrap:pretty}
.abt-share .abt-body:last-child{margin-bottom:0}
#about-place .abt-h{margin-bottom:24px}
.abt-subhead{font:400 var(--swv-text-h3)/1.15 var(--swv-serif);letter-spacing:-.6px;margin:0 0 18px}
.abt-lead{font:400 var(--swv-text-body-serif)/21px var(--copy-face);letter-spacing:-.48px;color:var(--swv-neutral-600);margin:0 auto 20px}
.abt-lead .abt-s{display:block}
.abt-sec--dark .abt-lead{color:var(--swv-neutral-300)}
.abt-body{font:400 var(--swv-text-body-serif)/21px var(--copy-face);letter-spacing:-.48px;color:var(--swv-neutral-700);margin:0 auto 20px;max-width:364px}
.abt-sec--dark .abt-body{color:var(--swv-neutral-100)}
.abt-sec--dark .abt-body:last-child{margin-bottom:0}
#about-idea .abt-body{margin-bottom:40px;max-width:360px}
#about-people .abt-body{max-width:340px}
#about-people .abt-h{margin-bottom:14px}

/* 01 The idea's count cards: three in a row, the middle one shaded. */
.abt-count-grid{margin:0 auto;width:100%;text-align:left;display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.abt-count-card{border:1px solid var(--swv-neutral-200);border-radius:4px;padding:12px;
  display:flex;flex-direction:column;gap:24px;box-sizing:border-box}
.abt-count-card--dim{background:var(--swv-neutral-100)}
.abt-count-k{display:block;font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;color:var(--tx-light-muted)}
.abt-count-n{display:block;font:400 var(--swv-text-numerals-md)/1.12 var(--swv-sans);color:var(--swv-accent-950)}

/* 03 Life beyond your front door: three cards, one row at desktop, a column
   at phone width (the scroll rules below stack them). Each has its own solid
   ground so one can slide over another. */
.abt-life-grid{margin-top:26px;text-align:left;display:flex;flex-direction:column;gap:16px}
.abt-life-card{border:1px solid var(--swv-neutral-200);border-radius:4px;padding:12px 16px 16px;box-sizing:border-box;
  background:var(--swv-neutral-50);display:flex;flex-direction:column}
.abt-life-card--dark{background:var(--swv-accent-950);color:var(--swv-neutral-50);border-color:var(--swv-accent-950)}
/* The Village's three cards carry three grounds (Dave, 10 Oct): pale green,
   the deep green card, sand. Every text colour on both light grounds clears
   6:1. The rule takes a step darker than its ground so it still reads. */
#about-life .abt-life-card:nth-child(1):not(.abt-life-card--dark){background:var(--swv-accent-100);border-color:var(--swv-accent-100)}
#about-life .abt-life-card:nth-child(1):not(.abt-life-card--dark) .abt-life-rule{background:var(--swv-accent-200)}
#about-life .abt-life-card:nth-child(3):not(.abt-life-card--dark){background:var(--swv-neutral-200);border-color:var(--swv-neutral-200)}
#about-life .abt-life-card:nth-child(3):not(.abt-life-card--dark) .abt-life-rule{background:var(--swv-neutral-300)}
.abt-life-n{font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;color:var(--tx-light-muted);margin-bottom:12px}
.abt-life-card--dark .abt-life-n{color:var(--swv-neutral-400)}
.abt-life-t{font:400 var(--swv-text-body-lg)/1.18 var(--copy-face);margin:0 0 8px}
.abt-life-sub{font:400 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;color:var(--tx-light-muted);margin:0 0 16px}
.abt-life-card--dark .abt-life-sub{color:var(--swv-neutral-300)}
.abt-life-rule{height:1px;background:var(--swv-neutral-200);margin:auto 0 16px}
.abt-life-card--dark .abt-life-rule{background:rgba(250,250,245,.16)}
.abt-life-over{font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;text-transform:uppercase;color:var(--swv-accent-950);margin-bottom:8px}
.abt-life-card--dark .abt-life-over{color:var(--swv-neutral-50)}
.abt-life-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0}
.abt-life-list li{font:400 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;color:var(--tx-light-muted);padding-left:16px;position:relative}
.abt-life-list li::before{content:"";position:absolute;left:4px;top:7px;width:2px;height:2px;border-radius:99px;background:var(--tx-light-muted)}
.abt-life-card--dark .abt-life-list li{color:var(--swv-neutral-300)}
.abt-life-card--dark .abt-life-list li::before{background:var(--swv-neutral-300)}
#about-life{padding-bottom:14px}
/* Two rows, broken where the copy says (a newline in lifeHeading), never more. */
#about-life .abt-h{margin-bottom:16px;max-width:none}
/* At desktop the three cards share six rows through subgrid, so each
   divider, label and list starts on the same line as its neighbours whatever
   one card's copy wraps to. Rows are named by part, since a card without a
   sub or a label still has to leave that row to the others. */
@media(min-width:861px){
  .abt-life-grid{display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(6,auto);column-gap:8px;row-gap:0}
  .abt-life-card{display:grid;grid-row:span 6;grid-template-rows:subgrid;align-content:start}
  .abt-life-n{grid-row:1}
  .abt-life-t{grid-row:2}
  .abt-life-sub{grid-row:3}
  .abt-life-rule{grid-row:4;margin-top:0}
  .abt-life-over{grid-row:5}
  .abt-life-list{grid-row:6}
}

/* The sign in popup (NR5): a native modal dialog, as a centred card on
   the Gracie panel's own ground. The login page's own wording and buttons. */
dialog.swv-si-dlg{margin:auto;width:min(440px,calc(100vw - 32px));max-height:calc(100svh - 32px);overflow:auto;padding:40px 24px 28px;
  border:0;border-radius:6px;background:var(--swv-neutral-50);color:var(--swv-neutral-950);box-shadow:0 30px 90px rgba(0,0,0,.5);
  text-align:center}
dialog.swv-si-dlg[open]{display:flex;flex-direction:column;align-items:center;gap:14px;animation:swvSiIn 240ms ease both}
dialog.swv-si-dlg::backdrop{background:color-mix(in srgb,var(--swv-neutral-950) 55%,transparent)}
.swv-si-h{margin:0 0 6px;font:400 var(--swv-text-h3)/1.15 var(--swv-serif);letter-spacing:-.6px}
.swv-si-google{width:100%;max-width:240px;gap:10px}
.swv-si-note{margin:0;font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600)}
/* the emailed link: the access card's pill field and the kit's own button, as wide as the Google button */
.swv-si-or{margin:0;font:400 var(--swv-text-body)/20px var(--swv-sans);color:var(--swv-neutral-600)}
.swv-si-form{display:flex;flex-direction:column;gap:12px;width:100%;max-width:240px;min-width:0;text-align:left}
.swv-si-field{display:flex;flex-direction:column;gap:4px}
.swv-si-cap{font:400 var(--swv-text-hint)/14px var(--swv-sans);letter-spacing:.14em;text-transform:uppercase;color:var(--swv-neutral-600)}
.swv-si-field input{box-sizing:border-box;width:100%;height:36px;padding:0 16px;border-radius:var(--swv-radius-pill);
  background:var(--swv-neutral-100);border:1px solid var(--swv-neutral-300);
  font:400 var(--swv-text-body)/18px var(--swv-sans);color:var(--swv-neutral-950)}
.swv-si-field input::placeholder{color:var(--swv-neutral-600)}
.swv-si-field input:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px;border-color:var(--swv-accent-950)}
.swv-si-send{width:100%}
.swv-si-status{margin:0;min-height:18px;font:400 var(--swv-text-caption)/18px var(--swv-sans);color:var(--swv-neutral-600);overflow-wrap:anywhere;text-align:center}
@media (max-width:860px){
  .swv-si-field input{min-height:var(--swv-control-h)}
}
.swv-si-x{position:absolute;top:6px;right:6px;width:44px;height:44px;padding:0;border:0;border-radius:99px;background:transparent;color:var(--swv-neutral-950);cursor:pointer}
.swv-si-x i{position:absolute;left:50%;top:50%;width:20px;height:20px;transform:translate(-50%,-50%)}
.swv-si-x i::before,.swv-si-x i::after{content:"";position:absolute;left:0;top:9px;width:20px;height:2px;background:currentColor;transform:rotate(45deg)}
.swv-si-x i::after{transform:rotate(-45deg)}
.swv-si-x:focus-visible{outline:2px solid var(--swv-accent-500);outline-offset:2px}
@keyframes swvSiIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){
  dialog.swv-si-dlg[open]{animation:none}
}

/* 04 The people: one even grid (comment 29). The founders' photo spans the
   full width, then the six tiles share equal cells: two across on a phone,
   three across on a desktop, so no row is left with a gap. */
.abt-collage{margin:28px 16px 0;text-align:left;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
#about-people{container-type:inline-size;padding-bottom:0}
.abt-mosaic{display:contents}
.abt-people-photo,.abt-mosaic-item{margin:0;border-radius:4px;overflow:hidden;min-width:0}
.abt-people-photo{grid-column:1/-1;aspect-ratio:16/9}
.abt-mosaic-item{aspect-ratio:4/5}
.abt-people-photo img,.abt-mosaic-item img{display:block;width:100%;height:100%;object-fit:cover}
@media(min-width:861px){
  .abt-collage{grid-template-columns:repeat(3,1fr);gap:16px}
}

/* The closing call to action, over its own photo, ABOUT_CTA, not the hero's. */
.abt-cta{position:relative;min-height:var(--abt-ch);box-sizing:border-box;padding-block:clamp(64px,10svh,120px);overflow:hidden;display:flex;align-items:center;justify-content:center;background:var(--swv-accent-950)}
/* The closing photo settles to rest as its top reaches the top of the window.
   With no footer under it the page would end a little before that, so here it
   is never shorter than the window. */
body:has(#foot:empty) .abt-cta{min-height:max(var(--abt-ch),100vh);min-height:max(var(--abt-ch),100svh)}
.abt-cta-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.abt-cta-veil{position:absolute;inset:0;background:linear-gradient(180deg, rgba(10,12,8,.06), rgba(10,12,8,.26))}
.abt-cta-mid{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:0 clamp(16px,6vw,96px);color:var(--swv-neutral-50)}
.abt-cta-desc,.abt-end-desc{font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:.02em;margin:0}
.abt-cta-h,.abt-end-h{font:400 var(--swv-text-h1)/1.1 var(--swv-serif);letter-spacing:-1px;margin:0}

/* RN104: how the page closes (abtCloseHTML). The words and three ways on, one
   under the other, beside up to six Instagram posts three across; stacked on a
   phone. The text film's end screen and the closing band of a page without the
   film both use it, on a dark ground. A post is as large as the window's height
   and width both allow, so two rows fit between the header and the film's bar. */
.abt-close{--abt-post:clamp(96px,min((100svh - 340px) / 2.6,(100vw - 600px) / 3),232px);
  display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:clamp(32px,5vw,96px);
  width:100%;max-width:1160px;margin:0 auto;color:var(--swv-neutral-50);text-align:left}
.abt-close-copy{display:flex;flex-direction:column;align-items:center;gap:16px;min-width:0;text-align:center}
.abt-close--solo{grid-template-columns:minmax(0,1fr)}
.abt-close-acts{display:flex;flex-direction:column;align-items:stretch;gap:12px;width:min(100%,300px);margin-top:16px}
.abt-close-acts .swv-btn{min-height:44px}
.abt-close-reach{align-self:center;padding:10px 16px;border-radius:4px;color:var(--swv-neutral-50);
  font:500 var(--swv-text-body)/1.33 var(--swv-sans);text-decoration:none}
.abt-close-reach:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}
.abt-close-posts{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,var(--abt-post));gap:8px}
.abt-close-post{position:relative;display:block;aspect-ratio:4/5;overflow:hidden;border-radius:6px;background:rgba(250,250,245,.08)}
.abt-close-post img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .5s cubic-bezier(.22,1,.36,1)}
.abt-close-post:hover img{transform:scale(1.04)}
.abt-close-post:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:3px}
/* a reel or a video carries a small play mark */
.abt-close-post.is-film::before{content:"";position:absolute;z-index:1;top:10px;right:10px;border:7px solid transparent;border-left:11px solid #FFFFFF;border-right:0;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
/* An outlined button for a dark ground, the second of a pair after .swv-btn--light.
   The line is a shadow, so the button is the same size as the filled one. */
.swv-btn--line{background:transparent;color:var(--swv-neutral-50);box-shadow:inset 0 0 0 1px rgba(250,250,245,.6)}
.swv-btn--line:hover{background:rgba(250,250,245,.12)}
.swv-btn--line svg{width:18px;height:18px;flex:none}

/* RN104: the text film's end screen (abtEndHTML), over its last frame and under
   its bar, which stays: a dark wash for the words, the close in the room the
   header and the bar leave, and Watch again at the bar's left edge, just above
   it. It fades in when the film ends and out when it plays again. */
.abt-end{position:absolute;inset:0;z-index:1;box-sizing:border-box;display:flex;flex-direction:column;justify-content:center;justify-content:safe center;
  padding:76px clamp(16px,6vw,96px) calc(var(--film-band) + 60px);background:rgba(12,11,9,.58);
  visibility:hidden;opacity:0;transition:opacity .6s ease,visibility 0s .6s}
.abt-end.is-on{visibility:visible;opacity:1;transition:opacity .6s ease}
.abt-end-again{position:absolute;left:clamp(16px,4vw,48px);bottom:calc(var(--film-band) + 8px);display:inline-flex;align-items:center;gap:8px;
  min-height:44px;padding:0 12px 0 0;border:0;border-radius:4px;background:none;color:var(--swv-neutral-50);
  font:500 var(--swv-text-body)/1.33 var(--swv-sans);cursor:pointer}
.abt-end-again:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:2px}
/* A phone, and any upright window such as a tablet held upright, stacks the words
   over the posts; the tablet's posts are larger. */
@media(max-width:860px),(max-aspect-ratio:1/1){
  .abt-close{grid-template-columns:minmax(0,1fr);justify-items:center;gap:24px}
  .abt-close-posts{grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,360px)}
}
@media(min-width:861px) and (max-aspect-ratio:1/1){ .abt-close{gap:40px} .abt-close-posts{width:min(100%,560px)} }
@media(max-width:860px){
  .abt-close-copy{gap:12px}
  .abt-close-acts{width:min(100%,320px);margin-top:8px;gap:10px}
  .abt-end{padding:72px 16px calc(var(--film-band) + 56px)}
  .abt-end-h{font-size:var(--swv-text-h2);line-height:1.1}
}
/* A short phone keeps one row of posts, a shorter one none; a window as short as a
   phone on its side puts the three ways on in a row. */
@media(max-width:860px) and (max-height:780px){ .abt-close-posts li:nth-child(n+4){display:none} }
@media(max-width:860px) and (max-height:640px){ .abt-end .abt-close-posts,.abt-end-desc{display:none} }
@media(max-height:500px){
  .abt-end{padding-top:60px}
  .abt-end .abt-close-acts{flex-direction:row;flex-wrap:wrap;justify-content:center;width:auto}
  .abt-end-h{font-size:var(--swv-text-h3)}
}

@media(max-width:860px){
  /* A phone frame is tall and narrow, so the desktop pool is too small for
     three centred lines. A gentle dark band the same shape covers the text
     instead, the photo still clear above and below it. */
  .abt{--abt-hh:clamp(560px,100vh,800px);--abt-ch:clamp(560px,100vh,712px);
    --abt-hh:clamp(560px,100svh,800px);--abt-ch:clamp(560px,100svh,712px)}
  .abt-hero-veil{background:linear-gradient(180deg,
    rgba(10,9,6,0) 0%, rgba(10,9,6,.32) 22%, rgba(10,9,6,.52) 38%,
    rgba(10,9,6,.52) 68%, rgba(10,9,6,.32) 84%, rgba(10,9,6,.12) 100%)}
  .abt-hero-h{font-size:var(--swv-text-h1);line-height:44px;letter-spacing:-1.2px}
  .abt-sec{padding:64px 0}
  #about-idea{padding-top:64px;padding-bottom:60px}
  .abt-h{font-size:var(--swv-text-h2);line-height:36px;letter-spacing:-.8px;max-width:none}
  .abt-h--sm{font-size:var(--swv-text-h3);line-height:1.15}
  .abt-body,#about-idea .abt-body,#about-people .abt-body{max-width:none}
  .abt-count-card--wide{grid-column:1/-1}
  .abt-count-grid{grid-template-columns:1fr 1fr}
  .abt-life-t{font-size:var(--swv-text-body-lg);line-height:1.19}
  .abt-sec--torn{aspect-ratio:auto;min-height:720px}
  .abt-sec.abt-sec--torn{padding:0}
  #about-place + .abt-sec{padding-top:65px}
  .abt-sec--torn{--abt-u:1px;--abt-s:.6px;--abt-t0:300px}
  .abt-place-in{min-height:720px}
  .abt-cta-h{font-size:var(--swv-text-h1)}
}

/* ==========================================================================
   About's scroll driven motion. The scroll position is the only clock:
   nothing plays on its own and nothing loops. Both guards must hold, so a
   browser without view() timelines, or a reader who asked for less motion,
   gets the finished page as the rules above paint it. Only transform,
   translate, opacity, clip-path and one registered integer
   are animated, never a layout property, and there is no blur. */
@property --abt-n{syntax:"<integer>";inherits:true;initial-value:0}

@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {

  @keyframes abtRise{ from{opacity:0; transform:translateY(48px)} to{opacity:1; transform:translateY(0)} }
  @keyframes abtRiseBig{ from{opacity:0; transform:translateY(96px)} to{opacity:1; transform:translateY(0)} }
  @keyframes abtHeroZoom{ from{transform:scale(1)} to{transform:scale(1.25)} }
  @keyframes abtHeroDim{ from{opacity:0} to{opacity:.55} }
  @keyframes abtHeroOut{ from{opacity:1; transform:translateY(0)} to{opacity:0; transform:translateY(-110px)} }
  @keyframes abtWordUp{ from{transform:translateY(112%)} to{transform:translateY(0)} }
  @keyframes abtCount{ from{--abt-n:0} to{--abt-n:var(--abt-to)} }
  @keyframes abtEdgeTop{ from{transform:translateY(max(24cqi,200px))} to{transform:translateY(0)} }
  @keyframes abtEdgeBot{ from{transform:translateY(calc(-1*max(24cqi,200px)))} to{transform:translateY(0)} }
  @keyframes abtCutTop{ from{transform:translateY(var(--abt-t0))} to{transform:translateY(0)} }
  @keyframes abtCutBot{ from{transform:translateY(calc(-1*var(--abt-t0)))} to{transform:translateY(0)} }
  @keyframes abtDrift{ from{transform:translateY(-9%)} to{transform:translateY(9%)} }
  @keyframes abtWipeC{ from{clip-path:inset(0 50% 0 50%)} to{clip-path:inset(0 0 0 0)} }
  @keyframes abtTile{ from{translate:0 var(--abt-r,0px)} to{translate:0 calc(var(--abt-r,0px) * -1)} }
  @keyframes abtTileImg{ from{transform:scale(1.25) translateY(var(--abt-d,0px))} to{transform:scale(1.25) translateY(calc(var(--abt-d,0px) * -1))} }
  @keyframes abtCtaZoom{ from{transform:scale(1.2)} to{transform:scale(1)} }

  /* 1 Hero, pinned. The hero sticks for one extra viewport of scroll; the
     idea section is pulled up over it by the same amount, so it slides up
     like a curtain while the photo keeps zooming and the words rise away. */
  .abt-hero-wrap{height:calc(var(--abt-hh) + 100svh)}
  .abt-hero{position:sticky;top:0;z-index:1}
  .abt-main{margin-top:-100svh;z-index:2}
  /* RN104: the text film's page does not scroll, so its hero is not pinned */
  .abt--text-film .abt-hero-wrap{height:auto}
  .abt--text-film .abt-main{margin-top:0}
  .abt-hero-img,.abt-hero-vid{animation:abtHeroZoom linear both; animation-timeline:scroll(root block); animation-range:0px 90svh}
  /* The text film's words are in its picture, inside its own safe zones, so
     it does not zoom with the photo. */
  #v-about .abt-hero-vid--text{animation:none}
  .abt-hero::after{content:"";position:absolute;inset:0;background:#0a0c08;opacity:0;pointer-events:none;
    animation:abtHeroDim linear both; animation-timeline:scroll(root block); animation-range:0px 90svh}
  .abt-hero-mid > *{animation:abtHeroOut linear both; animation-timeline:scroll(root block); animation-range:0px 50svh}
  .abt-hero-mid > *:nth-child(2){animation-range:4svh 46svh}
  .abt-hero-mid > *:nth-child(3){animation-range:8svh 54svh}

  /* 2 The idea. Each heading word rises out of its own mask, in reading order;
     the three counts run up from 0 as their cards come into view. */
  .abt-wm{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.16em;margin-bottom:-.16em;view-timeline:--abt-wm block}
  .abt-wm > i{display:inline-block;animation:abtWordUp linear both; animation-timeline:--abt-wm;
    animation-range:cover calc(4% + var(--k) * 8%) cover calc(26% + var(--k) * 8%)}
  #about-idea .abt-body{animation:abtRise linear both; animation-timeline:view(); animation-range:cover 5% cover 28%}
  .abt-mo-cards > *{animation:abtRise linear both; animation-timeline:view(); animation-range:cover 6% cover 30%}
  .abt-mo-cards > *:nth-child(2){animation-range:cover 9% cover 33%}
  .abt-mo-cards > *:nth-child(3){animation-range:cover 12% cover 36%}
  .abt-count-n{position:relative;color:transparent;animation:abtCount linear both; animation-timeline:view(); animation-range:cover 8% cover 40%}
  .abt-count-n::after{content:counter(abtn);counter-reset:abtn var(--abt-n);position:absolute;left:0;top:0;color:var(--swv-accent-950)}

  /* 3 The place. The torn edge layers slide apart from a sliver as the band
     enters, and the photo layer drifts against the scroll, 18% of its own
     height across the pass. */
  .abt-mo-place{view-timeline:--abt-place block}
  .abt-edge{will-change:transform;animation:abtEdgeTop linear both; animation-timeline:--abt-place; animation-range:entry 0% cover 42%}
  .abt-edge--bot{animation-name:abtEdgeBot}
  /* The cut edges start a sliver apart and land on the reference's places at
     rest, the band centred in the window (cover 50%), then hold. */
  .abt-cut{will-change:transform;animation:abtCutTop linear both; animation-timeline:--abt-place; animation-range:entry 0% cover 50%}
  .abt-cut--bot{animation-name:abtCutBot}
  .abt-place-img{animation:abtDrift linear both; animation-timeline:--abt-place; animation-range:cover 0% cover 100%}
  .abt-place-in > *{animation:abtRise linear both; animation-timeline:--abt-place; animation-range:entry 55% cover 40%}
  .abt-place-in > *:nth-child(2){animation-range:entry 60% cover 44%}
  .abt-place-in > *:nth-child(3){animation-range:entry 65% cover 48%}
  .abt-place-in > *:nth-child(4){animation-range:entry 70% cover 52%}
  .abt-place-in > *:nth-child(5){animation-range:entry 75% cover 56%}

  /* 4 Life. Phone: each card sticks a little lower than the last and the next
     slides over it. Desktop: the three rise in turn. */
  .abt-life-card{animation:abtRiseBig linear both; animation-timeline:view(); animation-range:entry 0% entry 70%}
  .abt-life-card:nth-child(2){animation-range:entry 10% entry 80%}
  .abt-life-card:nth-child(3){animation-range:entry 20% entry 90%}
  @media(max-width:860px){
    .abt-life-card{position:sticky;top:72px}
    .abt-life-card:nth-child(2){top:88px}
    .abt-life-card:nth-child(3){top:104px}
    .abt-life-grid{padding-bottom:8px}
  }

  /* 6b A share in the village. The heading and the two paragraphs rise in
     turn on the band's own pass; with no support or reduced motion they just
     sit there. */
  .abt-share{view-timeline:--abt-share block}
  .abt-share > *{animation:abtRise linear both; animation-timeline:--abt-share; animation-range:entry 40% cover 40%}
  .abt-share > *:nth-child(2){animation-range:entry 45% cover 44%}
  .abt-share > *:nth-child(3){animation-range:entry 50% cover 48%}

  /* 7 The people: the founders' photo wipes open from its centre; each column
     of tiles moves against the scroll at its own rate, every tile in a column
     at the same rate and every rate within 8px of the photo's, so no gap
     between tiles ever closes; the picture inside drifts too. */
  .abt-mo-collage{view-timeline:--abt-collage block}
  .abt-people-photo{animation:abtWipeC linear both, abtTile linear both;
    animation-timeline:view(), --abt-collage; animation-range:entry 0% cover 40%, cover 0% cover 100%}
  .abt-people-photo,.abt-mosaic-item{--abt-r:8px;--abt-d:30px}
  .abt-people-photo{--abt-d:0px}
  .abt-mosaic-item{animation:abtTile linear both; animation-timeline:--abt-collage; animation-range:cover 0% cover 100%}
  .abt-collage img{animation:abtTileImg linear both; animation-timeline:--abt-collage; animation-range:cover 0% cover 100%}
  .abt-mosaic-item:nth-child(odd){--abt-r:6px;--abt-d:-32px}
  .abt-mosaic-item:nth-child(even){--abt-r:10px;--abt-d:34px}
  @media(min-width:861px){
    .abt-people-photo{--abt-r:20px;--abt-d:22px}
    .abt-mosaic-item:nth-child(3n+1){--abt-r:20px;--abt-d:-28px}
    .abt-mosaic-item:nth-child(3n+2){--abt-r:24px;--abt-d:28px}
    .abt-mosaic-item:nth-child(3n){--abt-r:28px;--abt-d:-28px}
  }

  /* 10 The closing call: the photo settles from 1.2 to 1.0 as it enters while
     the heading rises. No pin, so nothing is left behind it but the footer. */
  .abt-cta-wrap{view-timeline:--abt-cta block}
  .abt-cta-img{animation:abtCtaZoom linear both; animation-timeline:--abt-cta; animation-range:cover 8% cover 52%}
  /* With no footer the page ends as the band's top meets the window's, which
     is cover 50% for a band a window tall: the photo is at rest there, not 2%
     short of it with nowhere left to scroll. */
  body:has(#foot:empty) .abt-cta-img{animation-range:cover 8% cover 50%}
  .abt-cta-mid{animation:abtRiseBig linear both; animation-timeline:--abt-cta; animation-range:cover 20% cover 50%}

  /* will-change only on the layers that move all the time. */
  .abt-hero-img,.abt-hero-vid,.abt-place-img,.abt-cta-img,.abt-collage img{will-change:transform}
}
}

/* ==========================================================================
   About motion v2, task 2 (H55): hero, idea and closing. Every rule here sits
   under a data-mv2 attribute, so nothing below paints or moves unless the gate
   is on and the section's variant is new. The line boxes, reels and outline
   are built at runtime (engine.js, aboutMv2Sync) under the same condition.
   mv2-t2-start */
/* One clipped box per rendered line. The clip reaches past the line box so
   ascenders and descenders stay inside it, and it is a clip, not padding, so
   the lines keep exactly the height and spacing the type already has. */
#v-about[data-mv2="on"] .abt-ln{display:block;clip-path:inset(-.16em -.2em -.28em -.2em)}
#v-about[data-mv2="on"] .abt-ln-i{display:block;white-space:nowrap}
/* The odometer: a one digit window over a strip of 0 to 9. Settled, the strip
   rests on its digit. Tabular lining figures keep every column the same width. */
#v-about[data-mv2-idea="new"] .abt-count-n.abt-reel{display:flex;color:var(--swv-accent-950);
  font-variant-numeric:lining-nums tabular-nums;animation:none}
#v-about[data-mv2-idea="new"] .abt-count-n.abt-reel::after{content:none}
/* The window is the digit's own line box (same font, same line height), and the
   strip is a pseudo element of ten lines, so a whole digit is exactly 10% of it. */
#v-about[data-mv2-idea="new"] .abt-rl{display:block;position:relative;overflow:hidden;color:transparent;
  view-timeline:--mv2-rl block}
#v-about[data-mv2-idea="new"] .abt-rl-s{position:absolute;left:0;top:0;color:var(--swv-accent-950);
  transform:translateY(calc(var(--d) * -10%))}
#v-about[data-mv2-idea="new"] .abt-rl-s::before{content:"0\A 1\A 2\A 3\A 4\A 5\A 6\A 7\A 8\A 9";white-space:pre}
/* The closing button: an outline that can be drawn, under a fill that can fade.
   Settled, the stroke sits exactly on the pill's edge in the fill's own colour. */
#v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light{isolation:isolate;background:transparent}
#v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light:hover{background:transparent}
#v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light::after{content:"";position:absolute;inset:0;z-index:-1;
  border-radius:inherit;background:var(--swv-neutral-50);transition:background-color .2s cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light:hover::after{background:var(--swv-neutral-200)}
#v-about[data-mv2-closing="new"] .abt-cta-ol{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
#v-about[data-mv2-closing="new"] .abt-cta-ol rect{x:.5px;y:.5px;width:calc(100% - 1px);height:calc(100% - 1px);rx:17.5px;ry:17.5px;
  fill:none;stroke:var(--swv-neutral-50);stroke-width:1px;stroke-dasharray:1;stroke-dashoffset:0}

@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
  /* Intensity is --mv2-<section>-intensity, 0 to 1.5, set on #v-about. */
  @keyframes abtMv2HeroWin{ from{clip-path:inset(0 0 round 0)}
    to{clip-path:inset(calc(6svh * var(--mv2-hero-intensity,1)) calc(6vw * var(--mv2-hero-intensity,1)) round calc(28px * var(--mv2-hero-intensity,1)))} }
  @keyframes abtMv2HeroMedia{ from{transform:scale(calc(1 + .1 * var(--mv2-hero-intensity,1)))} to{transform:scale(1)} }
  @keyframes abtMv2HeroMid{ from{transform:translateY(0)} to{transform:translateY(calc(-7svh * var(--mv2-hero-intensity,1)))} }
  @keyframes abtMv2HeroLift{ from{transform:translateY(0)} to{transform:translateY(-145%)} }
  /* --mv2-k is the owning section's intensity (idea or closing), set on the
     line boxes below. At 0 the lines start where they end: still and whole. */
  @keyframes abtMv2LineUp{ from{transform:translateY(calc(145% * var(--mv2-k,1)))} to{transform:translateY(0)} }
  @keyframes abtMv2LineBlur{ from{transform:translateY(calc(145% * var(--mv2-k,1)));filter:blur(calc(6px * var(--mv2-k,1)))} to{transform:translateY(0);filter:none} }
  @keyframes abtMv2Reel{ from{transform:translateY(calc(var(--d) * -10% * (1 - min(var(--mv2-idea-intensity,1),1))))} to{transform:translateY(calc(var(--d) * -10%))} }
  @keyframes abtMv2Draw{ from{stroke-dashoffset:min(var(--mv2-closing-intensity,1),1)} to{stroke-dashoffset:0} }
  @keyframes abtMv2Fill{ from{opacity:0} to{opacity:1} }
  @keyframes abtMv2Label{ from{color:transparent} to{color:var(--swv-accent-950)} }

  /* Hero. The frame pulls back from full bleed into a rounded window while
     the photo settles from 1.1; the lines then leave through their own masks,
     eyebrow first, title last, all before the idea has covered the middle. */
  #v-about[data-mv2-hero="new"] .abt-hero{animation:abtMv2HeroWin linear both;
    animation-timeline:scroll(root block);animation-range:0px 62svh}
  #v-about[data-mv2-hero="new"] .abt-hero-img,#v-about[data-mv2-hero="new"] .abt-hero-vid{
    animation:abtMv2HeroMedia linear both;animation-timeline:scroll(root block);animation-range:0px 62svh}
  #v-about[data-mv2-hero="new"] .abt-hero::after{animation:none;opacity:0}
  #v-about[data-mv2-hero="new"] .abt-hero-mid{animation:abtMv2HeroMid linear both;
    animation-timeline:scroll(root block);animation-range:0px 46svh}
  #v-about[data-mv2-hero="new"] .abt-hero-mid > *{animation:none}
  #v-about[data-mv2-hero="new"] .abt-hero-mid .abt-ln-i{animation:abtMv2HeroLift linear both;
    animation-timeline:scroll(root block)}
  #v-about[data-mv2-hero="new"] .abt-hero-desc .abt-ln-i{animation-range:calc(0svh + var(--i) * 3svh) calc(18svh + var(--i) * 3svh)}
  #v-about[data-mv2-hero="new"] .abt-hero-p .abt-ln-i{animation-range:calc(4svh + var(--i) * 3svh) calc(24svh + var(--i) * 3svh)}
  #v-about[data-mv2-hero="new"] .abt-hero-h .abt-ln-i{animation-range:calc(9svh + var(--i) * 5svh) calc(30svh + var(--i) * 5svh)}
  /* The text film holds still: its words are in its picture, and the settle
     from 1.15 at rest crops their first letters on a phone. */
  #v-about[data-mv2-hero="new"] .abt-hero-vid--text{animation:none}

  /* Idea. Heading lines rise and sharpen one after another; body lines rise,
     with no blur; the counts roll as their cards come in. */
  #v-about[data-mv2-idea="new"] #about-idea .abt-h,#v-about[data-mv2-idea="new"] #about-idea .abt-body{view-timeline:--mv2-ln block}
  #v-about[data-mv2-idea="new"] #about-idea .abt-body{animation:none}
  #v-about[data-mv2-idea="new"] #about-idea .abt-ln-i{--mv2-k:var(--mv2-idea-intensity,1)}
  #v-about[data-mv2-idea="new"] #about-idea .abt-h .abt-ln-i{animation:abtMv2LineBlur linear both;
    animation-timeline:--mv2-ln;animation-range:cover calc(4% + var(--i) * 6%) cover calc(26% + var(--i) * 6%)}
  #v-about[data-mv2-idea="new"] #about-idea .abt-body .abt-ln-i{animation:abtMv2LineUp linear both;
    animation-timeline:--mv2-ln;animation-range:cover calc(4% + var(--i) * 3%) cover calc(24% + var(--i) * 3%)}
  #v-about[data-mv2-idea="new"] .abt-rl-s{animation:abtMv2Reel linear both;animation-timeline:--mv2-rl;
    animation-range:cover calc(6% + var(--j) * 4%) cover calc(40% + var(--j) * 4%)}

  /* Closing. The photo settles as today; the lines rise in turn, then the
     button's outline is drawn, then its fill and label arrive. */
  #v-about[data-mv2-closing="new"] .abt-cta-mid{animation:none}
  #v-about[data-mv2-closing="new"] .abt-cta-mid .abt-ln-i{--mv2-k:var(--mv2-closing-intensity,1)}
  #v-about[data-mv2-closing="new"] .abt-cta-mid .abt-ln-i{animation:abtMv2LineUp linear both;animation-timeline:--abt-cta}
  #v-about[data-mv2-closing="new"] .abt-cta-desc .abt-ln-i{animation-range:cover 14% cover 28%}
  #v-about[data-mv2-closing="new"] .abt-cta-h .abt-ln-i{
    animation-range:cover calc(18% + var(--i) * 3.5%) cover calc(32% + var(--i) * 3.5%)}
  #v-about[data-mv2-closing="new"] .abt-cta-ol rect{animation:abtMv2Draw linear both;animation-timeline:--abt-cta;
    animation-range:cover 32% cover calc(44% + 6% * (max(var(--mv2-closing-intensity,1),1) - 1))}
  #v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light::after{animation:abtMv2Fill linear both;animation-timeline:--abt-cta;animation-range:cover 42% cover 50%}
  #v-about[data-mv2-closing="new"] .abt-cta-mid .swv-btn--light{animation:abtMv2Label linear both;animation-timeline:--abt-cta;animation-range:cover 43% cover 50%}
}
}
/* mv2-t2-end */

/* ==========================================================================
   About motion v2, task 3 (H55): village and people. Every rule sits
   under a data-mv2 attribute. The pin spacer, the sticky stage and every
   animation exist only inside the two guards, so a browser without view
   timelines, or reduced motion, paints the settled rules further up.
   mv2-t3-start */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
  /* Intensity is --mv2-<section>-intensity, 0 to 1.5, set on #v-about. */
  @keyframes abtMv2Scatter{
    from{transform:translate(calc(var(--sx) * 1vw * var(--mv2-people-intensity,1)),calc(var(--sy) * 1vw * var(--mv2-people-intensity,1))) rotate(calc(var(--sr) * 1deg * var(--mv2-people-intensity,1)));opacity:0}
    67%{opacity:1}
    to{transform:translate(0,0) rotate(0deg);opacity:1} }

  /* Village, the deck (H59, slides H62, beats H64, pace H66). engine.js builds
     this at runtime: a wrapper in the section holding the pinned stage and an
     empty spacer that gives the pin its length: a screen for the title
     (--intro), three beats for each of the twelve points (--beats: the render
     wipes in, the text arrives, the text is left to be read) and a screen for
     the closing stack (--end). The first beat is the move from one point to the
     next and keeps half a screen (--beat); the other two are the text and are
     25svh each (--text), so a point is 100svh (it was 220, H70) and the page
     glides from one point to the next. The
     stage sticks below the nav, and the section's own eyebrow, heading and
     lead are its first slide, centred (.abt-deck-intro). Inside it the render
     is on the left 60% and the real card grid fills the stage, so the stack at
     the end is the visitor's own row with its subgrid alignment. Until then
     each card is moved with translate into the last column, where only the one
     that owns the point shows. engine.js writes data-step (-1 on the title),
     data-card and data-phase (intro, steps or stack) on the stage and the
     classes on the card and the point, and data-beat (0 on the title, then 1 to
     3) for the caption and the mark; every move from there is a transition
     of 800ms or more, so nothing here animates on a clock of its own and
     nothing is hurried. --settle and --rise are the image's start scale and
     the text's rise, both scaled by the village intensity (1.06 and 16px at
     1.5). */
  /* The deck spans the section's whole row and is centred at every width,
     and takes its own width off that row: left in the section's middle column
     (already 16px in from each edge) a phone deck stood 16px from the left
     edge and 48px from the right (H69). */
  #v-about[data-mv2-village="new"] .abt-deck-wrap{--deck-ease:cubic-bezier(.22,1,.36,1);--intro:100svh;--beat:50svh;--text:25svh;--beats:3;--end:100svh;
    grid-column:1/-1;justify-self:center}
  #v-about[data-mv2-village="new"] .abt-deck-spacer{display:block;height:calc(var(--intro) + var(--deck-n,12) * (var(--beat) + (var(--beats) - 1) * var(--text)) + var(--end))}
  #v-about[data-mv2-village="new"] .abt-deck{--settle:calc(1 + .04 * var(--mv2-village-intensity,1));
    --rise:calc(10.667px * var(--mv2-village-intensity,1))}
  #v-about[data-mv2-village="new"] .abt-deck-frame{position:absolute;left:0;isolation:isolate;border-radius:4px;overflow:hidden;
    background:var(--swv-accent-950);clip-path:inset(0);
    transition:opacity 900ms var(--deck-ease),transform 900ms var(--deck-ease),clip-path 1100ms cubic-bezier(.76,0,.24,1)}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-deck-frame{opacity:0;transform:scale(.94);pointer-events:none}
  /* On the title slide the frame is wiped shut from the bottom up, so the first
     render arrives with the same downward wipe the points use, and a scroll
     back above takes it away the same way. */
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="intro"] .abt-deck-frame{clip-path:inset(0 0 100% 0);pointer-events:none}
  /* Slide 0: the section's eyebrow, heading and lead, moved in by engine.js and
     centred in the stage with nothing else showing. On the next slide it lifts
     32px and fades over 900ms, and a scroll back above brings it down again. */
  #v-about[data-mv2-village="new"] .abt-deck-intro{position:absolute;inset:0;z-index:4;display:flex;flex-direction:column;align-items:center;justify-content:center;
    text-align:center;opacity:0;transform:translateY(-32px);pointer-events:none;
    transition:opacity 900ms var(--deck-ease),transform 900ms var(--deck-ease)}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="intro"] .abt-deck-intro{opacity:1;transform:none;pointer-events:auto}
  /* A deck that has just been built (a first load, or a rebuild after a width
     change) lands on the slide the scroll is at without playing anything:
     engine.js puts this class on it and takes it off with the first change of
     point or phase, so that move is animated as usual. The caption itself is
     left out, so its opacity still fades when a beat brings it in before that
     first change; its words' entrance (below) stays held, and engine.js stops
     the fade by hand for the write that builds the deck. */
  #v-about[data-mv2-village="new"] .abt-deck.abt-deck-snap *:not(.abt-deck-cap),
  #v-about[data-mv2-village="new"] .abt-deck.abt-deck-snap *::after{transition:none !important;animation:none !important}
  /* The incoming render fades up over the outgoing one, which stays put for the
     fade and is then let go at once, so the two never dip through the ground.
     The slow settle from --settle to 1 belongs to the incoming one alone. */
  #v-about[data-mv2-village="new"] .abt-deck-img{position:absolute;inset:0;width:100%;height:100%;max-width:none;object-fit:cover;
    opacity:0;z-index:0}
  /* A new point, in the manner of the home panel's hero gallery turned on its
     side: the old render is clipped away from the top down (from the bottom up
     when scrolling back, --dir), drifting the same way and dimming, while the
     new one settles in under it from a little above and a little larger. One
     slow in, slow out curve, a touch over a second. */
  #v-about[data-mv2-village="new"] .abt-deck-img[data-on]{opacity:1;z-index:2;
    animation:abtDeckImgIn 1100ms cubic-bezier(.76,0,.24,1) both}
  #v-about[data-mv2-village="new"] .abt-deck-img[data-out]{opacity:1;z-index:3;
    animation:abtDeckImgOut 1100ms cubic-bezier(.76,0,.24,1) both}
  @keyframes abtDeckImgOut{
    from{clip-path:inset(0);transform:none;filter:none}
    to{clip-path:inset(calc(50% + 50% * var(--dir,1)) 0 calc(50% - 50% * var(--dir,1)) 0);transform:translateY(calc(8% * var(--dir,1)));filter:brightness(.78)} }
  @keyframes abtDeckImgIn{
    from{transform:translateY(calc(-6% * var(--dir,1))) scale(var(--settle))}
    to{transform:none} }
  #v-about[data-mv2-village="new"] .abt-deck-img:not([src]),#v-about[data-mv2-village="new"] .abt-deck-img[data-broken]{visibility:hidden}
  /* The render drifts gently against the scroll: --deck-f is how far through
     its point the scroll is (engine.js, 0 to 1), so the picture slides from 2.5%
     below its middle to 2.5% above it over the whole point (three beats), without
     starting again at each beat. The 6% scale leaves 3% of
     spare picture on every side, so no edge shows. Written as the individual
     translate and scale properties so they compose with the wipe keyframes'
     transform. A render being wiped away keeps the --deck-f it was let go with. */
  #v-about[data-mv2-village="new"] .abt-deck-img[data-on],
  #v-about[data-mv2-village="new"] .abt-deck-img[data-out]{scale:1.06;translate:0 calc((0.5 - var(--deck-f, .5)) * 5%)}
  /* Each card keeps its own ground in the deck, as the side panel beside its
     renders and again in the stack, so the three colours read the same
     everywhere. */
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-card{--di:0;position:relative;top:auto;z-index:1;animation:none;
    opacity:0;pointer-events:none;transform:translateY(var(--rise))}
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-card:nth-child(2){--di:1}
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-card:nth-child(3){--di:2}
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-card.abt-deck-on{opacity:1;transform:none;pointer-events:auto}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-life-card{opacity:1;transform:none;pointer-events:auto}
  /* The card that comes in reveals from the top down, following the points. */
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="steps"] .abt-life-card.abt-deck-on{animation:abtDeckDown 900ms cubic-bezier(.76,0,.24,1) both}
  @keyframes abtDeckDown{ from{clip-path:inset(0 0 100% 0)} to{clip-path:inset(0 0 -4% 0)} }
  /* The slide headline over the render: a soft green fade at the foot of the
     frame (80% under the words, enough for 4.5:1 over a white pixel), the
     count small, the point large in the serif. It is not there on a point's
     first beat; engine.js puts .is-in on it for the second and third, and it
     fades and rises in. */
  #v-about[data-mv2-village="new"] .abt-deck-cap{position:absolute;left:0;right:0;bottom:0;z-index:3;padding:96px 32px 28px;
    color:var(--swv-neutral-50);pointer-events:none;display:flex;flex-direction:column;gap:8px;
    background:linear-gradient(to top,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 0%,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 55%,color-mix(in srgb,var(--swv-accent-950) 0%,transparent) 100%);
    opacity:0;transition:opacity 800ms var(--deck-ease)}
  #v-about[data-mv2-village="new"] .abt-deck-cap.is-in{opacity:1}
  #v-about[data-mv2-village="new"] .abt-deck-count{font:500 var(--swv-text-body)/1.33 var(--swv-sans);letter-spacing:-.36px;color:var(--swv-neutral-200)}
  /* The third beat is the read: --deck-b (engine.js, 0 to 1 through that beat,
     0 before it) lifts the name 12px and eases its tightened spacing open to
     the type's own over the beat, so the words are never a still while they are
     read. Transform, not translate, because the entrance below owns translate. */
  #v-about[data-mv2-village="new"] .abt-deck-name{font:400 clamp(28px,3.2vw,48px)/1.08 var(--swv-serif);
    letter-spacing:calc(-.5px + var(--deck-b,0) * .5px);transform:translateY(calc(var(--deck-b,0) * -12px));text-wrap:balance}
  #v-about[data-mv2-village="new"] .abt-deck-cap.is-in .abt-deck-name{animation:abtDeckName 1000ms cubic-bezier(.76,0,.24,1) 120ms both}
  #v-about[data-mv2-village="new"] .abt-deck-cap.is-in .abt-deck-count{animation:abtDeckName 800ms cubic-bezier(.76,0,.24,1) both}
  @keyframes abtDeckName{from{clip-path:inset(0 0 100% 0);translate:0 -14px}to{clip-path:inset(0 0 -20% 0);translate:0 0}}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-deck-cap{opacity:0}
  #v-about[data-mv2-village="new"] .abt-deck :is(.abt-life-n,.abt-life-sub,.abt-life-over,.abt-life-rule){transition:color 800ms,background-color 800ms}
  /* The points: the lit one in full ink with the mark beside it, the rest of
     the card at the resting opacity. In the stack every point is plain and the mark is gone. */
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-list{position:relative}
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-list li{transition:opacity 800ms var(--deck-ease),color 800ms}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="steps"] .abt-life-list li{opacity:var(--tx-rest-opacity)}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="steps"] .abt-life-list li.abt-deck-pt{opacity:1;color:var(--swv-accent-950)}
  /* On the deep green card the lit point and its mark turn to the page white. */
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="steps"] .abt-life-card--dark .abt-life-list li.abt-deck-pt{color:var(--swv-neutral-50)}
  #v-about[data-mv2-village="new"] .abt-deck .abt-life-card--dark .abt-deck-mark{background:var(--swv-neutral-50)}
  #v-about[data-mv2-village="new"] .abt-deck-mark{position:absolute;left:0;top:0;width:8px;height:2px;background:var(--swv-accent-950);
    transform:translateY(calc(var(--mark-y,0px) + 7px));transition:transform 900ms var(--deck-ease),opacity 800ms}
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-deck-mark{opacity:0}
  /* Nothing is lit on a point's first beat, so the mark is not there either; it
     fades in on the second, sliding from where it was to the point that lights. */
  #v-about[data-mv2-village="new"] .abt-deck[data-phase="steps"][data-beat="1"] .abt-deck-mark{opacity:0}
  /* A hairline under the card that fills as the points go by. */
  #v-about[data-mv2-village="new"] .abt-deck-bar{position:absolute;right:16px;bottom:0;width:calc((100% - 16px) / 3 - 32px);height:1px;
    background:var(--swv-neutral-200);transition:opacity 800ms}
  #v-about[data-mv2-village="new"] .abt-deck-bar::after{content:"";position:absolute;inset:0;background:var(--swv-accent-950);
    transform-origin:0 50%;transform:scaleX(var(--deck-p,0));transition:transform 900ms var(--deck-ease)}
  #v-about[data-mv2-village="new"] .abt-deck:is([data-phase="stack"],[data-phase="intro"]) .abt-deck-bar{opacity:0}
  @media(min-width:861px){
    #v-about[data-mv2-village="new"] .abt-deck-wrap{width:calc(100% - 64px);max-width:1200px;
      margin-top:8px}
    #v-about[data-mv2-village="new"] .abt-deck{position:sticky;top:72px;height:min(760px,calc(100svh - 96px))}
    #v-about[data-mv2-village="new"] .abt-deck-frame{top:0;bottom:0;width:60%}
    #v-about[data-mv2-village="new"] .abt-deck .abt-life-grid{height:100%;margin:0;align-content:center}
    #v-about[data-mv2-village="new"] .abt-deck .abt-life-card{translate:calc((2 - var(--di)) * (100% + 8px)) 0;
      transition:opacity 800ms var(--deck-ease),transform 800ms var(--deck-ease),translate 900ms var(--deck-ease),background-color 800ms,border-color 800ms}
    #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-life-card{translate:0 0;
      transition-delay:calc(var(--di) * 80ms)}
  }
  /* Village, phone. The same deck, stacked: the render on top at about half the
     window, the card under it, the stage pinned below the nav bar. The cards
     share one cell instead of sliding, and the stack is today's single column.
     The stage may grow past the window for the column; its height is never a
     reference for the pin. A card that changes place between the two layouts
     fades in again, which hides the jump. */
  @keyframes abtDeckIn{ from{opacity:0;transform:translateY(var(--rise))} to{opacity:1;transform:none} }
  @keyframes abtDeckStack{ from{opacity:0;transform:translateY(var(--rise))} to{opacity:1;transform:none} }
  @media(max-width:860px){
    #v-about[data-mv2-village="new"] .abt-deck-wrap{width:calc(100% - 32px);margin-top:8px}
    #v-about[data-mv2-village="new"] .abt-deck{position:sticky;top:64px;min-height:calc(100svh - 64px)}
    /* The stage may be taller than the window for the card column, so the title
       is centred in the window's share of it and not in the whole stage. The 64px
       is the pinned About header's 60px row and 4px of air, so the stage never
       sticks under it; the 1440 stage, 72px down, already clears it. */
    #v-about[data-mv2-village="new"] .abt-deck-intro{bottom:auto;height:calc(100svh - 64px)}
    #v-about[data-mv2-village="new"] .abt-deck-frame{top:0;right:0;height:52svh}
    #v-about[data-mv2-village="new"] .abt-deck .abt-life-grid{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;
      margin:0;padding-top:calc(52svh + 20px);transition:padding-top 800ms var(--deck-ease)}
    #v-about[data-mv2-village="new"] .abt-deck .abt-life-card{grid-area:1/1;transition:opacity 800ms var(--deck-ease),transform 800ms var(--deck-ease),background-color 800ms,border-color 800ms}
    #v-about[data-mv2-village="new"] .abt-deck .abt-life-card.abt-deck-on{animation:abtDeckIn 900ms var(--deck-ease) both}
    #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-life-grid{display:flex;flex-direction:column;gap:16px;padding-top:0}
    #v-about[data-mv2-village="new"] .abt-deck[data-phase="stack"] .abt-life-card{transition:none;
      animation:abtDeckStack 900ms var(--deck-ease) both;animation-delay:calc(var(--di) * 90ms)}
    #v-about[data-mv2-village="new"] .abt-deck-bar{right:0;width:100%}
  }

  /* People. The photo wipes in first. Each tile starts loosely scattered, always
     moving outward from its neighbours' side so two never cross, and settles into the grid by the time its own top is at about 35% of the
     screen, a column after the one before. Its column drift (translate) is
     today's and runs on alongside. */
  #v-about[data-mv2-people="new"] .abt-people-photo{animation-range:entry 0% entry 100%, cover 0% cover 100%}
  #v-about[data-mv2-people="new"] .abt-mosaic-item{
    animation:abtTile linear both, abtMv2Scatter linear both;
    animation-timeline:--abt-collage, view();
    animation-range:cover 0% cover 100%, cover calc(4svh + var(--mc) * 3svh) cover calc(62svh + var(--mc) * 3svh)}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(1){--sx:-8;--sy:2;--sr:-3}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(2){--sx:7;--sy:5;--sr:2.5}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(3){--sx:-10;--sy:1;--sr:4}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(4){--sx:9;--sy:6;--sr:3.5}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(5){--sx:-6;--sy:8;--sr:-2}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(6){--sx:11;--sy:4;--sr:-4}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(odd){--mc:0}
  #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(even){--mc:1}
  @media(min-width:861px){
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(3n+1){--mc:0}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(3n+2){--mc:1}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(3n){--mc:2}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(1){--sx:-9}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(2){--sx:3}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(3){--sx:12}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(4){--sx:-12}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(5){--sx:-3}
    #v-about[data-mv2-people="new"] .abt-mosaic-item:nth-child(6){--sx:10}
  }
}
}
/* mv2-t3-end */
/* mv2-t4-start */
/* The watercolor band (About motion v2, task 4). Everything here is built or
   toggled by JS only while the gate is on, so with it off nothing matches.
   svg: engine.js wraps the two edge layers in .abt-edges and puts the filter
   beside them; the filter has to sit on a parent because a filter runs before
   the layer's own mask. The layers are widened by --abt-ext each side (the mask
   art moves with them) so the displacement never pulls in the photo at the
   band's left and right edges.
   liquid: the band and its four slides, below. */
#v-about[data-mv2="on"] .abt-edges{position:absolute;inset:0;pointer-events:none;filter:url(#abt-mv2-warp)}
#v-about[data-mv2="on"] .abt-mv2-svg{position:absolute;width:0;height:0;overflow:hidden}
#v-about[data-mv2="on"] .abt-edges .abt-edge{--abt-ext:24px;
  left:calc(50% - 720*var(--abt-u) - var(--abt-ext));width:calc(1440*var(--abt-u) + 2*var(--abt-ext))}
#v-about[data-mv2="on"] .abt-edges .abt-edge--top{
  -webkit-mask-position:-32px -32px,left calc(var(--abt-ext) - var(--abt-pad)*var(--abt-u)) bottom calc(-1*var(--abt-trim)*var(--abt-u));
  mask-position:-32px -32px,left calc(var(--abt-ext) - var(--abt-pad)*var(--abt-u)) bottom calc(-1*var(--abt-trim)*var(--abt-u))}
#v-about[data-mv2="on"] .abt-edges .abt-edge--bot{
  -webkit-mask-position:-32px -32px,left calc(var(--abt-ext) - var(--abt-pad)*var(--abt-u)) top calc(-1*var(--abt-trim)*var(--abt-u));
  mask-position:-32px -32px,left calc(var(--abt-ext) - var(--abt-pad)*var(--abt-u)) top calc(-1*var(--abt-trim)*var(--abt-u))}
/* The band (place=liquid, slides H62, H65). engine.js builds .abt-place-stage
   inside #about-place only in this mode and gives the section .abt-place--bloom
   (.abt-place--still as well under reduced motion or without view timelines).
   The section is a 500svh pin; the stage sticks for 400svh, a screen for each
   of four slides. The first opens a full width band of the photo, with the
   plain page left above and below it. The second brings in the eyebrow,
   heading and subheading, the third the two paragraphs, and in the fourth the
   copy leaves again and the band closes to the plain page, so The Village
   follows in normal flow and nothing slides over the stage. engine.js writes
   data-slide (0 to 3) on the section and the rules below answer it with timed
   transitions: nothing here is scrubbed by the scroll, so a line is never
   caught between two positions. The torn edge layers and the veil are not
   used. The photo is painted by an svg layer (.abt-place-svg, built by
   engine.js) under a mask that is one vector path, redrawn only while the
   scroll moves, so the photo itself is never warped. The server's img stays in
   the page and is hidden while that layer is up (.abt-place--drip). */
#v-about[data-mv2="on"] #about-place.abt-place--bloom{aspect-ratio:auto;min-height:0;height:500vh;height:500svh;
  background:var(--swv-neutral-50)}
#v-about[data-mv2="on"] #about-place.abt-place--still{height:auto}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-stage{position:sticky;top:0;height:100vh;height:100svh;overflow:hidden}
#v-about[data-mv2="on"] #about-place.abt-place--still .abt-place-stage{position:relative}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-media{background:var(--swv-neutral-50)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-veil{display:none}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-img{top:0;height:100%;object-fit:cover;animation:none;transform:none;will-change:auto}
/* The svg layer fills the media box, which is the stage, so its units are the
   stage's pixels. It is hidden (abt-blot-none) while the band is shut, and the
   server's photo is hidden whenever the layer is up, so the photo is only ever
   seen through the layer's mask or, once the band is open, plain inside it. */
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-svg{position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-svg.abt-blot-none{visibility:hidden}
#v-about[data-mv2="on"] #about-place.abt-place--drip .abt-place-img{visibility:hidden}
/* The copy is centred over the stage on a soft radial wash of the page's own
   near black, behind the text block only, so the canopy darkens naturally
   rather than taking on a colour. The wash reaches 65% of the block's own
   width and height beyond each side (percentages, so it follows the block
   whatever its height), and the first stop holds 72% out to 70% of the
   ellipse's radii. That plateau ellipse has semi axes 0.7 * (0.5 + 0.65) = 0.805
   of the block's width and height, so a block corner sits at 0.88 of its
   radius, inside it, for any block size. Its alpha and that geometry are held
   by a unit test, which keeps the copy at 4.5 to 1 over a white photo pixel.
   The wash is off on the blank page and while the band opens, comes in with the
   second slide and goes with the copy when the band closes. */
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-in{position:absolute;left:50%;top:50%;translate:-50% -50%;
  width:min(672px,100%);height:auto;min-height:0;z-index:1}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-in::before{content:"";position:absolute;inset:-65%;z-index:-1;pointer-events:none;
  background:radial-gradient(ellipse closest-side at 50% 50%,color-mix(in srgb,var(--swv-neutral-950) 72%,transparent) 70%,color-mix(in srgb,var(--swv-neutral-950) 0%,transparent) 100%);
  transition:opacity 900ms cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom:not(.abt-place--still) .abt-place-in::before{opacity:0}
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="1"] .abt-place-in::before,
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="2"] .abt-place-in::before{opacity:1}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-in > *{animation:none}
/* A block whose copy could not be split into lines comes in whole with the
   second slide instead, and goes with the closing. The still mode (reduced
   motion) is left alone: its copy is always there. */
#v-about[data-mv2="on"] #about-place.abt-place--bloom:not(.abt-place--still) .abt-place-in:not(.abt-place-lines){opacity:0;
  transition:opacity 1100ms cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="1"] .abt-place-in:not(.abt-place-lines),
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="2"] .abt-place-in:not(.abt-place-lines){opacity:1}
/* The place copy, a line at a time (engine.js aboutPlaceLines). Each line rises
   out of its own mask: group 0 (the eyebrow, heading and subheading) with the
   second slide, group 1 (the paragraphs) with the third, 1100ms each, a line
   110ms after the one before it in its group (--li). A scroll back above a
   slide sinks its lines again, a little quicker and all together, and so do
   both groups when the closing slide begins: the copy leaves before the band
   closes. Only the bloom mode splits the copy; the still mode under reduced
   motion keeps it whole. */
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-in.abt-place-lines .abt-ln{display:block;overflow:hidden;padding:0 .04em .12em}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-place-in.abt-place-lines .abt-ln-i{display:inline-block;
  transform:translateY(110%);opacity:0;
  transition:transform 700ms cubic-bezier(.22,1,.36,1),opacity 700ms cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="1"] .abt-place-in.abt-place-lines .abt-ln[data-g="0"] .abt-ln-i,
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="2"] .abt-place-in.abt-place-lines .abt-ln[data-g="0"] .abt-ln-i,
#v-about[data-mv2="on"] #about-place.abt-place--bloom[data-slide="2"] .abt-place-in.abt-place-lines .abt-ln[data-g="1"] .abt-ln-i{
  transform:none;opacity:1;
  transition:transform 1100ms cubic-bezier(.22,1,.36,1) calc(var(--li,0) * 110ms),opacity 1100ms cubic-bezier(.22,1,.36,1) calc(var(--li,0) * 110ms)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-h,
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-subhead{color:var(--swv-neutral-50)}
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-eyebrow,
#v-about[data-mv2="on"] #about-place.abt-place--bloom .abt-body{color:var(--swv-neutral-200)}
/* mv2-t4-end */
/* rooms-start
   Village, "rooms" variant (H56, preview only). engine.js adds .abt-life--rooms
   to #about-life, puts a photo into each card, wraps the detail blocks in
   .abt-room-more and keeps data-open (1, 2, 3 or all) and --room-w (the open
   column's text width) current. Desktop: one flex row, the open card wider.
   Phone: three full width cards, all open. Every word stays in the DOM, and
   the detail blocks keep one width, so nothing reflows while a column resizes.
   --open is 1 on the open cards and 0 on the others, and what differs between
   the two is worked out from it, so it can transition. All text sits at the
   bottom of the card: a closed room shows its number and title there with the
   photo clear above; an open one grows its details upward from the title.
   Stacking: the card isolates its own context, the photo sits at -1 above the
   card's ground, the two scrims (::before, ::after) at 0 are generated content
   that paints after the photo, and the text at 1. */
#v-about[data-mv2="on"] #about-life.abt-life--rooms{--o:calc(1 + 1.2 * var(--mv2-village-intensity, 1))}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card{
  --open:0;--more:0fr;position:relative;isolation:isolate;overflow:hidden;box-sizing:border-box;border:0;border-radius:4px;padding:24px;
  display:flex;flex-direction:column;justify-content:flex-end;
  color:var(--swv-neutral-50);background:var(--swv-accent-950);animation:none}
#v-about[data-mv2="on"] #about-life.abt-life--rooms[data-open="1"] .abt-life-card:nth-child(1),
#v-about[data-mv2="on"] #about-life.abt-life--rooms[data-open="2"] .abt-life-card:nth-child(2),
#v-about[data-mv2="on"] #about-life.abt-life--rooms[data-open="3"] .abt-life-card:nth-child(3),
#v-about[data-mv2="on"] #about-life.abt-life--rooms[data-open="all"] .abt-life-card{--open:1;--more:1fr}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-room-img{
  position:absolute;inset:0;z-index:-1;width:100%;height:100%;max-width:none;object-fit:cover;
  transform:scale(calc(1 + .08 * var(--mv2-village-intensity, 1) * (1 - var(--open))));
  filter:brightness(calc(1 - .14 * (1 - var(--open)))) saturate(calc(1 - .1 * (1 - var(--open))));
  transition:transform 1200ms cubic-bezier(.22,1,.36,1),filter 1200ms cubic-bezier(.22,1,.36,1)}
/* Two scrims. ::before is always on and carries the number and title of a
   closed room. ::after fades in with the room and carries the open details. */
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::before,#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::after{content:"";position:absolute;inset:0;z-index:0;pointer-events:none}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::before{
  background:linear-gradient(to top,color-mix(in srgb,var(--swv-accent-950) 65%,transparent) 0%,color-mix(in srgb,var(--swv-accent-950) 65%,transparent) 30%,color-mix(in srgb,var(--swv-accent-950) 0%,transparent) 48%)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::after{
  background:linear-gradient(to top,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 0%,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 60%,color-mix(in srgb,var(--swv-accent-950) 0%,transparent) 82%);
  opacity:var(--open);transition:opacity 900ms cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card > :not(.abt-room-img){position:relative;z-index:1}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card:focus-visible{outline:2px solid var(--swv-neutral-50);outline-offset:-6px}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-n{color:var(--swv-neutral-50)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-t{color:var(--swv-neutral-50);font-size:var(--swv-text-h4);line-height:1.18}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-sub{color:var(--swv-neutral-200)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-rule{background:rgba(250,250,245,.24);margin:0 0 16px}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-over,
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-list li{color:var(--swv-neutral-50)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-list li::before{background:var(--swv-neutral-200)}
/* The details open upward from the title: a grid row that goes from 0fr to 1fr. */
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-room-more{display:grid;grid-template-rows:var(--more);transition:grid-template-rows 900ms cubic-bezier(.22,1,.36,1)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-room-more-i{min-height:0;overflow:hidden;padding-top:8px}
#v-about[data-mv2="on"] #about-life.abt-life--rooms :is(.abt-life-sub,.abt-life-rule,.abt-life-over,.abt-life-list){
  width:var(--room-w, 320px);max-width:none;opacity:var(--open);
  transform:translateY(calc((1 - var(--open)) * 12px));
  transition:opacity 600ms ease,transform 600ms ease;transition-delay:calc(var(--open) * 250ms)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-rule{transition-delay:calc(var(--open) * 310ms)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-over{transition-delay:calc(var(--open) * 370ms)}
#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-list{transition-delay:calc(var(--open) * 430ms)}
@media(min-width:861px){
  /* The rooms want more than the page's 640px text column: the row spans the
     whole section, up to 1200px, with 32px clear of each edge. */
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-grid{display:flex;flex-direction:row;gap:8px;
    grid-column:1/-1;justify-self:center;width:calc(100% - 64px);max-width:1200px}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card{
    min-width:0;
    flex:1 1 0;flex-grow:calc(1 + (var(--o) - 1) * var(--open));height:clamp(520px,72svh,680px);
    transition:flex-grow 900ms cubic-bezier(.22,1,.36,1)}
}
@media(max-width:860px){
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card{--open:1 !important;--more:1fr !important;top:auto;min-height:clamp(520px,78svh,680px)}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::after{background:linear-gradient(to top,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 0%,color-mix(in srgb,var(--swv-accent-950) 80%,transparent) 58%,color-mix(in srgb,var(--swv-accent-950) 0%,transparent) 80%)}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms :is(.abt-room-img,.abt-room-more,.abt-life-sub,.abt-life-rule,.abt-life-over,.abt-life-list),#v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::after{transition:none}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms :is(.abt-life-sub,.abt-life-rule,.abt-life-over,.abt-life-list){width:auto}
}
@media(prefers-reduced-motion:reduce){
  #v-about[data-mv2="on"] #about-life.abt-life--rooms{--o:1}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card{--open:1 !important;--more:1fr !important;transition:none}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card::after{transition:none}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-room-img{transform:none;filter:none;transition:none}
  #v-about[data-mv2="on"] #about-life.abt-life--rooms :is(.abt-room-more,.abt-life-sub,.abt-life-rule,.abt-life-over,.abt-life-list){transition:none}
}
/* Phone reveal: the photo uncovers from a soft inset mask and the text rises,
   as the card enters. Not under reduced motion, which shows the stack as it is. */
@supports (animation-timeline: view()){
  @media(max-width:860px) and (prefers-reduced-motion:no-preference){
    @keyframes abtRoomIn{ from{clip-path:inset(8% 6% round 4px);transform:scale(1.1)} to{clip-path:inset(0 round 4px);transform:scale(1)} }
    @keyframes abtRoomTxt{ from{opacity:0;transform:translateY(16px)} to{opacity:1;transform:translateY(0)} }
    #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-room-img{
      animation:abtRoomIn linear both;animation-timeline:view();animation-range:entry 0% cover 40%}
    #v-about[data-mv2="on"] #about-life.abt-life--rooms .abt-life-card > :not(.abt-room-img){
      animation:abtRoomTxt linear both;animation-timeline:view();animation-range:entry 20% cover 45%}
  }
}
/* rooms-end */

/* ==========================================================================
   Gracie's launcher, #c-fab: ONE button, always bottom right (R1), and gone
   only while her chat is open (R2) or a layer covers the whole screen (R5):
   the loading curtain, the site menu, a full screen film, the turn of the
   details over the whole screen, the staff editor.
   Nothing else hides or moves it, and the engine never writes its display, so
   a new page needs no rule here. Where the Homes bar sits shut at the foot it
   stands on the bar's line just left of it (--fab-shift, R6), and beside the
   villa end card the same way (R7), both set below where they show.
   docs/requirements/gracie-launcher.md has the reasons.
   ========================================================================== */
#c-fab{position:fixed;z-index:820;right:calc(16px + var(--fab-shift,0px));bottom:16px;
  display:flex;align-items:center;justify-content:center;gap:0;
  width:44px;height:44px;padding:0;
  background:var(--wg-gracie,var(--swv-gold));color:var(--wg-gracie-ink,var(--swv-neutral-950));
  border:0;border-radius:var(--tv-launcher-radius,8px);
  box-shadow:var(--tv-launcher-shadow,0 6px 16px rgba(12,11,9,.12));
  transition:bottom .4s cubic-bezier(.22,1,.36,1),right .4s cubic-bezier(.22,1,.36,1),box-shadow .35s cubic-bezier(.22,1,.36,1)}
#c-fab:hover{opacity:.82;background:var(--wg-gracie,var(--swv-gold))}
#c-fab:active{background:var(--wg-gracie,var(--swv-gold));box-shadow:var(--tv-launcher-shadow,0 6px 16px rgba(12,11,9,.12))}
body.chat-open #c-fab,html.swv-loading #c-fab,body.vd-open #c-fab,body.on-story-film #c-fab,
body.swv-dflipping #c-fab,html[data-alt="1"] body.editing #c-fab,
body:has(#swv-panel[data-state="expanded"]) #c-fab{display:none}
/* Beside the bar (R6, Dave 24 Oct: "the chat icon button wrongly on top of the panel. Ensure
   consistent on the left side instead"): wherever the Homes bar sits shut at the foot, on Explore
   and on a home's details alike, the launcher stands on its line 8px to its left, the same 44px
   tall (the bar may sit in the map, a villa's overview or a facility). On a wide screen that is the
   bar's width in from the right; on a phone the launcher takes the left gutter (16px in: 100% less
   its 44px and both gutters) and the bar starts after it. Open,
   the bar is a panel the launcher floats over at the corner, so its list ends 60px short of the
   foot. While a villa's end card is up the launcher stands beside the card instead (R7, below).
   The phone's space under a page's last line still reads --fab-bar-lift. */
:root{--fab-bar-lift:calc(var(--swv-control-h) + 8px);--homes-bar-w:var(--swv-panel-w)}
:where(body.on-map:has(#map-wrap.mv1):has(#swv-panel[data-state="collapsed"]):not(:has(#swv-ov.swv-ov-carded)),body:has(#v-details.active #swv-panel[data-state="collapsed"])){--fab-shift:calc(var(--homes-bar-w) + 8px)}
/* On a phone only the bar with bedroom tabs runs the width of the screen */
@media(max-width:860px){
  :where(body.on-map:has(#map-wrap.mv1):has(#swv-panel[data-tabs="beds"][data-state="collapsed"]):not(:has(#swv-ov.swv-ov-carded)),body:has(#v-details.active #swv-panel[data-tabs="beds"][data-state="collapsed"])){--fab-shift:calc(100% - 76px)}
  :where(body:not(.chat-open):not(.vd-open):not(.on-story-film):not(.swv-dflipping):is(.on-map:has(#map-wrap.mv1):not(:has(#swv-ov.swv-ov-carded)),:has(#v-details.active))) #swv-panel[data-tabs="beds"][data-state="collapsed"]{left:calc(var(--swv-space-16) + 52px)}
  /* Gracie's note opens from the launcher's side, so it stays on the screen */
  :where(body.on-map:has(#map-wrap.mv1):has(#swv-panel[data-tabs="beds"][data-state="collapsed"]):not(:has(#swv-ov.swv-ov-carded)),body:has(#v-details.active #swv-panel[data-tabs="beds"][data-state="collapsed"])) .fab-bubble{right:auto;left:14px;transform-origin:bottom left;border-radius:14px 14px 14px 5px}
  /* and over the end card, from the left and above the card */
  body:has(#swv-ov.swv-ov-carded) .fab-bubble{right:auto;left:14px;bottom:calc(var(--swv-ov-cta-h) + 24px);transform-origin:bottom left;border-radius:14px 14px 14px 5px}
}
/* R7 (Dave, 24 Oct: "the chat icon button is wrongly above the Explore Details Card ... on the left of
   the Explore Details card, this is standard default"): from the first of the card's turn the
   launcher stands at its foot on its left, 8px off, as it does beside the bar. */
body:has(#swv-ov.swv-ov-carded){--fab-shift:calc(var(--swv-ov-cta-w) + 8px)}
/* R8: while the Homes panel is open the launcher is not drawn, so neither is a note of Gracie's that
   points at it. */
body:has(#swv-panel[data-state="expanded"]) .fab-bubble{display:none}
/* RN41, RN65 (Dave, 28 Oct: on desktop "it's not expected to be a small icon button", and "the icon
   button should be our existing expand icon, not home"): on a phone, shut, that village's panel is its
   toggle alone, the size and corner of Gracie's launcher beside it, the expand mark in the middle. A wide
   screen keeps the bar, The Homes and the expand mark. Open, it is the panel as before. */
@media(max-width:860px){
  body:has(#swv-panel[data-state="collapsed"]:not([data-tabs="beds"])){--homes-bar-w:44px}
  #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]){width:44px;left:auto;border-radius:8px}
  /* shutting, the button is already where it rests, at the sheet's foot on the right, so the sheet folds into it */
  #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]) .swv-panel-head{margin:auto 0 0 auto}
  /* RN67: the button grows into the sheet and back with no snap. Every length that moves has a number at
     both ends (no auto, no left and right pair), and the panel is anchored by its right edge in both states,
     so the width, the height and the foot all ease and it runs in Safari without interpolate-size. The
     sheet's box is the same one the generic rule gave it, 8px in from each side. */
  #swv-panel[data-state="expanded"]:not([data-tabs="beds"]){left:auto;right:var(--swv-space-8);width:calc(100% - 2*var(--swv-space-8))}
  /* The sheet's rows are laid out at the sheet's own width the whole time, pinned to the edge it grows from, so the
     title and the tabs are revealed as it widens instead of wrapping and re-flowing every frame. At rest the row
     and the sheet are the same width. */
  #swv-panel[data-state="expanded"]:not([data-tabs="beds"])>*{box-sizing:border-box;align-self:flex-end;width:calc(100vw - var(--sbw,0px) - 2*var(--swv-space-8))}
  #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]) .swv-panel-title{display:none}
  #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]) .swv-panel-toggle{width:44px;height:44px;margin:0;border-radius:8px}
  /* the toggle fills a panel that clips (overflow:hidden), so its focus ring is drawn inside it */
  #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]) .swv-panel-toggle:focus-visible{outline-offset:-2px}
}
@media(prefers-reduced-motion:reduce){#c-fab{transition:none}}


/* Curtain footer: the active view is an opaque layer over a fixed #foot, #views::after reserves its
   height, and engine.js adds html.swv-curtain only when the window is as tall as the footer. */
html.swv-curtain #views>.view.active{position:relative;z-index:1}
/* The body's own ground as a fallback only: :where() adds no specificity, so a view's own
   ground (#v-about) is never overridden by it. */
html.swv-curtain :where(#views>.view.active){background-color:var(--cream)}
html.swv-curtain #views>.view.active::after{content:"";position:absolute;left:0;right:0;top:100%;height:96px;
  background:linear-gradient(to bottom,rgba(0,0,0,.28),transparent);pointer-events:none}
html.swv-curtain #views::after{content:"";display:block;height:var(--foot-h)}
html.swv-curtain #foot{position:fixed;left:0;right:0;bottom:0;z-index:0}
/* A village with no footer (swvFooterHTML returns nothing) shows none: an
   empty #foot has no height, so the curtain above never arms for it either,
   and the overview's own copy leaves no blank band (its padding is the room
   it kept for the card) under the last picture.
   !important because showView writes display inline on every route. */
#foot:empty,#swv-ov-foot:empty{display:none!important}
html.swv-curtain body.on-map #views::after,html.swv-curtain body.story-open #views::after{display:none}

/* The cover's panes opening as a curtain (engine.js openCoverCurtain). The
   About layer holds a copy of the About hero, so it reuses the hero's own
   rules with the scroll driven motion and the pin switched off. The Explore
   layer holds the loader's first sketch on the loader's own ground. */
.cv1-curtain{position:fixed;inset:0;z-index:650;overflow:hidden;background:var(--swv-neutral-50)}
/* RN77: the text film page's hero is shorter than the window over the village's green (RN62), so its
   curtain carries that page's class and ground, and lands on what the page shows */
.cv1-curtain.abt--text-film{background:var(--swv-accent-950)}
.cv1-curtain .abt-hero{position:relative;top:auto;z-index:auto;overflow:visible}
.cv1-curtain .abt-hero::after{content:none}
.cv1-curtain .abt-hero-img{position:absolute;inset:auto;max-width:none;animation:none;will-change:transform}
.cv1-curtain .abt-hero-mid > *{animation:none}
.cv1-curtain .cv1-curtain-pane{position:absolute;box-sizing:border-box;grid-area:auto;background:transparent!important;pointer-events:none}
.cv1-curtain-sketch{position:absolute;inset:0;overflow:hidden;opacity:0;background:var(--swv-neutral-200)}
/* the masked Explore curtain shows the page through until its silhouette covers it */
.cv1-curtain--mask{background:transparent}
.cv1-curtain-sketch img{position:absolute;max-width:none;mix-blend-mode:multiply;will-change:transform}
/* A phone's pane carries a flat 20% black under its words (.cv1-r::before).
   The layer keeps it on the photo only, and fades it as the words go, so it
   never tints the page ground below the hero. */
@keyframes cv1CurtainDim{to{opacity:0}}
@media(max-width:860px){
  .cv1-curtain .cv1-curtain-pane::before{content:none}
  .cv1-curtain .abt-hero::before{content:"";position:absolute;inset:-100px;z-index:1;background:rgba(0,0,0,.2);
    pointer-events:none;animation:cv1CurtainDim .315s linear forwards}
}

/* Dave's About floor: 16px, so the shared shell that sits on that page rises
   to body too. Elsewhere the floor is caption. */
body:has(#v-about.active) :is(a.swv-btn,.vd-k,.vd-list em,#c-head .t .caps,#c-send,#fac-sheet .over,
  .fac .fs,#fac-sheet .fs-imgs figcaption,#fs-close,#lightbox .lb-hint,
  #c-head button,#c-chips button,.msg.ag .who,.msg .act,.msg .gline,#c-note){font-size:var(--swv-text-body)}

/* One expand and hide icon, closed as drawn and turned 180deg when open. */
.xi{display:block;width:24px;height:24px;flex:none;transition:transform .2s var(--ease)}
[aria-expanded="true"] .xi,details[open]>summary .xi,.fitem.open .xi{transform:rotate(180deg)}
.vsw-chev{display:inline-flex;vertical-align:middle;margin-left:6px}
/* The chapter bar pill is as tall as its 12px text, so the icon takes the text's line height
   (14px, then 1lh where it is supported) and the pill keeps the height it had with a text arrow. */
.vsw-chev .xi{width:14px;height:14px;width:1lh;height:1lh}
@media(prefers-reduced-motion:reduce){.xi{transition:none}}

/* FOCUS RING, the one rule every keyboard focus ends on. The per component
   rules above keep choosing where the ring sits (offset, radius); this decides
   its colours, because a single sage or accent outline measured 1.1 to 2.7 to
   one on bone grounds and over photos. White outline plus a dark halo: the
   pair holds 3:1 against any ground (WCAG 1.4.11). Text fields are left out
   of the halo: a mouse click counts as focus-visible on them, and they show
   focus with their own underline or glow. */
:focus-visible{outline-color:var(--tx-focus)!important}
:focus-visible:not(input,textarea,select){box-shadow:0 0 0 5px var(--tx-focus-halo)!important}

/* ============================================================
   /contact (#v-contact), drawn only for a village with a get in touch form of
   its own (hasContactPage in engine.js; the form is styled by getintouch.css): a dark page in the menu's own green (the .wg-menu
   panel's --swv-accent-950 and its lifted centre). A narrow form and the
   office, its map and the three ways to reach the team sit side by side under
   the heading, and the fire pit render fills the right edge, darkened and
   faded into the green so there is no seam. A phone stacks the form and the
   office and leaves the photograph out. Text is the kit's near white, the
   accent is the chat launcher's gold, and every pairing below was measured:
   4.5:1 or better for text, 3:1 for the field edges. */
/* 9 Oct: a white, light mode page. Dark ink on white (16.1:1), labels 7.5:1, field edge 4.6:1
   (3:1 needed), error 8:1. The gold is kept for the button, whose label is dark on it (8:1); on white it
   is 2:1, so the office label and the icons take the deep gold the on-bone pages already use (7.1:1).
   Hover goes DOWN to the forest ink here, as on bone, and the focus ring flips to ink with a white halo,
   because the page's white hover and white ring would vanish on white. */
#v-contact{--cnt-ink:var(--tx-light-heading);--cnt-gold:#6F5410;
  --hover:#1D2415;--hover-on:#E8E6DA;--tx-focus:var(--swv-accent-950);--tx-focus-halo:var(--cnt-ground,#FFFFFF);
  background:var(--cnt-ground,#FFFFFF);color:var(--cnt-ink)}
/* 11 Oct: the ground is a token, --cnt-ground, for a village whose brand has no plain white. Temple
   Village sets its cream, #F0E9D9, where the same pairings measure: ink 13.4:1, labels 6.1:1, field
   edge 3.9:1 (3:1 needed), the deep gold 5.9:1, error 6.4:1. The focus ring's halo is the ground's
   own colour, as it was white on white. */
#v-contact .cnt{position:relative;box-sizing:border-box;display:flex;align-items:center;justify-content:center;
  min-height:100vh;min-height:100svh;padding:clamp(104px,15vh,150px) clamp(24px,7vw,120px) clamp(40px,6vh,72px)}
#v-contact .cnt-main{position:relative;z-index:1;min-width:0;width:100%;max-width:760px}
#v-contact .cnt-cols{display:flex;flex-wrap:wrap;align-items:flex-start;gap:clamp(32px,4vw,64px)}
#v-contact .cnt-l{flex:none;width:clamp(260px,21vw,320px)}
#v-contact .cnt-top{position:absolute;z-index:3}
/* the mark is the ink file, drawn as it is: dark on white; the burger takes the ink */
#v-contact .cnt-top .dhero-mark{height:44px;opacity:1}
#v-contact .dhero-burger i{background:var(--cnt-ink)}
#v-contact .cnt-h{margin:0 0 clamp(20px,3.5vh,36px);font:500 clamp(44px,5.4vw,84px)/1 var(--swv-serif);letter-spacing:.01em;text-transform:uppercase;white-space:nowrap}
/* the office */
#v-contact .cnt-r{flex:1 1 300px;min-width:0;max-width:340px;padding-top:4px}
#v-contact .cnt-k{margin:0 0 12px;font:500 var(--swv-text-caption)/1 var(--swv-sans);letter-spacing:.18em;text-transform:uppercase;color:var(--cnt-gold)}
#v-contact .cnt-h2{margin:0;font:500 clamp(30px,2.8vw,44px)/1.1 var(--swv-serif)}
/* each comma part of the address is one unit that does not break inside, so
   the lines fall between parts, never in the middle of one */
#v-contact .cnt-addr{max-width:24ch;margin:14px 0 0;font:400 var(--swv-text-body)/1.45 var(--swv-sans);font-style:normal}
#v-contact .cnt-addr span{display:inline-block;max-width:100%;overflow-wrap:anywhere}  /* a part stays on one line when it fits, wraps when it is wider than the column */
/* The map in the village's colours: Google's tiles taken to grey and then to a
   light leaf green, inside a thin green edge. The edge is on the box, since a
   filter on the frame would turn it green too. */
#v-contact .cnt-mapbox{margin-top:24px;border-radius:12px;box-shadow:0 0 0 1.5px var(--swv-accent-600)}
#v-contact .cnt-map{display:block;width:100%;height:clamp(90px,13vh,130px);border:0;border-radius:12px;
  background:var(--swv-neutral-200);filter:grayscale(1) sepia(.45) hue-rotate(42deg) saturate(.75) contrast(.95)}
/* the three ways to reach the team: a gold line icon and the value, each a link */
#v-contact .cnt-cards{display:flex;flex-direction:column;gap:4px;margin-top:20px}
#v-contact .cnt-card{display:flex;align-items:center;gap:22px;min-height:44px;color:var(--cnt-ink);text-decoration:none}
#v-contact .cnt-ico{flex:none;display:flex;color:var(--cnt-gold)}
#v-contact .cnt-value{font:400 var(--swv-text-body)/1.2 var(--swv-sans);overflow-wrap:anywhere}
/* LineLabel, ported once from wgrace.co (apps/studio/src/components/Buttons.css,
   .line-label): the quiet third choice's underline, on Contact's values and on
   About's Reach out and Watch again (RN108). A line 2.6 times the label wide,
   whole at both ends with a gap in the middle, drawn as a background so it
   follows a wrapped label. At rest it shows one whole end, so a link is still
   told from the words round it without hover; on hover or keyboard focus it
   slides to the other end, the gap passing under the words (0.95s,
   cubic-bezier(.65,0,.35,1), as there). Going back is instant and unseen, both
   ends being alike. Every village: :root body. */
:root body .line-label{padding-bottom:3px;
  background:linear-gradient(90deg,currentColor 0 38.4615%,transparent 38.4615% 61.5385%,currentColor 61.5385% 100%) 100% 100%/260% 1px no-repeat}
:root body :is(a,button):is(:hover,:focus-visible) .line-label{background-position:0% 100%;transition:background-position .95s cubic-bezier(.65,0,.35,1)}
@media(prefers-reduced-motion:reduce){
  :root body :is(a,button):is(:hover,:focus-visible) .line-label{transition:none}
}
#v-contact .cnt-card:focus-visible{outline:2px solid var(--tx-focus);outline-offset:4px;border-radius:6px}
/* the label stays in the link's name and out of sight */
#v-contact .cnt-label{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* The photograph is the page's second column on a window wide enough for both:
   a plain rounded card under the header, with no fade and nothing drawn over
   it. The form and the office sit in the first column, centred and only as wide
   as they are, so the white to their left and the white between them and the
   picture are the same. The picture never sets the row's height: it fills
   whatever the words make. Narrower than this the picture is left out (its
   <source> in contactHTML has the same width, so the file is not asked for)
   and the words keep the page, centred. */
#v-contact .cnt-img{display:none}
/* off a phone the words are only as wide as they are, so centred is centred to the eye */
@media(min-width:861px){#v-contact .cnt-main{width:fit-content}}
@media(min-width:1180px){
  #v-contact .cnt-has-img{--cnt-pad:clamp(24px,2.5vw,48px);display:grid;grid-template-columns:minmax(calc(760px + 2*var(--cnt-pad)),1fr) minmax(0,1fr);
    align-items:center;padding:96px var(--cnt-pad) var(--cnt-pad) 0}
  #v-contact .cnt-has-img .cnt-main{justify-self:center}
  #v-contact .cnt-img{display:block;position:relative;align-self:stretch;border-radius:12px;overflow:hidden;background:var(--swv-neutral-200)}
  #v-contact .cnt-img img{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:cover;object-position:30% 72%}
}
@media(max-width:860px){
  /* one block for the phone: the foot of the page clears the chat button, which sits over the corner */
  #v-contact .cnt, #v-contact .cnt-has-img{display:block;min-height:0;padding:96px 16px 104px}
  #v-contact .cnt-main{max-width:none}
  #v-contact .cnt-cols{flex-direction:column;align-items:stretch;gap:40px}
  #v-contact .cnt-l{width:auto}
  #v-contact .cnt-top .dhero-mark{height:36px}
  #v-contact .cnt-h{font-size:var(--swv-text-h1);line-height:1.1;margin-bottom:20px;white-space:normal}
  #v-contact .cnt-r{max-width:none;padding-top:0}
  #v-contact .cnt-h2{font-size:var(--swv-text-h3)}
  #v-contact .cnt-map{height:110px}
}

/* The chat button on the white /contact page: its gold is 2:1 against white, so it takes a 1px edge at
   neutral-500 (4.6:1). :where keeps the specificity of a bare #c-fab, so the Lantern's hover glow still wins. */
:where(body:has(#v-contact.active)) #c-fab{box-shadow:0 0 0 1px var(--swv-neutral-500),0 6px 16px rgba(12,11,9,.12)}

/* ==========================================================================
   /story as a film (ASSETS.STORY_FILM; storyFilmHTML in engine.js).
   The video fills the window on black and is shown whole (contain), so the
   text edited into its lower left and the speaker are never cropped. The top
   bar stays over it with no scrim, and nothing is drawn at the bottom: the
   footer and the Grace launcher would sit on the native controls.
   !important on #foot's display, which showView writes inline.
   ========================================================================== */
#v-story .story-film{position:fixed;inset:0;background:var(--tv-film-band,#000)}
#v-story .story-film-vid{display:block;width:100%;height:100%;object-fit:contain;background:var(--tv-film-band,#000)}
/* Framed like a film in a cinema: two equal black bands, the top one the bar's
   own height, so the bar and its buttons always stand on black. Between them the
   picture always runs the full width (Dave, 18 Oct): on a wide window it fills
   the space and its top and bottom are cropped (cover), on a phone it is its own
   16:9 at that width and shown whole (the film is 16:9). */
body.on-story-film #v-story .story-film-vid{position:absolute;left:0;top:50%;width:100%;height:min(calc(100% - 2 * var(--film-band)),56.25vw);transform:translateY(-50%);object-fit:cover}
body.on-story-film .nav::before,
body.on-story-film .nav:not(.on-cream):not(.scrolled)::before{opacity:0}
/* A cinema: black above and below the picture at every window size. The cause
   of the lighter band was behind the bar, not in it: the bar is transparent
   over the page's own ground (body, #0F0F0F, never #000), and once the page
   was scrolled .nav.scrolled paints it cream at 98%. Both are put right here:
   the ground is black, the bar never takes a ground of its own, and it is
   taller (96px, 80px on a phone) so its two buttons have room. */
html:has(body.on-story-film),body.on-story-film{background:var(--tv-film-band,#000)}
body.on-story-film{--nav-h:104px;--film-band:var(--nav-h)}
body.on-story-film .nav.scrolled{background:transparent;color:var(--paper);border-bottom:0;backdrop-filter:none}
@media(max-width:700px){ body.on-story-film{--nav-h:80px;--film-band:88px} }
/* A short window (a phone on its side) keeps the bands slim, so the full width
   picture is not cropped to a sliver. */
@media(max-height:500px){ body.on-story-film{--nav-h:64px;--film-band:64px} }

/* The film as a story (settings.filmScenes; filmWire in engine.js): the browser's
   bar is gone and one of our own lies over the foot of the picture. The current
   scene's title in the site's title type, a play and pause button beside it, and
   one thin segment per scene, as wide as the scene is long, each filling as it
   plays. A segment is a button with a tall hit area around a 3px line. It sits
   in the black band under the picture, so it needs no fade to hold. The About
   hero's text film has the same bar (.abt-hero .film-bar), with a fade of its own. */
.film-scenes .film-bar,.abt-hero .film-bar{position:absolute;left:0;right:0;bottom:0;box-sizing:border-box;height:var(--film-band);display:flex;flex-direction:column;
  justify-content:flex-end;padding:0 clamp(16px,4vw,48px) 6px;color:var(--paper)}
.film-head{display:flex;align-items:center;gap:16px;min-height:44px;margin-bottom:6px}
.film-title{margin:0;display:grid;font-family:var(--serif);font-size:clamp(22px,2.6vw,34px);line-height:1.1;font-weight:400;
  clip-path:inset(-.15em -.3em -.25em)}
/* A scene change (filmTitleSet): the new title rises from under the line while
   the old one drifts up and fades, about 900ms, no bounce. The first title
   waits a beat for the film to appear. Reduced motion never gets a span. */
.film-t{grid-area:1/1;animation:filmTitleIn 900ms cubic-bezier(.2,.7,.1,1) both}
.film-t.is-first{animation-delay:350ms}
.film-t.is-out{animation:filmTitleOut 900ms cubic-bezier(.2,.7,.1,1) both}
@keyframes filmTitleIn{from{transform:translateY(105%);opacity:0}to{transform:none;opacity:1}}
@keyframes filmTitleOut{from{transform:none;opacity:1}to{transform:translateY(-45%);opacity:0}}
@media(prefers-reduced-motion:reduce){.film-t{animation:none}.film-t.is-out{display:none}}
/* The film's opening (R75; filmOpening): dark over everything but the review
   panel, the wordmark rising into the middle and holding, then going before
   the dark lifts onto the film's first still. Slow on purpose: the mark takes
   1.4s to arrive, the dark 1.6s to lift. A press lifts it, so it takes the
   pointer while it stands. */
.film-open{position:fixed;inset:0;z-index:1400;display:grid;place-items:center;background:var(--cover-ground,#000);
  transition:opacity 1.6s cubic-bezier(.45,0,.25,1) .8s}
/* Two copies of the mark in one cell (R92): a faint one, and an ink one over it
   revealed from the left by --fo-p, the share of the film in memory (filmOpening).
   The home page's cover fills its mark the same way (landing.css .tvh-hold). */
.film-open-mark{grid-area:1/1;width:min(300px,56vw);height:auto;opacity:0;transform:translateY(10px);
  transition:opacity 1.4s var(--tv-out,cubic-bezier(.16,1,.3,1)),transform 2s var(--tv-out,cubic-bezier(.16,1,.3,1))}
.film-open.is-mark .film-open-mark{opacity:.28;transform:none}
.film-open.is-mark .film-open-ink{opacity:1}
.film-open-ink{clip-path:inset(0 calc(100% - var(--fo-p,.06) * 100%) 0 0)}
.film-open.is-mark .film-open-ink{transition:opacity 1.4s var(--tv-out,cubic-bezier(.16,1,.3,1)),transform 2s var(--tv-out,cubic-bezier(.16,1,.3,1)),clip-path .5s cubic-bezier(.22,.61,.36,1)}
/* Taking over from the film's cover, the mark is already up: no second rise. */
.film-open.is-held .film-open-mark{transition:none}
.film-open.is-held .film-open-ink{transition:clip-path .5s cubic-bezier(.22,.61,.36,1)}
.film-open.is-lift{opacity:0;pointer-events:none}
.film-open.is-lift .film-open-mark{opacity:0;transition:opacity .8s ease}
/* The narration's captions (R74; filmWire): one sentence at a time, centred
   a little above the foot of the picture, in the village's sans and never the
   title serif. No box: a soft shadow holds the line against a bright frame,
   as the hero's words do. The picture's height is the video's own expression,
   so the line follows it at every window size. A line leaves before the next
   comes; with less motion it is swapped in place. On a phone the picture is a
   short band, so the line sits just under it, in the black; on its side the
   bands are slim (64px), so the line stays on the picture. */
.film-cap{position:absolute;left:50%;bottom:calc(50% - min(calc(100% - 2 * var(--film-band)),56.25vw) / 2 + clamp(16px,3.4vh,40px));
  width:min(34em,calc(100% - 2 * clamp(64px,9vw,136px)));margin:0;transform:translateX(-50%);text-align:center;text-wrap:balance;
  font:400 clamp(16px,1.35vw,21px)/1.5 var(--swv-sans,"Manrope",system-ui,sans-serif);letter-spacing:.015em;color:var(--paper);
  text-shadow:0 1px 2px rgba(15,15,15,.75),0 0 20px rgba(15,15,15,.5);opacity:0;pointer-events:none;
  transition:opacity .45s var(--tv-out,cubic-bezier(.16,1,.3,1))}
.film-cap.is-on{opacity:1}
@media(max-width:700px) and (min-height:501px){ .film-cap{bottom:auto;top:calc(50% + min(calc(100% - 2 * var(--film-band)),56.25vw) / 2 + 16px);width:calc(100% - 40px)} }
@media(prefers-reduced-motion:reduce){ .film-cap{transition:none} }
/* RN56: a click on the film plays or pauses it (filmWire), so it shows a pointer;
   the bar and its controls keep their own cursor. RN71: a tap answers at once, the
   page still scrolls and zooms over the film, and a hold shows no callout or selection. */
.film-scenes,.abt-hero:has(.abt-hero-vid--text.is-on){cursor:pointer;touch-action:pan-y pinch-zoom;-webkit-touch-callout:none;user-select:none;-webkit-user-select:none}
.film-bar{cursor:auto}
.film-pp{flex:none;position:relative;width:44px;height:44px;border:1px solid color-mix(in srgb,var(--paper) 70%,transparent);border-radius:50%;color:var(--paper)}
.film-pp::before{content:"";position:absolute;left:17px;top:13px;border:9px solid transparent;border-left:14px solid currentColor;border-right:0}
.film-snd{flex:none;display:grid;place-items:center;width:44px;height:44px;padding:0;border:1px solid color-mix(in srgb,var(--paper) 70%,transparent);border-radius:50%;color:var(--paper)}
.film-snd[aria-pressed="true"] .film-snd-off,.film-snd[aria-pressed="false"] .film-snd-on{display:none}
.film-snd:focus-visible{outline:2px solid var(--paper);outline-offset:3px}
.film-pp.is-playing::before{left:15px;top:14px;width:5px;height:16px;border:0;background:currentColor;box-shadow:9px 0 0 currentColor}
.film-pp:focus-visible,.film-seg:focus-visible{outline:2px solid var(--paper);outline-offset:3px}
.film-segs{display:flex;gap:6px}
.film-seg{position:relative;min-width:8px;height:24px;padding:0}
.film-seg::before,.film-seg i{content:"";position:absolute;left:0;right:0;top:10px;height:3px;border-radius:2px}
.film-seg::before{background:color-mix(in srgb,var(--paper) 35%,transparent)}
.film-seg i{background:var(--paper);transform:scaleX(0);transform-origin:left}
.film-seg:hover::before{background:color-mix(in srgb,var(--paper) 55%,transparent)}
/* RN109: a scene's preview (filmTipWire): a still of the scene's opening and
   its title, on the film's own dark, over the segment under the pointer or the
   keyboard. It rises 6px into place as it fades in, then glides from segment to
   segment; it never takes the pointer, and holds still for less motion. */
.film-tip{position:absolute;left:0;bottom:var(--tip-b,64px);z-index:2;box-sizing:border-box;width:calc(var(--tip-w,192px) + 12px);
  display:flex;flex-direction:column;gap:8px;padding:6px 6px 10px;border-radius:8px;
  background:rgba(12,11,9,.94);box-shadow:0 16px 40px rgba(0,0,0,.45);color:var(--paper);pointer-events:none;
  opacity:0;transform:translate3d(var(--tip-x,0px),6px,0);transition:opacity .2s ease,transform .25s cubic-bezier(.22,1,.36,1)}
.film-tip.is-on{opacity:1;transform:translate3d(var(--tip-x,0px),0,0)}
.film-tip.is-still{transition:none}
.film-tip-pic{display:block;width:100%;border-radius:4px;overflow:hidden;background:color-mix(in srgb,var(--paper) 10%,transparent)}
.film-tip-pic canvas{display:block;width:100%;height:100%}
.film-tip-t{padding:0 4px;font:500 14px/1.3 var(--swv-sans,"Manrope",system-ui,sans-serif);letter-spacing:.01em;text-align:center;text-wrap:balance}
@media(prefers-reduced-motion:reduce){ .film-tip{transition:none} }
/* The arrows (R50): a thin chevron at each side of the picture, one scene back
   or on. At 60% they never compete with the film, and come up full under the
   pointer or the keyboard, with a small step the way they point. They stand
   over the picture's edges, with a soft shadow to hold against a bright frame.
   A 48px target around a 32px line of one pixel. */
.film-nav{position:absolute;top:50%;width:48px;height:48px;margin-top:-24px;display:grid;place-items:center;padding:0;border:0;background:none;
  color:var(--paper);opacity:.6;cursor:pointer;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45));
  transition:opacity .4s var(--tv-out,cubic-bezier(.16,1,.3,1)),transform .4s var(--tv-out,cubic-bezier(.16,1,.3,1))}
.film-nav--prev{left:clamp(8px,2.5vw,40px)}
.film-nav--next{right:clamp(8px,2.5vw,40px)}
.film-nav:hover,.film-nav:focus-visible{opacity:1}
@media(hover:hover){ .film-nav--prev:hover{transform:translateX(-3px)} .film-nav--next:hover{transform:translateX(3px)} }
.film-nav:focus-visible{outline:2px solid var(--paper);outline-offset:3px}
/* With the scene editor open (?_edit=true) the next arrow stands clear of it. */
.film-scenes:has(.film-ed) .film-nav--next{right:calc(min(460px,100% - 24px) + clamp(12px,3vw,32px) + 8px)}
@media(prefers-reduced-motion:reduce){ .film-nav{transition:opacity .01ms} .film-nav--prev:hover,.film-nav--next:hover{transform:none} }

/* The scene editor, ?_edit=true only: a dark card at the right, below the bar. */
.film-ed{position:absolute;top:calc(var(--nav-h) + 8px);right:clamp(12px,3vw,32px);width:min(460px,calc(100% - 24px));
  max-height:calc(100% - var(--nav-h) - 200px);overflow:auto;padding:16px;background:rgba(15,15,15,.88);color:var(--paper);
  border:1px solid color-mix(in srgb,var(--paper) 25%,transparent);font-family:var(--swv-sans,"Manrope",system-ui,sans-serif);font-size:14px}
.film-ed h2{margin:0 0 2px;font-family:var(--serif);font-size:24px;font-weight:400}
.film-ed-note{margin:0 0 12px}
.film-ed-list{list-style:none;margin:0 0 12px;padding:0;display:grid;gap:10px}
.film-ed-row{display:grid;grid-template-columns:1fr 84px;gap:6px}
.film-ed-row input{min-width:0;box-sizing:border-box;height:36px;padding:0 8px;font:inherit;color:var(--paper);background:transparent;border:1px solid color-mix(in srgb,var(--paper) 45%,transparent)}
.film-ed .tv-btn{min-height:36px;padding:0 12px;font-size:13px}
.film-ed-foot{display:flex;flex-wrap:wrap;gap:8px}
.film-ed-auth{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 10px}
.film-ed-msg{min-height:1.4em;margin:10px 0 0}
@media(max-width:700px){ .film-ed{max-height:calc(100% - var(--nav-h) - 170px)} }
body.on-story-film #foot{display:none!important}

/* ==========================================================================
   Register Interest (riPillHTML, riDialogHTML in engine.js).
   The pill sits left of the burger in .nav and .mapnav, and not on a phone,
   where the bar is the logo and the burger (R71). Where it shows, the pill and
   burger keep their size (flex:none) and the logo is the one that gives way
   (min-width:0 with object-fit:contain), so the three can never overlap.
   Explore the Village is on the story's bar only, and not on a phone, where
   the menu leads with it.
   ========================================================================== */
.ri-pill{flex:none;box-sizing:border-box;white-space:nowrap;max-width:min(46vw,240px);overflow:hidden;text-overflow:ellipsis;cursor:pointer}
/* The outlined pill on the map's floating bar. In the top bar the pill is the
   shared primary (buttons.css, .tv-btn), which brings its own look. */
.ri-pill:not(.tv-btn){min-height:36px;padding:8px 16px 9px;border:var(--tv-ri-border,1px solid currentColor);border-radius:99px;
  background:var(--tv-button-fill,transparent);color:var(--tv-button-ink,inherit);box-shadow:var(--tv-button-shadow,none);font:500 12px/1 var(--swv-sans,system-ui,sans-serif);font-weight:var(--tv-button-weight,500);letter-spacing:.08em;text-transform:uppercase}
@media(pointer:coarse){ .ri-pill:not(.tv-btn){min-height:44px} }
/* Explore the Village is the shared secondary, on the story's bar only, so its
   hiding has to outrank .tv-btn's display (buttons.css comes after this file). */
.nav .ri-explore{display:none}
body.on-story .nav .ri-explore{display:inline-flex;flex:none;margin-right:6px;white-space:nowrap}
/* has-ri is set by engine.js only when the pill is drawn; every rule that
   reshapes the bar for it hangs on that, so other villages are unchanged. */
body.has-ri .nav .brand{min-width:0;flex:0 1 auto}
body.has-ri .nav .brand-mono{max-width:100%;object-fit:contain;flex:0 1 auto}
body.has-ri .nav .nav-right{flex:none}
body.has-ri .mapnav .dhero-mark{min-width:0;max-width:100%;flex:0 1 auto;object-fit:contain}
.mapnav .ri-pill{margin-left:auto;color:#E6E1D1}
@media(max-width:700px){
  body.on-story .nav .ri-explore{display:none}
  /* R71: on a phone the bar is the logo and the menu (Dave, 24 Oct: keep it minimalist). The
     form is the menu's Contact, and the chat's. */
  .nav .ri-pill,.mapnav .ri-pill{display:none}
}

/* ---------- Temple Village's Views look (engine.js, exploreMounted "views") ----------
   body.explore-views is set by that look alone, so nothing here touches Secret Waterfall's turn
   or Temple Village's 3D plan. The pin is a small white disc with a thin warm edge and the
   home's plot number (pinLabel), lifting and brightening under the pointer or the keyboard and
   when picked; a sold or reserved home's is softer, still pressable. The photograph carries the
   colour, so the chrome is white. The resting ring of the other maps is off here, and so is the wider target, unless the
   village asks for pin reach (settings.pinReach), which brings Secret Waterfall's rings and reach. */
body.explore-views #map-wrap.mv1:not(.pin-reach) #hotspots .dot:not(.off)::after{display:none}
body.explore-views #map-wrap.mv1 #hotspots .dot{transition:opacity .8s var(--tv-out,cubic-bezier(.16,1,.3,1))}
/* the rings of these pins (pin reach) are the disc's own white, and brighter: the village's pin colour is dark and a faint white ring is lost on a bright photograph */
body.explore-views #map-wrap.mv1 #hotspots,#map3d .mp3d-rings{--rest-peak:.9}
body.explore-views #map-wrap.mv1 #hotspots .dot{--ring-ink:#fff}
body.explore-views #map-wrap.mv1 #hotspots .dot:not(.amen) .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.reserved .pt{
  flex:none;width:26px;height:26px;display:flex;align-items:center;justify-content:center;
  background:rgba(255,255,255,.94);color:var(--swv-accent-950);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--swv-accent-300) 90%,transparent),0 1px 8px rgba(15,12,6,.34);
  transition:transform .5s var(--tv-out,cubic-bezier(.16,1,.3,1)),background .4s,box-shadow .4s}
body.explore-views #map-wrap.mv1 #hotspots .dot.reserved .pt{
  background:rgba(255,255,255,.62);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--swv-accent-300) 55%,transparent),0 1px 6px rgba(15,12,6,.2)}
body.explore-views #map-wrap.mv1 #hotspots .dot:not(.amen):hover .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot:not(.amen):focus-visible .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.picked .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.reserved:hover .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.reserved:focus-visible .pt{
  transform:translateY(-2px);background:#fff;
  box-shadow:0 0 0 1.5px var(--swv-accent-300),0 6px 14px rgba(15,12,6,.4)}
/* a 32px target at the least around the disc (a phone's is the 44px rule above) */
@media(min-width:861px){
  body.explore-views #map-wrap.mv1:not(.pin-reach) #hotspots .dot::before{content:"";position:absolute;inset:-4px;border-radius:50%}
}
/* a facility's pin: a smaller disc, and its short name on hover or focus in place of the card */
body.explore-views #map-wrap.mv1 #hotspots .dot.amen .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.amen.big .pt{
  width:14px;height:14px;background:rgba(255,255,255,.94);
  box-shadow:0 0 0 1px color-mix(in srgb,var(--swv-accent-300) 90%,transparent),0 1px 8px rgba(15,12,6,.34);
  transition:transform .5s var(--tv-out,cubic-bezier(.16,1,.3,1)),background .4s,box-shadow .4s}
body.explore-views #map-wrap.mv1 #hotspots .dot.amen:hover .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.amen:focus-visible .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.amen.lit .pt{transform:translateY(-2px);background:#fff}
/* with bedColors the disc takes the pin's colour on rest, hover and pick alike (the lift stays) */
body.explore-views #map-wrap.mv1 #hotspots .dot.bed:not(.reserved) .pt,
body.explore-views #map-wrap.mv1 #hotspots .dot.bed.amen.big .pt{background:var(--bed);color:var(--bed-ink,var(--tv-cream,#F0E9D9))}
body.explore-views #map-wrap.mv1 #hotspots .dot.amen .pt-name{
  position:absolute;left:50%;bottom:calc(100% - 2px);transform:translate(-50%,4px);padding:4px 10px;border-radius:12px;white-space:nowrap;
  background:rgba(22,20,16,.78);color:var(--swv-neutral-50);font:500 var(--swv-text-hint)/1.2 var(--swv-sans);
  opacity:0;pointer-events:none;transition:opacity .3s ease,transform .3s ease}
body.explore-views #map-wrap.mv1 #hotspots .dot.amen.lit .pt-name{opacity:1;transform:translate(-50%,0)}
/* a hovered or focused facility shows its card, as a home does, and its name steps aside for it */
body.explore-views #map-wrap.mv1 #hotspots .dot.amen:is(:hover,:focus-visible) .pt-name{opacity:0}
/* the names of the areas (viewsAreas places each; the look is the 3D map's, above): centred on
   their spot, and under a pin's card while it shows one (R241, below).
   They show on a phone too (R222): the same placing keeps them off every pin and control. */
#hotspots .mp3d-area{position:absolute;transform:translate(-50%,-50%);z-index:1}
/* R241: a pin's reach is wide on the photographs, so the names sit above the pins (the lit ones are at 2),
   and a pin raised for its card (hovered, focused or picked) above the names, so the card covers only what
   it covers. No name hides for a card any more: hiding them all while any pin was hovered left a name out of
   reach, since a pointer crosses some pin's reach on its way to almost any name. A pointer or a tap on a
   name is the name's, even inside a raised pin's reach (engine.js pinNameAt). Names never sit on a pin's own
   disc (viewsAreaPlaces). */
body.explore-views #hotspots .mp3d-area{z-index:3}
body.explore-views #map-wrap.mv1 #hotspots .dot:is(:hover,:focus-visible,.picked){z-index:4}
/* the zoom (engine.js viewsZoomAt): a pin and a name stay one size on a picture that grows, a name
   scaling about its own spot. The names stay however close the picture is (R189): a press on one frames
   its area (engine.js viewsFrame), zooming out as far as the area needs to fit. */
body.explore-views #hotspots .dot,body.explore-views #hotspots .mp3d-area{scale:var(--views-inv,1)}
body.explore-views #hotspots .mp3d-area{transform-origin:0 0}
/* what the panel's extra travel shows behind the picture (engine.js viewsBackdropSync): the photograph
   showing, blurred, darkened a little and scaled so its blurred edge is out of the box. Under #map-cam,
   and wholly covered by the picture when the panel is shut. One filtered image, nothing animated. */
#views-backdrop{position:absolute;inset:0;overflow:hidden;pointer-events:none}
#views-backdrop img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transform:scale(1.08);filter:blur(28px) brightness(calc(.82 * (1 - var(--views-dusk,0))))}
/* the larger still, fading in over the one it replaces in the same box */
.views-detail{pointer-events:none}
/* Where the picture's own edge comes into view (the panel lets it be pulled past its limit), it melts
   into the blurred backdrop: the stills and the detail fade to clear over the outer 56 screen px on all
   four sides. The mask is in the image's own px, so --views-inv (1 over the zoom) holds it at 56 on
   screen. Not the pins, the names or the wall canvas. */
body.explore-views #map-cam>img,body.explore-views #map-turn img,body.explore-views #map-cam>img.views-detail{
  --vf:calc(56px * var(--views-inv,1));
  -webkit-mask-image:linear-gradient(to right,transparent,#000 var(--vf)),linear-gradient(to left,transparent,#000 var(--vf)),linear-gradient(to bottom,transparent,#000 var(--vf)),linear-gradient(to top,transparent,#000 var(--vf));
  mask-image:linear-gradient(to right,transparent,#000 var(--vf)),linear-gradient(to left,transparent,#000 var(--vf)),linear-gradient(to bottom,transparent,#000 var(--vf)),linear-gradient(to top,transparent,#000 var(--vf));
  -webkit-mask-composite:source-in;mask-composite:intersect}
/* the Views look's light curtain (engine.js viewsWallSync): a canvas in the picture's own box, so it
   pans and zooms with the photograph, under the pins (#hotspots is 10) and never in the way of a press */
#views-wall{position:absolute;left:0;top:0;width:100%;height:100%;z-index:9;pointer-events:none}
/* the arrival: the pins and the arrows come in after the photograph (engine.js viewsArrive) */
#map-wrap.views-arriving #hotspots .dot,#map-wrap.views-arriving #hotspots .mp3d-area,#map-wrap.views-arriving .map-turn-nav{opacity:0!important;transition:none!important}
/* the arrows at the edges: the about page's .film-nav, hidden at the end of the turn, out of the
   way of the panel when it stands open, and gone while a drag is turning the picture */
/* Over a photograph, not black: a little firmer than the about page's (85% at rest, a 1.5px
   line), still full only under the pointer or the keyboard. */
.map-turn-nav{z-index:12;opacity:.85}
.map-turn-nav path{stroke-width:1.5}
.map-turn-nav[hidden]{display:none}
/* over jungle a one pixel line is lost, so the arrow brings a little dusk of its own, as the area names do */
.map-turn-nav::before{content:"";position:absolute;inset:-18px;border-radius:50%;pointer-events:none;z-index:-1;
  background:radial-gradient(closest-side,rgba(22,20,16,.8),rgba(22,20,16,.5) 55%,rgba(22,20,16,0))}
body.panning .map-turn-nav{opacity:0;pointer-events:none}
@media(min-width:861px){
  .film-nav--next.map-turn-nav{transition:opacity .4s var(--tv-out,cubic-bezier(.16,1,.3,1)),transform .4s var(--tv-out,cubic-bezier(.16,1,.3,1)),right .4s var(--tv-out,cubic-bezier(.16,1,.3,1))}
  #map-wrap:has(#swv-panel[data-state="expanded"]) .film-nav--next.map-turn-nav{right:calc(var(--swv-panel-w) + 16px + clamp(8px,2.5vw,40px))}
}
@media(max-width:860px){.map-turn-nav{top:36%}}
/* The top view button (engine.js, viewsTopPress): the shared glass secondary, smaller, at the foot
   of the map's left corner, where nothing else lives: the Homes bar and the chat button stand at the
   right, the arrows halfway up the sides and the names over the picture. A phone's bar spans the
   foot, so there it stands just above it. Over the wait cover (15) so it is seen while the map loads.
   Under #map-wrap, as the arrows are, so buttons.css's own .tv-btn position does not win. */
#map-wrap > .views-top-btn{position:absolute;z-index:16;left:clamp(16px,2.5vw,40px);bottom:16px;min-height:44px;padding:0 18px 0 14px;gap:8px;
  font-size:14px;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
#map-wrap > .views-top-btn[hidden]{display:none}
/* the zoom readout, edit mode only (engine.js viewsReadoutSync): quiet, top left under the bar, away from the button and the Homes bar */
#map-wrap > .views-readout{position:absolute;z-index:16;left:clamp(16px,2.5vw,40px);top:88px;margin:0;padding:4px 10px;border-radius:4px;font-size:12px;line-height:1.4;color:var(--swv-neutral-50);background:rgba(22,20,16,.55);pointer-events:none}
body.editing #map-wrap > .views-readout{display:none}
.views-top-ico{display:grid;place-items:center;width:20px;height:20px}
.views-top-ico .swv-spin{--spin:18px}
.views-top-ico.is-loading svg{display:none}
#map-wrap > .views-top-btn[aria-disabled="true"]{cursor:progress}
body.editing #map-wrap > .views-top-btn{display:none}
/* The village's sound control (.club-toggle, 96px wide) owns the desktop's bottom left corner again since
   R230 (R216's speaker at the top left is a phone's): the button stands beside it, on its line */
#map-wrap:has(> .club-toggle) > .views-top-btn{left:128px;bottom:12px}
/* R194: on a phone the button stands at the bottom right just above The Homes bar, as a map's control does */
@media(max-width:860px){#map-wrap > .views-top-btn,#map-wrap:has(> .club-toggle) > .views-top-btn{left:auto;right:16px;bottom:72px}}
/* R188: on a phone the button is a 44px square, the compass alone (its words hidden, its name kept for a
   screen reader), in Temple Village's dark green glass (#263F35): faint while a photograph shows,
   stronger, still see through, while the top view shows (is-on). The glass dims what is behind it so the
   compass keeps 3 to 1 over the brightest photograph (WCAG 1.4.11, phone-top-button.test.js) */
@media(max-width:860px){
#map-wrap > .views-top-btn{width:44px;min-width:44px;padding:0;justify-content:center;background:var(--tv-views-glass,rgba(38,63,53,.42));border-color:rgba(240,233,217,.28);color:#F0E9D9;-webkit-backdrop-filter:blur(8px) brightness(.7);backdrop-filter:blur(8px) brightness(.7)}
#map-wrap > .views-top-btn .views-top-lbl{display:none}
#map-wrap > .views-top-btn.is-on{background:var(--tv-views-glass-on,rgba(38,63,53,.78));border-color:rgba(240,233,217,.5)}
}
/* the top view is the 3D map: the photographs' arrows rest while it shows */
#map-wrap.map-3d .map-turn-nav{display:none}
/* reduced motion: the Views pins, names and arrows change at once, without moving */
@media(prefers-reduced-motion:reduce){
  body.explore-views #hotspots .dot,body.explore-views #hotspots .dot *,body.explore-views #hotspots .mp3d-area,.map-turn-nav{transition:none!important}
}

/* ==========================================================================
   The Lantern. The light is for every village (:root body below) and
   wireLantern() attaches on any of them; the label roll is Secret
   Waterfall's (body[data-project-slug], which shell.html.j2 and
   login.html.j2 stamp on that village alone). A warm light follows the
   cursor inside the chat launcher and the collapsed Homes bar, and a soft
   bloom spills onto the picture around them, like a lamp lit at dusk. The
   label rolls the way wgrace.co's buttons do.

   The light is ::before, clipped by the button's own edge; the bloom is a
   box-shadow, which an overflow:hidden parent does not clip. Only opacity and
   box-shadow change, so nothing moves: no layout, no transform. The
   cursor position is --lx and --ly, written by wireLantern() in engine.js.
   A button the keyboard reaches gets the same light, centred and still, and
   its focus ring stays. Where hover does not exist (touch) or motion is
   reduced the light is still, the same one the keyboard gets.
   ========================================================================== */
/* RollLabel, ported from apps/studio/src/components/Buttons.css (.roll-label,
   roll-label, --roll-travel) with the same keyframes, timing and easing: on
   hover or keyboard focus the label leaves past the button's top edge and
   returns from its bottom edge. The parent of a .roll-label clips it. Ported
   once; the markup says which labels roll. */
body[data-project-slug] :has(> .roll-label){overflow:hidden}
body[data-project-slug] .roll-label{display:block}
body[data-project-slug] :is(a,button):is(:hover,:focus-visible) .roll-label{animation:roll-label .7s}
@keyframes roll-label{
  0%{transform:translateY(0);animation-timing-function:cubic-bezier(.55,0,1,.45)}
  45%{transform:translateY(calc(-1 * var(--roll-travel,3.5em)))}
  45.01%{transform:translateY(var(--roll-travel,3.5em));animation-timing-function:cubic-bezier(0,.55,.45,1)}
  100%{transform:translateY(0)}
}
@media(prefers-reduced-motion:reduce){
  body[data-project-slug] :is(a,button):is(:hover,:focus-visible) .roll-label{animation:none}
}
/* Every primary and secondary button rolls too; engine.js rollLabel() wraps
   their words on the first hover. A disabled button stays still: the same
   weight as the hover rule above, and after it. */
body[data-project-slug] :is(a,button):is(:disabled,[aria-disabled="true"]) .roll-label{animation:none}

/* The light itself is for every village, so it is scoped by :root body, the
   same weight as body[data-project-slug]. The yellow launcher glows in its
   own yellow (or the village's own launcher colour, --wg-gracie-rgb), the
   cream bar in a warm white. */
:root body #c-fab{
  --lantern-core:rgb(255 247 214 / .8);--lantern-shape:circle 46px;--lantern-bloom:var(--wg-gracie-rgb,223 176 47);
  --lantern-rest:0 6px 16px rgb(12 11 9 / .12);isolation:isolate}
:root body #swv-panel[data-state="collapsed"]{
  --lantern-core:rgb(var(--wg-gracie-rgb,223 176 47) / .26);--lantern-shape:ellipse 150px 64px;--lantern-bloom:255 238 205;
  --lantern-rest:0 12px 32px rgb(12 11 9 / .16)}
/* the phone's 44px homes button (RN41, RN65) glows round, as the launcher beside it does */
@media(max-width:860px){:root body #swv-panel[data-state="collapsed"]:not([data-tabs="beds"]){--lantern-shape:circle 46px}}
:root body :is(#c-fab,#swv-panel[data-state="collapsed"])::before{
  content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;pointer-events:none;opacity:0;
  background:radial-gradient(var(--lantern-shape) at var(--lx,50%) var(--ly,50%),
    var(--lantern-core),color-mix(in srgb,var(--lantern-core) 50%,transparent) 36%,
    color-mix(in srgb,var(--lantern-core) 14%,transparent) 70%,transparent);
  transition:opacity .35s cubic-bezier(.22,1,.36,1)}
@media(hover:hover) and (pointer:fine){
  :root body :is(#c-fab,#swv-panel[data-state="collapsed"]):hover{
    box-shadow:var(--lantern-rest),0 0 22px 2px rgb(var(--lantern-bloom) / .6),0 0 64px 16px rgb(var(--lantern-bloom) / .32)}
  :root body :is(#c-fab,#swv-panel[data-state="collapsed"]):hover::before{opacity:1}
  /* the lamp replaces the dimming these buttons used to do on hover */
  :root body #c-fab:hover,
  :root body #swv-panel .swv-panel-title:hover{opacity:1}
}
:root body #c-fab:focus-visible,
:root body #swv-panel[data-state="collapsed"]:has(:focus-visible){
  box-shadow:var(--lantern-rest),0 0 22px 2px rgb(var(--lantern-bloom) / .6),0 0 64px 16px rgb(var(--lantern-bloom) / .32)}
:root body #c-fab:focus-visible::before,
:root body #swv-panel[data-state="collapsed"]:has(:focus-visible)::before{opacity:1}
@media(prefers-reduced-motion:reduce){
  :root body :is(#c-fab,#swv-panel[data-state="collapsed"])::before{transition:none}
}
