/* Summit Realty — "Ridgeview House" demo listing.
 *
 * REBUILT 2026-10-11 to match the current real-client register (see
 * videographer-site-flips/pytches-close-site, the live Mortimers of
 * Woodbridge build this is now modelled on): restrained UK estate-agent
 * type and spacing, a hairline-and-whitespace sidebar instead of boxed
 * cards, GBP "offers in excess of" pricing, "Arrange a viewing" CTAs.
 *
 * REVERSED later the same day: the canvas walkthrough was kept auto-playing
 * in-page below, on the reasoning that ~260 real outreach pitches already
 * promise "no button to ignore". Sir overrode that himself, twice, plainly:
 * he wants it click-to-enter. It now lives behind #walkthrough, a door, not
 * a corridor — the same structural pattern pytches-close-site's own
 * walkthrough already uses (data-wt-open/data-wt-close, Escape, browser
 * back). The outreach copy that promised otherwise has been corrected for
 * future sends in ../outreach-templates.md — already-sent history is left
 * alone, see that file's own note.
 *
 * WHAT DID NOT CHANGE: every mechanic the scroll-driven canvas walkthrough
 * depends on — .wt-photo-stage, #scroll-container's height/z-index, the fixed
 * .scroll-section positioning trick, and the exact class names app.js
 * queries (.section-label/.section-heading/.section-body/.cta-button/.stat,
 * .stat-number[data-value]) — is untouched below. Only WHERE it runs
 * changed (now inside #walkthrough, its own scroll container — see the
 * comment above that rule) plus this file's color/type/static-section work.
 */

:root {
  --paper: #f5f3ef;
  --white: #fff;
  --ink: #1c1b19;
  --ink-soft: #6b665f;
  --ink-faint: #9b958c;
  --line: #ddd8d0;
  --line-soft: #eae6df;
  --slate: #3d4f57;
  --slate-deep: #221b14;      /* near-black warm charcoal, used behind the canvas */
  --slate-deep-78: rgba(34, 27, 20, .82);
  --gold: #c9a567;            /* single warm accent, used sparingly — labels/suffixes only */
  --bg-light: var(--paper);
  --bg-dark: var(--slate-deep);
  --text-on-light: var(--ink);
  --text-on-dark: #f3efe7;
  --accent: var(--gold);
  --muted: var(--ink-soft);
  --font-display: 'EB Garamond', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
  --max: 1200px;
  --pad: clamp(1.25rem, 5vw, 3.5rem);
  /* Measured height of .mobile-bar, 2026-10-16. Was hard-coded as a 50px
     body padding that did not match the bar's real 47px+ and did nothing for
     the viewport-height hero sitting under it. */
  --mobile-bar-h: 52px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  background: var(--bg-light);
  color: var(--text-on-light);
  font-family: var(--font-body);
  overflow-x: hidden;
}
img { display: block; max-width: 100%; }
a { color: inherit; }

.label {
  font-family: var(--font-body); font-size: .66rem; font-weight: 500;
  letter-spacing: .17em; text-transform: uppercase; color: var(--ink-faint);
  line-height: 1.5;
}
.wrap { max-width: var(--max); margin: 0 auto; padding: 0 var(--pad); }
.band { padding: clamp(4.5rem, 10vw, 8.5rem) 0; }

/* ===== Loader ===== */
/* Real feedback live 2026-10-17, twice over. First: "when it loads it looks
   strange" — a flat, opaque void with brand text and a bare percentage
   floating alone, nothing to say it's the room arriving rather than the
   page being broken. Fixed with a backdrop-filter blur of the canvas
   itself, showing the first loaded frame softly through the loading text.
   Second: that blur picked up every new frame as it streamed in behind it,
   so the backdrop kept re-blurring different content — "don't like the
   flashing colours in the background". Replaced with a fixed, curated
   crossfade of real listing photography (loader-bg-1/2/3.jpg, see
   index.html and the JS cycle below) instead of anything live off the
   canvas — calm, on-brand, and never changes unpredictably underneath the
   loading text. This only ever shows during the Virtual Tour's canvas
   frame-load (see ensureCanvasFrames in app.js) — #loader starts
   `class="loaded"` on a normal page load and is never touched otherwise. */
#loader {
  position: fixed; inset: 0; z-index: 9999;
  background: var(--slate-deep);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1.5rem;
  overflow: hidden;
  transition: opacity 0.6s ease, visibility 0.6s ease;
}
#loader.loaded { opacity: 0; visibility: hidden; pointer-events: none; }
.loader-bg-photos { position: absolute; inset: 0; z-index: 0; }
.loader-bg-photo {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; filter: brightness(.5) saturate(1.05);
  transition: opacity 2s ease;
}
.loader-bg-photo.active { opacity: 1; }
.loader-brand, #loader-bar-track, #loader-percent { position: relative; z-index: 1; }
.loader-brand {
  font-family: var(--font-display);
  font-size: 1.5rem; letter-spacing: .14em;
  color: var(--text-on-dark);
  font-weight: 400;
}
.loader-brand span { color: var(--accent); font-style: italic; }
#loader-bar-track {
  width: 220px; height: 1px; background: rgba(243,239,231,0.15);
}
#loader-bar { width: 0%; height: 100%; background: var(--accent); transition: width 0.2s ease; }
#loader-percent {
  font-size: 0.72rem; letter-spacing: 0.15em; color: var(--ink-faint);
  font-family: var(--font-body);
}

/* ===== Header ===== */
/* Real bug found live 2026-10-17, Sir's own eyes: mix-blend-mode:difference
   only ever guarantees contrast against pure black or white — against an
   actual photo (this hero's sky and leaves, both mid-tone, saturated) it
   produces whatever odd intermediate colour the blend maths land on, which
   on the right-hand "Arrange a viewing" link over green leaves came out a
   washed-out, barely-legible purple. A plain dark scrim behind the bar
   (same technique the walkthrough's own .wt-bar already uses successfully)
   guarantees the same reliable contrast against any photo, not just the
   ones that happen to blend well. */
.site-header {
  position: fixed; top: 0; left: 0; right: 0; z-index: 500;
  padding: 1.6rem 5vw;
  background: linear-gradient(180deg, rgba(10,10,10,.45) 0%, rgba(10,10,10,0) 100%);
  transition: background-color .25s ease, border-color .25s ease;
}
.site-header nav { display: flex; justify-content: space-between; align-items: center; }
.logo {
  font-family: var(--font-display); font-weight: 400; font-style: normal;
  font-size: 1.28rem; letter-spacing: .05em; color: #fff;
  transition: color .15s ease;
}
.logo span { font-style: italic; }
/* Address/price, hidden until the header goes solid — same fade-in pattern
   as the current client template's topbar-mid, so the bar reads as a plain
   brand mark over the hero and picks up the listing facts once solid. */
.header-mid {
  display: flex; align-items: baseline; gap: 1.4rem;
  opacity: 0; transform: translateY(4px); transition: opacity .35s ease, transform .35s ease;
  color: #fff;
}
.site-header.solid .header-mid { opacity: 1; transform: none; }
.header-mid .h-addr { font-family: var(--font-display); font-size: .95rem; }
.header-mid .h-price { font-family: var(--font-body); font-size: .72rem; letter-spacing: .08em; color: rgba(243,239,231,.65); }
.site-header.solid .header-mid .h-price { color: var(--ink-soft); }
@media (max-width: 900px) { .header-mid { display: none; } }
.nav-links { display: flex; gap: 2.5rem; align-items: center; }
.nav-links a {
  color: #fff; text-decoration: none; font-size: 0.68rem;
  letter-spacing: 0.15em; text-transform: uppercase; font-weight: 500;
  transition: color .15s ease;
}
/* Real bug found 2026-10-11: mix-blend-mode:difference reads cleanly over
   the dark hero/canvas, but once scrolled into the plain-light "Full
   details" sections below, a fixed difference-blended header garbles and
   overlaps the page's own heading/footer text instead of reading as a
   clean bar. Solved the same way the current client template's sticky
   topbar does it: go solid-background the moment the hero scrolls out of
   view (toggled by an IntersectionObserver on .hero-standalone in app.js),
   same mechanism, no blend-mode past that point. */
.site-header.solid {
  mix-blend-mode: normal;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}
.site-header.solid .logo,
.site-header.solid .nav-links a { color: var(--ink); }

/* ===== Hero ===== */
/* Structure follows the current template's hero-card pattern: a full-bleed
   photograph (an actual interior still from the walkthrough footage, not a
   drone aerial) with a solid dark card inset bottom-left carrying the real
   listing facts, and a separate pill bottom-right cueing the scroll. */
/* `.hero-wrap` is the positioning anchor for `.hero-card` now (added
   2026-10-16 so the card could move out of `.hero-standalone` in the HTML —
   see the markup comment). Desktop's `position:absolute` card still anchors
   to it with no visual change; it only matters on mobile, where the card
   becomes a real block instead. */
.hero-wrap { position: relative; }
.hero-standalone {
  position: relative; z-index: 10;
  height: 100svh; width: 100%;
  background-color: var(--slate-deep);
  /* Reversed 2026-10-16, real feedback live: the fit-to-height/gutters
     approach (letterboxing a portrait frame on a wide desktop screen)
     looked exactly as bad as it sounds — dead brown bars either side eating
     most of the screen. Sir's own words: "that's not a good idea." Back to
     a full-bleed `cover` crop on every viewport, same as every other site
     in this line — position tuned to keep the house itself in frame rather
     than defaulting to dead-centre, which cropped into roofline and sky. */
  background-image: linear-gradient(180deg, rgba(20,17,13,.12) 0%, rgba(20,17,13,.5) 100%),
                     url('../frames/gallery-exterior.webp');
  background-size: cover; background-position: center 45%;
  color: var(--text-on-dark);
}
/* Real feedback live 2026-10-16, twice over: first the hires swap, still
   not enough — stretching ANY resolution of a 1080x1920 source to `cover`
   a much wider desktop screen upscales past its real ceiling and reads
   soft, no matter which frame or export is used. Real fix: stop stretching
   the sharp photo edge-to-edge at all. `.hero-bg-sharp` (the real <img>,
   see index.html) sits at height:100% of the viewport, which only ever
   scales it DOWN from 1920px tall, never up — genuinely sharp on any
   screen. The sides are filled by this same image, blurred and darkened,
   so there's a tasteful backdrop instead of dead bars, and because it's
   blurred, its own softness from the exact same upscaling is invisible.
   Mobile is completely unaffected — this whole block is desktop-only, and
   .hero-bg-sharp/::before are display:none below 821px by default. */
.hero-bg-sharp { display: none; }
@media (min-width: 821px) {
  .hero-standalone { overflow: hidden; }
  .hero-standalone::before {
    content: ""; position: absolute; inset: -60px; z-index: 0;
    background-image: url('../frames/hero-real-exterior.jpg');
    background-size: cover; background-position: center 45%;
    filter: blur(60px) brightness(.45) saturate(1.15);
  }
  .hero-standalone::after {
    content: ""; position: absolute; inset: 0; z-index: 2;
    background: linear-gradient(180deg, rgba(20,17,13,.15) 0%, rgba(20,17,13,.55) 100%);
  }
  .hero-bg-sharp {
    display: block; position: absolute; z-index: 1;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    height: 100%; width: auto; max-width: none;
  }
}
.hero-card {
  position: absolute; left: var(--pad); bottom: clamp(1.5rem, 4vw, 3.2rem);
  width: min(400px, calc(100% - var(--pad) * 2));
  /* z-index added 2026-10-16, same restructure as the color fix above: as a
     child of .hero-standalone this never needed one (a child always paints
     above its own parent's background regardless of z-index numbers), but
     as a sibling it was losing the stacking order to .hero-standalone's own
     z-index:10 and rendering invisibly behind the photo on desktop. */
  z-index: 20;
  background: var(--slate-deep-78);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  padding: clamp(1.6rem, 2.6vw, 2.2rem) clamp(1.5rem, 2.4vw, 2.1rem) clamp(1.5rem, 2.4vw, 2rem);
  /* Explicit now, not inherited — added 2026-10-16 when the card moved out
     from inside .hero-standalone (which carried this via inheritance) to a
     sibling of it. Caught live: address/price and the Floorplan/Room
     dimensions/Photographs links went near-invisible, same dark-on-dark,
     the instant the card stopped being its child. */
  color: var(--text-on-dark);
}
.hero-card .label { color: rgba(243,239,231,.65); margin-bottom: .9rem; }
.hero-heading {
  font-family: var(--font-display); font-weight: 400;
  font-size: clamp(2.1rem, 3.4vw, 2.85rem); line-height: 1.08;
  margin-bottom: .9rem; color: var(--text-on-dark);
}
.hero-meta {
  font-family: var(--font-body); font-size: .7rem; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1.9; opacity: .88;
}
/* Image walkthrough tile inside the hero card, added 2026-10-16 — a real
   picture to tap rather than a text link buried in the list. Hidden on
   desktop, where the card already opens into the full details below it and
   the hero-links row's own "Walk through the house" tlink still exists. */
.hero-wt-tile {
  display: none; width: 100%; border: none; padding: 0; margin: 1.1rem 0 0;
  cursor: pointer; appearance: none; background: none; text-align: left;
  position: relative; border-radius: 4px; overflow: hidden;
}
.hero-wt-tile img { display: block; width: 100%; height: 7rem; object-fit: cover; }
.hero-wt-tile-label {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: .6rem;
  background: linear-gradient(0deg, rgba(10,10,10,.75), rgba(10,10,10,.35));
  color: #fff; font-family: var(--font-body); font-size: .74rem; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase;
}
@media (max-width: 780px) { .hero-wt-tile { display: block; } }

.hero-links {
  display: flex; flex-wrap: wrap; gap: .6rem 1.25rem;
  margin: 1.4rem 0 1.5rem; padding-top: 1.3rem; border-top: 1px solid rgba(255,255,255,.22);
}
.tlink {
  font-family: var(--font-body); font-size: .72rem; letter-spacing: .04em;
  text-decoration: none; border-bottom: 1px solid currentColor;
  padding-bottom: 3px; opacity: .85; transition: opacity .2s ease;
}
.tlink:hover { opacity: 1; }

.btn {
  display: inline-block; font-family: var(--font-body); font-size: .715rem; font-weight: 500;
  letter-spacing: .15em; text-transform: uppercase; line-height: 1;
  padding: 1.05em 1.9em; border: 1px solid #fff; border-radius: 2px;
  background: #fff; color: var(--slate-deep); text-decoration: none; cursor: pointer;
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.btn:hover { background: transparent; color: #fff; }

/* Added 2026-10-16, Sir's direct ask — a second hero CTA, "Virtual tour",
   next to "Arrange a viewing", opening the scroll-driven canvas walkthrough
   (see .btn-tour and data-wt-mode="canvas"). Needs its own row so the two
   buttons sit side by side rather than .btn alone stretching full-width. */
.hero-cta-row { display: flex; flex-wrap: wrap; gap: .8rem; }
/* Gold outline so it reads as a clearly different action from the solid
   white "Arrange a viewing" .btn, not a second copy of the same button. */
.btn-tour {
  background: transparent; border-color: var(--accent); color: var(--accent);
}
.btn-tour:hover { background: var(--accent); color: var(--slate-deep); border-color: var(--accent); }

/* Now a real <button> (data-wt-open — opens #walkthrough), not a scroll
   cue, so it gets cursor/appearance resets a <div> never needed. */
.scroll-indicator {
  position: absolute; right: clamp(1.5rem, 4vw, var(--pad)); bottom: clamp(2rem, 6vw, 4.5rem);
  display: inline-flex; align-items: center; gap: .9rem;
  font-family: var(--font-body); font-size: .95rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: #fff;
  background: rgba(10,10,10,.92); backdrop-filter: blur(10px) saturate(140%);
  border: 2px solid rgba(255,255,255,.85); border-radius: 999px;
  padding: 1.3em 2.3em; cursor: pointer; appearance: none;
  box-shadow: 0 6px 28px rgba(0,0,0,.5), 0 0 0 0 rgba(255,255,255,.5);
  animation: wt-pulse 2.6s ease-in-out infinite;
  transition: background .2s ease, transform .2s ease;
}
.scroll-indicator:hover, .scroll-indicator:focus-visible {
  background: #000; transform: scale(1.06); animation-play-state: paused;
}
@keyframes wt-pulse {
  0%, 100% { box-shadow: 0 6px 28px rgba(0,0,0,.5), 0 0 0 0 rgba(255,255,255,.45); }
  50% { box-shadow: 0 6px 28px rgba(0,0,0,.5), 0 0 0 10px rgba(255,255,255,0); }
}
/* A play wedge, not a down-chevron — a chevron reads "scroll for more",
   which is exactly the implied auto-play cue sir asked to remove. */
.wt-enter-icon {
  width: 0; height: 0; margin-left: 2px;
  border-top: 7px solid transparent; border-bottom: 7px solid transparent;
  border-left: 11px solid #fff;
}

@media (max-width: 780px) {
  /* Third real iteration, same day, all from Sir's own phone. First cut:
     card visible instantly, ate the whole first screen. Second cut: card
     as an absolute overlay that faded in on scroll - better, but a fast
     scroll could blow straight past it before the fade finished, since it
     never physically occupied space of its own. Final shape: `.hero-card`
     is a plain block in normal flow, a real sibling of `.hero-standalone`
     (see the HTML restructure) sitting immediately below the full-screen
     photo. Scrolling down by any amount, fast or slow, physically moves
     you into it - nothing to animate, nothing to miss. No JS needed for
     this any more either. */
  .hero-standalone { height: 100svh; }
  .hero-card {
    position: static; width: auto; left: auto; right: auto; bottom: auto;
    padding: 1.5rem var(--pad) 1.8rem;
    background: var(--slate-deep); backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .hero-heading { font-size: 1.85rem; margin-bottom: .6rem; }
  .hero-links { margin: 1rem 0 1.1rem; padding-top: 1rem; }
  /* Tap targets, 2026-10-16. Audited on an iPhone profile: five of the seven
     tappable things above the fold were under the 44px minimum a thumb can
     reliably hit — the hero links were 18px and the walkthrough entry button
     itself only 21px. Padding rather than height, so nothing reflows. */
  .tlink { padding-top: .75rem; padding-bottom: .75rem; }
  .hero-links { gap: 0 1.25rem; margin-top: 1rem; }
  .wt-entry-btn { min-height: 44px; }
}

@media (prefers-reduced-motion: no-preference) {
  .hero-standalone { will-change: opacity; }
}

/* ===== Canvas ===== (mechanic — unchanged, minus the scroll-linked
   circle-wipe reveal: that animated clip-path was keyed to scrollContainer's
   scroll progress, which only existed because the walkthrough used to be
   auto-triggered in-page. Entry is a click now, not a scroll, and
   #walkthrough itself snaps open instantly — same convention
   pytches-close-site's own #walkthrough uses, no transition on the overlay
   shell — so there's nothing left for a clip-path wipe to key off. */
/* Reversed again, same day: the canvas walkthrough isn't gone, it's a
   second, separate "Virtual tour" entry point now (see .btn-tour in
   index.html) — Sir had already promised a scroll-driven tour by email, and
   the real <video> stays the default for every other "walk through" trigger.
   #walkthrough opens in one mode or the other via html.wt-mode-canvas,
   toggled in app.js's doOpen(mode) — hidden here by default (video mode),
   shown when that class is present. */
/* Real rebuild 2026-10-17, Sir's direct call after a video-frame scrub
   engine never looked right no matter how many times it was patched:
   "build the entire scroll-driven section out of the images and not the
   video". Full-bleed real photography, one room at a time, two stacked
   <img> elements crossfading (see app.js's showPhoto) — no canvas, no
   contain-fit margins, no sampled background colour to get wrong. */
/* Real feedback live 2026-10-17, Sir's own words: "the background
   shouldn't be dark. It should be something else." — swapped to the same
   warm paper tone the "Full details" sections below already use, so the
   framed photo reads like a real print matted on a light board rather
   than floating in a dark void. .wt-bar/.wt-hint get dark-ink overrides
   further down to stay legible against it — .wt-close and the photo's own
   caption chip already carry their own backgrounds, so neither needed one. */
.wt-photo-stage {
  display: none; position: fixed; inset: 0; z-index: 1; background: var(--paper);
}
html.wt-mode-canvas .wt-photo-stage { display: block; }
/* Real feedback live 2026-10-17, Sir's own words: "the room takes up the
   whole screen... I think it being this big just kind of makes it look a
   little bit [off]" — edge-to-edge read as too much. Pulled in from the
   viewport into a real framed box instead (margin here, not on .wt-photo
   itself), with its own clip/radius/shadow so the Ken Burns zoom below
   stays contained inside the frame rather than bleeding past its edges. */
.wt-photo-frame {
  position: absolute; inset: clamp(2rem, 6vh, 4.5rem) clamp(2.5rem, 7vw, 6rem);
  box-sizing: border-box; border: 14px solid #fff;
  border-radius: 4px; overflow: hidden;
  box-shadow: 0 40px 100px rgba(0,0,0,.55);
}
.wt-photo {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: 0; transition: opacity .6s ease;
  transform: scale(1); transform-origin: center center;
}
/* Real feedback live 2026-10-17, Sir's own words: "the walkthrough images
   could do with some animation... what is this?" — a static photo with
   nothing but a crossfade on the way in read as flat. A slow continuous
   Ken Burns zoom (the standard real-estate-slideshow treatment) while a
   photo is on screen, restarted cleanly on each reveal by app.js's
   showPhoto (removes then re-adds this animation so it never looks stuck
   mid-cycle after a quick room change). */
.wt-photo.on-top { opacity: 1; z-index: 2; animation: wt-kenburns 9s ease-in-out infinite alternate; }
@keyframes wt-kenburns {
  from { transform: scale(1); }
  to { transform: scale(1.1); }
}
/* Nudged in from the raw viewport edges by the same amount as the frame's
   own inset (above) plus a little breathing room — sits inside the
   photo's own bottom-left corner now that the frame isn't full-bleed. */
/* Moved top-right 2026-10-17, Sir's direct ask ("in the top right or top
   left, a card saying what room we're in"). Sits just inside the photo
   frame's own top-right corner, clear of .wt-close (top-right of the
   whole screen, a separate higher layer) rather than colliding with it. */
.wt-photo-caption {
  position: fixed;
  right: calc(clamp(2.5rem, 7vw, 6rem) + 1.4rem);
  top: calc(clamp(2rem, 6vh, 4.5rem) + 1.4rem);
  z-index: 10; color: var(--text-on-dark);
  background: var(--slate-deep-78); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border-radius: 4px; padding: 1rem 1.4rem;
}
.wt-photo-label {
  display: block; font-family: var(--font-body); font-size: .62rem;
  letter-spacing: .16em; text-transform: uppercase; color: rgba(243,239,231,.6);
  margin-bottom: .35rem;
}
.wt-photo-room-name {
  display: block; font-family: var(--font-display); font-size: 1.4rem; font-weight: 400;
}

/* ===== Walkthrough overlay — the door ===== (NEW 2026-10-11)
   #walkthrough is a fixed, full-viewport element that is its own scroll
   container (overflow-y:auto here). .wt-photo-stage/#dark-overlay/.marquee-wrap
   above and below are all position:fixed, so they stay viewport-relative
   regardless of which element the visitor is actually scrolling — exactly
   the same reasoning pytches-close-site's own #walkthrough CSS documents
   for its pinned stage. app.js points ScrollTrigger's `scroller` at this
   element instead of the window; nothing else about the engine changed. */
#walkthrough {
  position: fixed; inset: 0; z-index: 550;
  background: var(--slate-deep);
  display: none;
  /* overflow-y changed auto -> hidden 2026-10-16: native scroll position no
     longer drives anything (see app.js's room-by-room rebuild) — wheel and
     touch are captured directly as discrete step gestures instead, so a
     real scrollable area here would just fight the canvas for the same
     input with nothing to show for it. */
  overflow: hidden;
  overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
}
html.wt-open #walkthrough { display: block; }
html.wt-open, html.wt-open body { overflow: hidden; }

/* Fixed chrome over the walkthrough — brand + close, findable at every
   point in the scroll, same "never a trap" reasoning as the reference
   build: close button, Escape key, and the browser back button (app.js)
   are all live for as long as #walkthrough is open. */
.wt-bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 70;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 1.4rem clamp(1.1rem, 5vw, 2.8rem);
  color: var(--text-on-dark); pointer-events: none;
  background: linear-gradient(180deg, rgba(20,17,13,.55) 0%, rgba(20,17,13,0) 100%);
}
.wt-bar > * { pointer-events: auto; }
.wt-brand {
  font-family: var(--font-body); font-size: .68rem; font-weight: 500; letter-spacing: .18em;
  text-transform: uppercase; opacity: .85;
}
.wt-close {
  display: inline-flex; align-items: center; gap: .6rem; cursor: pointer;
  font-family: var(--font-body); font-size: .68rem; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: #fff; background: rgba(18,20,21,.5);
  border: 1px solid rgba(255,255,255,.4); border-radius: 999px;
  padding: .7em 1.2em; backdrop-filter: blur(8px); transition: background .2s ease;
}
.wt-close:hover, .wt-close:focus-visible { background: rgba(255,255,255,.2); }
.wt-close svg { width: 11px; height: 11px; }

/* Moved to the top 2026-10-16, Sir's direct ask — this used to sit at the
   bottom of the screen, easy to miss entirely on a quick look. */
.wt-hint {
  position: fixed; left: 50%; top: clamp(4.5rem, 10vw, 6rem); transform: translateX(-50%);
  z-index: 70; color: rgba(243,239,231,.85); font-family: var(--font-body); font-size: .68rem;
  font-weight: 500; letter-spacing: .18em; text-transform: uppercase; text-align: center;
  pointer-events: none; transition: opacity .7s ease; white-space: nowrap;
}
.wt-hint.gone { opacity: 0; }
/* Hidden by default (video mode), shown in canvas mode only — same
   html.wt-mode-canvas switch as .wt-photo-stage above. */
.wt-hint { display: none; }
html.wt-mode-canvas .wt-hint { display: block; }
/* Both .wt-bar's brand text and .wt-hint were built light-on-dark — now
   that the margin behind them is the warm paper tone, not slate, they'd
   be nearly invisible without this. .wt-close is untouched: its own dark
   pill background already carries its own contrast regardless of what's
   behind it, same reason the photo's caption chip needed no change either. */
html.wt-mode-canvas .wt-bar {
  background: linear-gradient(180deg, rgba(245,243,239,.9) 0%, rgba(245,243,239,0) 100%);
}
html.wt-mode-canvas .wt-brand { color: var(--ink); }
html.wt-mode-canvas .wt-hint { color: var(--ink-soft); }


/* Real video — the default "walk through" experience everywhere (hero tile,
   the Photographs section's own entry button). Virtual tour (a second,
   separate button, see .btn-tour) opens the scroll-driven canvas instead;
   html.wt-mode-canvas swaps which of the two is visible. */
.wt-video {
  display: block; position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; background: #000;
  cursor: pointer;
}
html.wt-mode-canvas .wt-video { display: none; }
/* The one extra stop past the last real room (see VIDEO_STOP_INDEX in
   app.js) — the real walkthrough video, optional, paused by default.
   app.js physically moves the real <video> (and its control bar) inside
   .wt-photo-frame for this stop, so it just needs to fill that box
   exactly the way .wt-photo already does — the frame itself (inset,
   border, radius, shadow, overflow:hidden) is what actually places and
   frames it; this rule only needs to make the video obey that box. */
html.wt-mode-canvas.wt-video-stop .wt-video {
  display: block; position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: contain; z-index: 2;
}
html.wt-mode-canvas.wt-video-stop .wt-photo { opacity: 0 !important; }
/* Real feedback live 2026-10-17, Sir's direct ask: the real walkthrough
   file is genuinely portrait (720x1280, verified via ffprobe) — cropping
   it with object-fit:cover into the same wide, landscape-shaped box the
   photos use was cutting real content off the sides. The frame itself
   reshapes to the video's own aspect ratio for this one stop (still
   centred, still the same top/bottom margins as every other room) so
   the whole portrait frame shows, nothing cropped. */
html.wt-mode-canvas.wt-video-stop .wt-photo-frame {
  left: 50%; right: auto; transform: translateX(-50%);
  width: auto; aspect-ratio: 9 / 16;
  max-width: calc(100vw - 2 * clamp(2.5rem, 7vw, 6rem));
}
/* Custom control bar this page fully owns, 2026-10-16 — see the HTML
   comment on .wt-video for why native `controls` got dropped, and why a
   second pass was needed: the first cut only had play/pause + fullscreen,
   and Sir's direct follow-up asked for real pause/seek/speed control. */
.wt-video-bar {
  display: flex; position: absolute; left: 0; right: 0; bottom: 0; z-index: 60;
  align-items: center; gap: .7rem; padding: .7rem .9rem;
  background: linear-gradient(0deg, rgba(10,10,10,.85) 0%, rgba(10,10,10,0) 100%);
}
html.wt-mode-canvas .wt-video-bar { display: none; }
html.wt-mode-canvas.wt-video-stop .wt-video-bar { display: flex; }
/* 44px minimum on every tappable control — same tap-target bar the hero
   links were audited against earlier tonight, a thumb needs to hit these
   reliably first time. */
.wt-vbtn {
  width: 44px; height: 44px; flex: none; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.4); border-radius: 6px;
  color: #fff; cursor: pointer; appearance: none; font-family: var(--font-body);
  font-size: .78rem; font-weight: 600;
}
.wt-vbtn svg { width: 20px; height: 20px; }
.wt-video-time {
  flex: none; color: #fff; font-family: var(--font-body); font-size: .68rem;
  letter-spacing: .04em; white-space: nowrap; min-width: 5.2em;
}
.wt-video-seek {
  flex: 1 1 auto; min-width: 0; appearance: none; height: 4px; border-radius: 2px;
  background: rgba(255,255,255,.3); outline: none;
}
.wt-video-seek::-webkit-slider-thumb {
  appearance: none; width: 16px; height: 16px; border-radius: 50%; background: #fff; cursor: pointer;
}
.wt-video-seek::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: #fff; border: none; cursor: pointer;
}
.wt-video-fullscreen { position: static; }

@media (max-width: 780px) {
  .wt-brand { font-size: .62rem; letter-spacing: .14em; }
  .wt-close { font-size: .62rem; padding: .65em 1em; }
}

/* ===== Dark overlay ===== (mechanic — unchanged) */
#dark-overlay {
  display: none; position: fixed; inset: 0; z-index: 50;
  background: var(--bg-dark);
  opacity: 0; pointer-events: none;
}
html.wt-mode-canvas #dark-overlay { display: block; }

/* ===== Marquee ===== (mechanic — unchanged) */
.marquee-wrap {
  display: none;
  position: fixed; top: 46%; left: 0; z-index: 40;
  width: 200%; pointer-events: none;
  opacity: 0;
}
html.wt-mode-canvas .marquee-wrap { display: block; }
.marquee-text {
  font-family: var(--font-display); font-weight: 400; font-style: italic;
  font-size: 13vw; white-space: nowrap;
  color: rgba(243,239,231,0.07);
  -webkit-text-stroke: 1px rgba(243,239,231,0.16);
}

/* ===== Scroll container ===== (mechanic — unchanged) */
/* display:none added 2026-10-16 alongside the desktop video switch — this
   leftover canvas-engine div (invisible, but still stacked at z-index:60)
   was found eating every tap meant for the video on mobile; now that the
   video replaces the canvas on desktop too, the same tap-stealing bug would
   hit there as well without this. */
#scroll-container { display: none; position: relative; height: 1500vh; z-index: 60; }

.scroll-section {
  position: fixed; top: 50%; left: 0; width: 100%;
  transform: translateY(-50%);
  opacity: 0;
  /* Matches the opacity default — see the pointerEvents comment in
     setupSectionAnimation() in app.js for the click-through bug this
     closes (an invisible fixed section eating clicks meant for the one
     stacked visibly on top of it). */
  pointer-events: none;
}
.section-inner { max-width: 40vw; }

.align-left { padding-left: 6vw; padding-right: 55vw; }
.align-right { padding-left: 55vw; padding-right: 6vw; }
.align-left .section-inner, .align-right .section-inner { max-width: 40vw; }

.section-content .section-inner {
  background: var(--slate-deep-78);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding: 2.2rem 2.4rem;
  border-radius: 2px;
}
.section-content .section-label {
  display: block; font-family: var(--font-body); font-size: 0.68rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 1.2rem; font-weight: 500;
}
.section-content .section-heading {
  font-family: var(--font-display); font-weight: 400;
  font-size: 3.2rem; line-height: 1.1; margin-bottom: 1.2rem;
  color: var(--text-on-dark);
}
.section-content .section-body {
  font-size: 1.05rem; line-height: 1.65; color: rgba(243,239,231,0.78);
  font-family: var(--font-display);
}

/* ===== Stats ===== (class names are JS hooks — unchanged) */
.section-stats {
  width: 100%; padding: 0 8vw;
}
.stats-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 3vw;
}
.stat { display: flex; flex-direction: column; align-items: flex-start; }
.stat-number {
  font-family: var(--font-display); font-weight: 400;
  font-size: 4.6vw; color: var(--text-on-dark); line-height: 1;
}
.stat-suffix {
  font-family: var(--font-body); font-size: 1rem; color: var(--accent); margin-top: 0.6rem;
  text-transform: uppercase; letter-spacing: 0.1em;
}
.stat-label {
  font-family: var(--font-body); font-size: 0.78rem; color: rgba(243,239,231,0.55); margin-top: 0.35rem;
}

/* ===== CTA ===== */
.section-cta .section-inner {
  background: var(--slate-deep-78);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  padding: 2.2rem 2.4rem;
  border-radius: 2px;
}
.section-cta .section-label {
  display: block; font-family: var(--font-body); font-size: 0.68rem; letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--accent); margin-bottom: 1.2rem; font-weight: 500;
}
.section-cta .section-heading {
  font-family: var(--font-display); font-weight: 400;
  font-size: 3.6rem; line-height: 1.08; margin-bottom: 1.2rem;
  color: var(--text-on-dark);
}
.section-cta .section-body {
  font-size: 1.05rem; line-height: 1.65; color: rgba(243,239,231,0.78); margin-bottom: 1.8rem;
  max-width: 34ch; font-family: var(--font-display);
}
.cta-button {
  display: inline-block; font-family: var(--font-body);
  padding: 1.05em 1.9em; border: 1px solid #fff;
  background: #fff; color: var(--slate-deep);
  text-decoration: none; font-size: 0.715rem; font-weight: 500; letter-spacing: 0.15em;
  text-transform: uppercase; border-radius: 2px; cursor: pointer; appearance: none;
  transition: background 0.25s ease, color .25s ease;
}
.cta-button:hover { background: transparent; color: #fff; }

/* ===== Full details — the current real-client register ===== */
.details-section {
  position: relative; z-index: 90;
  background: var(--bg-light); color: var(--text-on-light);
}
h1, h2, h3 { font-family: var(--font-display); font-weight: 400; line-height: 1.14; }
.section-heading-md { font-size: clamp(1.6rem, 2.8vw, 2.2rem); margin: 0.6rem 0 1.4rem; }
.sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 2rem;
  margin-bottom: clamp(1.4rem, 3vw, 2rem); flex-wrap: wrap; }
.sec-head .section-heading-md { margin: 0; }

/* Overview: sticky hairline sidebar + prose, straight off the current
   client template (Aucoot's two-column spine). */
.overview { padding-top: clamp(3.5rem, 7vw, 6rem); }
.ov-grid { display: grid; grid-template-columns: 230px minmax(0,1fr); gap: clamp(2.5rem, 6vw, 6rem); }
@media (max-width: 940px) { .ov-grid { grid-template-columns: 1fr; gap: 3rem; } }

.sidebar { position: sticky; top: 2.5rem; align-self: start; font-family: var(--font-body); }
.sb-block { padding: 0 0 1.15rem; margin-bottom: 1.15rem; border-bottom: 1px solid var(--line); }
.sb-block:last-child { border-bottom: 0; }
.sb-block .label { display: block; margin-bottom: .5rem; }
.sb-block p, .sb-block li { font-size: .9rem; line-height: 1.6; font-family: var(--font-body); }
.sb-block ul { list-style: none; }
.sb-block a { display: block; text-decoration: none; border-bottom: 1px solid var(--line);
  padding: .15rem 0; width: max-content; transition: border-color .2s ease; }
.sb-block a:hover { border-bottom-color: var(--ink); }
.sb-sub { color: var(--ink-soft); font-size: .8rem !important; }
@media (max-width: 940px) {
  .sidebar { position: static; display: grid; grid-template-columns: repeat(auto-fit,minmax(150px,1fr));
    gap: 0 2rem; border-top: 1px solid var(--line); padding-top: 1.6rem; }
  .sb-block { border-bottom: 1px solid var(--line-soft); }
  .sb-block:last-child { border-bottom: 1px solid var(--line-soft); }
}

.standfirst {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.9vw, 2.05rem); line-height: 1.3; font-style: italic;
  max-width: 24ch; margin-bottom: clamp(2.2rem, 4vw, 3.2rem);
}
.prose { max-width: 36em; font-family: var(--font-display); font-size: 1.14rem; line-height: 1.7; }
.prose p { margin-bottom: 1.3em; }
.prose p:last-child { margin-bottom: 0; }
.prose .first { font-size: 1.2rem; }
.prose h3 {
  font-family: var(--font-body); font-size: .68rem; font-weight: 500; letter-spacing: .17em;
  text-transform: uppercase; color: var(--ink-faint);
  margin: 2.8rem 0 1.1rem;
}
.prose h3:first-child { margin-top: 0; }

/* Photographs set between passages of prose, two up with a real gutter —
   straight off the current client template. */
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(.8rem, 1.6vw, 1.4rem);
  margin: clamp(2.2rem, 4vw, 3.2rem) 0; max-width: 36em; }
.pair figure { margin: 0; }
.pair img { width: 100%; aspect-ratio: 4/3; object-fit: cover; background: var(--line-soft); }
.pair figcaption { font-family: var(--font-body); font-size: .72rem; color: var(--ink-faint);
  margin-top: .65rem; line-height: 1.5; }
@media (max-width: 620px) { .pair { grid-template-columns: 1fr; } }

/* Outline button for light (paper/white) section backgrounds — .btn itself
   is a solid white pill built for the dark hero, which would read invisible
   on a white "details-section", so the "show all" control gets its own
   ink-bordered variant matching the current client template's .btn. */
.btn-outline {
  display: inline-block; font-family: var(--font-body); font-size: .715rem; font-weight: 500;
  letter-spacing: .15em; text-transform: uppercase; line-height: 1;
  padding: 1.05em 1.9em; border: 1px solid var(--ink); border-radius: 2px;
  background: none; color: var(--ink); text-decoration: none; cursor: pointer;
  transition: background .25s ease, color .25s ease;
}
.btn-outline:hover { background: var(--ink); color: var(--paper); }

.worth { margin-top: clamp(3rem, 6vw, 4.5rem); border-top: 1px solid var(--ink); padding-top: 1.4rem; }
.worth .label { display: block; margin-bottom: 1rem; }
.worth ul { list-style: none; columns: 2; column-gap: 3rem; }
.worth li { break-inside: avoid; font-size: 1.02rem; line-height: 1.55; font-family: var(--font-display);
  padding: .7rem 0; border-bottom: 1px solid var(--line-soft); }
@media (max-width: 700px) { .worth ul { columns: 1; } }

/* Photographs — a real gallery grid with a collapse/expand control, the
   section the original build was missing entirely against the current
   client template. */
.photographs { background: var(--white); border-top: 1px solid var(--line); }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(.8rem, 1.6vw, 1.4rem); }
@media (max-width: 620px) { .grid { grid-template-columns: 1fr; } }
.grid a { display: block; overflow: hidden; background: var(--line-soft); aspect-ratio: 4/3; }
.grid img { width: 100%; height: 100%; object-fit: cover; transition: transform .9s cubic-bezier(.2,.7,.2,1); }
.grid a:hover img { transform: scale(1.035); }
.grid.collapsed a:nth-child(n+9) { display: none; }
.more-row { text-align: center; margin-top: clamp(2rem, 4vw, 3rem); }

/* Lightbox for the photograph grid. z-index 600 is load-bearing: .site-header
   sits at z-index 500 (it has to clear the canvas walkthrough and footer),
   so a lower lightbox z-index leaves the fixed header intercepting clicks on
   the close button — caught live via an automated click that timed out on
   ".lb .x" with "site-header intercepts pointer events" in the trace. */
.lb { position: fixed; inset: 0; z-index: 600; background: rgba(10,11,12,.95); display: none;
  place-items: center; padding: clamp(1rem, 5vw, 3.5rem); }
.lb.open { display: grid; }
.lb img { max-width: 100%; max-height: 86vh; object-fit: contain; }
.lb button { position: absolute; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.3);
  color: #fff; font-size: 1.4rem; line-height: 1; cursor: pointer; width: 46px; height: 46px;
  border-radius: 999px; transition: background .2s ease; }
.lb button:hover, .lb button:focus-visible { background: rgba(255,255,255,.22); }
.lb .x { top: clamp(1rem, 3vw, 1.8rem); right: clamp(1rem, 3vw, 1.8rem); }
.lb .prev { left: clamp(.6rem, 2vw, 1.8rem); top: 50%; transform: translateY(-50%); }
.lb .next { right: clamp(.6rem, 2vw, 1.8rem); top: 50%; transform: translateY(-50%); }
.lb-cap { position: absolute; left: 0; right: 0; bottom: clamp(.8rem, 2.4vw, 1.6rem); text-align: center;
  color: rgba(255,255,255,.8); font-family: var(--font-body); font-size: .73rem; padding: 0 4rem; }

/* Floorplan — same SVG asset as the original build, just restyled to the
   current template's bordered white-panel treatment. */
.floorplan { background: var(--white); border-top: 1px solid var(--line); }
.floorplan .wrap { max-width: 1100px; }
.floorplan-note { color: var(--ink-soft); max-width: 50ch; font-size: 1rem; margin-top: -1rem; margin-bottom: 1.6rem; font-family: var(--font-body); }
.floorplan-wrap { border: 1px solid var(--line); padding: 2rem; background: #fff; }
.floorplan-svg { width: 100%; height: auto; display: block; }
.fp-room { fill: none; stroke: var(--ink); stroke-width: 1.5; }
.fp-room-name { font-family: var(--font-display); font-weight: 400; font-size: 15px; fill: var(--ink); text-anchor: middle; }
.fp-dim { font-family: var(--font-body); font-size: 11px; fill: var(--ink-soft); text-anchor: middle; }
.fp-room-name-vert { font-family: var(--font-display); font-weight: 400; font-size: 13px; fill: var(--ink-soft); text-anchor: middle; }
.fp-caption { font-family: var(--font-body); font-weight: 500; font-size: 11px; letter-spacing: 0.12em;
  fill: var(--ink-faint); text-anchor: middle; text-transform: uppercase; }

/* Room dimensions — hairline rows, straight off the current template. */
.numbers { background: var(--paper); border-top: 1px solid var(--line); }
.num-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px,1fr));
  gap: clamp(2.4rem, 4vw, 3.6rem); margin-top: clamp(2rem, 4vw, 3rem); }
.num-group h3 { font-family: var(--font-body); font-size: .68rem; font-weight: 500; letter-spacing: .17em;
  text-transform: uppercase; color: var(--ink); padding-bottom: .85rem; border-bottom: 1px solid var(--ink); }
.num-group h3.spaced { margin-top: 2.8rem; }
.num-group dl > div { display: flex; justify-content: space-between; align-items: baseline;
  gap: 1.2rem; padding: .8rem 0; border-bottom: 1px solid var(--line); }
.num-group dt { font-size: 1.02rem; font-family: var(--font-display); }
.num-group dd { margin: 0; text-align: right; font-family: var(--font-body); font-size: .85rem;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.num-group dd em { display: block; font-style: normal; font-size: .74rem; color: var(--ink-faint); margin-top: .2rem; }
.num-foot { margin-top: 2.4rem; font-size: .92rem; color: var(--ink-soft); max-width: 60ch; font-family: var(--font-body); }

/* Location — a text column plus one real exterior photograph, straight off
   the current client template's two-column loc-grid. */
.location { background: var(--white); border-top: 1px solid var(--line); }
.loc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2.5rem, 6vw, 5rem); align-items: center; }
@media (max-width: 820px) { .loc-grid { grid-template-columns: 1fr; } }
.loc-grid img { width: 100%; aspect-ratio: 4/3; object-fit: cover; background: var(--line-soft); }
.loc-grid .prose { max-width: 38em; }

/* Walkthrough entry — a small, modest two-column band (image + copy +
   button), not a hero takeover. Sir's explicit ask 2026-10-11: pull the
   walkthrough trigger out of the hero into its own section here, between
   the details overview and Photographs, sized down from the hero pill. */
.walkthrough-entry {
  position: relative; min-height: 70vh;
  background-image: linear-gradient(rgba(10,10,10,.18), rgba(10,10,10,.3)), url('../frames/listing-23.jpg');
  background-size: cover; background-position: center;
  display: flex; align-items: center; justify-content: center;
}
/* Second real mobile bug, found 2026-10-16 alongside the hero-card one: a
   16:9 frame forced to cover a narrow, tall section crops to an
   unrecognisable close-up (a picture frame reads as an abstract smear, a
   rug reads as a random texture) above and below the white card. No single
   crop position fixes this for every property's frame, so mobile drops the
   photo entirely for the same clean paper background used elsewhere on the
   page - the white card was already the whole point of this section. Placed
   here, after the base rule, so source order doesn't let the base rule win
   the tie the earlier mobile media-query block lost it to. */
@media (max-width: 780px) {
  .walkthrough-entry { background-image: none; background-color: var(--paper); min-height: auto; padding: 3rem 0; }
}
.wt-entry-card {
  background: var(--slate-deep-78);
  backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,.22);
  box-shadow: 0 10px 40px rgba(0,0,0,.25);
  padding: clamp(2rem, 4vw, 3.2rem) clamp(2.2rem, 5vw, 4rem);
  max-width: 34em; text-align: center;
  color: var(--text-on-dark);
}
.wt-entry-copy { color: rgba(243,239,231,.8); margin: .8rem 0 1.6rem; }
.wt-entry-btn { display: inline-flex; align-items: center; gap: .7rem; }
.wt-entry-btn .wt-enter-icon { border-left-color: var(--accent); }
.wt-entry-btn:hover .wt-enter-icon { border-left-color: var(--slate-deep); }

/* Viewing / contact — the slate band, same as the current template. */
.viewing { background: var(--slate); color: #fff; }
.view-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2.5rem, 6vw, 5rem); }
@media (max-width: 820px) { .view-grid { grid-template-columns: 1fr; } }
.viewing h2 { font-size: clamp(1.8rem, 3.2vw, 2.5rem); margin-bottom: 1.2rem; }
.viewing .intro { font-size: 1.08rem; color: rgba(255,255,255,.82); max-width: 34ch; margin-bottom: 2.4rem; font-family: var(--font-display); }
.viewing .label { color: rgba(255,255,255,.6); display: block; margin-bottom: .5rem; }
.agent-block { padding-top: 1.4rem; border-top: 1px solid rgba(255,255,255,.25); margin-top: 1.8rem; }
.agent-block p { font-size: 1.02rem; line-height: 1.6; font-family: var(--font-body); }
.phone { display: inline-block; font-family: var(--font-display); font-size: clamp(1.5rem, 3vw, 2.1rem); text-decoration: none;
  border-bottom: 1px solid rgba(255,255,255,.4); margin-top: .6rem; font-variant-numeric: tabular-nums; }
.phone:hover { border-bottom-color: #fff; }
.form { display: grid; gap: .75rem; }
.form input, .form textarea {
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.3); color: #fff;
  padding: .95rem 1.05rem; font-family: var(--font-body); font-size: .92rem; border-radius: 2px; width: 100%;
}
.form input:focus, .form textarea:focus { outline: 2px solid rgba(255,255,255,.65); outline-offset: 1px; }
.form input::placeholder, .form textarea::placeholder { color: rgba(255,255,255,.55); }
.form button { margin-top: .4rem; border: none; cursor: pointer; }
.sent { font-size: .92rem; color: rgba(255,255,255,.85); margin-top: .4rem; font-family: var(--font-body); }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

/* ===== Footer ===== (z-index 100 is load-bearing — see the comment on
   #scroll-container above; every .scroll-section is position:fixed, so the
   footer needs to sit above the highest fixed layer or the persisted CTA
   text bleeds through it.) Single small-print paragraph, no marketing
   columns — straight off the current client template's minimal footer. */
.site-footer {
  position: relative; z-index: 100;
  background: var(--bg-dark); color: var(--text-on-dark);
  padding: 3rem 0; font-size: .88rem; line-height: 1.7;
}
.site-footer .wrap { max-width: var(--max); }
.site-footer p { max-width: 80ch; color: rgba(243,239,231,.72); }

/* Mobile action bar — present on every current client build, absent from
   the old Summit Realty; cheap addition, consistent with the register. */
.mobile-bar { display: none; }
@media (max-width: 780px) {
  .mobile-bar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 110; display: grid;
    grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line);
    border-top: 1px solid var(--line);
  }
  .mobile-bar a {
    background: var(--paper); text-align: center; padding: 1.05rem .5rem; text-decoration: none;
    font-family: var(--font-body); font-size: .67rem; font-weight: 500; letter-spacing: .14em;
    text-transform: uppercase; color: var(--ink);
  }
  .mobile-bar a.primary { background: var(--ink); color: var(--paper); }
  body { padding-bottom: var(--mobile-bar-h); }

  /* Real fault, measured 2026-10-16: the hero is min-height:100svh, so its
     own card still ran underneath this fixed bar no matter what padding the
     body carried — on an iPhone profile the hero's "Arrange a viewing"
     button was sliced in half by it, which is the first thing a prospect
     opening the pitch link on a phone saw. The body padding only ever moved
     content that comes AFTER the viewport-height hero. */
  .hero-standalone { min-height: calc(100svh - var(--mobile-bar-h)); }
  .hero-card { padding-bottom: calc(1.6rem + env(safe-area-inset-bottom, 0px)); }
  .mobile-bar a { padding-bottom: calc(1.05rem + env(safe-area-inset-bottom, 0px)); }
}

/* ===== Mobile ===== */
@media (max-width: 768px) {
  .nav-links a:not(.nav-cta) { display: none; }
  .align-left, .align-right { padding-left: 6vw; padding-right: 6vw; }
  .align-left .section-inner, .align-right .section-inner { max-width: 100%; }
  .section-content .section-heading { font-size: 2.1rem; }
  .stats-grid { grid-template-columns: repeat(2, 1fr); gap: 6vw 4vw; }
  .stat-number { font-size: 10vw; }
  .section-cta .section-heading { font-size: 2.3rem; }
  #scroll-container { height: 900vh; }
  .marquee-text { font-size: 20vw; }
}

/* ─────────────────── reduced motion ───────────────────
   Decorative transforms only — the canvas walkthrough itself is scroll
   position mapped straight onto which frame is drawn, not animation. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
