/* ==========================================================================
   Home search — search results, map and listing detail.

   Built on the tokens and components already in styles.css (--cedar, --pine,
   --ink, .btn, .wrap, .bb …) rather than a parallel system, so the MLS pages
   read as part of the site instead of an embedded product.

   The interaction model follows the reference search UI (pill filters with
   anchored popovers, an All Filters sheet, a list/map toggle, draw-on-map),
   but the surface is Ari's: a pine panel with cedar accents rather than the
   reference's white-on-white.

   Load order matters: this file must come after styles.css.
   ========================================================================== */

:root{
  --hs-top:92px;
  /* The header shrinks to .solid once the page scrolls, and the filter bar has
     to stick just under that condensed height — not the tall initial one, or
     it leaves a gap that the results scroll through. */
  --hs-stick:64px;
  --hs-bar:78px;         /* filter panel height, desktop */
  --hs-gap:clamp(1rem,1.6vw,1.5rem);
  --hs-r:10px;           /* the rounded-corner radius used across cards/panels */
}

.hs{background:var(--snow);min-height:100vh}
.hs-main{padding-top:var(--hs-top)}

/* ---------------------------------------------------------------- titlebar */

.hs-head{padding:clamp(1.5rem,3vw,2.4rem) clamp(1.1rem,4vw,3.6rem) 0;max-width:1600px;margin:0 auto}
.hs-head h1{font-family:var(--bb);font-weight:400;letter-spacing:.045em;line-height:.95;
  text-transform:uppercase;font-size:clamp(2rem,4.2vw,3.1rem);color:var(--ink)}
.hs-head .hs-sub{color:var(--body);font-size:.98rem;margin-top:.5rem;line-height:1.7}

/* ============================================================== filter panel
   A pine slab, per Ari's search-panel styling, holding the keyword field, the
   filter pills and the view controls. */

.hs-filters{position:sticky;top:var(--hs-stick);z-index:60;background:var(--snow);
  padding:.9rem clamp(1.1rem,4vw,3.6rem);margin-top:clamp(1.1rem,2.4vw,1.7rem)}
.hs-panel{max-width:1600px;margin:0 auto;background:var(--pine);border-radius:var(--hs-r);
  padding:.85rem .9rem;display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}

/* keyword field */
/* The basis matches min-width on purpose. Flexbox decides where to break lines
   using each item's flex *base* size and never considers that an item could
   shrink instead — so with a 250px basis the row was measured at 250 + pills +
   the right-hand controls even though the field was perfectly willing to give
   60px back. Selecting Rancho Peñasquitos widens the area pill by ~87px, that
   measurement crossed the panel width by 45px, and the whole right-hand group
   wrapped to a second line while the field sat there 566px wide. With the two
   equal the measurement is honest: the row breaks only when it genuinely
   cannot fit, and grow still lets the field take whatever is left over. */
.hs-q{flex:1 1 190px;min-width:190px;position:relative;display:flex;align-items:center}
.hs-q svg{position:absolute;left:.85em;width:15px;height:15px;color:rgba(247,246,242,.5);
  pointer-events:none}
.hs-q input{width:100%;background:rgba(247,246,242,.07);border:1px solid var(--wline);
  border-radius:6px;color:var(--snow);font-family:var(--ns);font-size:.9rem;
  padding:.72em .9em .72em 2.5em;min-height:44px;transition:border-color .3s var(--ease)}
.hs-q input::placeholder{color:rgba(247,246,242,.45)}
.hs-q input:focus{outline:none;border-color:var(--cedar2);background:rgba(247,246,242,.1)}
/* Submit, inside the field. Hidden wherever Search Homes is on the bar — it
   exists for the phone layout, which drops that button. See buildPanel. */
.hs-qgo{display:none}
@media(pointer:coarse){
  .hs-qgo{display:grid;place-items:center;position:absolute;right:5px;
    width:38px;height:38px;padding:0;border:none;border-radius:5px;cursor:pointer;
    background:var(--cedar);color:var(--snow)}
  .hs-qgo svg{position:static;width:17px;height:17px;color:currentColor;pointer-events:none}
  .hs-qgo:active{background:var(--cedar2)}
  /* Clear the button so a long city name never runs underneath it. */
  .hs-q input{padding-right:3.1em}
}

/* pills */
.hs-pills{display:flex;gap:.5rem;align-items:center;min-width:0}
.hs-pill{position:relative}
.hs-pill>button{display:inline-flex;align-items:center;gap:.5em;font-family:var(--ns);
  font-size:.88rem;color:var(--snow);background:transparent;border:1px solid var(--wline);
  border-radius:100px;padding:.62em 1.1em;min-height:44px;cursor:pointer;white-space:nowrap;
  transition:background .3s var(--ease),border-color .3s var(--ease),color .3s var(--ease)}
.hs-pill>button:hover{border-color:rgba(247,246,242,.42);background:rgba(247,246,242,.06)}
/* A backstop, not a design: every label in AREAS fits well inside this, so
   nothing truncates in normal use. It exists because the area label falls back
   to the raw ?city= value when that value is not one of the known areas, and an
   unbounded string there would push the row apart again. */
.hs-pill>button .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:9rem}
.hs-pill>button .cx{width:8px;height:8px;border-right:1.5px solid currentColor;
  border-bottom:1.5px solid currentColor;transform:rotate(45deg) translate(-2px,-2px);
  transition:transform .3s var(--ease);flex:none}
.hs-pill.open>button{background:var(--snow);border-color:var(--snow);color:var(--ink)}
.hs-pill.open>button .cx{transform:rotate(-135deg) translate(-2px,-2px)}
/* A pill holding a value reads as cedar so active filters are visible at a glance. */
.hs-pill.set>button{background:var(--cedar);border-color:var(--cedar);color:var(--snow)}
.hs-pill.set.open>button{background:var(--snow);border-color:var(--snow);color:var(--ink)}

/* popovers */
.hs-pop{position:absolute;top:calc(100% + .5rem);left:0;z-index:80;background:var(--snow);
  border:1px solid var(--line);border-radius:var(--hs-r);
  box-shadow:0 18px 44px rgba(10,16,22,.24);padding:1rem;display:none;
  min-width:290px;animation:hsPop .18s var(--ease)}
.hs-pill.open .hs-pop{display:block}
.hs-pop.right{left:auto;right:0}
@keyframes hsPop{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}
@media(prefers-reduced-motion:reduce){.hs-pop{animation:none}}

.hs-pop .lbl{font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;color:var(--fog);
  margin-bottom:.5rem;display:block}
.hs-range{display:flex;align-items:center;gap:.6rem}
.hs-range input{flex:1;min-width:0;background:transparent;border:1px solid var(--line);
  border-radius:6px;padding:.62em .7em;font-family:var(--ns);font-size:.9rem;color:var(--ink);
  min-height:42px}
.hs-range input:focus{outline:none;border-color:var(--cedar)}
.hs-range span{color:var(--fog);font-size:.84rem}

/* option rows (beds / baths) */
.hs-opts{display:flex;gap:.35rem;flex-wrap:wrap}
.hs-opt{font-family:var(--ns);font-size:.86rem;color:var(--ink);background:var(--snow2);
  border:1px solid transparent;border-radius:6px;padding:.5em .85em;cursor:pointer;
  min-width:44px;transition:background .25s var(--ease),color .25s var(--ease)}
.hs-opt:hover{background:#e6e3da}
.hs-opt.on{background:var(--pine);color:var(--snow)}

/* property-type tiles */
.hs-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:.45rem}
.hs-tile{display:flex;flex-direction:column;align-items:center;gap:.45rem;padding:.85rem .5rem;
  border:1px solid var(--line);border-radius:8px;background:var(--snow);cursor:pointer;
  font-size:.78rem;color:var(--ink);transition:border-color .25s var(--ease),background .25s var(--ease)}
.hs-tile svg{width:20px;height:20px;color:var(--body)}
.hs-tile:hover{border-color:var(--fog)}
.hs-tile.on{border-color:var(--cedar);background:rgba(192,138,62,.09)}
.hs-tile.on svg{color:var(--cedar)}

.hs-popfoot{display:flex;justify-content:space-between;align-items:center;gap:.6rem;
  margin-top:.9rem;padding-top:.8rem;border-top:1px solid var(--line2)}
.hs-clear{background:none;border:none;color:var(--body);font-size:.82rem;cursor:pointer;
  text-decoration:underline;padding:.3em 0}
.hs-clear:hover{color:var(--ink)}

/* right-hand controls */
.hs-right{display:flex;gap:.5rem;align-items:center;margin-left:auto}
.hs-seg{display:inline-flex;background:rgba(247,246,242,.09);border:1px solid var(--wline);
  border-radius:100px;padding:3px}
.hs-seg button{font-family:var(--ns);font-size:.84rem;color:rgba(247,246,242,.72);
  background:transparent;border:none;border-radius:100px;padding:.5em 1.1em;cursor:pointer;
  min-height:36px;transition:background .3s var(--ease),color .3s var(--ease)}
.hs-seg button.on{background:var(--snow);color:var(--ink)}
/* 36px is under the 44px touch minimum, and List/Map is the control that
   switches the whole view — the one thing on the bar a phone visitor reaches
   for most. syncBar() measures the bar rather than assuming its height, so
   the map's fold calculation follows this on its own. */
@media(pointer:coarse){ .hs-seg button{min-height:44px} }

.hs-ghost{font-family:var(--ns);font-size:.88rem;color:var(--snow);background:transparent;
  border:1px solid var(--wline);border-radius:100px;padding:.62em 1.1em;min-height:44px;
  cursor:pointer;white-space:nowrap;transition:background .3s var(--ease),border-color .3s var(--ease)}
.hs-ghost:hover{background:rgba(247,246,242,.08);border-color:rgba(247,246,242,.42)}
.hs-ghost .n{display:inline-block;background:var(--cedar);color:var(--snow);border-radius:100px;
  font-size:.7rem;padding:.1em .5em;margin-left:.45em}

.hs-go{font-family:var(--bb);font-size:.86rem;letter-spacing:.13em;text-transform:uppercase;
  background:var(--cedar);border:1px solid var(--cedar);color:var(--snow);border-radius:6px;
  padding:.85em 1.6em .7em;min-height:44px;cursor:pointer;white-space:nowrap;
  transition:background .3s var(--ease),border-color .3s var(--ease)}
.hs-go:hover{background:var(--cedar2);border-color:var(--cedar2)}

/* -------------------------------------------------------------------- chips */

.hs-chips{display:flex;gap:.45rem;flex-wrap:wrap;max-width:1600px;margin:.7rem auto 0}
.hs-chips:empty{display:none}
.hs-chip{display:inline-flex;align-items:center;gap:.5em;font-size:.76rem;color:var(--ink);
  background:var(--snow2);border:1px solid var(--line);border-radius:100px;padding:.4em .5em .4em .9em}
.hs-chip button{background:none;border:none;cursor:pointer;color:var(--fog);font-size:1rem;
  line-height:1;padding:0 .25em;border-radius:50%;position:relative}
.hs-chip button:hover{color:var(--ink)}
/* The glyph renders at 18x16, which is a fine mark to look at and far too
   small a thing to hit with a thumb — dismissing a search area was a game of
   chance on a phone. The visible × keeps its size so the chip keeps its shape;
   an invisible overlay carries the 44px target instead. Coarse pointers only:
   on a mouse the small target is not a problem, and a 44px box would overlap
   the neighbouring chip's hover. */
@media(pointer:coarse){
  .hs-chip button::after{content:'';position:absolute;top:50%;left:50%;
    transform:translate(-50%,-50%);width:44px;height:44px}
}

/* ------------------------------------------------------------------ layout */

.hs-body{max-width:1600px;margin:0 auto;padding:0 clamp(1.1rem,4vw,3.6rem) 4rem;
  display:grid;grid-template-columns:minmax(0,1fr) minmax(0,46%);gap:var(--hs-gap);
  align-items:start}
.hs-results{min-width:0;padding-top:1.4rem}

/* The map holds the viewport while the results column scrolls past it —
   standard for search UIs and the reason the two are siblings, not nested. */
.hs-mapwrap{position:sticky;top:calc(var(--hs-stick) + var(--hs-bar) + 1rem);
  height:calc(100vh - var(--hs-stick) - var(--hs-bar) - 2rem);
  margin-top:1.4rem;border-radius:var(--hs-r);background:var(--snow2);overflow:hidden;
  border:1px solid var(--line)}
.hs-map{width:100%;height:100%}

/* List-only view: the map column collapses and cards use the full width. */
.hs-body.list-only{grid-template-columns:minmax(0,1fr)}
.hs-body.list-only .hs-mapwrap{display:none}

/* ------------------------------------------------- pin card (phone map mode)

   Tapping a price pin raises this one home over the map. Only ever one, only
   ever the home that was tapped — which is what separates it from the strip
   of unrelated cards that used to sit here. It lives inside .hs-mapwrap so it
   is bounded by the map rather than the viewport, and so it disappears with
   the map when the view switches back to the list. */
.hs-mapcard{position:absolute;left:.7rem;right:.7rem;bottom:.7rem;z-index:6;
  margin:0 auto;max-width:420px;display:none;
  transform:translateY(12px);opacity:0;
  transition:transform .28s var(--ease),opacity .28s var(--ease)}
.hs-mapcard.show{display:block}
.hs-mapcard.in{transform:none;opacity:1}
.hs-mapcard .hs-card{box-shadow:0 18px 44px rgba(19,28,36,.30)}
.hs-mapcard .hs-photo{aspect-ratio:16/9}
.hs-mapcard .hs-cin{padding:.55rem .35rem .4rem}
.hs-mapcard .hs-price{font-size:1.2rem}
.hs-mapcard .hs-courtesy{display:none}
/* Sits beside the share button in the card's own overlay row. */
.hs-mapcard .hs-mapx{width:34px;height:34px;border-radius:50%;border:none;cursor:pointer;
  background:rgba(19,28,36,.62);color:var(--snow);font-size:1.15rem;line-height:1;
  display:grid;place-items:center;backdrop-filter:blur(4px);
  transition:background .3s var(--ease)}
.hs-mapcard .hs-mapx:hover{background:var(--cedar)}

/* ------------------------------------------------------------------ counts */

.hs-count{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;
  flex-wrap:wrap;margin-bottom:1.1rem}
.hs-count .n{font-family:var(--bb);font-size:1.25rem;letter-spacing:.06em;color:var(--ink)}
.hs-count .n em{font-style:normal;color:var(--cedar)}
.hs-count .hs-sortwrap{display:flex;align-items:center;gap:.5rem}
.hs-count .hs-sortwrap span{font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;color:var(--fog)}
.hs-count select{font-family:var(--ns);font-size:.88rem;color:var(--ink);background:transparent;
  border:1px solid var(--line);border-radius:6px;padding:.5em 2em .5em .7em;cursor:pointer;
  appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,var(--fog) 50%),
                   linear-gradient(135deg,var(--fog) 50%,transparent 50%);
  background-position:calc(100% - 1em) 55%,calc(100% - .72em) 55%;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
.hs-count select:focus{outline:none;border-color:var(--cedar)}

/* ------------------------------------------------------------------- cards */

/* Two across beside the map, and a fixed count rather than auto-fill.
   auto-fill let the column count drift with the viewport — three cards at
   1600px, two at 1200 — so the same search looked like a different product on
   two machines, and at three across the photo was too small to tell one
   kitchen from another. Two is the width the photo needs; the map keeps the
   rest. */
.hs-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  grid-auto-rows:1fr;gap:var(--hs-gap);align-items:stretch}
/* With the map gone there is roughly twice the width, so one more column
   rather than two very wide cards. */
.hs-body.list-only .hs-grid{grid-template-columns:repeat(3,minmax(0,1fr))}

/* Flex column, and both children are block: .hs-photo and .hs-cin are <a>
   elements, which are inline by default — and `aspect-ratio` is ignored on an
   inline box, which is what made the photos size to their natural height and
   the cards come out between 185px and 633px tall. */
.hs-card{display:flex;flex-direction:column;position:relative;background:var(--snow);
  border-radius:var(--hs-r);height:100%;padding:6px;
  transition:transform .4s var(--ease),box-shadow .4s var(--ease)}
.hs-card:hover{transform:translateY(-3px);box-shadow:0 16px 36px rgba(19,28,36,.12)}
.hs-card.is-active{box-shadow:0 0 0 2px var(--cedar),0 16px 36px rgba(19,28,36,.12)}

/* Inset from the card edge, so the hover shadow reads as a frame around the
   photo rather than stopping flush against it. */
.hs-photo{display:block;position:relative;aspect-ratio:4/3;background:var(--snow2);
  overflow:hidden;border-radius:calc(var(--hs-r) - 3px);flex:none}
.hs-photo img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .7s var(--ease)}
.hs-card:hover .hs-photo img{transform:scale(1.045)}
.hs-photo.none{display:flex;align-items:center;justify-content:center;color:var(--fog);
  font-size:.78rem;letter-spacing:.14em;text-transform:uppercase}


.hs-badge{position:absolute;top:.7rem;left:.7rem;font-family:var(--bb);font-size:.72rem;
  letter-spacing:.13em;text-transform:uppercase;background:rgba(19,28,36,.78);color:var(--snow);
  padding:.42em .8em .3em;border-radius:6px;backdrop-filter:blur(3px)}
.hs-badge.soon{background:rgba(192,138,62,.92)}
.hs-badge.pend{background:rgba(59,68,76,.85)}

/* Circular share overlay, as on the reference cards. */
.hs-ov{position:absolute;top:calc(.6rem + 6px);right:calc(.6rem + 6px);display:flex;
  gap:.4rem;z-index:2}
.hs-ovb{width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;
  background:rgba(19,28,36,.6);color:var(--snow);display:grid;place-items:center;
  backdrop-filter:blur(4px);transition:background .3s var(--ease),transform .3s var(--ease)}
.hs-ovb:hover{background:var(--cedar);transform:scale(1.07)}
.hs-ovb svg{width:16px;height:16px}

/* Fixed rows so every card is the same height even when a listing is missing
   its bed/bath counts or its MLS number. The blocks below always render — a
   missing value leaves the line empty rather than collapsing it. */
.hs-cin{display:block;padding:.7rem .3rem .75rem;flex:1 1 auto}
.hs-facts,.hs-addr,.hs-mls,.hs-courtesy{min-height:1.3em}
.hs-price{font-family:var(--bb);font-size:1.3rem;letter-spacing:.03em;color:var(--ink);line-height:1}
.hs-facts{font-size:.84rem;color:var(--body);margin-top:.4rem}
/* One line, ellipsised rather than wrapped — a wrapped address makes the grid
   rows uneven and the ZIP is the part that falls off. */
.hs-addr{color:var(--ink);font-size:.86rem;margin-top:.25rem;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hs-withheld{color:var(--fog);font-style:italic}
.hs-mls{font-size:.74rem;color:var(--fog);margin-top:.2rem}
.hs-courtesy{margin-top:.4rem;font-size:.66rem;line-height:1.5;color:var(--fog)}

/* ------------------------------------------------------------- pagination */

.hs-pager{display:flex;align-items:center;justify-content:center;gap:.8rem;margin-top:2.4rem;
  flex-wrap:wrap}
.hs-pager .pg{font-size:.82rem;color:var(--body);letter-spacing:.04em}

.hs-btn{font-family:var(--bb);font-size:.82rem;letter-spacing:.13em;text-transform:uppercase;
  padding:.85em 1.5em .7em;border:1px solid var(--ink);color:var(--ink);background:transparent;
  cursor:pointer;min-height:44px;border-radius:6px;white-space:nowrap;
  transition:background .35s var(--ease),color .35s var(--ease),border-color .35s var(--ease)}
.hs-btn:hover{background:var(--ink);color:var(--snow)}
.hs-btn.solid{background:var(--cedar);border-color:var(--cedar);color:var(--snow)}
.hs-btn.solid:hover{background:var(--ink);border-color:var(--ink)}
.hs-btn.ghost{border-color:var(--line);color:var(--body)}
.hs-btn.ghost:hover{background:var(--ink);border-color:var(--ink);color:var(--snow)}
.hs-btn.light{border-color:var(--wline);color:var(--snow)}
.hs-btn.light:hover{background:var(--snow);color:var(--ink);border-color:var(--snow)}
.hs-btn[disabled]{opacity:.4;cursor:not-allowed}
.hs-btn[disabled]:hover{background:transparent;color:var(--ink);border-color:var(--line)}

/* ------------------------------------------------------- states & skeleton */

.hs-state{text-align:center;padding:clamp(2.5rem,7vw,4.5rem) 1.5rem;border:1px solid var(--line);
  background:var(--snow);border-radius:var(--hs-r)}
.hs-state .ico{font-size:1.9rem;line-height:1;margin-bottom:.9rem}
.hs-state h3{font-family:var(--bb);font-size:1.5rem;letter-spacing:.05em;text-transform:uppercase;
  color:var(--ink);margin-bottom:.6rem}
.hs-state p{color:var(--body);max-width:44ch;margin:0 auto;line-height:1.75;font-size:.95rem}
.hs-state .acts{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin-top:1.5rem}

.hs-skel{border-radius:var(--hs-r);overflow:hidden}
.hs-skel .sk{background:linear-gradient(90deg,var(--snow2) 25%,#e6e3da 37%,var(--snow2) 63%);
  background-size:400% 100%;animation:hsShim 1.4s ease infinite}
.hs-skel .sk.photo{aspect-ratio:4/3;border-radius:var(--hs-r)}
.hs-skel .sk.bar{height:11px;margin:.6rem .25rem;border-radius:3px}
.hs-skel .sk.bar.w40{width:40%}.hs-skel .sk.bar.w70{width:70%}.hs-skel .sk.bar.w55{width:55%}
@keyframes hsShim{0%{background-position:100% 0}100%{background-position:-100% 0}}
@media(prefers-reduced-motion:reduce){.hs-skel .sk{animation:none}}

/* --------------------------------------------------------------- map bits */

/* Google's own controls, toned down. The stock Map/Satellite switch and zoom
   stack are chunky, square and loud against a quiet page, so they are scaled
   back and rounded to sit with the rest of the panel furniture. These target
   Google's internal class names, which is why each rule is defensive: if a
   Maps release renames one, the control simply reverts to its default look
   rather than disappearing. */
.hs-map .gm-style-mtc>button{
  height:30px!important;min-width:0!important;padding:0 .8em!important;
  font-family:var(--ns)!important;font-size:.76rem!important;font-weight:600!important;
  letter-spacing:.02em!important;color:var(--ink)!important;
  border:none!important;box-shadow:none!important}
.hs-map .gm-style-mtc:first-of-type>button{border-radius:7px 0 0 7px!important}
.hs-map .gm-style-mtc:last-of-type>button{border-radius:0 7px 7px 0!important}
/* The control group's own wrapper carries the outline and the shadow. */
.hs-map .gm-style-mtc{box-shadow:none!important}
.hs-map div[role="menubar"]{border-radius:8px!important;overflow:hidden!important;
  box-shadow:0 2px 8px rgba(19,28,36,.16)!important}

/* Zoom: a compact rounded pair rather than the tall default stack.

   Maps writes the housing's size inline — width:40px;height:81px, being two
   40px buttons and a hairline. Shrinking the buttons to 30 without releasing
   that height left 20px of blank white hanging below the minus, which is what
   the control looked like: a long empty pill. height:auto lets the box close
   up around the buttons. The hairline carries an inline width:30px and 5px
   side margins, sized for the wider box, so it is unset the same way. */
.hs-map .gm-bundled-control .gmnoprint>div{
  border-radius:8px!important;box-shadow:0 2px 8px rgba(19,28,36,.16)!important;
  width:30px!important;height:auto!important;overflow:hidden!important}
.hs-map .gm-bundled-control .gmnoprint>div>div{width:auto!important;margin:0 6px!important}
/* The button holds four images — one per control state (rest, hover, active,
   disabled) — stacked exactly on top of each other with Maps' own
   `position:absolute`. Idle, only one has any size; on hover Maps brings a
   second to size too and relies on the stack to keep them on top of each
   other, telling them apart by colour rather than by hiding one.

   flex + position:static was tried here first and centred the idle icon
   correctly, but it also pulled every sized image into normal flow — so the
   moment a hover state added a second one, they landed side by side as two
   pluses instead of one. Keeping position:absolute and centring with
   inset:0;margin:auto preserves the stack: however many images are sized at
   once, they still sit exactly on top of each other. */
.hs-map .gm-bundled-control button{width:30px!important;height:30px!important;
  padding:0!important}
.hs-map .gm-bundled-control button img{width:14px!important;height:14px!important;
  margin:auto!important;position:absolute!important;inset:0!important;
  transform:none!important}

/* Price pills. Positioned by the OverlayView, so the transform does the
   centring rather than a margin. */
.hs-pin{position:absolute;transform:translate(-50%,-100%);background:var(--pine);color:var(--snow);
  font-family:var(--bb);font-size:.8rem;letter-spacing:.04em;padding:.36em .6em .26em;
  border-radius:6px;white-space:nowrap;border:1px solid rgba(247,246,242,.22);cursor:pointer;
  box-shadow:0 2px 8px rgba(19,28,36,.34);transition:background .2s var(--ease),transform .2s var(--ease)}
.hs-pin:hover,.hs-pin.is-active{background:var(--cedar);transform:translate(-50%,-100%) scale(1.08);
  z-index:3}

/* ==========================================================================
   Listing detail
   ========================================================================== */

/* One height for the photo band, whatever the listing has in it: eight shots,
   two, one, or none. The reference does the same thing — its gallery is a
   fixed 296px on a phone and 360px above that, and every photo inside it is
   cropped to fit rather than allowed to set the height. That is what keeps the
   price, the address and the facts on the first screen on every listing
   instead of only the ones that happen to have landscape photography. */
.ld{max-width:1280px;margin:0 auto;padding:0 clamp(1.1rem,4vw,3.6rem);
  --gal-h:clamp(300px,29vw,400px)}

.ld-crumbs{display:flex;align-items:center;justify-content:space-between;gap:1rem;
  border-bottom:1px solid var(--line);padding:.9rem 0;flex-wrap:wrap;
  position:sticky;top:var(--hs-stick);background:var(--snow);z-index:55}
.ld-crumbs .path{display:flex;align-items:center;gap:.55rem;font-size:.84rem;color:var(--body);
  min-width:0}
.ld-crumbs .path a{color:var(--cedar)}
.ld-crumbs .path a:hover{text-decoration:underline}
.ld-crumbs .path .cur{color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ld-jump{display:flex;gap:1.2rem}
.ld-jump a{font-size:.7rem;letter-spacing:.18em;text-transform:uppercase;color:var(--body)}
.ld-jump a:hover{color:var(--cedar)}

/* -------- gallery mosaic: one lead frame plus a 2x2 of supporting shots ---- */

/* 2:1:1 with 16/9 cells, which is what the reference does: the mosaic lands
   around 360px instead of 500, so price, address and the agent card are all
   above the fold on a laptop. */
.ld-gal{display:grid;grid-template-columns:2fr 1fr 1fr;grid-template-rows:1fr 1fr;
  gap:.5rem;margin-top:1.3rem;position:relative;height:var(--gal-h)}
.ld-gal .g{position:relative;overflow:hidden;background:var(--snow2);cursor:zoom-in;
  border:none;padding:0;display:block;width:100%;height:100%}
.ld-gal .g img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .7s var(--ease)}
.ld-gal .g:hover img{transform:scale(1.04)}
/* Nothing inside the mosaic sets its height any more — the band does, and the
   tracks divide it. Aspect ratios on the cells would fight that and win. */
.ld-gal .g.lead{grid-row:1/3;aspect-ratio:auto;min-height:0;
  border-radius:var(--hs-r) 0 0 var(--hs-r)}
.ld-gal .g.sm{aspect-ratio:auto;min-height:0}
/* Only the outer corners round, so the mosaic reads as one panel. */
.ld-gal .g.sm:nth-of-type(3){border-radius:0 var(--hs-r) 0 0}
.ld-gal .g.sm:nth-of-type(5){border-radius:0 0 var(--hs-r) 0}
.ld-gal.one .g.lead,.ld-gal.few .g.lead{border-radius:var(--hs-r) 0 0 var(--hs-r)}
.ld-gal.one .g.lead{border-radius:var(--hs-r)}

.ld-allshots{position:absolute;right:.85rem;bottom:.85rem;background:var(--snow);
  border:1px solid var(--ink);color:var(--ink);font-family:var(--bb);font-size:.78rem;
  letter-spacing:.13em;text-transform:uppercase;padding:.7em 1.2em .55em;cursor:pointer;
  border-radius:6px;box-shadow:0 3px 12px rgba(19,28,36,.2)}
.ld-allshots:hover{background:var(--ink);color:var(--snow)}

/* Share sits over the photo panel, top right, as on the reference. */
.ld-galov{position:absolute;top:.85rem;right:.85rem;display:flex;gap:.5rem;z-index:3}
.ld-galov button{width:42px;height:42px;border-radius:50%;border:none;cursor:pointer;
  background:rgba(19,28,36,.62);color:var(--snow);display:grid;place-items:center;
  backdrop-filter:blur(4px);transition:background .3s var(--ease),transform .3s var(--ease)}
.ld-galov button:hover{background:var(--cedar);transform:scale(1.07)}
.ld-galov button svg{width:18px;height:18px}

/* One photo.

   The mosaic's height normally comes from the 2x2 of supporting shots. With a
   single photo there is nothing beside the lead to bound it, so the row was
   auto and the frame took the photo's own proportions at full column width —
   a 768x1024 portrait came out 1,553px tall and pushed the price, the facts
   and the agent card entirely below the fold.

   The frame is given the height the full mosaic would have had, and the photo
   is contained rather than cropped inside it: when a listing has exactly one
   picture, cutting the top and bottom off it is the one thing not to do. The
   space either side is filled with a blurred copy of the same shot, passed in
   as --shot by gallery() in homesearch.js. */
.ld-gal.one{grid-template-columns:1fr;grid-template-rows:1fr}
.ld-gal.one .g.lead{grid-row:auto;aspect-ratio:auto;min-height:0;height:100%;
  background-image:var(--shot);background-size:cover;background-position:center}
.ld-gal.one .g.lead::before{content:"";position:absolute;inset:0;
  backdrop-filter:blur(30px) saturate(.65);background:rgba(19,28,36,.28)}
.ld-gal.one .g.lead img{position:relative;object-fit:contain}
/* The zoom on hover would show the blurred fill sliding under a still photo. */
.ld-gal.one .g.lead:hover img{transform:none}
/* --gal-rows is set inline from the photo count — see gallery(). */
.ld-gal.few{grid-template-columns:1.55fr 1fr;align-items:stretch;
  grid-template-rows:repeat(var(--gal-rows,2),1fr)}
.ld-gal.few .g.lead{grid-row:1/-1}
.ld-gal.few .g.sm{aspect-ratio:auto;min-height:0}
/* Positional selectors here have to count from the lead, not from .g.sm:
   :first-of-type / :last-of-type match on the element name, and the siblings
   include the lead button and the "All N Photos" button — so :last-of-type is
   always the latter and never a photo. The smN class carries the count. */
.ld-gal.few .g.sm:nth-of-type(2){border-radius:0 var(--hs-r) 0 0}
.ld-gal.few.sm1 .g.sm:nth-of-type(2){border-radius:0 var(--hs-r) var(--hs-r) 0}
.ld-gal.few.sm2 .g.sm:nth-of-type(3){border-radius:0 0 var(--hs-r) 0}
.ld-gal.few.sm3 .g.sm:nth-of-type(4){border-radius:0 0 var(--hs-r) 0}

/* A listing with no photos gets the same band as one with eight — 999 N
   Pacific #E212 has none, and a 16/7 panel put a full screen of empty grey
   above the price. */
.ld-nogal{margin-top:1.3rem;height:var(--gal-h);background:var(--snow2);border:1px solid var(--line);
  border-radius:var(--hs-r);position:relative;
  display:flex;align-items:center;justify-content:center;color:var(--fog);
  letter-spacing:.16em;text-transform:uppercase;font-size:.8rem}

/* --------------------------------------------------------- detail columns */

.ld-cols{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:clamp(1.6rem,3.6vw,3.2rem);
  align-items:start;margin-top:1.8rem;padding-bottom:4rem}
.ld-mainc{min-width:0}

.ld-quick{display:flex;gap:1.5rem;flex-wrap:wrap;color:var(--body);font-size:.95rem}
.ld-quick b{color:var(--ink);font-weight:600}
.ld-price{font-family:var(--bb);font-size:clamp(2.4rem,5.4vw,3.4rem);letter-spacing:.02em;
  color:var(--ink);line-height:1;margin-top:.8rem}
.ld-addr{font-size:1.05rem;color:var(--ink);margin-top:.5rem;line-height:1.6}

.ld-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.7rem;
  margin-top:1.6rem}
.ld-fact{border:1px solid var(--line);border-radius:8px;padding:.9rem 1rem;background:var(--snow)}
.ld-fact .k{font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;color:var(--fog)}
.ld-fact .v{font-family:var(--bb);font-size:1.3rem;letter-spacing:.03em;color:var(--ink);
  margin-top:.35rem;line-height:1}

.ld-sec{margin-top:clamp(2.2rem,4.5vw,3.2rem);scroll-margin-top:calc(var(--hs-top) + 60px)}
.ld-sec h2{font-family:var(--bb);font-size:clamp(1.5rem,2.6vw,1.9rem);letter-spacing:.05em;
  text-transform:uppercase;color:var(--ink);padding-bottom:.7rem;border-bottom:1px solid var(--line)}

.ld-meta{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;margin-top:1.1rem}
.ld-stat{font-family:var(--bb);font-size:.76rem;letter-spacing:.13em;text-transform:uppercase;
  background:var(--pine);color:var(--snow);padding:.42em .85em .3em;border-radius:6px}
.ld-stat.soon{background:var(--cedar)}
.ld-stat.pend{background:var(--body)}
.ld-meta .m{font-size:.84rem;color:var(--body)}

.ld-remarks{color:var(--body);line-height:1.85;margin-top:1.1rem;font-size:1rem}
.ld-remarks.clip{max-height:11.2em;overflow:hidden;
  -webkit-mask-image:linear-gradient(180deg,#000 65%,transparent);
  mask-image:linear-gradient(180deg,#000 65%,transparent)}
.ld-more{margin-top:.9rem}

/* Interior / Exterior / Other, so a long feature table reads as three short
   ones instead of forty undifferentiated rows. */
.ld-grp{font-family:var(--bb);font-size:1rem;letter-spacing:.16em;text-transform:uppercase;
  font-weight:400;color:var(--cedar);margin-top:2rem;padding-bottom:.6rem;
  border-bottom:1px solid var(--line)}
.ld-grp:first-of-type{margin-top:1.2rem}
.ld-dl{margin-top:1.1rem}
.ld-dl .row{display:grid;grid-template-columns:minmax(140px,30%) 1fr;gap:1.2rem;
  padding:.8rem 0;border-bottom:1px solid var(--line2)}
.ld-dl .row:last-child{border-bottom:none}
.ld-dl dt{font-size:.88rem;color:var(--fog)}
.ld-dl dd{font-size:.92rem;color:var(--ink);font-weight:600;line-height:1.6;word-break:break-word}

.ld-mapbox{height:clamp(240px,34vw,360px);border:1px solid var(--line);border-radius:var(--hs-r);
  margin-top:1.1rem;background:var(--snow2);overflow:hidden}
.ld-copy{margin-top:.9rem}

/* --------------------------------------------------------------- side card */

.ld-side{position:sticky;top:calc(var(--hs-top) + 12px)}
.ld-agent{border:1px solid var(--line);border-radius:var(--hs-r);background:var(--snow);padding:1.3rem}
.ld-agent .who{display:flex;align-items:center;gap:.85rem}
.ld-agent .who img{width:56px;height:56px;border-radius:50%;object-fit:cover;
  object-position:center 22%;background:var(--snow2);flex:none}
.ld-agent .who .nm{font-family:var(--bb);font-size:1.1rem;letter-spacing:.05em;color:var(--ink);
  line-height:1.1}
.ld-agent .who .ro{font-size:.8rem;color:var(--body);margin-top:.2rem;line-height:1.4}
.ld-acts{display:flex;flex-direction:column;gap:.6rem;margin-top:1.15rem}
.ld-acts .hs-btn{width:100%;text-align:center;justify-content:center}
.ld-callrow{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--line2);
  font-size:.84rem;color:var(--body);text-align:center}
.ld-callrow a{color:var(--cedar)}
.ld-callrow a:hover{text-decoration:underline}

/* ------------------------------------------------- phone contact popover */

/* Desktop keeps the sidebar card; this is its phone form, switched on in the
   960px block. Namespaced ld-fab rather than reusing the homepage's .fab
   classes from styles.css — that file owns .fab/.fabcard/.fabbtn globally and
   inheriting them would drag in the hero's entry animation and layout. */
.ld-fab{display:none;position:fixed;right:1rem;bottom:1rem;z-index:120;
  flex-direction:column;align-items:flex-end;gap:.8rem;pointer-events:none}
.ld-fabbtn{width:60px;height:60px;border-radius:50%;padding:0;flex:none;
  border:2px solid var(--cedar);background:var(--pine);overflow:hidden;cursor:pointer;
  box-shadow:0 10px 30px -8px rgba(6,12,18,.55);pointer-events:auto;
  transition:transform .35s var(--ease)}
.ld-fabbtn img{width:100%;height:100%;object-fit:cover;object-position:center 22%;display:block}
.ld-fab.open .ld-fabbtn{transform:scale(.9)}
.ld-fabcard{width:320px;max-width:calc(100vw - 2rem);transform-origin:bottom right;
  transform:scale(.92) translateY(10px);opacity:0;pointer-events:none;position:relative;
  transition:transform .32s var(--ease),opacity .32s var(--ease)}
.ld-fab.open .ld-fabcard{transform:none;opacity:1;pointer-events:auto}
.ld-fabcard .ld-agent{box-shadow:0 24px 60px -18px rgba(6,12,18,.5)}
.ld-fabx{position:absolute;top:.6rem;right:.6rem;width:30px;height:30px;border-radius:50%;
  border:none;background:var(--pine);color:var(--snow);font-size:1.1rem;line-height:1;
  display:flex;align-items:center;justify-content:center;cursor:pointer}
.ld-fabx:hover{background:var(--cedar)}
/* The hamburger drawer sits above everything; the puck must not float over it. */
body.menuopen .ld-fab{opacity:0;visibility:hidden}

/* ------------------------------------------------------------- compliance */

/* IDX display requirements. These are not decoration and must not be hidden:
   the listing-brokerage courtesy line, the MLS disclaimer, the attribution
   contact and the data-refresh stamp are all conditions of the feed licence. */
.ld-legal{border-top:1px solid var(--line);margin-top:2.6rem;padding:1.4rem 0 0;
  font-size:.76rem;line-height:1.7;color:var(--fog)}
.ld-legal .courtesy{color:var(--body);font-weight:600;margin-bottom:.6rem;font-size:.82rem}
.ld-legal p{margin-bottom:.6rem;max-width:80ch}
.ld-legal .stamp{color:var(--fog)}
.hs-legal{max-width:1600px;margin:0 auto;padding:1.6rem clamp(1.1rem,4vw,3.6rem) 3rem;
  border-top:1px solid var(--line);font-size:.74rem;line-height:1.7;color:var(--fog)}
.hs-legal p{margin-bottom:.55rem;max-width:90ch}

/* ==========================================================================
   Modals — tour wizard, contact, share, all filters
   ========================================================================== */

/* height wins over the `bottom` in inset:0 — when top, bottom and height are
   all set, bottom is the one dropped — so this is top:0 plus the height of
   whatever is actually visible. Falls back to the full window where
   visualViewport is not available. See syncVisualViewport in homesearch.js. */
.hs-modal{position:fixed;inset:0;height:var(--hs-vvh,100%);
  z-index:400;display:none;align-items:center;justify-content:center;
  padding:1.2rem;overscroll-behavior:contain}
.hs-modal.open{display:flex}
.hs-modal .veil{position:absolute;inset:0;background:rgba(10,16,22,.62);backdrop-filter:blur(2px)}
.hs-dialog{position:relative;background:var(--snow);border-radius:var(--hs-r);
  /* 90% of the modal, which is now the visible viewport, rather than 90vh —
     vh does not shrink for the keyboard. */
  width:min(560px,100%);max-height:min(90%,860px);display:flex;flex-direction:column;
  box-shadow:0 30px 70px rgba(10,16,22,.34);overflow:hidden}
.hs-dialog.wide{width:min(720px,100%)}
/* A class, not a bare <header> element. styles.css styles `header` globally as
   the site's fixed nav bar (position:fixed; width:100%), which a nested
   <header> inside the dialog inherits — that is what made the modal's title
   bar stretch across the whole viewport instead of sitting on the dialog. */
.hs-dlghd{background:var(--pine);color:var(--snow);flex:none;position:relative;width:auto;
  padding:1.05rem 1.3rem;display:flex;align-items:center;justify-content:space-between;gap:1rem}
.hs-dlghd h3{font-family:var(--bb);font-size:1.25rem;letter-spacing:.09em;
  text-transform:uppercase;color:var(--snow);margin:0}
.hs-x{background:none;border:none;color:var(--snow);font-size:1.5rem;line-height:1;cursor:pointer;
  padding:.1em .3em;opacity:.75}
.hs-x:hover{opacity:1}
/* overscroll-behavior:contain stops a flick that runs past the end of this
   scroller from handing the rest of the gesture to the page behind the
   dialog — the other half of the scroll lock in homesearch.js, and the half
   that keeps the background still *during* the drag rather than putting it
   back afterwards. */
.hs-dialog .bd{padding:1.3rem;overflow:auto;flex:1 1 auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch}
.hs-dialog .ft{flex:none;border-top:1px solid var(--line);padding:.9rem 1.3rem;
  display:flex;align-items:center;justify-content:space-between;gap:.7rem;background:var(--snow)}
.hs-dialog .ft .grow{flex:1}

/* Property echo, so the form never asks "which home?". Stacked banner rather
   than a thumbnail-beside-text row, matching the reference share dialog: a
   wide photo carrying the status badge, then price, facts, full address with
   ZIP, and the MLS number. Same block in every modal. */
.hs-mini{border:1px solid var(--line);border-radius:8px;overflow:hidden;
  margin-bottom:1.2rem;background:var(--snow2)}
.hs-mini .hsm-ph{position:relative;display:block;aspect-ratio:16/7;background:var(--snow2)}
.hs-mini .hsm-ph img{width:100%;height:100%;object-fit:cover;display:block}
.hs-mini .hsm-ph .hs-badge{top:.6rem;left:.6rem}
.hs-mini .hsm-t{min-width:0;padding:.85rem .95rem .9rem}
.hs-mini .hsm-pp{font-family:var(--bb);font-size:1.35rem;letter-spacing:.03em;color:var(--ink);
  line-height:1}
.hs-mini .hsm-ff{font-size:.84rem;color:var(--body);margin-top:.4rem}
.hs-mini .hsm-aa{font-size:.86rem;color:var(--ink);margin-top:.3rem;line-height:1.45}
.hs-mini .hsm-mm{font-size:.74rem;color:var(--fog);margin-top:.25rem}

/* Who the message actually reaches — sits under the property echo on Contact. */
.hs-who{display:flex;align-items:center;gap:.8rem;margin-bottom:1.2rem;
  font-size:.82rem;line-height:1.5;color:var(--body)}
.hs-who img{width:46px;height:46px;border-radius:50%;object-fit:cover;
  object-position:center 22%;background:var(--snow2);flex:none}
.hs-who b{color:var(--ink);font-family:var(--bb);font-weight:400;font-size:1rem;
  letter-spacing:.05em}

.hs-row{display:grid;grid-template-columns:1fr 1fr;gap:.9rem}
.hs-modal .field{margin-bottom:1.1rem}
.hs-modal .field label{display:block;font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--fog);margin-bottom:.45rem}
.hs-modal .field input,.hs-modal .field select,.hs-modal .field textarea{width:100%;
  background:transparent;border:1px solid var(--line);border-radius:6px;padding:.7em .8em;
  font-family:var(--ns);font-size:.94rem;color:var(--ink);min-height:44px}
.hs-modal .field textarea{min-height:104px;resize:vertical;line-height:1.6}
.hs-modal .field input:focus,.hs-modal .field select:focus,.hs-modal .field textarea:focus{
  outline:none;border-color:var(--cedar)}
.hs-modal .field .err{color:#a4302a;font-size:.76rem;margin-top:.35rem;display:none}
.hs-modal .field.bad input,.hs-modal .field.bad select,.hs-modal .field.bad textarea{border-color:#a4302a}
.hs-modal .field.bad .err{display:block}
.hs-cnt{text-align:right;font-size:.72rem;color:var(--fog);margin-top:.3rem}
.hs-consent{font-size:.74rem;color:var(--fog);line-height:1.6;margin:-.3rem 0 1.1rem}
.hs-formmsg{padding:.8rem 1rem;border-radius:6px;font-size:.88rem;margin-bottom:1rem;display:none}
.hs-formmsg.show{display:block}
.hs-formmsg.bad{background:#fbeceb;color:#8d2822;border:1px solid #e6c3bf}

.hs-done{text-align:center;padding:1.6rem .4rem}
.hs-done .ico{font-size:2rem;margin-bottom:.7rem;color:var(--cedar)}
.hs-done h4{font-family:var(--bb);font-size:1.4rem;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink);margin-bottom:.5rem}
.hs-done p{color:var(--body);line-height:1.75;font-size:.94rem;max-width:38ch;margin:0 auto}
/* Which home the message was about, restated on the confirmation. */
.hs-doneprop{margin:1.3rem auto 0;padding:.9rem 1rem;max-width:34ch;background:var(--snow2);
  border:1px solid var(--line);border-radius:8px}
.hs-doneprop .hsm-pp{font-family:var(--bb);font-size:1.15rem;letter-spacing:.03em;color:var(--ink);
  line-height:1}
.hs-doneprop .hsm-aa{font-size:.85rem;color:var(--ink);margin-top:.3rem;line-height:1.45}
.hs-doneprop .hsm-mm{font-size:.73rem;color:var(--fog);margin-top:.25rem}

/* ---- tour wizard ---- */

.hs-wiz{display:grid;grid-template-columns:210px 1fr;gap:1.3rem}
.hs-wiz .ask{min-width:0}
/* In the narrow wizard rail the banner would be a letterbox slit, so the photo
   goes back to a normal 4:3 and the price steps down a size. */
.hs-wiz .prop .hs-mini{margin-bottom:0}
.hs-wiz .prop .hs-mini .hsm-ph{aspect-ratio:4/3}
.hs-wiz .prop .hs-mini .hsm-pp{font-size:1.1rem}
.hs-wiz .prop .hs-mini .hsm-t{padding:.7rem .75rem .75rem}
.hs-wiz .ask h4{font-family:var(--bb);font-size:1.2rem;letter-spacing:.05em;text-transform:uppercase;
  color:var(--ink)}
.hs-wiz .ask p.lead{color:var(--body);font-size:.9rem;line-height:1.65;margin:.4rem 0 1.2rem}

.hs-radios{display:flex;gap:1.3rem;margin-bottom:1.2rem}
.hs-radios label{display:inline-flex;align-items:center;gap:.5em;font-size:.92rem;color:var(--ink);
  cursor:pointer}
.hs-radios input{accent-color:var(--cedar);width:17px;height:17px;min-height:0}

/* calendar */
/* In flow, not floating. The dialog body is `overflow:auto`, so an absolutely
   positioned drop-down is clipped by it the moment the month grid is taller
   than the space left below the field — which it always was at the bottom of
   the form. In flow it simply pushes the rest of the form down and the body
   scrolls to it, which cannot overflow anything. */
.hs-cal{border:1px solid var(--line);border-radius:8px;padding:.7rem;background:var(--snow);
  margin-top:.45rem;width:100%;max-width:320px;box-sizing:border-box}
.hs-cal[hidden]{display:none}
.hs-calhd{display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem}
.hs-calhd b{font-family:var(--bb);font-size:1rem;letter-spacing:.06em;color:var(--ink);font-weight:400}
.hs-calhd button{background:none;border:1px solid var(--line);border-radius:6px;width:28px;height:28px;
  cursor:pointer;color:var(--ink);min-height:0;line-height:1}
.hs-calhd button:hover{background:var(--snow2)}
.hs-calhd button[disabled]{opacity:.3;cursor:not-allowed}
.hs-calgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;text-align:center}
.hs-calgrid .dow{font-size:.64rem;letter-spacing:.1em;text-transform:uppercase;color:var(--fog);
  padding:.35rem 0}
.hs-calgrid button{background:none;border:none;border-radius:6px;padding:.4rem 0;font-size:.84rem;
  color:var(--ink);cursor:pointer;min-height:0;aspect-ratio:1;display:grid;place-items:center}
.hs-calgrid button:hover:not([disabled]){background:var(--snow2)}
.hs-calgrid button[disabled]{color:#c9c6bd;cursor:not-allowed}
.hs-calgrid button.on{background:var(--cedar);color:var(--snow)}
.hs-calgrid button.today{font-weight:700}

.hs-slots{display:grid;grid-template-columns:1fr 1fr;gap:.45rem}
.hs-slots button{border:1px solid var(--line);border-radius:6px;background:var(--snow);
  font-family:var(--ns);font-size:.85rem;color:var(--ink);padding:.65em .5em;cursor:pointer;
  transition:border-color .25s var(--ease),background .25s var(--ease)}
.hs-slots button:hover{border-color:var(--fog)}
.hs-slots button.on{background:var(--pine);border-color:var(--pine);color:var(--snow)}

/* ---- all-filters sheet ---- */

.hs-fsec{border-bottom:1px solid var(--line2);padding:1.1rem 0}
.hs-fsec:first-child{padding-top:0}
.hs-fsec>button.hd{width:100%;display:flex;align-items:center;justify-content:space-between;
  background:none;border:none;cursor:pointer;padding:0;font-family:var(--bb);font-size:1.05rem;
  letter-spacing:.07em;text-transform:uppercase;color:var(--ink);min-height:0}
.hs-fsec>button.hd .cx{width:9px;height:9px;border-right:1.5px solid var(--fog);
  border-bottom:1.5px solid var(--fog);transform:rotate(45deg);transition:transform .25s var(--ease)}
.hs-fsec.closed>button.hd .cx{transform:rotate(-45deg)}
.hs-fsec .body{padding-top:.9rem}
.hs-fsec.closed .body{display:none}
.hs-fsearch{position:relative;margin-bottom:.5rem}
.hs-fsearch input{width:100%;border:1px solid var(--line);border-radius:6px;padding:.7em .8em;
  font-family:var(--ns);font-size:.9rem;min-height:44px;background:var(--snow2);color:var(--ink)}
.hs-fsearch input:focus{outline:none;border-color:var(--cedar);background:transparent}

/* ------------------------------------------------------------- lightbox */

.hs-lb{position:fixed;inset:0;z-index:500;background:rgba(8,13,18,.95);display:none;
  overscroll-behavior:contain;touch-action:none}
.hs-lb.open{display:block}
/* The frame comes last in the markup and every child here is positioned, so
   without a stacking order of its own the photo paints over the controls. On a
   desktop that went unnoticed — the frame's 5rem side inset left the arrows
   clear of it — but the inset collapses to ~6vw on a phone, which put the
   image on top of both arrows and swallowed the taps. The controls are given
   the higher layer explicitly rather than by reordering the markup: the close
   button is deliberately first in the DOM so it is the first thing reached by
   keyboard and by a screen reader. */
.hs-lb .frame{position:absolute;inset:clamp(2.6rem,7vw,4.5rem) clamp(1rem,6vw,5rem);z-index:1}
.hs-lb img{width:100%;height:100%;object-fit:contain}
.hs-lb .nav,.hs-lb .close,.hs-lb .idx{z-index:2}
/* Dark rather than the old translucent white: over a bright photo — and on a
   phone the arrows always sit over the photo, there is no margin to put them
   in — a 14%-white fill on a near-white sky is invisible. A dark scrim with a
   light rim reads against anything. */
.hs-lb .nav{position:absolute;top:50%;transform:translateY(-50%);background:rgba(8,13,18,.58);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  border:1px solid rgba(247,246,242,.42);color:var(--snow);font-size:1.5rem;line-height:1;
  width:48px;height:48px;cursor:pointer;border-radius:50%;
  display:grid;place-items:center;padding:0}
.hs-lb .nav:hover{background:rgba(8,13,18,.8);border-color:rgba(247,246,242,.7)}
.hs-lb .prev{left:clamp(.5rem,2vw,1.6rem)}
.hs-lb .next{right:clamp(.5rem,2vw,1.6rem)}
.hs-lb .close{position:absolute;top:1rem;right:clamp(1rem,3vw,2rem);background:none;border:none;
  color:var(--snow);font-size:2rem;line-height:1;cursor:pointer;opacity:.8}
.hs-lb .close:hover{opacity:1}
.hs-lb .idx{position:absolute;bottom:1.1rem;left:50%;transform:translateX(-50%);color:var(--snow);
  font-size:.82rem;letter-spacing:.1em;opacity:.85}

/* ==========================================================================
   Broken media

   A feed URL that 404s, times out, or decodes to nothing. The browser's own
   answer to that is a torn-page icon and the alt text, which on a card for a
   two-million-dollar house reads as a broken site rather than a listing with a
   bad photo. markBrokenPhoto() in homesearch.js strips the src and marks the
   box that owned it, so what shows instead is the same quiet placeholder a
   listing with no photos at all gets.

   One rule for every surface — card, gallery tile, modal mini and lightbox —
   because all four already give the photo a positioned box. It sits down here,
   after those components, on purpose: each of them styles its own `img` with a
   descendant selector of equal specificity, so source order is what decides.
   ========================================================================== */

.img-fail{background:var(--snow2)}
.img-fail img[data-broken]{display:none}
.img-fail::after{content:'Photo Unavailable';position:absolute;inset:0;
  display:grid;place-items:center;text-align:center;padding:.5rem;
  font-family:var(--bb);font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fog)}
/* The gallery tiles are zoom-in buttons; a dead one has nothing to zoom into. */
.ld-gal .g.img-fail{cursor:default}
.hs-lb .frame.img-fail{background:transparent}
.hs-lb .frame.img-fail::after{color:rgba(247,246,243,.65);font-size:.82rem}

/* ==========================================================================
   Responsive
   ========================================================================== */

@media(max-width:1200px){
  .hs-panel{padding:.75rem}
  .hs-pill>button,.hs-ghost{font-size:.84rem;padding:.6em .9em}
}

@media(max-width:1120px){
  .hs-body{grid-template-columns:minmax(0,1fr) minmax(0,42%)}
  /* The third column stops fitting a photo and a price line here. */
  .hs-body.list-only .hs-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .ld-cols{grid-template-columns:minmax(0,1fr) 300px}
}

@media(max-width:960px){
  /* Below this the split stops earning its width: the map becomes a toggled
     full-width view rather than a squeezed column beside two-across cards.
     The list is the default here — see the `view` default in homesearch.js.

     The overlay stops below the filter panel and sits under it in the stack
     (z-index 50 against the panel's 60) so the toggle that opened the map is
     still reachable to close it again. */
  /* The pills wrap onto their own row in this band, so the panel is taller
     than the desktop default the map height is calculated against. */
  :root{--hs-bar:194px}
  .hs-body{display:block;padding-bottom:3rem}
  /* The map takes the place the results occupied, in normal flow. An earlier
     attempt pinned it with position:fixed under the sticky filter bar, which
     needed a measured offset and a scroll lock and still disagreed with itself
     about where the bar was. Staying in flow means the browser does that work:
     the map lands under the bar because it comes after it. */
  /* 1.2rem is this element's own top margin — the only thing between the
     bottom of the sticky bar and the top of the map. --hs-bar is measured
     from the real element in syncBar(), so no fudge factor is needed. */
  /* relative, not static, so the pin card can be absolutely positioned inside
     it — and `top:auto` with it, because the desktop rule sets a sticky
     offset of stick+bar+1rem. position:static ignores `top`; position:relative
     honours it, so switching pushed the map 211px down the page and out
     through the bottom of the fold. */
  .hs-mapwrap{position:relative;top:auto;
    height:calc(100dvh - var(--hs-stick) - var(--hs-bar) - 1.2rem);
    min-height:340px;margin:1.2rem 0 0;display:none}
  .hs-body.map-open .hs-mapwrap{display:block}
  /* Map mode is the map. A permanent strip of cards along the bottom was
     tried and read as clutter — floating listings with no relationship to
     what the visitor was looking at. Tapping a pin raises that one home
     instead; see .hs-mapcard. */
  .hs-body.map-open .hs-results{display:none}
  /* Map mode fills the fold, and the IDX legal block and the footer follow it
     in normal flow for anyone who scrolls past. Neither is hidden — the feed
     licence requires the disclaimer and attribution to be displayed, and the
     footer carries the brokerage details and DRE notices. */
  .hs-results{padding-top:1.2rem}

  .ld-cols{display:flex;flex-direction:column}
  /* The inline agent card cost a whole screen above the photos on a phone, so
     it becomes the floating puck instead — always reachable, never in the way.
     Search results keep no puck: it belongs to a specific home. */
  .ld-side{display:none}
  .ld-fab{display:flex}
  /* One lead across the top with the supporting shots in a row beneath. The
     .one/.few variants have to be named explicitly: they are more specific
     than a bare .ld-gal, so listing them here is what actually collapses the
     side-by-side desktop tracks. */
  .ld{--gal-h:clamp(260px,56vw,340px)}
  .ld-gal,.ld-gal.one,.ld-gal.few{grid-template-columns:1fr 1fr;
    grid-template-rows:1.9fr 1fr}
  /* Nothing left in the row track for the lead to span. */
  .ld-gal.one{grid-template-rows:1fr}
  .ld-gal .g.lead,.ld-gal.few .g.lead{grid-row:auto;grid-column:1/3;min-height:0;
    aspect-ratio:auto;border-radius:var(--hs-r) var(--hs-r) 0 0}
  .ld-gal .g.sm,.ld-gal.few .g.sm{aspect-ratio:auto;min-height:0;border-radius:0}
  /* Three photos is the whole grid at this width: the lead and two supporting
     shots. Four thumbnails under the lead came out around 160px wide each on a
     phone, which is a contact sheet rather than a look at the house. The rest
     of the set is not lost — "All N Photos" still opens every one.

     nth-of-type counts the lead as 1, so the two that stay are 2 and 3 and
     anything from 4 on is dropped. It counts buttons, not .g.sm elements —
     the "All N Photos" control is a sibling button too, which is why these are
     written positionally rather than as :last-of-type. */
  .ld-gal .g.sm:nth-of-type(n+4){display:none}
  .ld-gal .g.sm:nth-of-type(2){border-radius:0 0 0 var(--hs-r)}
  .ld-gal .g.sm:nth-of-type(3){border-radius:0 0 var(--hs-r) 0}
  /* A listing with exactly one supporting shot would leave a hole beside it,
     so that one runs the full width instead. */
  .ld-gal.sm1 .g.sm:nth-of-type(2){grid-column:1/3;aspect-ratio:auto;
    border-radius:0 0 var(--hs-r) var(--hs-r)}
  .ld-jump{display:none}
  .hs-wiz{grid-template-columns:1fr}
  .hs-wiz .prop .hs-mini{margin-bottom:.2rem}
  .hs-wiz .prop .hs-mini .hsm-ph{aspect-ratio:16/7}
}

@media(max-width:760px){
  /* On a phone the panel is only the search field and All Filters. The pill
     row is dropped rather than made scrollable: every pill's control already
     lives inside the All Filters sheet, so a sideways-scrolling duplicate of
     it just costs a third of the fold. The List/Map switch stays — it is a
     view control, not a filter, and without it the map is unreachable. */
  /* Two rows now, not three — the map height calc reads this. */
  :root{--hs-bar:140px}
  .hs-panel{flex-wrap:wrap;gap:.55rem}
  .hs-q{flex:1 1 100%}
  .hs-pills{display:none}
  .hs-right{margin-left:0;width:100%;justify-content:space-between;gap:.45rem}
  .hs-right .hs-ghost{flex:1;justify-content:center;min-height:42px;font-size:.86rem}
  .hs-go{display:none}
  /* 42px to match the All Filters button it shares this row with. At 34 it was
     both under the touch minimum and visibly shorter than its neighbour, and
     since the row's height is already set by that 42px button, matching it
     costs the bar nothing — the fold is unchanged. */
  .hs-seg button{min-height:42px;padding:.45em 1.05em;font-size:.84rem}
  .hs-pop{position:fixed;left:1rem;right:1rem;top:auto;bottom:1rem;min-width:0;
    max-height:70vh;overflow:auto}
}

@media(max-width:640px){
  :root{--hs-top:76px;--hs-stick:56px;--hs-bar:138px}
  .hs-filters{padding:.7rem 1.1rem}
  .hs-panel{padding:.7rem;gap:.5rem}
  /* The reference puts the search bar at the top of the phone fold. The hero
     still earns its place for SEO and orientation, but not at full height. */
  .hs-head{padding-top:1.1rem}
  .hs-head h1{font-size:clamp(1.5rem,6.4vw,1.9rem)}
  .hs-head .hs-sub{font-size:.88rem;line-height:1.55;margin-top:.4rem}
  .hs-filters{margin-top:.9rem}
  /* One card per row on a phone. .hs-body.list-only .hs-grid is the more
     specific selector, so it has to be named here or the list view would stay
     two across at 360px.

     minmax(0,1fr), not a bare 1fr: a grid track's default minimum width is
     its content's min-content size, not 0. A bare 1fr let a card's own
     content set the column wider than the viewport whenever something inside
     it couldn't shrink — the courtesy line or address — so the card
     overflowed off the right edge instead of wrapping, and the page picked up
     a few pixels of horizontal scroll along with it. That read as the whole
     list sitting off-center: flush right, with the gap only showing on the
     left. */
  .hs-grid,.hs-body.list-only .hs-grid{grid-template-columns:minmax(0,1fr);gap:1rem}
  /* Count and sort share one line, as on the reference — stacked they cost a
     whole card's worth of scroll before the first listing. */
  .hs-count{margin-bottom:.9rem;flex-wrap:nowrap;align-items:center}
  .hs-count .n{font-size:1rem;white-space:nowrap}
  .hs-count .hs-sortwrap span{display:none}
  .hs-count select{padding:.4em 1.8em .4em .6em;font-size:.84rem}
  .hs-tiles{grid-template-columns:repeat(2,1fr)}
  .hs-legal{padding:1.4rem 1.1rem 2.4rem}

  /* Phone cards: a wider photo and a slightly larger price, since there is
     only one card per row and nothing to keep it consistent with. */
  .hs-photo{aspect-ratio:3/2}
  .hs-price{font-size:1.45rem}
  .hs-addr{font-size:.9rem}
  .hs-grid{grid-auto-rows:auto}

  /* The supporting shots used to be display:none here, which left a phone
     showing one photo of the three the listing actually has. They stay — the
     two-across row is still legible at 320px. */
  .ld-gal,.ld-gal.one,.ld-gal.few{gap:.35rem}
  .ld-gal.one .g.lead{grid-column:1/3;border-radius:var(--hs-r)}
  .ld-allshots{right:.5rem;bottom:.5rem;font-size:.68rem;padding:.6em .9em .45em}
  .ld-galov{top:.5rem;right:.5rem}
  .ld-galov button{width:36px;height:36px}
  .ld-facts{grid-template-columns:1fr 1fr}
  .ld-dl .row{grid-template-columns:1fr;gap:.2rem;padding:.7rem 0}
  .hs-row{grid-template-columns:1fr}
  .hs-modal{padding:.7rem}
  /* % of the modal, not vh — the modal is sized to the visible viewport, so
     this shrinks with the keyboard where a vh never would. */
  .hs-dialog{max-height:94%}
  .hs-dialog .bd{padding:1rem}
  .hs-dialog .ft{padding:.8rem 1rem}
  .hs-dlghd{padding:.9rem 1rem}
  .hs-dlghd h3{font-size:1.1rem}
  /* This used to be white-space:normal, which let a long address wrap and
     squeeze "Home Search" into two words on two lines with the separator
     stranded between them. One line, ellipsised — the full address is in the
     h1 immediately below, so nothing is lost by truncating it here. */
  .ld-crumbs .path{gap:.4rem;font-size:.8rem;flex-wrap:nowrap}
  .ld-crumbs .path a{white-space:nowrap;flex:none}
  .ld-crumbs .path .cur{white-space:nowrap;font-size:.8rem;min-width:0}
  .hs-slots{grid-template-columns:1fr}
}

/* Phone landscape. There is barely a fold here: header plus filter bar take
   more than half of it, so the map's 340px floor and a full-height card strip
   between them overflowed the viewport by 142px. Everything gives a little —
   the map takes whatever JS measures as left over, and the strip drops to a
   photo with a price on it. */
@media(max-height:520px){
  .hs-dialog{max-height:96%}
  .hs-mapwrap{min-height:0}
  /* The raised card would cover the whole map in landscape, so it drops the
     photo and keeps the facts. */
  .hs-mapcard .hs-photo{display:none}
  .hs-mapcard{max-width:320px;left:auto}
}

/* ==========================================================================
   iOS focus zoom

   Safari and Chrome on iOS zoom the page in when a text field smaller than
   16px takes focus. Nothing here was: the panel field and every input in the
   sheet sit at 14.4px, the sort select at 13.44px. That one threshold caused
   both of the reported faults, which is why they only ever showed on a phone:

     - the zoom makes the layout wider than the window, so the page can now be
       panned sideways — the "scrolling behind the viewport" horizontally
     - the zoom plus the keyboard pushes the caret out of the visible area, so
       the field being typed into is off-screen

   16px is the threshold, not a preference; a hair under still zooms. Applied
   on coarse pointers only, so the desktop keeps the smaller, tidier control
   sizes the design was drawn at. Listed explicitly rather than as a bare
   `input` so each rule out-specifies the one it corrects.
   ========================================================================== */

@media(pointer:coarse){
  .hs-q input,
  .hs-range input,
  .hs-fsearch input,
  .hs-count select,
  .hs-modal .field input,
  .hs-modal .field select,
  .hs-modal .field textarea{font-size:16px}
}
