/* viewer2.css — the LIVE public viewer's layout/theme (the /view2 redesign,
 * promoted to /view on founder go 2026-08-02; /view-legacy keeps the old page
 * on viewer.css alone for one release).
 * Loaded AFTER viewer.css: the base file keeps owning the shop overlay, AR
 * interstitial internals, pills, badges, err-cards; this file restructures the
 * page into two variants scoped by a root class, flipped live by
 * viewer2-extras.js:
 *   html.v2-hybrid    — full-bleed stage hero + listing document below
 *   html.v2-immersive — the whole viewport IS the viewer (Matterport-style,
 *                       IVRIS dark-green chrome, right rail / bottom sheet)
 * Interop rules (design-spec R1-R17): [hidden]{display:none!important} stays
 * authoritative; v2 never uses `hidden` for its own chrome states (classes /
 * data-attrs only); the 899px breakpoint matches viewer-app's matchMedia
 * exactly; no transforms on ancestors of fixed elements.
 */

:root{
  /* shared */
  --v2-doc-max:1120px;
  --v2-sticky-h:112px;          /* hybrid mobile bar reserve; extras syncs real height */
  --v2-focus:0 0 0 2px var(--paper),0 0 0 4px rgba(106,173,63,.55);
  /* hybrid glass */
  --v2-glass:rgba(255,255,255,.78);
  --v2-glass-strong:rgba(255,255,255,.92);
  --v2-glass-border:rgba(17,17,17,.08);
  --v2-glass-shadow:0 4px 24px rgba(14,18,16,.18);
  --v2-blur:16px;
  --v2-stage-h:min(78vh,920px);
  --v2-stage-h-lap:min(72vh,780px);
  /* mobile hero reaches lower (founder 2026-07-30: "lower the bottom") */
  --v2-stage-h-mob:min(74svh,660px);
  --v2-stage-h-sm:min(70svh,560px);
  --v2-gallery-row:clamp(260px,40vh,440px);
  --v2-gallery-row-mob:clamp(180px,30vh,300px);
  /* immersive dark STAGE chrome (panels went white in the 2026-07-30 merge —
     these now dress only the stage, the dark gallery, the name chip, and the
     picker/err surfaces that sit directly on the dark page) */
  --v2-bg:#0E1210; --v2-panel:#151917; --v2-panel-2:#1A1F1C;
  --v2-dglass:rgba(14,18,16,.72);
  --v2-line:#2A302C;
  --v2-text:#F2F4F0; --v2-text-2:#D6DAD4; --v2-text-3:#9AA096; --v2-text-4:#7B8177;
  --v2-green-hi:#8FD063;
  --v2-rail-w:340px;
  --v2-bar-space:100px;         /* immersive action bar; extras syncs real height */
  --v2-sheet-peek:124px;
  /* [r17 #2] MEASURED viewport height — the hybrid column's one length unit.
     Seeded from the unit so a no-JS / failed-boot page renders exactly like
     r16; viewer2-extras overwrites it with window.innerHeight in px (inline
     style on <html> beats this rule). See trackViewportHeight() for WHY the
     unit alone was not enough on iOS. Hybrid (mobile) only — the immersive
     desktop rules keep their own dvh, so ≥900px is byte-for-byte unchanged. */
  --v2-vh:100vh;
}
@supports (height:100dvh){:root{--v2-vh:100dvh}}

/* ============================================================ SHARED V2 === */
/* slot wrappers never add boxes around the stage chrome */
.v2-slot-stage-tabs,.v2-slot-stage-tray{display:contents}

/* [r40] A CONTROL STRIP WITH NOTHING IN IT IS NOT A STRIP. FOUNDER: "when no
   photos are ai staged the section that holds the ui for the toggled staged vs
   unstaged button just delete that section so the spacing is compressed. you
   will see that that section just has 2 lines with nothing in between the
   lines."
   Every .micro-block is hidden per tab, and the photos one is hidden outright on
   a listing with nothing staged — but the CONTAINERS around them kept their own
   padding and their neighbours' 1px borders, which is the two lines with a gap
   between them. `:empty` cannot catch this: a container whose children are all
   [hidden] still HAS children. `:has()` asks the question that actually
   matters — is anything in here going to paint? — and collapses the box when
   the answer is no. Both variants, and the immersive rail slots too, because
   the same blocks are re-parented into the rail. */
#micro:not(:has(> .micro-block:not([hidden]))),
.v2-rail-controls:not(:has(> * > .micro-block:not([hidden]))):not(:has(> .micro-block:not([hidden]))){
  display:none;border:0;padding:0;margin:0}

/* [FRONTDOOR] static steps above the overhead plan (founder 2026-07-30):
   "be at the actual home, look at the front door" — both variants */
.ar-intro-kicker{margin:0 6px;font:700 11px/1 var(--font-display);letter-spacing:.14em;
  text-transform:uppercase;color:var(--green-600)}
.ar-intro-steps{margin:0 6px;padding:0;list-style:none;counter-reset:aris;
  display:flex;flex-direction:column;gap:8px;text-align:left}
.ar-intro-steps li{position:relative;padding-left:31px;font:500 13.5px/1.5 var(--font);
  color:var(--ink-700);counter-increment:aris}
.ar-intro-steps li b{color:var(--ink-900)}
.ar-intro-steps li::before{content:counter(aris);position:absolute;left:0;top:1px;
  width:21px;height:21px;border-radius:50%;background:var(--green-050);
  border:1px solid var(--green-200);color:var(--green-600);
  font:700 11px/21px var(--font);text-align:center}

/* [CONTROLS REDESIGN 2026-07-30] one control language, no jargon:
   — FLOOR: worded chips ("Floor 1 · Floor 2 · Floor 3") — can never be
     confused with the numbered staging circles; the redundant label goes.
     viewer-app builds one .lp per floor from projectFloorList, so 3-floor
     homes get a third chip automatically.
   — STAGING: renamed row label (was LAYOUT) over the numbered circles
   — ✦: a labeled "✦ Shuffle" chip instead of a bare glyph */
.floor-switch .layout-lbl{display:none}
.floor-switch .lp{width:auto;border-radius:999px;padding:0 14px;font-size:12.5px}
/* margin, not a trailing space — .lp is a flex box and trims spaces */
.floor-switch .lp::before{content:"Floor";margin-right:4px}
.layout-switch:not(.floor-switch) .layout-lbl{font-size:0;letter-spacing:0}
.layout-switch:not(.floor-switch) .layout-lbl::before{content:"Staging";
  font-size:11px;letter-spacing:.06em}
.lp[aria-label^="Shuffle"]{width:auto;border-radius:999px;padding:0 13px;font-size:12.5px}
.lp[aria-label^="Shuffle"]::after{content:"Shuffle";margin-left:5px;font-size:12px}

/* [V2 DARK PLAN] the Dimensions plan FLOATS on the dark stage — the canvas
   bitmap is transparent (fill2d dark flag) and the base white CSS card
   behind it goes too (founder 2026-07-30: "no white box") */
html.v2-hybrid #planCanvas,html.v2-immersive #planCanvas{
  background:none;border-radius:0;box-shadow:none}

/* the caption's "Tap furniture to shop" reads in brand green (founder) */
html.v2-hybrid .stage-cap b,html.v2-immersive .stage-cap b{color:var(--green-600)}

/* .vp is a flex column; auto margins shrink the picker to content width —
   force full width so the card grid actually gets its columns */
html.v2-hybrid .vp-picker,html.v2-immersive .vp-picker{width:100%}

/* focus is always visible — glass, dark chrome and paper alike */
html.v2-hybrid :is(button,a,select,input):focus-visible{outline:none;box-shadow:var(--v2-focus)}
html.v2-immersive :is(button,a,select,input):focus-visible{outline:2px solid var(--v2-green-hi);outline-offset:2px;box-shadow:none}

@media (prefers-reduced-motion:reduce){
  .v2-hybrid *,.v2-immersive *{animation:none !important;transition-duration:.01ms !important}
}

/* [COMPARE 2026-07-30] staged⇄unstaged slider on every photo (extras
   builds it from viewer-app's exported roomPhotoSet): opens at 50%, labels
   snap, geometry follows the letterboxed <img>, drag = pointer events. */
/* [AXIS LOCK] touch-action:pan-y, NOT none: a vertical swipe that starts on a
   photo must scroll the PAGE (founder — "you hit a photo and it stops
   scrolling"). pan-y hands vertical panning back to the browser while still
   delivering the horizontal gesture to the slider; extras' own axis lock is
   what stops the first few px moving the divider. */
.switch-fig.v2-compare{position:relative;touch-action:pan-y;cursor:ew-resize}
.switch-fig.v2-compare .stage-pill,.switch-fig.v2-compare .empty-tag{display:none}
.v2-cmp-top{position:absolute;overflow:hidden;pointer-events:none;z-index:2}
.v2-cmp-top img{object-fit:cover;display:block;max-width:none}
/* [r74e/h] NO COVER-CROP HERE — the rule that stood here is WITHDRAWN.
   Founder: "on the left and right side of the original for some reason the ai
   staged photo shows through." I first fixed that by forcing the base image to
   object-fit:cover, and the round-6 block near the end of this file already
   records the founder rejecting exactly that: "photos render at their NATURAL
   aspect inside an aspect-locked frame — no cover-crop 'wide' look"
   (2026-07-31). Overriding it would have traded his bug for his own rejected
   look.
   The bleed is fixed at BOTH real causes instead: the frame is locked to the
   ORIGINAL's aspect (enhanceFig), and every render is resampled to the
   original's exact pixel dimensions before it is saved (autojob
   renderAtSourceSize). Same shape both sides means contain letterboxes
   nothing, and there is no gap for anything to show through. Homes already
   published with bucket-shaped renders keep a thin bar until they are
   restaged — honest, and cheaper than cropping every photo forever. */
.v2-cmp-handle{position:absolute;z-index:4;width:2px;background:#fff;
  box-shadow:0 0 10px rgba(0,0,0,.55);pointer-events:none;transform:translateX(-1px)}
.v2-cmp-grip{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
  width:30px;height:30px;border-radius:50%;background:#fff;color:var(--ink-700);
  font-size:13px;display:flex;align-items:center;justify-content:center;
  box-shadow:0 2px 12px rgba(0,0,0,.45)}
.v2-cmp-label{position:absolute;z-index:5;appearance:none;border:0;cursor:pointer;
  border-radius:999px;padding:6px 12px;font:600 11.5px/1 var(--font);
  background:rgba(14,18,16,.72);color:#fff;
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
  transition:background .18s var(--ease-out)}
.v2-cmp-label:hover{background:rgba(14,18,16,.9)}

/* photo-viewer caption — same chrome as the plan caption; only while the
   Photos pane is up (decorative :has — absent support just hides it) */
.v2-photo-cap{display:none}
.vp-stage:has(.pane[data-tab="photos"]:not([hidden])) .v2-photo-cap{display:block}

/* =========================================================================
   HYBRID — full-bleed stage hero + listing document
   ========================================================================= */

/* immersive-only chrome is fully absent here */
html.v2-hybrid .v2-rail,html.v2-hybrid .v2-action-bar{display:none}

/* full-bleed listing shell (override BOTH base rules — see 899px block too) */
html.v2-hybrid .vp-listing{max-width:none;padding:0;margin:0}

/* ---- stage hero ---- */
html.v2-hybrid .v2-stage-shell{position:relative;width:100%;border-bottom:1px solid var(--line)}
/* caption (a #stage child) anchors to the MEDIA box, not the shell — on
   mobile the micro strip sits under the media inside the shell and the
   caption must never cover it */
html.v2-hybrid .vp-stage{display:block;gap:0;position:relative}
html.v2-hybrid .stage-media{height:var(--v2-stage-h);min-height:520px}
html.v2-hybrid .stage-3d,html.v2-hybrid .stage-2d,html.v2-hybrid .stage-2d3d,
html.v2-hybrid .stage-photos{border-radius:0;border:0;box-shadow:none}

/* glass recipe — one consistent chrome over every pane (micro left the
   overlay 2026-07-30: controls live in the white document, like the rail) */
html.v2-hybrid #viewSelector,html.v2-hybrid .stage-cap{
  background:var(--v2-glass);
  -webkit-backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  border:1px solid var(--v2-glass-border);box-shadow:var(--v2-glass-shadow)}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  html.v2-hybrid #viewSelector,html.v2-hybrid .stage-cap{
    background:var(--v2-glass-strong)}
}

/* floating tabs — top center over the media */
html.v2-hybrid #viewSelector{position:absolute;top:18px;left:50%;transform:translateX(-50%);
  z-index:24;width:max-content;max-width:calc(100vw - 24px);padding:4px;border-radius:999px}
html.v2-hybrid #viewSelector button{padding:12px 22px;font-size:14.5px}
html.is-native.v2-hybrid #viewSelector{top:calc(12px + env(safe-area-inset-top,0px))}

/* controls — a document section right under the title, mirroring the rail */
html.v2-hybrid .v2-doc-controls{padding:16px 0;border-bottom:1px solid var(--line)}
html.v2-hybrid .v2-doc-controls:empty{display:none}
html.v2-hybrid .v2-doc-controls .micro-block{flex-direction:row;flex-wrap:wrap;
  align-items:center;gap:12px 16px}
html.v2-hybrid .v2-doc-controls .floor-switch .layout-lbl{width:auto;margin:0 4px 0 0}
html.v2-hybrid .layout-lbl{color:var(--ink-500)}

/* caption pill — bottom center */
html.v2-hybrid .stage-cap{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);
  z-index:24;margin:0;padding:8px 16px;border-radius:999px;white-space:nowrap;
  max-width:calc(100vw - 32px);overflow:hidden;text-overflow:ellipsis;color:var(--ink-700)}

/* 2D plan — BLACK like immersive (founder 2026-07-30): pane + both mode
   containers share the stage black; the canvas sheet is dark via the same
   fill2d page flag. Top clearance keeps the floating tabs off the TOTAL SQ
   FT band. */
html.v2-hybrid .pane[data-tab="plan"]{background:var(--v2-bg)}
html.v2-hybrid .stage-2d,html.v2-hybrid .stage-2d3d{background:none;
  border-radius:0;box-shadow:none}
/* [V2 ZOOM PARITY] founder 2026-08-04: the parametric fills the stage like
   the furnished flat view (.stage-2d3d is full-bleed) — the old inset padding
   made the same home render visibly smaller. The plan may pass under the
   floating tab pill exactly like the furnished canvas does — AT EVERY WIDTH
   (r18b: the mobile padding-top clearance is gone too; see the ≤899 note). */
html.v2-hybrid .stage-2d{padding:0}
html.v2-hybrid .stage-photos{background:var(--paper-50);padding:84px clamp(16px,4vw,48px) 44px}

/* ---- photos: justified rows — no more portrait-in-a-white-void ---- */
html.v2-hybrid .room-section{display:flex;flex-wrap:wrap;align-items:flex-start;gap:14px;margin:0 0 26px}
html.v2-hybrid .room-section + .room-section{border-top:1px solid var(--line);padding-top:26px}
html.v2-hybrid .room-title{width:100%;margin:0;font-size:22px}
html.v2-hybrid .switch-fig{flex:0 1 auto;max-width:100%;min-height:0}
html.v2-hybrid .switch-fig + .switch-fig{margin-top:0}
html.v2-hybrid .switch-fig img{height:var(--v2-gallery-row);width:auto;max-width:100%;
  max-height:none;object-fit:contain}
html.v2-hybrid .photo-count,html.v2-hybrid .room-section .muted{width:100%;margin:0}
html.v2-hybrid #photosBody .err-card{width:100%;max-width:640px;margin:32px auto}

/* ---- listing document below the stage (mirrors the rail's stack) ---- */
html.v2-hybrid .v2-doc{max-width:var(--v2-doc-max);margin:0 auto;padding:0 28px}
html.v2-hybrid .v2-doc-head{display:block;padding:20px 0 14px;border-bottom:1px solid var(--line)}
/* condensed title row — name LEFT, facts after, like immersive */
html.v2-hybrid .v2-title-block{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 14px}
html.v2-hybrid .v2-title-block .v2-facts{margin:0;font-size:15.5px}
html.v2-hybrid .v2-title-block .vp-home{font-size:28px;text-align:left}
.v2-facts{margin:8px 0 0;font:600 15px/1.4 var(--font);color:var(--ink-700)}
.v2-facts .v2-dot{color:var(--ink-300);margin:0 7px}
/* AR handoff — its own document block (QR card on desktop) */
html.v2-hybrid #arHandoff{display:flex;padding:16px 0;gap:10px;border-bottom:1px solid var(--line)}
/* [r31 #12] A LISTING WITH NO AR LEAVES NO HOLE. Founder: "since qr is gone now
   on desktop it changes formatting of top section making it much bigger where
   it says (room name) 1 bd·1 ba·566 sqft·1 floor. compare co door vs co no door
   and you can see what i mean for desktop."
   Both children hide themselves when the listing ships no AR (viewer-app sets
   [hidden] on #qrWrap and #arBtnWrap), but the CONTAINER kept its 16px top and
   bottom padding and its 1px divider — 33px of empty band pushing the facts row
   down and reading as a formatting change. The immersive variant already had
   this guard for #qrWrap alone (see .v2-rail-arslot below); this is the hybrid
   one, and it tests BOTH children because either can be the visible half. */
html.v2-hybrid #arHandoff:has(> #qrWrap[hidden]):has(> #arBtnWrap[hidden]){display:none}
html.v2-hybrid .qr-wrap{flex:0 0 auto;width:clamp(300px,26vw,340px)}
html.v2-hybrid .v2-sticky-shop{display:none}
html.v2-hybrid .v2-hy-desc{display:none} /* desc line is a mobile-bar row only */

/* shop the look — mirrors the rail block: no kicker, green lede, left copy */
html.v2-hybrid .shop-h{display:none}
html.v2-hybrid .shop-lede{color:var(--green-600);font-size:17px}
html.v2-hybrid .shop-body{margin:0 0 18px;max-width:640px}
/* ONE divider between shop and footer (founder 2026-07-30: it showed the
   shop's border-bottom + the footer's border-top with a margin gap — two
   lines): the shop block has no border; the footer's border-top, flush
   against it, is the single line. */
html.v2-hybrid .shop-look{max-width:none;margin:0;padding:20px 0 26px;text-align:left;
  border-bottom:0;background:none}

/* footer aligns with the doc column, left like the rail foot */
html.v2-hybrid .vp-foot{max-width:var(--v2-doc-max);text-align:left;
  margin:0 auto;padding:16px 28px 24px}
html.v2-hybrid .vp-foot .disclaimer{margin:0 0 8px}
html.v2-hybrid .vp-foot .foot-stage{margin:0 0 7px}

/* pane err-cards float centered on the media */
html.v2-hybrid .pane > .err-card{position:relative;z-index:1;margin:calc(var(--v2-vh) * .15) auto 0;max-width:520px;
  border-radius:14px;box-shadow:var(--shadow-md)}

/* picker — roomier, no header exists on this page */
html.v2-hybrid .vp-picker{padding:72px 24px 80px}
html.v2-hybrid .vp-picker h2{font-size:clamp(24px,3vw,30px)}
html.v2-hybrid .pick-grid{grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px}
html.v2-hybrid .pick-card{border-radius:16px;padding:20px}
html.v2-hybrid .pick-card .pick-name{font-family:var(--font-display);font-size:17px}

/* disabled AR button (viewer-app sets it when AR is unpublished) */
html.v2-hybrid .btn-ar:disabled{background:#B8C4AE;color:rgba(255,255,255,.85);
  box-shadow:none;cursor:not-allowed;transform:none}

/* ---- hybrid motion (load-in; #stageCap deliberately NOT animated — JS
   re-toggles it per tab and a delayed animation reads as flicker) ---- */
@keyframes v2-fade{from{opacity:0}to{opacity:1}}
@keyframes v2-rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes v2-rise-c{from{opacity:0;transform:translate(-50%,10px)}to{opacity:1;transform:translate(-50%,0)}}
html.v2-hybrid #stageMedia{animation:v2-fade .32s var(--ease-out) both}
html.v2-hybrid #viewSelector{animation:v2-rise-c .3s var(--ease-out) .08s both}
html.v2-hybrid #micro{animation:v2-rise .3s var(--ease-out) .14s both}
html.v2-hybrid .v2-title-block{animation:v2-rise .32s var(--ease-out) .12s both}
html.v2-hybrid #arHandoff{animation:v2-fade .3s var(--ease-out) .18s both} /* opacity ONLY — fixed-bar ancestor rule */
html.v2-hybrid .pane{animation:v2-fade .24s var(--ease-out) both}
html.v2-hybrid .micro-block{animation:v2-rise .22s var(--ease-out) both}

/* ---- hybrid laptop ---- */
@media (max-width:1200px){
  html.v2-hybrid .stage-media{height:var(--v2-stage-h-lap);min-height:460px}
  html.v2-hybrid .v2-doc{padding:0 24px}
}

/* ---- hybrid mobile ≤899 (matches viewer-app matchMedia exactly) ---- */
@media (max-width:899px){
  html.v2-hybrid .vp-listing{max-width:none;padding:0}
  /* the FOLD lands just below the STAGING row (founder 2026-07-30, same stop
     as the immersive peek): hero = viewport minus sticky bar minus the
     MEASURED title+controls block (extras syncs --v2-doc-top-h — a fixed px
     guess breaks the moment a floor-chip row appears). Shop text scrolls. */
  /* [r16 #48 → r17 #2] the column is measured, not unit-derived: --v2-vh is
     window.innerHeight in px (extras). dvh still seeds the var, so the r16
     behaviour survives with JS off. */
  html.v2-hybrid .stage-media{
    height:calc(var(--v2-vh) - var(--v2-sticky-h) - var(--v2-doc-top-h,164px));
    min-height:340px}
  /* [r17 #1] the pill HUGS the top edge of the black stage in a BROWSER —
     founder r17 ("higher still" after r16 landed it at 12px). Safari's URL bar
     already clears the notch, so in-browser there is nothing to clear and any
     offset reads as dead space; env() still pushes it down where the page
     really does reach the notch (standalone/landscape), and the native
     override at :165 keeps the wrapper's full safe-area clearance. */
  html.v2-hybrid #viewSelector{top:calc(env(safe-area-inset-top,0px) + 4px)}
  html.v2-hybrid #viewSelector button{padding:10px 14px;font-size:13.5px}
  /* [V2 ZOOM PARITY r18b] NO pane top clearance on mobile either: the old
     padding-top (env + 56px, room for the tab pill) made the parametric wrap
     a SHORTER box than the furnished .stage-2d3d, so the same ×1.14 fit gave
     fewer px/m — the founder's "parametric renders smaller". The wraps are
     now the SAME box at every width; the plan's own proportional margin keeps
     it visually clear, and it may pass under the floating pill exactly like
     the furnished canvas does (base :205 rule already zeroes the padding). */
  html.v2-hybrid .stage-cap{bottom:12px;padding:7px 13px;font-size:12px}
  /* photos: BLACK, edge-to-edge (founder 2026-07-30 — like immersive) */
  html.v2-hybrid .pane[data-tab="photos"]{background:var(--v2-bg)}
  /* [r17 #2] NO padding-bottom on this scroller: WebKit drops a scroller's
     bottom padding from its scrollable extent (the same trap r16 documented
     for .v2-doc), so the last room's photo stopped flush against the pane
     edge, half under the compare caption. A ::after block is counted by every
     engine — it clears the caption pill (bottom:12px + ~35px tall). */
  html.v2-hybrid .stage-photos{background:var(--v2-bg);
    padding:calc(env(safe-area-inset-top,0px) + 56px) 0 0}
  html.v2-hybrid .stage-photos::after{content:"";display:block;height:64px}
  /* NATIVE wrapper keeps the 40px NOTCH BUFFER (founder 2026-07-30): fullscreen
     webview, no browser chrome above — the pill rule itself lives at :165
     (html.is-native wins on specificity); this keeps the PHOTOS clearance.
     [r18b] the native .stage-2d clearance is gone with the browser one above —
     zoom parity needs the parametric wrap to be the furnished wrap's box. */
  html.is-native.v2-hybrid .stage-photos{
    padding-top:calc(max(env(safe-area-inset-top,0px), 40px) + 60px)}
  html.v2-hybrid .switch-fig{width:100%}
  html.v2-hybrid .switch-fig img{width:100%;height:auto;max-height:none}
  html.v2-hybrid .room-section{gap:10px}
  html.v2-hybrid .room-title{color:var(--v2-text);padding:0 16px}
  html.v2-hybrid .photo-count,html.v2-hybrid .room-section .muted{color:var(--v2-text-3);padding:0 16px}
  html.v2-hybrid .room-section + .room-section{border-color:var(--v2-line)}
  html.v2-hybrid .v2-doc{padding:0 16px}
  html.v2-hybrid .v2-doc-head{padding:14px 0 10px}
  /* condensed title row — same sizes as the immersive peek */
  html.v2-hybrid .v2-title-block .v2-facts{font-size:14px}
  html.v2-hybrid .v2-title-block .vp-home{font-size:24px}
  html.v2-hybrid .v2-doc-controls{padding:12px 0 14px}
  html.v2-hybrid .v2-doc-controls .micro-block{gap:10px 12px}
  html.v2-hybrid .v2-doc-controls .seg2 button{padding:7px 13px;font-size:12px}
  html.v2-hybrid .v2-doc-controls .layout-lbl{width:auto;margin:0 6px 0 0}
  html.v2-hybrid .shop-look{padding:16px 0 20px}
  /* THE STICKY ACTION BAR — #arHandoff itself goes fixed; the SAME 899px
     matchMedia in viewer-app flips #qrWrap⇄#arBtnWrap inside it. Buyer
     actions only (founder-approved 2026-07-29); never the sales credit. */
  /* [r16 #48] bottom padding: in a BROWSER the bar sits right on Safari's URL
     bar — the old 14px floor (phone corner-radius guard for the fullscreen
     wrapper) held the buttons visibly high above it. env() alone still lifts
     them over the home indicator when Safari's chrome minimizes (inset goes
     0 → ~34px); the native override below keeps the wrapper floor. */
  html.v2-hybrid #arHandoff{position:fixed;left:0;right:0;bottom:0;z-index:400;
    display:grid;grid-template-columns:1.4fr 1fr;grid-template-areas:"desc desc" "ar shop" "hint hint";
    gap:6px 10px;padding:7px 14px calc(8px + env(safe-area-inset-bottom,0px));
    background:var(--v2-glass-strong);
    -webkit-backdrop-filter:blur(18px) saturate(1.3);backdrop-filter:blur(18px) saturate(1.3);
    border-top:1px solid var(--line);box-shadow:0 -6px 24px rgba(14,18,16,.08);animation:none;
    border-bottom:0}
  html.v2-hybrid .v2-hy-desc{display:block;grid-area:desc;margin:0;text-align:center;
    font-size:11.5px;line-height:1.35;color:var(--ink-500)}
  html.v2-hybrid #arBtnWrap{display:contents}
  html.v2-hybrid #viewArBtn{grid-area:ar;height:48px;padding:0 16px;margin:0;
    border-radius:14px;font-size:15px}
  html.v2-hybrid .v2-sticky-shop{display:block;grid-area:shop;height:48px;border-radius:14px;
    background:var(--paper);border:1.5px solid var(--ink-900);color:var(--ink-900);
    font:600 14.5px/1 var(--font);cursor:pointer}
  html.v2-hybrid #arHint{grid-area:hint;margin:0;text-align:center;font-size:11.5px;line-height:1.35}
  html.v2-hybrid #arHint:empty{display:none}
  /* [r31 #12, mobile half] "then mobile where it says shop all on bottom button
     now where view in ar is gone make it equal buffer on the top/bottom of that
     space." With no AR the grid's "ar" cell is empty, so Shop All sat in the
     right-hand column against a blank left one, and the bar kept its
     asymmetric 7px-top / 8px-bottom padding — which reads as the button
     hanging low in the space. Shop All takes the whole row and the padding is
     symmetric. The safe-area inset stays ADDITIVE on the bottom: it is the
     home indicator, not decoration, and dropping it would put the button under
     the gesture bar. */
  /* [r37] AND THE BAR STAYS. FOUNDER: "the shop all button is completely gone
     on mobile when it should have been on the bottom."
     The top-level rule (search: ":has(> #qrWrap[hidden]):has(> #arBtnWrap
     [hidden]){display:none}") is my own r31 fix for the DESKTOP hybrid doc
     flow, where #arHandoff with no QR and no AR button is an empty band. On a
     PHONE the same bar is the fixed bottom action bar and it carries
     #v2StickyShop — so on a listing with no front door it hid Shop All along
     with the emptiness, and the very next rule below (the one-column no-AR
     layout) had nothing left to lay out. Re-shown here, inside the phone query
     only: the desktop collapse is untouched. */
  html.v2-hybrid #arHandoff:has(> #qrWrap[hidden]):has(> #arBtnWrap[hidden]){display:grid}
  html.v2-hybrid #arHandoff:has(> #arBtnWrap[hidden]){
    grid-template-columns:1fr;grid-template-areas:"desc" "shop" "hint";
    padding:10px 14px calc(10px + env(safe-area-inset-bottom,0px))}
  html.is-native.v2-hybrid #arHandoff:has(> #arBtnWrap[hidden]){
    padding-bottom:calc(10px + max(env(safe-area-inset-bottom,0px), 14px))}
  /* native wrapper: fullscreen — keep the corner-radius/home-indicator floor */
  html.is-native.v2-hybrid #arHandoff{
    padding-bottom:calc(6px + max(env(safe-area-inset-bottom,0px), 14px))}
  /* reserve scroll room so the fixed bar never covers the AB 723 footer —
     extras keeps --v2-sticky-h synced to the real bar height */
  html.v2-hybrid .vp-picker{padding:48px 16px 64px}
}

/* ---- hybrid small ≤420 ---- */
@media (max-width:420px){
  html.v2-hybrid .stage-media{
    height:calc(var(--v2-vh) - var(--v2-sticky-h) - var(--v2-doc-top-h,160px));
    min-height:320px}
  html.v2-hybrid .v2-title-block .vp-home{font-size:22px}
  html.v2-hybrid .v2-title-block .v2-facts{font-size:13px}
  html.v2-hybrid #viewSelector button{padding:9px 12px;font-size:13px}
  html.v2-hybrid #viewArBtn,html.v2-hybrid .v2-sticky-shop{height:46px;font-size:14px}
}

/* =========================================================================
   IMMERSIVE (MERGED 2026-07-30) — the viewport is the viewer, dressed in the
   hybrid WHITE chrome: white tabs + caption glass over the dark stage, white
   always-open right rail (desktop) / white drag-up sheet (mobile). Founder
   direction: immersive layout + hybrid white elements; no rail collapse.
   ========================================================================= */

html.v2-immersive body{background:var(--v2-bg);color:var(--v2-text-2)}
html.v2-immersive.v2-has-listing body{overflow:hidden}

/* hybrid-only chrome is fully absent here */
html.v2-immersive .v2-doc{display:none}
html.v2-immersive .v2-sticky-shop{display:none}

/* rail geometry — always open (no collapse state exists anymore) */
html.v2-immersive{--v2-rail-cur:var(--v2-rail-w)}

/* ---- tabs: the hybrid white glass pill, top-center of the VISIBLE stage ---- */
html.v2-immersive #viewSelector{position:fixed;top:18px;left:calc((100vw - var(--v2-rail-cur))/2);
  transform:translateX(-50%);z-index:30;width:max-content;
  max-width:calc(100vw - var(--v2-rail-cur) - 24px);padding:4px;border-radius:999px;
  background:var(--v2-glass);
  -webkit-backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  border:1px solid var(--v2-glass-border);box-shadow:var(--v2-glass-shadow)}
html.v2-immersive #viewSelector button{padding:12px 22px;font-size:14.5px}
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  html.v2-immersive #viewSelector,html.v2-immersive .stage-cap{background:var(--v2-glass-strong)}
}

/* ---- stage: the page itself ---- */
html.v2-immersive .v2-stage-shell{position:static}
html.v2-immersive .vp-stage{display:block;gap:0;position:static}
html.v2-immersive .stage-media{position:fixed;inset:0;height:100vh;height:100dvh;min-height:0;z-index:0}
html.v2-immersive .stage-3d,html.v2-immersive .stage-2d,html.v2-immersive .stage-2d3d,
html.v2-immersive .stage-photos{border-radius:0;border:0;box-shadow:none}
html.v2-immersive .stage-3d{background:var(--v2-bg)} /* == WebGL clear → seamless */

/* 2D panes — BLACK throughout (founder 2026-07-30): pane backdrop, both
   mode containers, and the canvas sheet itself (fill2d dark flag) all share
   the stage black — no white card, no seams (84px clears the tabs) */
html.v2-immersive .pane[data-tab="plan"]{background:var(--v2-bg);
  padding:84px calc(var(--v2-rail-cur) + 24px) 64px 24px}
html.v2-immersive .stage-2d,html.v2-immersive .stage-2d3d{background:none;
  border-radius:0;box-shadow:none}
/* [V2 ZOOM PARITY] full-bleed like .stage-2d3d — see the hybrid note */
html.v2-immersive .stage-2d{padding:0}

/* photos — immersive dark gallery */
html.v2-immersive .stage-photos{background:var(--v2-bg);
  padding:84px calc(var(--v2-rail-cur) + 24px) 64px 24px}
html.v2-immersive #photosBody > *{max-width:980px;margin-left:auto;margin-right:auto}
html.v2-immersive .room-section{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));
  gap:14px;margin:0 0 36px;border-color:var(--v2-line)}
html.v2-immersive .room-section + .room-section{border-top:1px solid var(--v2-line);padding-top:26px}
html.v2-immersive .room-title,html.v2-immersive .room-section > p,
html.v2-immersive .room-section > .muted{grid-column:1/-1}
html.v2-immersive .room-title{color:var(--v2-text);position:sticky;top:0;z-index:3;
  padding:10px 0;margin:0;background:linear-gradient(var(--v2-bg) 78%,transparent)}
html.v2-immersive .switch-fig{background:var(--v2-panel);border:1px solid var(--v2-line);
  border-radius:14px}
html.v2-immersive .switch-fig + .switch-fig{margin-top:0}
html.v2-immersive .switch-fig img{background:var(--v2-panel);max-height:72vh}
html.v2-immersive .room-section .muted,html.v2-immersive .photo-count{color:var(--v2-text-3)}

/* caption chip — EXACTLY the hybrid pill (base 13px/ink-500 type, glass
   recipe, ink-700 text), just fixed + centered on the VISIBLE stage */
html.v2-immersive .stage-cap{position:fixed;bottom:18px;left:calc((100vw - var(--v2-rail-cur))/2);
  transform:translateX(-50%);z-index:24;margin:0;background:var(--v2-glass);
  -webkit-backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  backdrop-filter:blur(var(--v2-blur)) saturate(1.35);
  border:1px solid var(--v2-glass-border);box-shadow:var(--v2-glass-shadow);
  border-radius:999px;padding:8px 16px;
  color:var(--ink-700);white-space:nowrap;max-width:calc(100vw - var(--v2-rail-cur) - 32px);
  overflow:hidden;text-overflow:ellipsis;transition:opacity .2s}

/* ---- the WHITE right rail (desktop) — always open, never collapses ---- */
html.v2-immersive .v2-rail{position:fixed;top:0;right:0;bottom:0;z-index:40;
  width:var(--v2-rail-w);display:flex;flex-direction:column;background:var(--paper);
  border-left:1px solid var(--line);box-shadow:0 0 44px rgba(0,0,0,.38)}
html.v2-immersive .v2-rail-grab{display:none}
/* flex:0 1 auto — the scroll area sizes to its CONTENT, so the footer flows
   right after the shop block exactly like the hybrid document (founder
   2026-07-30: this was the "big gap" — the footer used to pin to the panel
   bottom). It still shrinks + scrolls when content exceeds the panel. */
html.v2-immersive .v2-rail-scroll{flex:0 1 auto;min-height:0;overflow-y:auto;
  overscroll-behavior:contain}
html.v2-immersive .v2-rail-scroll > * + *{border-top:1px solid var(--line)}
html.v2-immersive .v2-rail-scroll > *:empty{display:none;border:0}

/* sheet head = condensed title row (founder 2026-07-30, final order): home
   name on the LEFT, fact strip after it. The ±2% disclaimer lives ONLY in
   the footer. */
html.v2-immersive .v2-rail-title{display:flex;flex-wrap:wrap;align-items:baseline;
  gap:2px 12px;padding:18px 18px 14px}
html.v2-immersive .v2-rail-title .v2-facts{margin:0;font-size:15.5px}
html.v2-immersive .v2-rail-title .vp-home{font-size:28px;text-align:left;
  flex:0 1 auto;min-width:0}

/* controls row: Floor pills sit NEXT TO Furnished/Parametric (founder
   2026-07-30); the Layout switcher gets its own full row below */
html.v2-immersive .v2-rail-controls .micro-block{flex-direction:row;flex-wrap:wrap;
  align-items:center;gap:12px 16px}
html.v2-immersive .v2-rail-controls .layout-switch:not(.floor-switch){flex:1 1 100%}
/* FLOOR label rides inline with its pills so the switch hugs the
   Furnished/Parametric row (wraps tightly under it when the rail is narrow) */
html.v2-immersive .v2-rail-controls .floor-switch .layout-lbl{width:auto;margin:0 4px 0 0}
/* fact tiles retired in the merged design — the hybrid fact STRIP under the
   title replaced them (extras still fills the node; it just never shows) */
html.v2-immersive .v2-facttiles{display:none}

/* controls section (micro lives here) — base LIGHT component styles apply */
html.v2-immersive .v2-rail-controls{padding:16px 18px}

/* QR card — base light card (the .v2-hy-desc line inside the handoff is the
   HYBRID bar's row; immersive's action bar carries its own copy) */
html.v2-immersive .v2-hy-desc{display:none}
html.v2-immersive .v2-rail-arslot{padding:16px 18px}
html.v2-immersive .v2-rail-arslot:has(> .ar-handoff > #qrWrap[hidden]){padding:0;border-top:0}
/* [r31 #12] …and when the listing ships NO AR AT ALL the slot goes entirely,
   rather than collapsing to a zero-height bordered strip. */
html.v2-immersive .v2-rail-arslot:has(> .ar-handoff > #qrWrap[hidden]):has(> .ar-handoff > #arBtnWrap[hidden]){display:none}
html.v2-immersive #arHandoff{padding-top:0}

/* shop the look — IDENTICAL to the hybrid doc's bottom block (founder
   2026-07-30: "take the ui on the bottom of the hybrid and apply it"):
   no kicker, green lede, base 14.5px body, plain paper, no borders of its
   own — the footer's border-top is the single divider under it. */
html.v2-immersive .v2-rail-shopslot .shop-look{max-width:none;margin:0;padding:20px 18px 26px;
  border:0;text-align:left;background:none}
html.v2-immersive .shop-h{display:none}
html.v2-immersive .shop-lede{font-size:17px;color:var(--green-600)}
html.v2-immersive .shop-body{margin:0 0 18px;max-width:none}
html.v2-immersive .btn-shop-all{display:block;width:100%}

/* footer — flows after the shop block, styled EXACTLY like the hybrid doc
   footer (base 11.5px disclaimer, single border-top divider, left copy) */
html.v2-immersive .v2-rail-footslot{flex:0 0 auto}
html.v2-immersive .v2-rail-footslot .vp-foot{max-width:none;margin:0;background:var(--paper);
  border-top:1px solid var(--line);padding:16px 18px calc(22px + env(safe-area-inset-bottom,0px));
  text-align:left}
html.v2-immersive .v2-rail-footslot .disclaimer{color:var(--ink-500);margin:0 0 8px;max-width:none}
html.v2-immersive .v2-rail-footslot .disclaimer b{color:var(--ink-700)}
html.v2-immersive .v2-rail-footslot .foot-stage{color:var(--ink-500)}
html.v2-immersive .v2-rail-footslot .foot-stage:hover{color:var(--ink-700)}
html.v2-immersive .v2-rail-footslot .foot-logo{filter:none;opacity:1}
html.v2-immersive .v2-rail-footslot .foot-brand{color:var(--ink-500)}

/* page-end footer (picker/boot paths only) sits on the DARK page bg */
html.v2-immersive .vp > .vp-foot{border-color:var(--v2-line)}
html.v2-immersive .vp > .vp-foot .disclaimer{color:var(--v2-text-3)}
html.v2-immersive .vp > .vp-foot .disclaimer b{color:var(--v2-text-2)}
html.v2-immersive .vp > .vp-foot .foot-stage{color:var(--v2-text-4)}
html.v2-immersive .vp > .vp-foot .foot-stage:hover{color:var(--v2-text-3)}
html.v2-immersive .vp > .vp-foot .foot-logo{filter:brightness(0) invert(1);opacity:.7}
html.v2-immersive .vp > .vp-foot .foot-brand{color:var(--v2-text-4)}

/* ---- action bar (mobile; also mobile-UA tablets via .v2-mobile-ar) — the
   hybrid WHITE glass bar. #arHint keeps a row for real state feedback
   ("Preparing AR…"); the default Quick Look/Scene Viewer explainer is muted
   by extras (founder order 2026-07-30), so the row usually collapses. ---- */
html.v2-immersive .v2-action-bar{display:none;position:fixed;inset:auto 0 0 0;z-index:44;
  grid-template-columns:1.4fr 1fr;grid-template-areas:"desc desc" "ar shop" "hint hint";
  /* min 14px floor under the buttons even when env()=0 — phone corner radii
     must never clip AR/Shop (founder 2026-07-30) */
  column-gap:10px;row-gap:6px;
  padding:7px 14px calc(6px + max(env(safe-area-inset-bottom,0px), 14px));
  background:var(--v2-glass-strong);
  -webkit-backdrop-filter:blur(18px) saturate(1.3);backdrop-filter:blur(18px) saturate(1.3);
  border-top:1px solid var(--line);box-shadow:0 -6px 24px rgba(14,18,16,.08)}
html.v2-immersive .v2-bar-desc{grid-area:desc;margin:0;text-align:center;
  font-size:11.5px;line-height:1.35;color:var(--ink-500)}
html.v2-immersive .v2-slot-bar-ar{display:contents}
html.v2-immersive .v2-action-bar #arBtnWrap{display:contents}
html.v2-immersive .v2-action-bar #viewArBtn{grid-area:ar;height:48px;padding:0 16px;margin:0;
  border-radius:14px;font-size:15px}
html.v2-immersive .v2-action-bar #arHint{grid-area:hint;margin:0;text-align:center;
  font-size:11px;line-height:1.3;color:var(--ink-500);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
html.v2-immersive .v2-action-bar #arHint:empty{display:none}
html.v2-immersive .v2-bar-shop{grid-area:shop;height:48px;border-radius:14px;
  background:var(--paper);border:1.5px solid var(--ink-900);color:var(--ink-900);
  font:600 14px/1 var(--font);cursor:pointer;transition:border-color .18s,color .18s}
html.v2-immersive .v2-bar-shop:hover{border-color:var(--green);color:var(--green-600)}
html.v2-immersive .v2-bar-shop .v2-short{display:none}
html.v2-immersive .btn-ar:disabled{background:#B8C4AE;color:rgba(255,255,255,.85);
  box-shadow:none;cursor:not-allowed;transform:none}
/* tablets: mobile UA at ≥900px — viewer-app shows the AR button, the ≤899
   query never fires; the mirrored class keeps the bar (and button) reachable */
html.v2-immersive.v2-mobile-ar.v2-has-listing .v2-action-bar{display:grid}

/* err-cards + picker on dark */
html.v2-immersive .err-card{background:var(--v2-panel-2);border-color:var(--v2-line);
  border-left:3px solid var(--green);color:var(--v2-text-3)}
html.v2-immersive .err-card b{color:var(--v2-text)}
html.v2-immersive .pane > .err-card{position:relative;z-index:1;
  margin:96px auto 0;max-width:560px}
html.v2-immersive .vp-picker{padding:56px 24px 60px}
html.v2-immersive .vp-picker h2{color:var(--v2-text)}
html.v2-immersive .vp-picker .muted{color:var(--v2-text-3)}
html.v2-immersive .pick-card{background:var(--v2-panel-2);border-color:var(--v2-line);
  color:var(--v2-text)}
html.v2-immersive .pick-card:hover{border-color:var(--green);
  box-shadow:0 6px 22px rgba(106,173,63,.22)}
html.v2-immersive .pick-card .pick-date{color:var(--v2-text-3)}

/* AR interstitial — base LIGHT card (the white card pops on the dimmed
   stage; matches the merged white chrome). The shop overlay also stays on
   its base light theme — every panel surface in the merged design is white. */

/* ---- immersive motion ---- */
@keyframes v2-stage-in{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:none}}
@keyframes v2-pane-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
html.v2-immersive #stageMedia{animation:v2-stage-in .32s var(--ease-out) both}
html.v2-immersive #viewSelector{animation:v2-fade .26s var(--ease-out) .06s both}
html.v2-immersive .v2-rail{animation:v2-fade .32s var(--ease-out) .12s both}
html.v2-immersive .v2-action-bar{animation:v2-fade .26s var(--ease-out) .2s both}
html.v2-immersive .pane:not([hidden]){animation:v2-pane-in .24s var(--ease-out)}
html.v2-immersive .micro-block:not([hidden]){animation:v2-pane-in .2s var(--ease-out)}

/* ---- immersive laptop 900–1200 ---- */
@media (min-width:900px) and (max-width:1200px){
  html.v2-immersive{--v2-rail-w:304px}
}

/* ---- immersive mobile ≤899 — floating tabs + WHITE bottom sheet + bar ---- */
@media (max-width:899px){
  /* peek ends JUST below the LAYOUT row (founder 2026-07-30) — shop text and
     footer live behind the drag */
  html.v2-immersive{--v2-rail-cur:0px;--v2-sheet-peek:170px}
  /* the tab pill is the only top chrome now (the name lives in the sheet).
     NOTCH BUFFER (founder 2026-07-30): min 40px top clearance even when
     env() reports 0 (in-app webviews / cutout phones without env support),
     so the 2D/3D/Photos pill never hides under the notch. */
  html.v2-immersive #viewSelector{
    top:calc(max(env(safe-area-inset-top,0px), 40px) + 8px);left:50%;
    max-width:calc(100vw - 24px)}
  html.v2-immersive #viewSelector button{padding:10px 14px;font-size:13.5px}
  /* panes clear the tab pill + the sheet/bar */
  html.v2-immersive .pane[data-tab="plan"]{
    padding:calc(max(env(safe-area-inset-top,0px), 40px) + 62px) 16px
      calc(var(--v2-bar-space) + var(--v2-sheet-peek) + 16px) 16px}
  html.v2-immersive .stage-photos{
    padding:calc(max(env(safe-area-inset-top,0px), 40px) + 62px) 16px
      calc(var(--v2-bar-space) + var(--v2-sheet-peek) + 16px) 16px}
  html.v2-immersive .room-title{top:0;background:none}
  html.v2-immersive .pane > .err-card{margin:calc(max(env(safe-area-inset-top,0px), 40px) + 72px) auto 0}
  /* caption chip rides above the sheet peek — hybrid mobile pill sizing */
  html.v2-immersive .stage-cap{bottom:calc(var(--v2-bar-space) + var(--v2-sheet-peek) + 12px);
    left:50%;max-width:calc(100vw - 24px);padding:7px 13px;font-size:12px}
  html[data-v2-sheet="half"].v2-immersive .stage-cap,
  html[data-v2-sheet="full"].v2-immersive .stage-cap{opacity:0;pointer-events:none}
  /* the rail becomes the WHITE bottom sheet */
  html.v2-immersive .v2-rail{top:auto;left:0;right:0;bottom:var(--v2-bar-space);
    width:auto;height:calc(100vh - env(safe-area-inset-top,0px) - 96px - var(--v2-bar-space));
    height:calc(100dvh - env(safe-area-inset-top,0px) - 96px - var(--v2-bar-space));
    border:1px solid var(--line);border-bottom:0;border-radius:18px 18px 0 0;
    box-shadow:0 -12px 44px rgba(0,0,0,.42);
    transform:translateY(calc(100% - var(--v2-sheet-peek)));
    transition:transform .3s var(--ease-out)}
  html.v2-immersive .v2-rail.is-dragging{transition:none}
  html[data-v2-sheet="half"].v2-immersive .v2-rail{transform:translateY(calc(100% - 46vh));
    transform:translateY(calc(100% - 46dvh))}
  html[data-v2-sheet="full"].v2-immersive .v2-rail{transform:translateY(0)}
  html.v2-immersive .v2-rail-grab{display:block;padding:10px 0 6px;text-align:center;
    touch-action:none;cursor:grab}
  html.v2-immersive .v2-rail-grab span{display:inline-block;width:38px;height:4px;
    border-radius:999px;background:var(--ink-300)}
  html.v2-immersive .v2-sheet-head{display:block;touch-action:none}
  /* CONDENSED peek (founder 2026-07-30): name LEFT, facts after, controls
     visible below — the sticking-out part shows most of the UI without
     dragging, ending tight under the LAYOUT row (no dead gap) */
  html.v2-immersive .v2-rail-title{gap:2px 10px;padding:4px 16px 6px}
  html.v2-immersive .v2-rail-title .vp-home{font-size:24px;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  html.v2-immersive .v2-rail-title .v2-facts{font-size:14px}
  /* compact controls so Furnished/Parametric + FLOOR share a row and LAYOUT
     rides inline — the whole block fits inside the peek */
  html.v2-immersive .v2-rail-controls{padding:10px 16px}
  html.v2-immersive .v2-rail-controls .micro-block{gap:10px 12px}
  html.v2-immersive .v2-rail-controls .seg2 button{padding:7px 13px;font-size:12px}
  html.v2-immersive .v2-rail-controls .layout-lbl{width:auto;margin:0 6px 0 0}
  /* the shop TEXT block stays in the sheet below LAYOUT; only its button is
     gone — the action bar's "Shop all" IS the button on mobile */
  html.v2-immersive .v2-rail-shopslot .btn-shop-all{display:none}
  /* the action bar is on */
  html.v2-immersive.v2-has-listing .v2-action-bar{display:grid}
}

/* ---- immersive small ≤420 ---- */
@media (max-width:420px){
  html.v2-immersive #viewSelector button{padding:9px 12px;font-size:13px}
  html.v2-immersive .v2-rail-title .vp-home{font-size:22px}
  html.v2-immersive .v2-rail-title .v2-facts{font-size:13px}
  html.v2-immersive .v2-bar-shop .v2-long{display:none}
  html.v2-immersive .v2-bar-shop .v2-short{display:inline}
}

/* ==================================================== HYBRID PINNED HERO ==
 * Founder 2026-07-31: hybrid must NOT scroll as one page. The stage + doc head
 * + controls stay put; only the block BELOW the STAGING row (AR handoff → shop
 * → footer) moves. This keeps the existing fold contract ("first screen ends
 * just below STAGING") but makes the fold a real scroll boundary instead of a
 * page-scroll position. Desktop/laptop only (>=900px) — mobile keeps its hero +
 * fixed action bar. Mirrors the immersive body-lock at :335. The footer is
 * moved INTO the scroller by extras so it FLOWS after the shop block (the
 * pinned-footer dead gap was a repeated founder complaint).
 * FINAL 2026-07-31: applies at EVERY width — mobile hybrid pins the viewer
 * too ("only the bottom white part scrolls"); a small peek of the scroller
 * stays visible above the sticky bar as the touch surface + affordance. */
@media (min-width:0px){
  html.v2-hybrid.v2-has-listing,
  html.v2-hybrid.v2-has-listing body{height:100%;overflow:hidden}
  /* [r16 #48 → r17 #2] --v2-vh (measured px, dvh-seeded) is the column's
     length: iOS resolves dvh against a layout viewport that is still at the
     toolbar-retracted height for the frames right after load, which pushed
     #listing's bottom slice — exactly where the footer's last lines sit —
     below anything the buyer can see. Measured height cannot drift. */
  html.v2-hybrid.v2-has-listing #listing{
    height:var(--v2-vh);display:flex;flex-direction:column;overflow:hidden}
  html.v2-hybrid.v2-has-listing .v2-stage-shell{flex:0 0 auto}
  /* hero yields to the head/controls block + the peek strip of the scroller */
  html.v2-hybrid.v2-has-listing .stage-media{
    height:calc(var(--v2-vh) - var(--v2-doc-top-h,164px) - var(--v2-doc-peek,210px));
    min-height:340px}
  html.v2-hybrid.v2-has-listing .v2-doc{
    flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
  html.v2-hybrid.v2-has-listing .v2-doc-head,
  html.v2-hybrid.v2-has-listing .v2-doc-controls{flex:0 0 auto}
  html.v2-hybrid.v2-has-listing .v2-doc-scroll{
    flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
    scroll-behavior:smooth}
  html.v2-hybrid.v2-has-listing .v2-doc-scroll .vp-foot{padding-bottom:22px}
}
@media (min-width:900px) and (prefers-reduced-motion:reduce){
  html.v2-hybrid.v2-has-listing .v2-doc-scroll{scroll-behavior:auto}
}

/* ---------------------------------------------- "more below" scroll cue --
 * Bottom-RIGHT of the stage so it never covers the centred caption pill.
 * Fades out on first scroll and never appears when nothing overflows (which
 * is also why it stays invisible on mobile, where .v2-doc-scroll is inert). */
.v2-scroll-cue{display:none}
html.v2-hybrid.v2-has-listing .v2-scroll-cue{
  position:absolute;right:18px;bottom:18px;z-index:60;
  display:inline-flex;align-items:center;gap:7px;
  padding:9px 14px;border-radius:999px;border:1px solid var(--v2-glass-border);
  background:var(--v2-glass-strong);backdrop-filter:blur(var(--v2-blur));
  -webkit-backdrop-filter:blur(var(--v2-blur));
  box-shadow:var(--v2-glass-shadow);cursor:pointer;
  font-weight:600;font-size:13px;line-height:1;letter-spacing:.01em;
  color:var(--ink-700);
  transition:opacity .3s var(--ease-out),transform .3s var(--ease-out)}
html.v2-hybrid.v2-has-listing .v2-scroll-cue:hover{color:var(--green-600)}
html.v2-hybrid.v2-has-listing .v2-scroll-cue.is-gone{
  opacity:0;transform:translateY(8px);pointer-events:none}
html.v2-hybrid.v2-has-listing .v2-scroll-cue svg{
  width:13px;height:13px;animation:v2-cue-bob 1.9s ease-in-out infinite}
@keyframes v2-cue-bob{0%,100%{transform:translateY(-1.5px)}50%{transform:translateY(2px)}}
@media (max-width:899px){
  /* a row ABOVE the caption pill — they collided at phone widths */
  html.v2-hybrid.v2-has-listing .v2-scroll-cue{
    right:12px;bottom:52px;padding:8px 12px;font-size:12px}
}
@media (prefers-reduced-motion:reduce){
  html.v2-hybrid.v2-has-listing .v2-scroll-cue svg{animation:none}
}

/* FINAL (founder 2026-07-31): the desktop rail must FIT — nothing slides on
   the right side. Shorter viewports squeeze the sections instead. */
@media (min-width:900px){
  html.v2-immersive .v2-rail-scroll{overflow:hidden}
}
@media (min-width:900px) and (max-height:940px){
  html.v2-immersive .v2-rail-title{padding:10px 18px 6px}
  html.v2-immersive .v2-rail-title .vp-home{font-size:22px}
  html.v2-immersive .v2-rail-title .v2-facts{font-size:13px}
  html.v2-immersive .v2-rail-controls{padding:8px 18px}
  html.v2-immersive .v2-rail-controls .micro-block{gap:8px 12px}
  html.v2-immersive .v2-rail-arslot{padding:8px 18px}
  html.v2-immersive .v2-rail-arslot .qr-wrap{padding:8px 12px}
  html.v2-immersive .v2-rail-shopslot .shop-look{padding:10px 18px 12px}
  html.v2-immersive .shop-lede{font-size:15px}
  html.v2-immersive .shop-body{font-size:12.5px;margin:0 0 10px}
  html.v2-immersive .btn-shop-all{padding:10px 16px;font-size:13.5px}
  html.v2-immersive .v2-rail-footslot .vp-foot{padding:10px 18px 12px}
  html.v2-immersive .v2-rail-footslot .disclaimer{font-size:10px;margin:0 0 6px}
  html.v2-immersive .v2-rail-footslot .foot-stage{margin:0 0 5px;font-size:11px}
  html.v2-immersive .v2-rail-footslot .foot-brand{font-size:11px}
}

/* ================== FINAL POLISH (founder 2026-07-31 pm) — keep LAST ===== */
/* desktop rail: leftover height SPACES the sections out instead of pooling
   at the bottom (space-between is a no-op when content exactly fits) */
@media (min-width:900px){
  html.v2-immersive .v2-rail-scroll{flex:1 1 auto;display:flex;flex-direction:column;
    justify-content:space-between}
}

/* mobile: the ENTIRE white area (from the home name down) is ONE scroller;
   at rest it shows exactly title + controls (STAGING last), everything else
   scrolls into view. The tail wrapper dissolves into the flow. */
@media (max-width:899px){
  /* extra 36px strip below STAGING = home for the green "More" cue */
  html.v2-hybrid.v2-has-listing .stage-media{
    height:calc(var(--v2-vh) - var(--v2-sticky-h) - var(--v2-doc-top-h,164px) - 36px);
    min-height:300px}
  html.v2-hybrid.v2-has-listing .v2-doc{flex:1 1 auto;min-height:0;display:block;
    overflow-y:auto;overscroll-behavior:contain}
  /* [r16 #48] the bar reserve is a REAL in-flow spacer, not padding-bottom:
     WebKit drops a scroller's bottom padding from its scrollable extent, so
     on the founder's iPhone the scroll stopped ~the bar's height short and
     the "Stage with IVRIS" footer line stayed clipped under the fixed bar.
     A ::after block is counted by every engine (and never double-reserves).
     [r17 #2] the HEIGHT is now measured, not derived: --v2-doc-tail is the
     real overlap between this scroller's box and whichever lid comes first —
     the fixed bar's top edge or the bottom of what the browser is actually
     showing (extras: syncDocTail). The old formula added the bar's height AND
     env(safe-area-inset-bottom) even though the bar's own padding already
     carried that inset, and it assumed the scroller ended exactly at the
     visible edge — the two assumptions that left the sheet short on iOS. The
     formula stays as the var() fallback for the pre-boot frame. */
  html.v2-hybrid.v2-has-listing .v2-doc::after{content:"";display:block;
    height:var(--v2-doc-tail,
      calc(var(--v2-sticky-h) + env(safe-area-inset-bottom,0px) + 20px))}
  /* [r17 #3] a photo figure whose image has no intrinsic size yet is 0px
     tall. viewer-app (FROZEN) marks every photo loading="lazy", and a 0px box
     never comes near enough to any scrollport to trigger that load — so the
     room's SECOND staged angle stayed collapsed and invisible forever, and
     the founder saw one AI photo per room instead of two. Reserve a frame
     until enhanceFig locks the real aspect (.v2-fixed-ar); extras also
     switches these images to eager. */
  /* …sized off --v2-vh (MEASURED px), never a bare vh: on iOS `vh` resolves
     against the tall layout viewport, which is the exact unit class that broke
     the sheet in r16 #48. r17 banned dvh/svh/lvh and this 34vh slipped past. */
  html.v2-hybrid #photosBody .switch-fig:not(.v2-fixed-ar){min-height:calc(var(--v2-vh) * .34);
    background:var(--v2-panel)}   /* a decoding frame reads as a placeholder,
                                     not a hole in the black gallery */
  html.v2-hybrid.v2-has-listing #v2DocScroll{display:contents}
  /* photos: NO chrome box around figures — Original mode used to flash a
     white card around the letterboxed photo */
  html.v2-hybrid .switch-fig{background:none;border:0;box-shadow:none;border-radius:0}
}
html.v2-immersive .switch-fig{background:none;border:0}
html.v2-immersive .switch-fig img{background:none}

/* compare slider animates when the Staged/Original toggle (or a label) snaps
   it; live dragging stays 1:1 */
.v2-cmp-top{transition:clip-path .28s var(--ease-out)}
.switch-fig.v2-compare .v2-cmp-handle{transition:left .28s var(--ease-out)}
.switch-fig.v2-compare.is-dragging .v2-cmp-top,
.switch-fig.v2-compare.is-dragging .v2-cmp-handle{transition:none}

/* scroll cue → green "More" pill at the BOTTOM of the white area, centred
   above the action bar (founder moved it out of the viewer) */
html.v2-hybrid.v2-has-listing .v2-scroll-cue{
  position:fixed;left:50%;right:auto;top:auto;transform:translateX(-50%);
  bottom:calc(var(--v2-sticky-h) + 10px);z-index:401;
  background:var(--paper);border:1px solid var(--green-200);color:var(--green-600);
  box-shadow:var(--shadow-md)}
html.v2-hybrid.v2-has-listing .v2-scroll-cue svg{color:var(--green-600)}
html.v2-hybrid.v2-has-listing .v2-scroll-cue:hover{color:var(--green)}
html.v2-hybrid.v2-has-listing .v2-scroll-cue.is-gone{
  opacity:0;transform:translate(-50%,8px);pointer-events:none}

/* ---------------- round-2 polish (founder 2026-07-31 pm) — keep LAST ----- */
/* photos: ZERO chrome anywhere — no white card, no white letterbox fill,
   square corners, no divider line between rooms. Both form factors. */
html.v2-hybrid .switch-fig,html.v2-immersive .switch-fig{
  background:none;border:0;border-radius:0;box-shadow:none}
html.v2-hybrid .switch-fig img,html.v2-immersive .switch-fig img{
  background:none;border-radius:0}
html.v2-hybrid .room-section + .room-section,
html.v2-immersive .room-section + .room-section{border-top:0;padding-top:8px}

/* compare labels fade under the handle */
.v2-cmp-label{transition:opacity .15s var(--ease-out),background .18s var(--ease-out)}
.v2-cmp-label.is-hidden{opacity:0}

@media (min-width:900px){
  /* the stage spans only the area LEFT of the rail — the home + panes center
     over the tab selector instead of the full page */
  html.v2-immersive .stage-media{inset:0 var(--v2-rail-cur) 0 0;width:auto}
  html.v2-immersive .pane[data-tab="plan"]{padding:84px 24px 64px}
  html.v2-immersive .stage-photos{padding:84px 24px 64px}
  /* photos: BIGGER, one centred column, square */
  html.v2-immersive #photosBody > *{max-width:1000px}
  html.v2-immersive .room-section{display:block;max-width:1000px;margin:0 auto 40px}
  html.v2-immersive .room-section .switch-fig{margin:0 0 16px}
  html.v2-immersive .switch-fig img{width:100%;height:auto;max-height:80vh;object-fit:contain}
  /* rail: EQUAL buffer above and below every section — delivered by each
     section's OWN padding, stacked from the top.
     [r37] WAS justify-content:space-evenly, and that is the founder's "there is
     an empty band below the facts row on desktop on 2d/3d/photos tabs ... when
     the qr isnt below it breaks the layout ... the lines are off when it doesnt
     have ar since the qr code is gone it drops the line lower."
     MEASURED on the live showcase home at 1440x900: the rail is 725 px tall and
     its sections total 428 px, so space-evenly split 297 px of slack into five
     ~74 px gaps — one of them directly under the facts row. The gaps are a
     function of how much content the rail happens to have, so dropping the QR
     card grows every one of them and every divider slides down: the layout
     literally re-flows on a listing with no AR. flex-start pins the sections
     under the title, the buffer comes from padding (fixed, so it cannot
     re-flow), and the 1 px sibling border-tops stay put. Re-measured with the
     rule applied on the live page: gaps between sections 0 px, first section
     flush under the title. */
  html.v2-immersive .v2-rail-scroll{justify-content:flex-start}
}

@media (max-width:899px){
  /* flush stop right after STAGING — no strip, no shop-text sliver */
  html.v2-hybrid.v2-has-listing .stage-media{
    height:calc(var(--v2-vh) - var(--v2-sticky-h) - var(--v2-doc-top-h,164px));
    min-height:300px}
  /* the green More rides the RIGHT side */
  html.v2-hybrid.v2-has-listing .v2-scroll-cue{left:auto;right:12px;transform:none}
  html.v2-hybrid.v2-has-listing .v2-scroll-cue.is-gone{transform:translateY(8px)}
}

/* ---------------- round-3 polish (founder 2026-07-31 pm) — keep LAST ----- */
/* the More cue: smaller */
html.v2-hybrid.v2-has-listing .v2-scroll-cue{padding:6px 10px;font-size:11px;gap:5px}
html.v2-hybrid.v2-has-listing .v2-scroll-cue svg{width:11px;height:11px}

@media (max-width:899px){
  /* +5px stage crop hides the controls' bottom border at the fold — the
     "next section line" the founder kept seeing at rest */
  html.v2-hybrid.v2-has-listing .stage-media{
    height:calc(var(--v2-vh) - var(--v2-sticky-h) - var(--v2-doc-top-h,164px) + 5px)}
}

/* ---------------- r17 (founder 2026-08-04) — keep LAST ------------------- */
/* #3 BOTH staged angles per room, on the phone too. The room strip is a wrap
 * flex row and each figure is full width, so the second angle simply stacks —
 * what hid it was the zero-height frame above, not the layout. The count line
 * ("2 photos of this room") sits under the pair and must not be mistaken for
 * a divider, so it keeps the muted treatment it already had. */
@media (max-width:899px){
  html.v2-hybrid #photosBody .room-section{padding-bottom:6px}
  /* the pair reads as ONE room: tighter gap between the two angles than
     between rooms */
  html.v2-hybrid #photosBody .switch-fig + .switch-fig{margin-top:2px}
}

@media (min-width:900px){
  /* photos a notch smaller */
  html.v2-immersive #photosBody > *{max-width:860px}
  html.v2-immersive .room-section{max-width:860px}
  /* the STICKY room title (with its gradient band) was the scroll glitch —
     titles scroll away with their photos now */
  html.v2-immersive .room-title{position:static;background:none;padding:0 0 10px}
}

/* ---------------- round-4 polish (founder 2026-07-31 pm) — keep LAST ----- */
@media (min-width:900px){
  /* photos: SMALLER + a UNIFORM frame per figure — section heights no longer
     jump with each photo's aspect ratio or the Staged/Original flip */
  html.v2-immersive #photosBody > *{max-width:760px}
  html.v2-immersive .room-section{max-width:760px}
  html.v2-immersive .room-section .switch-fig{height:56vh;margin:0 0 16px}
  html.v2-immersive .switch-fig img{width:100%;height:100%;max-height:none;object-fit:contain}
  /* rail: a touch more air at the TOP of each section (footer unchanged) */
  html.v2-immersive .v2-rail-title{padding-top:20px}
  html.v2-immersive .v2-rail-controls{padding-top:18px}
  html.v2-immersive .v2-rail-arslot{padding-top:18px}
  html.v2-immersive .v2-rail-shopslot .shop-look{padding-top:20px}
}

/* ---------------- round-5 polish (founder 2026-07-31 pm) — keep LAST ----- */
/* the slider grip arrow is brand green */
.v2-cmp-grip{color:var(--green-600)}
@media (min-width:900px){
  /* photos a little bigger again */
  html.v2-immersive #photosBody > *{max-width:840px}
  html.v2-immersive .room-section{max-width:840px}
  /* uniform PHOTO size, not just uniform frame: cover-fill the fixed frame
     so the Original flip can't shrink the visible image */
  html.v2-immersive .switch-fig img{object-fit:cover}
}

/* ---------------- round-6 polish (founder 2026-07-31 pm) — keep LAST ----- */
/* photos render at their NATURAL aspect inside an aspect-locked frame (the
   staged photo's ratio, set inline by extras) — no cover-crop "wide" look,
   and Staged⇄Original never changes the frame size on either form factor */
.switch-fig.v2-fixed-ar{width:100%}
.switch-fig.v2-fixed-ar img{width:100%;height:100%;object-fit:contain}
@media (min-width:900px){
  html.v2-immersive .room-section .switch-fig{height:auto}
  html.v2-immersive .switch-fig img{object-fit:contain}
  html.v2-immersive .room-section .switch-fig.v2-fixed-ar{max-height:76vh}
}

/* ---------------- round-7 (founder 2026-07-31 pm) — keep LAST ------------ */
@media (min-width:900px){
  /* room names a step BIGGER, room photos a step SMALLER */
  html.v2-immersive .room-title{font-size:24px;padding:0 0 12px}
  html.v2-immersive #photosBody > *{max-width:720px}
  html.v2-immersive .room-section{max-width:720px}
}

/* round-7b — the same step AGAIN (founder): names bigger, photos smaller */
@media (min-width:900px){
  html.v2-immersive .room-title{font-size:28px;padding:0 0 14px}
  html.v2-immersive #photosBody > *{max-width:620px}
  html.v2-immersive .room-section{max-width:620px}
}

/* ---------------- r18b (founder 2026-08-04) — keep LAST ------------------ */
/* SHOP SHEET: the "Shop all furniture" button (shop.js .iso-cta-link, shown
   on the Shop-all listing view) is stacked in .iso-ctarow directly under the
   search/sort row with NO gap of its own — viewer.css (FROZEN base, also
   serves /view-legacy) gives the row only top padding. Founder: "the shop all
   furniture button in the ui that pops up doesnt have spacing with the other
   elements around/above it". Give it the sheet's own vertical rhythm (12px —
   the head's bottom padding / the mobile ctarow inset), exactly when a
   visible control row sits above it; this file is loaded by the live viewer
   page only, so the legacy page stays byte-identical. */
.iso-ctarow .iso-controls:not([hidden]) ~ .iso-cta{margin-top:12px}
