/* ============================================================================
   FGR site styles — matches the current RealHomes design.

   Loads after tokens.css, which defines every colour and the 62.5% root size.
   Plain CSS: no framework, no build step, in keeping with how this company
   already builds sites (see FG-Website/CLAUDE.md).
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }

/* The browser's own `[hidden] { display: none }` lives in the UA stylesheet,
   and ANY author rule outranks it regardless of specificity. `.prop-card`
   below sets display:flex, which silently defeats el.hidden = true and shows
   every filtered-out card. This restores it. */
[hidden] { display: none !important; }

.container {
    width: 100%;
    max-width: var(--fg-container);
    margin-inline: auto;
    padding-inline: var(--fg-gutter);
}
/* The theme is mobile-first min-width and steps the gutter four times. Without
   these the content ran edge-to-edge on a wide screen and too tight on a small
   one — the rebuild only had two max-width breakpoints. */
@media (min-width: 480px)  { .container { padding-inline: 3rem; } .prop-card__body { padding: 2.5rem; } }
@media (min-width: 1140px) { .container { padding-inline: 7rem; } }
@media (min-width: 1280px) { .container { padding-inline: 10rem; } }

/* The page's vertical rhythm, owned in one place. Previously the only vertical
   space on a content page came from the footer's margin-top, so every page had
   breathing room below its content and none above it. */
main > .container { padding-block: var(--fg-space-xl); }

.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden; clip-path: inset(50%);
    white-space: nowrap;
}

/* Keyboard users must be able to see where they are. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
    outline: 3px solid var(--fg-primary);
    outline-offset: 2px;
    border-radius: 2px;
}

.skip-link {
    position: absolute;
    left: var(--fg-space-md);
    top: -100px;
    z-index: 1000;
    padding: var(--fg-space-sm) var(--fg-space-md);
    background: var(--fg-primary);
    color: #fff;
    transition: top .15s ease-out;
}
.skip-link:focus { top: var(--fg-space-md); color: #fff; }

/* ------------------------------------------------------------------ header */
.site-header {
    background: var(--fg-header-bg);
    color: var(--fg-on-dark);
    position: relative;
    z-index: 40;
}
/* Live's .rh_header__wrap is 1280x140 with 20px of padding, sitting inside the
   1440 viewport at x=80 -- a WIDER box than the 1240px .container the page
   sections use, which is why the header is deliberately not a .container. */
.site-header__inner {
    display: flex;
    align-items: center;
    /* No flex gap: live butts the phone block straight against the menu, and
       .main-nav's margin-left:auto already separates them from the logo. A
       gap here pushed the whole menu 24px left of live's position. */
    gap: 0;
    min-height: 14rem;
    max-width: 128rem;
    margin-inline: auto;
    padding: 2rem;
}
.site-logo { display: flex; align-items: center; gap: var(--fg-space-sm); }
.site-logo img { height: 10rem; width: auto; }
.site-logo__text { color: #fff; font-size: 2.2rem; font-weight: 700; line-height: 1.05; }
.site-logo__text small { display: block; font-size: 1.2rem; font-weight: 400; opacity: .85; }

.main-nav { margin-left: auto; }
.main-nav ul {
    /* Live's items are adjacent -- Home ends at 675, Properties starts at 675
       -- and the ul carries 15px of left padding. The 4px gap made our menu
       24px wider than live's and shifted every link. */
    display: flex; align-items: center; gap: 0;
    list-style: none; margin: 0; padding: 0 0 0 1.5rem;
}
.main-nav a {
    display: block;
    /* Live: 13px 17px, giving a 50px-tall link. Ours was 10px 14px / 44px. */
    padding: 1.3rem 1.7rem;
    color: #fff;
    font-size: 1.5rem;
    border-radius: var(--fg-radius);
    transition: background .15s ease-out, color .15s ease-out;
}
/* White-on-dark: the header is --fg-header-bg (#303030). Not a page
   surface -- must NOT follow the page/band swap. */
.main-nav a:hover { background: rgba(255, 255, 255, .12); color: #fff; }
/* aria-current is the source of truth; the colour is the visual echo of it. */
.main-nav a[aria-current="page"] { background: var(--fg-primary); color: #fff; }

/* Live's icon and link are two SEPARATE elements (.rh_menu__user_phone holds
   a bare <svg> then a.contact-number, not one wrapping the other) -- icon
   18x18 @1211, link 106x17 @1234, 23px apart. A single <a> wrapping both
   made the clickable box 144px wide instead of the text's own 106. */
.header-phone {
    display: inline-flex; align-items: center; gap: .5rem;
    white-space: nowrap;
    padding-left: 1.5rem;
}
.header-phone a {
    /* Live's a.contact-number is 14px at weight 500, not 15px at 400. */
    color: #fff; font-size: 1.4rem; font-weight: 500; text-decoration: none;
}
/* Without an explicit size this SVG collapsed to 0x0 and the header showed no
   phone icon at all -- live's is 18x18 with 5px before the number. */
.header-phone svg { width: 1.8rem; height: 1.8rem; fill: currentColor; flex: 0 0 auto; color: #fff; }

.nav-toggle {
    display: none;
    margin-left: auto;
    min-width: 4.4rem; min-height: 4.4rem;   /* 44px touch target */
    background: none; border: 0; color: #fff; cursor: pointer;
}

/* -------------------------------------------------------------------- hero */
/* Symmetric by default. The oversized bottom padding exists only so the search
   panel can overlap the hero, so it belongs on the pages that HAVE one — every
   other page was getting 80px of dead space and then no gap at all before its
   content. */
.page-hero {
    position: relative;
    background: var(--fg-header-bg) center/cover no-repeat;
    color: #fff;
    padding: var(--fg-space-2xl) 0;
}
.page-hero--search { padding-bottom: 8rem; }
/* Deep enough for white text over ANY photo. The home hero uses fgr-slide2.jpg,
   whose pale sky left .page-hero__meta at 2.97:1 under the old .35/.55 ramp —
   below the 4.5:1 floor for 15px body text. */
.page-hero::after {
    content: ""; position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(0,0,0,.55), rgba(0,0,0,.72));
}
.page-hero > * { position: relative; z-index: 1; }
/* h2 covers the single-property hero, whose own title tag on live really is
   an <h2> (.rh_banner__title) -- the page's real <h1> is the head-row title
   below it (.prop-head__title h1), not this one. Every other .page-hero
   user keeps <h1> here; same rule, two tags. */
.page-hero h1, .page-hero h2 { color: #fff; font-size: 3.6rem; margin: 0; }
.page-hero__meta { margin-top: var(--fg-space-sm); opacity: .9; font-size: 1.5rem; }

/* ------------------------------------------------------------ search panel */
.search-panel {
    background: var(--fg-surface);
    /* Live's .rh_prop_search__fields carries NO box-shadow at all (computed
       style is literally "none") -- confirmed directly against the live
       page, not assumed. Ours had var(--fg-shadow-raised) here, a soft drop
       shadow live's white search box never has. */
    /* Pulls up through the container padding so the panel overlaps the hero.
       On listing pages the map now sits between the two, so the negative
       margin is cancelled there (see .listing-map-wrap + .container below) —
       otherwise the panel sat on top of the map. */
    margin-top: calc(-5.6rem - var(--fg-space-xl));
    position: relative; z-index: 5;
    display: grid;
    grid-template-columns: repeat(3, 1fr) auto;
}
.search-panel__field {
    display: flex; flex-direction: column; gap: .4rem;
    padding: 1.6rem 2rem;
    border-right: 1px solid var(--fg-border-light);
}
.search-panel__field label {
    font-size: 1.3rem; font-weight: 500; color: var(--fg-heading);
}
.search-panel__field select {
    appearance: none;
    border: 0; background: transparent;
    font: inherit; font-size: 1.5rem; color: var(--fg-body);
    min-height: 3rem;
    cursor: pointer;
}
/* Property Type: a real bootstrap-select (see type_picker() in
   05-build-site.py). Its own CSS ships zero .dropdown/.btn/.dropdown-menu
   base rules -- verified by reading src/vendor/bootstrap-select.min.css
   directly -- because it assumes the full Bootstrap framework sits under it.
   This site carries none of that framework anywhere else, so only the
   handful of classes bootstrap-select's own JS actually emits are shimmed
   here, not the framework itself. SHOW:"open" (confirmed in
   bootstrap-select.min.js) is the toggled class, so .open is what reveals
   the menu, not .show. */
.bootstrap-select { position: relative; display: block; width: 100%; }
/* ...except that width never actually applied. bootstrap-select ships
   .bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}
   -- three :not()s carrying their arguments' weight make it (0,4,0), so the
   plain (0,1,0) rule above always lost and every toggle rendered a fixed
   220px wide inside a 331px field. That parked the caret 91px short of the
   field's right edge; live's fills the cell and sits 26px in (both measured,
   same 1440px viewport). Matching their selector's shape is what beats it on
   specificity alone -- no !important needed, since their rule isn't one. */
.search-panel__field .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
    width: 100%;
}
.bootstrap-select > .dropdown-toggle {
    display: flex; align-items: center; justify-content: space-between; gap: .8rem;
    width: 100%; text-align: left; cursor: pointer;
    border: 0; background: transparent; font: inherit; font-size: 1.5rem;
    color: var(--fg-body); padding: 0; min-height: 3rem;
}
/* bootstrap-select.min.css forces the browser's raw native focus ring here
   with !important (its own ".bootstrap-select .dropdown-toggle:focus"
   rule) -- confirmed by reading the vendor file directly. That bypasses
   this site's own accessible focus style entirely (:where(...):focus-
   visible in the keyboard-nav section above, blue, 3px, keyboard-only), so
   these three fields were the only interactive elements on the page NOT
   getting it -- invisible on mouse click either way, but on keyboard tab
   they showed whatever raw outline the browser/OS defaults to instead of
   this site's own, which is exactly the kind of thing that looks
   "different" across browsers. !important vs !important is resolved by
   specificity same as any other tie, THEN load order -- matching the
   vendor selector's own specificity exactly (as a first attempt here did)
   just re-loses the load-order tiebreak, since bootstrap-select.min.css
   loads after site.css. .search-panel__field pushes this one strictly
   higher. */
.search-panel__field .bootstrap-select .dropdown-toggle:focus { outline: 0 !important; }
.search-panel__field .bootstrap-select .dropdown-toggle:focus-visible {
    outline: 3px solid var(--fg-primary) !important;
    outline-offset: 2px;
}
.bootstrap-select .filter-option-inner-inner {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* bootstrap-select's own CSS carries .dropdown-toggle.bs-placeholder{color:
   #999} for the "nothing picked yet" state -- which is every field here on
   first load. That bare rule is 2 classes deep; this one is 4 (adds
   .search-panel__field and the child combinator's own class), so it wins
   there regardless of load order. But the vendor stylesheet ALSO carries
   :hover/:focus/:active variants of that same rule at 3 classes + 1
   pseudo-class = the SAME (0,4,0) specificity as this one -- a tie, broken
   by cascade order, and bootstrap-select.min.css loads AFTER site.css on
   every page, so the vendor's lighter grey silently won back on hover/focus
   (confirmed live: rgb(68,68,68) on rest, rgb(153,153,153) on hover --
   live itself never dims on hover). Repeating the override for every state
   it needs to beat, not just the bare one, is what actually closes that. */
.search-panel__field .bootstrap-select > .dropdown-toggle.bs-placeholder,
.search-panel__field .bootstrap-select > .dropdown-toggle.bs-placeholder:hover,
.search-panel__field .bootstrap-select > .dropdown-toggle.bs-placeholder:focus,
.search-panel__field .bootstrap-select > .dropdown-toggle.bs-placeholder:active {
    color: var(--fg-meta);
}
.bootstrap-select .bs-caret { display: inline-flex; flex: 0 0 auto; }
.bootstrap-select .bs-caret .caret {
    display: inline-block; width: 0; height: 0;
    border-left: .4rem solid transparent; border-right: .4rem solid transparent;
    border-top: .5rem solid var(--fg-meta-icon);
}
.bootstrap-select .dropdown-menu {
    display: none; position: absolute; top: 100%; left: 0; z-index: 5;
    min-width: 100%; margin: .4rem 0 0; padding: 0; list-style: none;
    background: var(--fg-primary);
}
/* Real Bootstrap dropdown.js (see search_picker_assets() in
   05-build-site.py) toggles .open on the closest ancestor matching
   .dropdown -- the outer .bootstrap-select wrapper itself here -- not on
   .dropdown-menu, which carries a permanent, never-toggled "open" class of
   its own from bootstrap-select's template (confirmed: without real
   Bootstrap loaded, that class is present even before any click, which is
   what a hand-rolled shim missed and left the menu stuck open always). */
.bootstrap-select.open .dropdown-menu { display: block; }
/* This inner <ul> never gets .open appended (confirmed in the real DOM --
   only the outer .dropdown-menu it sits inside does), so it needs its own
   unconditional display:block to override the base rule above by class
   count rather than waiting on a state that never arrives here. */
.bootstrap-select .dropdown-menu.inner {
    display: block; margin: 0; padding: 0; max-height: 22rem; overflow-y: auto;
}
.bootstrap-select .dropdown-menu li a {
    display: block; padding: 1rem 1.5rem; color: #fff; text-decoration: none;
    cursor: pointer; transition: background .2s ease-in-out;
}
.bootstrap-select .dropdown-menu li a:hover,
.bootstrap-select .dropdown-menu li.selected a { background: var(--fg-primary-dark); }
.bootstrap-select .dropdown-menu .text { display: inline-block; }
.bootstrap-select .bs-searchbox { padding: .8rem 1.5rem; }
.bootstrap-select .bs-searchbox .form-control {
    width: 100%; border: 1px solid #fff; background: transparent; color: #fff;
    padding: .6rem 1rem; font: inherit; font-size: 1.4rem;
}
.bootstrap-select .bs-searchbox .form-control::placeholder { color: rgba(255, 255, 255, .7); }
.bootstrap-select .bs-actionsbox { padding: 0 1.5rem .8rem; }
.bootstrap-select .bs-actionsbox .btn-group { display: flex; }
.bootstrap-select .bs-actionsbox .btn {
    flex: 1; display: flex; align-items: center; justify-content: center;
    border: 1px solid rgba(255, 255, 255, .5); background: transparent; color: #fff;
    padding: .8rem; cursor: pointer; transition: background .2s ease-in-out;
}
/* Flush together as one bordered group, live's own look -- not two separate
   pills. The shared edge would otherwise double up to a 2px seam. */
.bootstrap-select .bs-actionsbox .btn + .btn { border-left: 0; }
.bootstrap-select .bs-actionsbox .btn:hover { background: var(--fg-primary-dark); }
.bootstrap-select .bs-actionsbox .btn svg { width: 1.6rem; height: 1.6rem; fill: currentColor; }
.bootstrap-select .no-results { padding: 1rem 1.5rem; color: #fff; background: transparent; }
/* bootstrap-select's own pure-CSS tick (.bs-ok-default, a border-drawn
   check) replaces live's tickIcon:'fa-check' -- a Font Awesome glyph this
   site has no other reason to load a whole icon font for. */
.bootstrap-select .bs-ok-default:after { border-color: #fff; }

.search-panel__submit {
    display: inline-flex; align-items: center; justify-content: center; gap: .8rem;
    min-width: 16rem; min-height: 4.4rem;
    padding: 0 2.4rem;
    background: var(--fg-primary); color: #fff;
    border: 0; font: inherit; font-size: 1.6rem; cursor: pointer;
    transition: background .15s ease-out;
}
.search-panel__submit:hover { background: var(--fg-primary-dark); }

/* ---------------------------------------------------------- results header */
.results-bar {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--fg-space-md); flex-wrap: wrap;
    padding: var(--fg-space-lg) 0;
}
/* Measured against live's .rh_pagination__stats: line-height 18px and margin 0.
   Ours inherited the body's 24px line-height and, being a <p>, also carried the
   UA's 1em margin -- which at font-size 15px is 15px top AND bottom. Inside
   .results-bar's flex row those margins count, so the row measured 94px against
   live's 86px. Neither value was declared anywhere; both were defaults, which is
   why grepping the stylesheet for them found nothing.

   NOT changed: live's colour here is #808080 and ours is --fg-body #767676. That
   is the deliberate, documented AA substitution recorded in tokens.css:30-41
   (live's #808080 measures 3.95:1 on white and fails WCAG AA), applied site-wide.
   It shows up as a mismatch on every element measured on this page and is a
   stated exception, not a defect. */
.results-bar__count { font-size: 1.5rem; line-height: 1.8rem; margin: 0; }

/* The sort control is live's own bootstrap-select, not a native <select> (see
   the results-bar markup in 05-build-site.py). The structural shim above --
   .bootstrap-select, its toggle, .bs-caret and the .open/.dropdown-menu
   reveal -- is deliberately unscoped, so this control gets the machinery for
   free and only needs live's skin. Every number here was measured on live's
   .rh_sort_controls at a 1440px content width:
     toggle   220x46 at x=958, padding 13px 15px, background #fff,
              border 1px solid #e0e0e0 with the LEFT side removed, radius 0,
              15px/18px weight 400 in #646464
     caret    8px wide, 4px top border in #808080 (the shared search-panel
              caret is 5px in --fg-meta-icon, so it is overridden here only)
     panel    #0079b8, no radius, no shadow, flush to the toggle (margin 0),
              max-height 220px = data-size 5 x 44px rows
     rows     15px/30px, padding 7px 24px, white
     selected #00669b -- NOT --fg-primary-dark (#00419e), which is what the
              search-panel pickers use. Two different darker blues on the same
              live page; measured, not assumed from the sibling component. */
.sort-controls { position: relative; }
/* bootstrap-select ships
   .bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}
   at (0,4,0). Its value happens to equal live's 220px here, but matching the
   selector's shape rather than leaning on the vendor default keeps the width
   declared on our side, so a vendor bump cannot silently move it. */
.sort-controls .bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
    width: 22rem;
}
.sort-controls .bootstrap-select > .dropdown-toggle {
    width: 22rem; min-width: 22rem;
    padding: 1.3rem 1.5rem;
    background: var(--fg-surface);
    border: 1px solid var(--fg-border); border-left: 0; border-radius: 0;
    font-size: 1.5rem; line-height: 1.8rem; font-weight: 400;
    color: #646464; white-space: nowrap;
    /* Live's button computes text-align:right while its label still renders
       left, because .filter-option sets left explicitly. Both are reproduced;
       dropping either one moves the label. */
    text-align: right;
}
.sort-controls .filter-option { text-align: left; }
.sort-controls .bs-caret .caret { border-top-width: .4rem; border-top-color: #808080; }
.sort-controls .dropdown-menu { margin-top: 0; }
.sort-controls .dropdown-menu.inner { max-height: 22rem; }
.sort-controls .dropdown-menu li a { padding: .7rem 2.4rem; line-height: 3rem; }
.sort-controls .dropdown-menu li a:hover,
.sort-controls .dropdown-menu li.selected a { background: #00669b; }
/* Live centers its bare "No Results Found!" message for a permanently
   zero-count taxonomy archive (measured directly: both the <h4> and its
   parent compute text-align: center there) -- a plain block default would
   left-align it instead, a real diff caught by comparing a rendered
   screenshot to live's, not by source alone. */
.rh_page__listing .no-results { text-align: center; }
/* Live's highlighted counts are <span class="highlight_stats"> at the inherited
   weight 400; ours are <strong>, whose UA default is 700, and we were pulling it
   back only as far as 500. Measured live: fontWeight 400. The <strong> stays --
   scripts/advance-search-check.mjs addresses these via "#result-count strong" --
   so the weight is reset here instead of changing the markup. */
.results-bar__count strong { color: var(--fg-primary); font-weight: 400; }
.results-bar select {
    min-height: 4.4rem; padding: 0 1.6rem;
    border: 1px solid var(--fg-border); background: var(--fg-surface);
    font: inherit; font-size: 1.4rem; border-radius: var(--fg-radius);
}

/* ------------------------------------------------------------- card grid  */
.prop-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
    gap: 3rem;          /* .rh_prop_card padding 0 15px 30px -> 30px gutter */
}

.prop-card {
    background: var(--fg-surface);
    /* shadow, not border — .rh_prop_card__wrap carries the resting shadow and
       no outline at all */
    box-shadow: var(--fg-shadow-card);
    display: flex; flex-direction: column;
    overflow: hidden;
    transition: box-shadow .2s linear;   /* the theme's house timing */
}
.prop-card:hover { box-shadow: var(--fg-shadow-raised); }

.prop-card__media { position: relative; margin: 0; }
.prop-card__media img {
    display: block; width: 100%;
    /* Reserving the box stops the grid reflowing as photos arrive — the
       highest-severity layout rule for a page that is mostly images. */
    aspect-ratio: 4 / 3;
    object-fit: cover;
    background: var(--fg-tint);
}
.prop-card__actions {
    position: absolute; right: .8rem; bottom: .8rem;
    display: flex; gap: .4rem;
}
.icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 4.4rem; height: 4.4rem;            /* 44px touch target */
    background: rgba(0, 0, 0, .35);
    border: 0; border-radius: 50%;
    color: #fff; cursor: pointer;
    transition: background .15s ease-out, color .15s ease-out;
}
.icon-btn:hover { background: rgba(0, 0, 0, .6); }
.icon-btn[aria-pressed="true"] { color: var(--fg-favourite-hover); }
.icon-btn svg { width: 2rem; height: 2rem; fill: currentColor; }

/* Live's card action icons are BARE white glyphs sitting straight on the photo,
   not filled discs. Measured on .rh_list_card__btns at a 1440px content width:
   each anchor is 24x24 with a transparent background and no radius, the two sit
   13px apart, and the pair is inset 20px from the thumbnail's right AND bottom
   edges. The glyph itself is the <path>'s own fill: #fff. Ours were 44px
   rgba(0,0,0,.35) circles inset 8px with a 4px gap.

   The 44px was a deliberate touch-target size. Going to live's 24px still meets
   WCAG 2.2 AA -- 2.5.8 Target Size (Minimum) asks for 24x24 CSS px and this
   lands exactly on it. What it gives up is 2.5.5 Target Size (Enhanced, AAA),
   which is where the 44px figure came from. That is a deliberate AAA-to-AA step
   down taken to match live, recorded here rather than left to be rediscovered.

   Hover was verified by driving a real browser and reading the <path>'s own
   computed fill under :hover -- NOT from measure.mjs --states, which reports the
   anchor and would have missed this. The anchor's color goes to #0079b8 on live,
   which is a red herring: the visible glyph is filled on the <path> and does not
   follow it. The real change is favourite -> #ea3d3d, compare -> #ea723d. Live
   runs no transition here (transition-duration 0s), so ours is dropped too. */
.prop-card--list .prop-card__actions,
.listing-side .prop-card__actions { right: 2rem; bottom: 2rem; gap: 1.3rem; }
.prop-card--list .prop-card__actions .icon-btn,
.listing-side .prop-card__actions .icon-btn {
    width: 2.4rem; height: 2.4rem; padding: 0;
    background: transparent; border-radius: 0; transition: none;
}
.prop-card--list .prop-card__actions .icon-btn:hover,
.listing-side .prop-card__actions .icon-btn:hover { background: transparent; }
.prop-card--list .prop-card__actions .icon-btn svg,
.listing-side .prop-card__actions .icon-btn svg { width: 2.4rem; height: 2.4rem; }
.prop-card--list .prop-card__actions .icon-btn[data-fav]:hover,
.listing-side .prop-card__actions .icon-btn[data-fav]:hover { color: var(--fg-favourite-hover); }
/* #ea723d measured on live's compare glyph; no token for it because, unlike
   --fg-favourite-hover, it has no matching inspiry_* setting to name it after. */
.prop-card--list .prop-card__actions .icon-btn[data-compare]:hover,
.listing-side .prop-card__actions .icon-btn[data-compare]:hover { color: #ea723d; }

.prop-card__body { padding: 2rem; display: flex; flex-direction: column; gap: 1.2rem; flex: 1; }
.prop-card__title { margin: 0; font-size: 1.8rem; }
.prop-card__title a { color: var(--fg-heading); }
.prop-card__title a:hover { color: var(--fg-primary); }
.prop-card__excerpt {
    margin: 0; font-size: 1.4rem; color: var(--fg-body);
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
}

.prop-card__meta {
    display: flex; flex-wrap: wrap; gap: var(--fg-space-md);
    margin-top: auto;
    padding-top: 1.2rem;
    border-top: 1px solid var(--fg-border-light);
}
/* Live separates the label from the value with a 10px margin-bottom on the
   label itself, not with a gap on the item -- measured 10px on BOTH the list
   card (.rh_list_card__details .rh_meta_titles) and the sidebar block card
   (.rh_prop_card--block .rh_meta_titles), so it is set once here. Ours had a
   2px flex gap and no margin. The label's line-height is the one thing that
   differs between the two card shapes: 15.6px on block cards, 19.5px on list
   cards (overridden below). */
.prop-meta__item { display: flex; flex-direction: column; gap: .2rem; font-size: 1.3rem; }
.prop-meta__label { color: var(--fg-heading); font-weight: 500; }
/* Scoped to the two card shapes measured against live on this page. The home,
   news and property-detail cards use the same .prop-meta__* classes but were
   matched against DIFFERENT live components (live's home cards are Elementor
   widgets with no .rh_meta_icon_wrapper at all), so a measurement taken here
   must not be pushed onto them. */
.prop-card--list .prop-meta__item, .listing-side .prop-meta__item { gap: 0; }
.prop-card--list .prop-meta__label, .listing-side .prop-meta__label {
    margin-bottom: 1rem; line-height: 1.56rem;
}
.prop-card--list .prop-meta__label { line-height: 1.95rem; }
/* Live's spacing inside one meta item, measured on the Area row: icon, 3px,
   the number, 4px, the unit. Ours ran a uniform 6px gap. The unit is its own
   element (see meta_item()'s `unit` in 05-build-site.py) at 15px/18px weight
   400 against the number's 13px/15.6px in --fg-meta.
   Live's unit colour is #808080; ours uses --fg-body (#767676) instead, which
   is the same site-wide AA substitution recorded in tokens.css:30-41 -- applied
   here for consistency rather than hardcoding live's failing value back in for
   one span. */
.prop-meta__value { display: inline-flex; align-items: center; gap: .6rem; color: var(--fg-meta); }
.prop-card--list .prop-meta__value, .listing-side .prop-meta__value { gap: .3rem; }
.prop-card--list .prop-meta__unit, .listing-side .prop-meta__unit {
    margin-left: .1rem; font-size: 1.5rem; line-height: 1.8rem;
    font-weight: 400; color: var(--fg-body);
}
.prop-meta__value svg { width: 1.8rem; height: 1.8rem; fill: var(--fg-meta-icon); flex: none; }
/* Live draws these meta icons in a 34x24 box -- a 24-unit viewBox rendered at
   24px tall and centred in a 34px-wide advance, so each glyph carries 5px of
   air either side. Measured 34px x 24px on BOTH live's list card and its
   sidebar block card. Ours were 18x18, which is what made our meta row read
   smaller than live's.
   Scoped to the two card shapes actually measured, NOT applied to every
   .prop-card. A first attempt excluded only .prop-card--home, on the grounds
   that live's home page has no .rh_meta_icon_wrapper at all (its cards are
   Elementor widgets) -- but that still swept in the similar-properties grids on
   the property pages, which were matched against a different live component.
   The Featured sidebar widget IS in scope everywhere it appears: live's own
   /news-2/ and /faqs/ carry the same Featured_Properties_Widget and measure the
   same 34x24 there, checked directly rather than assumed from this page.
   The FILL stays --fg-meta-icon: live's #b3b3b3 measures 2.10:1 and fails the
   3:1 WCAG 1.4.11 floor for meaningful graphics -- the deliberate substitution
   already recorded at tokens.css:45-47. Size is a defect; colour is a decision. */
.prop-card--list .prop-meta__value svg,
.listing-side .prop-meta__value svg { width: 3.4rem; height: 2.4rem; }

.prop-card__foot {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: var(--fg-space-sm);
    padding: 1.2rem 2rem;
    border-top: 1px solid var(--fg-border-light);
}
.prop-status { font-size: 1.4rem; font-weight: 500; color: var(--fg-heading); }
.prop-price { font-size: 1.7rem; font-weight: 500; color: var(--fg-primary); }
/* 12 of 86 properties have no price at all. Saying so is better than an empty
   gap that reads as a broken card. */
.prop-price--none { color: var(--fg-body); font-size: 1.4rem; font-weight: 400; font-style: italic; }

.badge-featured {
    border-radius: var(--fg-radius-label);   /* .property-label is never square */
    position: absolute; left: 0; top: 1.2rem;
    background: var(--fg-accent-teal-text); color: #fff;
    font-size: 1.2rem; padding: .4rem 1rem;
}

/* Live's card "Featured" ribbon, measured on .rh_label__featured_widget:
     block   position absolute, top -13px, left 0, background #0079b8, 74x26
     text    padding 5px 5px 5px 10px, 14px/16px, white, weight 400, no radius
     tail    an empty <span> at left:100%, 9px wide, drawn as a CSS triangle --
             border-left 9px solid #0079b8 with 13px transparent top and bottom
   Ours was a teal (#17847a) rounded rectangle at top:12px INSIDE the media box,
   with no tail: wrong colour, wrong shape, wrong place.

   Two things had to change for the ribbon to sit where live's does. .prop-card
   was overflow:hidden, which clips anything above the card's edge, and it was
   not a positioned ancestor, so `position:absolute` was resolving against
   .prop-card__media instead of the card. The clip moves onto the media itself,
   which is what it was actually for. Scoped to the two card shapes that carry
   this ribbon so the grid and home cards are untouched.

   NOTE: `overflow: visible` here only works because the duplicate
   `.prop-card--list { overflow: hidden }` further down the file was removed.
   Both selectors are (0,1,0), so the later one silently won and clipped every
   list-card ribbon in half. Do not reintroduce an overflow on .prop-card--list
   anywhere below this point. */
.prop-card--list, .listing-side .prop-card { position: relative; overflow: visible; }
.prop-card--list .prop-card__media, .listing-side .prop-card__media { overflow: hidden; }
.prop-card--list > .badge-featured, .listing-side .prop-card > .badge-featured {
    top: -1.3rem; left: 0; border-radius: 0;
    background: var(--fg-primary);
    font-size: 1.4rem; line-height: 1.6rem; font-weight: 400;
    padding: .5rem .5rem .5rem 1rem;
}
.prop-card--list > .badge-featured::after,
.listing-side .prop-card > .badge-featured::after {
    content: ""; position: absolute; left: 100%; top: 0;
    width: 0; height: 0;
    border: 1.3rem solid transparent; border-right: 0;
    border-left: .9rem solid var(--fg-primary);
}

/* ------------------------------------------------------------ empty state */
/* Live's zero-results card (.rh_alert-wrapper > .rh-no-results__content),
   measured at 1440px by triggering it directly (a real form submit with a
   nonsense keyword) rather than guessed: an 808px outer wrapper with
   40px/20px padding around a 768px white card. Fixed pixel widths don't
   survive to narrower viewports live wasn't measured at, so this reproduces
   the same PROPORTIONS -- a capped, centered card with the same internal
   padding -- fluidly instead. */
.empty-state {
    grid-column: 1 / -1;
    padding: 4rem 2rem;
    text-align: center;
}
.empty-state__card {
    max-width: 76.8rem; margin-inline: auto;
    background: #fff; border-radius: .8rem; padding: 2rem;
}
/* Live's image carries its OWN 20px top margin, on top of (not instead of)
   the card's 20px padding -- card-top to img-top is 40px total, not 20.
   Confirmed against the live DOM's own computed margin: "20px 284px 0px". */
.empty-state__img { display: block; margin: 2rem auto 0; width: 16rem; height: 18.4rem; }
.empty-state__card h3 {
    margin: 2rem 0 0; font-size: 2.2rem; line-height: 2.64rem; font-weight: 700;
    color: var(--fg-body);
}
.empty-state__actions { margin-top: 2rem; }
/* Live's button is 120x41 with its own smaller radius/padding -- not the
   site-wide .btn's 44px touch-target sizing. */
.empty-state__actions .btn {
    /* Live's button is 41px tall total; the site's default line-height (1.6
       ratio) pushed 12px of padding + text past that to 46. */
    width: auto; min-height: 4.1rem; padding: 1.2rem 1.5rem; line-height: 1.7rem;
    border-radius: .5rem; font-size: 1.4rem;
}
.empty-state__popular { margin-top: 1.6rem; }
.empty-state__popular p { margin: 0 0 1.6rem; font-size: 1.4rem; color: var(--fg-body); }
.empty-state__popular ul {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem;
}
.empty-state__popular a {
    display: inline-block; background: #f5f5f5; color: #444;
    border-radius: .5rem; padding: .5rem 1rem; font-size: 1.4rem;
    text-decoration: none;
}
.empty-state__popular a:hover { background: var(--fg-tint); }

/* ------------------------------------------------------------- pagination */
.pagination {
    display: flex; justify-content: center; align-items: center;
    /* wrap, or a long pager pushes the whole document sideways on a phone */
    flex-wrap: wrap;
    gap: .8rem;
    padding: var(--fg-space-lg) 0 var(--fg-space-2xl);
}
.pagination button, .pagination span {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 4.4rem; min-height: 4.4rem;
    padding: 0 1.2rem;
    background: var(--fg-surface); border: 1px solid var(--fg-border);
    border-radius: 999px; font-size: 1.5rem;
}
.pagination button { font: inherit; cursor: pointer; }
.pagination [aria-current="page"] { background: var(--fg-primary); border-color: var(--fg-primary); color: #fff; }

/* ---------------------------------------------------------- property page */
/* Live's single-property page repeats the title: a 36px <h2> in the hero
   (page-hero--search, shared with the archive pages' identical hero+search
   overlap), then this plain head row -- breadcrumb + h1 + address on the
   left, status + price on the right -- spanning the full 1240px container,
   above the two-column gallery/sidebar split. Measured live: head row
   838(title)+226(price) within 1240 total, price right-edge flush with the
   container, price text 24px (not the 17px .prop-price already used on
   cards elsewhere -- scoped up here, not changed at its source). */
/* Same 108px search-bar-overlap clearance as .page-hero--faqs/news +
   .prop-search + .container (see the long comment above that rule), but
   given its own class rather than joining that selector list: property
   detail shares .page-hero--search with the archive pages, and an archive
   with no mappable coordinates renders an empty map_block, which would make
   its own .container the search bar's immediate next sibling too --
   accidentally pulling in a padding-top that page was never measured for. */
/* Compound selector (not a lone class) so its specificity beats the
   site-wide `main > .container { padding-block }` rule -- a bare
   `.hero-search-body` (0,1,0) loses to that rule's (0,1,1) and the 10.8rem
   silently never applies. */
.container.hero-search-body { padding-top: 10.8rem; }
.prop-head {
    display: flex; justify-content: space-between; align-items: flex-start;
    gap: var(--fg-space-lg); flex-wrap: wrap;
    padding-bottom: var(--fg-space-lg);
}
.prop-head__title h1 { margin: 0; font-size: 2.4rem; font-weight: 500; color: var(--fg-heading); }
.prop-head__address { margin: .6rem 0 0; color: var(--fg-body); }
.prop-head__price { text-align: right; }
.prop-head__price .prop-price { font-size: 2.4rem; }
@media (max-width: 767px) {
    .prop-head__price { text-align: left; }
}

.prop-layout {
    display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--fg-space-xl);
    /* no padding-block: `main > .container` owns the page rhythm now, and
       keeping it here double-counted to 80px top and bottom */
    align-items: start;
}
.prop-section { margin-bottom: var(--fg-space-xl); }
/* the last section must not add a trailing margin on top of the container's
   own bottom padding */
.prop-section:last-child { margin-bottom: 0; }
.prop-section h2 { color: var(--fg-primary); font-size: 1.8rem; margin: 0 0 var(--fg-space-md); }

.gallery { display: grid; gap: .8rem; }
.gallery__main img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: var(--fg-tint); }
.gallery__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: .8rem; }
.gallery__thumbs button { padding: 0; border: 2px solid transparent; background: none; cursor: pointer; }
.gallery__thumbs button[aria-current="true"] { border-color: var(--fg-primary); }
.gallery__thumbs img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }

.detail-meta { display: flex; flex-wrap: wrap; gap: var(--fg-space-lg); padding: var(--fg-space-md) 0; border-block: 1px solid var(--fg-border-light); }

.feature-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); gap: .8rem; }
.feature-list li { display: flex; gap: .8rem; font-size: 1.4rem; }
.feature-list svg { width: 1.6rem; height: 1.6rem; fill: var(--fg-accent-teal); flex: none; }

.detail-table { width: 100%; border-collapse: collapse; font-size: 1.4rem; }
.detail-table th, .detail-table td { text-align: left; padding: .8rem 0; border-bottom: 1px solid var(--fg-border-light); }
.detail-table th { font-weight: 500; color: var(--fg-heading); width: 40%; }

/* ------------------------------------------------------------ agent panel */
.agent-card { border: 1px solid var(--fg-border-light); background: var(--fg-surface); }
.agent-card__head { padding: var(--fg-space-lg); text-align: center; }
.agent-card__head img { width: 9rem; height: 9rem; border-radius: 50%; object-fit: cover; }
.agent-card__name { margin: var(--fg-space-sm) 0 .4rem; font-size: 1.8rem; }
.agent-card dl { margin: var(--fg-space-md) 0 0; font-size: 1.4rem; display: grid; gap: .6rem; }
.agent-card dt { font-weight: 500; color: var(--fg-heading); }

/* ------------------------------------------------------------------ forms */
.form-field { display: flex; flex-direction: column; gap: .6rem; margin-bottom: var(--fg-space-md); }
.form-field label { font-size: 1.4rem; font-weight: 500; color: var(--fg-heading); }
.form-field .req { color: var(--fg-accent-red); }
.form-field input, .form-field textarea {
    min-height: 4.4rem; padding: 1rem 1.2rem;
    border: 1px solid var(--fg-border); border-radius: var(--fg-radius);
    font: inherit; font-size: 1.5rem; background: var(--fg-surface);
}
.form-field textarea { min-height: 12rem; resize: vertical; }
/* Only mark a field wrong once it HAS been judged wrong. An earlier
   :invalid:not(:placeholder-shown) rule fired on untouched empty required
   fields — these inputs carry no placeholder, so :placeholder-shown is false
   and every visitor was greeted by red borders. site.js sets aria-invalid on
   blur, and that is the single source of truth. */
.form-field [aria-invalid="true"] { border-color: var(--fg-accent-red); }
.field-error { font-size: 1.3rem; color: var(--fg-accent-red); min-height: 1.8rem; }

.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .8rem;
    min-height: 4.4rem; padding: 0 2rem;
    border: 0; border-radius: var(--fg-radius);
    font: inherit; font-size: 1.5rem; cursor: pointer;
    transition: background .15s ease-out, opacity .15s ease-out;
}
.btn--primary { background: var(--fg-primary); color: #fff; width: 100%; }
.btn--primary:hover { background: var(--fg-primary-dark); }
.btn--whatsapp { background: #25d366; color: #fff; }
.btn--call { background: var(--fg-heading); color: #fff; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }
.btn-row { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: .8rem; }

.form-status { font-size: 1.4rem; padding: var(--fg-space-sm) 0; }
.form-status[data-state="error"] { color: var(--fg-accent-red); }
.form-status[data-state="ok"] { color: var(--fg-accent-teal); }

/* ----------------------------------------------------------------- footer */
.site-footer {
    background: var(--fg-header-bg); color: #b3b3b3;
    /* Live: 70px above, 60px below, inside a 1280px box like the header. */
    padding: 7rem 0 6rem;
    /* no margin-top: the container above now owns the gap, so the rhythm does
       not depend on which element happens to come last */
}
.site-footer a { color: #b3b3b3; }
.site-footer a:hover { color: #fff; }
.site-footer__inner { max-width: 128rem; margin-inline: auto; padding-inline: 2rem; }
/* The logo sits in a band of its own above the columns, 160px tall around a
   300x100 mark -- not as a fourth column. */
.site-footer__logo { height: 16rem; display: flex; align-items: center; }
.site-footer__logo img { width: 30rem; height: 10rem; }
/* Four equal columns of 320: live registers a fourth widget area and leaves it
   empty, and that empty slot is what keeps the other three at 320 rather than
   letting them spread to 413. */
/* live's .rh_footer__widgets_wrap is padding: 0 0 20px */
.footer-cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; padding-bottom: 2rem; }
.footer-cols > * { padding-inline: 2rem; }
/* Live's footer headings are 15px with 20px beneath, and its links are
   15px/15px on a 24px pitch -- ours were 16px headings and 14px/22.4px links
   on a 30px pitch, which is most of the 87px the widget band was short. */
.site-footer h2 { color: #fff; font-size: 1.5rem; margin: 0 0 2rem; line-height: 1.2; }
.site-footer ul {
    list-style: none; margin: 0; padding: 0;
    display: grid; gap: .93rem; font-size: 1.5rem; line-height: 1.5rem;
}
/* The Property Types widget is styled differently from the menu widget beside
   it on live: 30px line-height carrying the pitch instead of a gap, 10px above
   the list, and the children nested rather than flattened. Each top-level item
   is 120px -- one parent line plus three child lines. */
.site-footer .footer-types { gap: 0; margin-top: 1rem; }
.site-footer .footer-types li { line-height: 3rem; }
.site-footer .footer-types .children {
    list-style: none; margin: 0; padding: 0; display: block;
}
/* live's section.widget carries 30px beneath itself */
.footer-cols > * { padding-bottom: 3rem; }

.footer-contact { display: grid; gap: 1.2rem; font-size: 1.4rem; }
.footer-contact div { display: flex; gap: 1rem; }
.footer-contact svg { width: 1.8rem; height: 1.8rem; fill: currentColor; flex: none; margin-top: .2rem; }
.site-footer__legal {
    margin: 0; padding: 0 2rem; min-height: 3rem; display: flex; align-items: center;
    font-size: 1.3rem;
}

/* ------------------------------------------------------------- responsive */
@media (max-width: 900px) {
    .prop-layout { grid-template-columns: 1fr; }
    .footer-cols { grid-template-columns: 1fr 1fr; }
    .search-panel { grid-template-columns: 1fr; }
    .search-panel__field { border-right: 0; border-bottom: 1px solid var(--fg-border-light); }
}

@media (max-width: 720px) {
    .nav-toggle { display: inline-flex; }
    .main-nav {
        display: none;
        position: absolute; left: 0; right: 0; top: 100%;
        background: var(--fg-header-bg);
        padding: var(--fg-space-sm) var(--fg-space-md) var(--fg-space-md);
    }
    .main-nav[data-open="true"] { display: block; }
    .main-nav ul { flex-direction: column; align-items: stretch; gap: 0; }
    .main-nav a { padding: 1.4rem 1rem; }
    .header-phone { display: none; }
    .page-hero h1 { font-size: 2.6rem; }
    .footer-cols { grid-template-columns: 1fr; gap: var(--fg-space-lg); }
    .btn-row { grid-template-columns: 1fr; }
}

/* Someone who has asked their OS for less motion should get less motion. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ------------------------------------------------- pages added in phase 2 */
/* Home, FAQs, news, agents, partners and the taxonomy archives. */

.page-hero--home { padding: 10rem 0; background-position: center; background-size: cover; }
.page-hero--home h1 { font-size: 4.2rem; max-width: 22ch; }

/* Long-form body copy from WordPress. Constrained to a readable measure —
   full-width paragraphs on a 1440px screen are the classic migration tell. */
.prose { max-width: 70ch; font-size: 1.6rem; }
.prose h2 { color: var(--fg-primary); font-size: 2rem; margin: var(--fg-space-xl) 0 var(--fg-space-sm); }
.prose h3 { font-size: 1.8rem; margin: var(--fg-space-lg) 0 var(--fg-space-sm); }
.prose p, .prose ul, .prose ol { margin: 0 0 var(--fg-space-md); }
.prose img { height: auto; border-radius: var(--fg-radius); }
.prose a { color: var(--fg-primary); text-decoration: underline; }

.link-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .8rem; }
.link-list li {
    display: flex; justify-content: space-between; gap: var(--fg-space-md);
    padding: 1.2rem 0; border-bottom: 1px solid var(--fg-border-light);
    font-size: 1.6rem;
}
.link-list a { color: var(--fg-heading); }
.link-list a:hover { color: var(--fg-primary); }
.link-list span { color: var(--fg-body); font-size: 1.4rem; white-space: nowrap; }

.partner-strip {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    gap: var(--fg-space-lg); align-items: center;
}
/* Partner logos arrive at wildly different aspect ratios — the Lusail mark is
   tall and portrait, UDC is a wide lockup. Without a fixed box the tall one
   renders several times the size of the others. */
.partner-strip img {
    width: 100%; max-width: 16rem; height: 7rem;
    object-fit: contain; object-position: center;
}
.partner-strip a { display: grid; gap: .8rem; justify-items: center; text-align: center; }
.partner-strip li { display: flex; justify-content: center; }

/* /our-agents/: live's own card is a wide row (photo, name/bio/contact,
   then a "Listed Properties" count + "View My Listings" link), inside the
   same .listing-layout + .listing-side sidebar shape the FAQ/News pages
   already use -- not a centered multi-column tile grid, which live has no
   equivalent of. */
.agent-row-card {
    background: var(--fg-surface); border: 1px solid var(--fg-border-light);
    display: grid; grid-template-columns: auto 1fr auto;
    gap: var(--fg-space-lg); align-items: start;
    padding: var(--fg-space-lg);
}
.agent-row-card__photo { width: 15.4rem; height: 15.4rem; object-fit: cover; }
.agent-row-card__body h2 { margin: 0 0 .8rem; font-size: 1.8rem; }
.agent-row-card__body h2 a { color: var(--fg-heading); }
.agent-row-card__bio { margin: 0 0 var(--fg-space-md); color: var(--fg-body); }
.agent-row-card__contact { display: flex; flex-wrap: wrap; gap: var(--fg-space-md); margin: 0; font-size: 1.4rem; }
.agent-row-card__stats { text-align: right; white-space: nowrap; }
.agent-row-card__count-label { margin: 0; font-size: 1.4rem; color: var(--fg-body); }
.agent-row-card__count { margin: .2rem 0 var(--fg-space-md); font-size: 2.4rem; font-weight: 500; color: var(--fg-heading); }
.agent-row-card__link { color: var(--fg-primary); font-weight: 500; }
@media (max-width: 720px) {
    .agent-row-card { grid-template-columns: 1fr; text-align: left; }
    .agent-row-card__stats { text-align: left; }
}

/* <details> gives keyboard support, screen-reader semantics and open/close
   for free — no JS, and it works before JS loads. The plus/minus icon below
   is pure CSS layered on top of that -- not a reason to rewrite this into a
   div+JS accordion.

   Every colour/size here is measured off live's real .qe-faq-toggle markup
   (the quick-and-easy-faqs plugin, themed by RealHomes) at 1920px:
   row background rgba(0,121,184,.1) -- --fg-primary at 10% -- padding 15px,
   no border-radius; opened body rgba(0,121,184,.05) at half that opacity;
   the icon is inline text (Font Awesome's fa-plus-circle live, "+"/"−" here)
   BEFORE the question, not a right-aligned chevron. */
.faq {
    border: 0;
    margin-bottom: 1.5rem;
}
.faq summary {
    list-style: none;
    padding: 1.5rem;
    /* Row height comes from padding + line-height, not a set height: live's
       is 30px (measured .qe-toggle-title), not the inherited 24px (1.6 base
       body ratio) -- the 6px-per-row gap that difference leaves compounds to
       126px lost over 21 rows by the bottom of the page. */
    font-size: 1.5rem; line-height: 3rem; font-weight: 400; color: var(--fg-heading);
    cursor: pointer;
    background: rgba(0, 121, 184, .1);
    display: flex; align-items: center; gap: .8rem;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::before {
    /* Sampled from live's rendered .fa-plus-circle: rgb(128,128,128), a plain
       grey -- not the primary blue everything else here uses. */
    content: "+"; color: #808080; font-size: 1.8rem; font-weight: 700;
    line-height: 1; flex: none;
}
.faq[open] summary::before { content: "−"; }  /* minus sign for the open state */
/* Live's real plugin jQuery-slides the body open/closed (~200ms); a plain
   height:auto can't be transitioned in CSS, so this is the standard
   grid-rows technique instead -- an approximation of live's animation, not
   a reproduction of its exact easing/timing (both are explicitly accepted
   as out of reach for a CSS-only approach). Padding lives on
   .faq__body-inner, not here: a grid container's own padding doesn't
   shrink to 0 along with its track height, so keeping it here would leave
   a visible gap in the "closed" state despite grid-template-rows: 0fr.
   The global prefers-reduced-motion rule (this file, ~line 638) already
   zeroes this transition's duration -- no separate override needed. */
.faq__body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows .2s ease;
    background: rgba(0, 121, 184, .05);
    font-size: 1.5rem;
}
.faq[open] .faq__body { grid-template-rows: 1fr; }
.faq__body-inner { overflow: hidden; min-height: 0; padding: 2.25rem 2.25rem .45rem; }
.faq__body p { margin: 0 0 var(--fg-space-sm); }

/* ---- the search bar overlaps this hero exactly like the property archives'
   -- measured live at 300px tall with the same ~47px overlap, so the
   mechanism (not the numbers) is copied from .page-hero--search + .prop-
   search below. A dedicated class rather than reusing .page-hero--search
   itself: that one also carries an eyebrow line and the grid/list view
   toggle, neither of which live's FAQ hero has. */
.page-hero--faqs, .page-hero--news {
    min-height: 30rem;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding-bottom: 8rem;
}
/* Live's overlay here is a real DOM element (.rh_banner__cover), not a
   gradient: a FLAT rgba(0,0,0,.5) -- the same flat value .page-hero--search
   already uses. This page DOES have a .page-hero__meta subtitle ("Buying,
   renting..."), so the older assumption that a meta line always means the
   deeper .55/.72 gradient (see the comment above .page-hero::after) doesn't
   hold here -- that assumption was never actually measured against FAQs'
   own live page before now. */
.page-hero--faqs::after, .page-hero--news::after { background: rgba(0, 0, 0, .5); }
/* Live's h1 is shrink-to-fit (86px around "FAQs"), not a full-width block. */
.page-hero--faqs h1, .page-hero--news h1, .page-hero--news h2 { display: inline-block; }
.page-hero--faqs + .prop-search,
.page-hero--news + .prop-search { height: 0; padding: 0; position: relative; z-index: 5; }
.page-hero--faqs + .prop-search .search-panel,
.page-hero--news + .prop-search .search-panel { margin-top: 0; position: relative; top: -4.7rem; }

/* Live's page title prints in a solid-primary band above the content --
   WordPress's own .entry-header/.entry-title for this page template, not
   part of the FAQ plugin (this mechanism is shared with /news-2/ too, hence
   the generic .title-band/.title-card names below rather than a faq-prefixed
   pair). Measured: background --fg-primary, white text, 21px/500. Live's box
   also sits in an oddly narrow (758px) WP content column; that width isn't
   worth reproducing here, since it's an artifact of a different, unrelated
   layout system -- this one lives in our own 1170px-ish .container like
   every other section title on the site. */
/* The search bar (see .page-hero--faqs + .prop-search) overlaps 47px into
   whatever follows the hero, and its own white box runs another 49px past
   that. The clearance for this has to live on .listing-layout's CONTAINER,
   not on .title-band itself: a margin on one grid child (.listing-main's
   title box) doesn't push its SIBLING (.listing-side) down too, and without
   it "Featured Properties" rendered half-hidden under the search bar.

   padding-top here is measured from where .prop-search's zero-height WRAPPER
   sits (right at the hero's own bottom edge, y=300 measured), NOT from the
   search bar's visually-overlapping white box (which floats up to y=253 via
   a negative margin and ends at y=349) -- padding on a flow element counts
   from the previous element's flow position, and .prop-search contributes
   NOTHING to flow. A first attempt set this to 6rem (60px) by literally
   copying the ~59px GAP measured between the search bar's visible bottom
   and live's title-box top -- which looked right in the CSS but rendered
   the two boxes only 11px apart, because 60px counted from y=300 lands at
   360, just past the search bar's OWN natural (unshifted) position, not
   past its shifted one. The real number is what live's title box measures
   from y=300: 408 - 300 = 108px.

   This selector is also the exact one that cost several fix-relaunch cycles
   when it was scoped ONLY to .page-hero--faqs and silently didn't apply to
   a second page reusing the same hero mechanism -- extended to
   .page-hero--news up front this time rather than repeating that mistake. */
.page-hero--faqs + .prop-search + .container,
.page-hero--news + .prop-search + .container { padding-top: 10.8rem; }
/* Measured live: the band itself is 120px tall against ~72px of actual
   content (text + its own padding) -- the rest is a deliberate min-height,
   not a rendering quirk -- and it sits 30px clear of the tab bar below it,
   not 24 (var(--fg-space-lg)). Both values literal because they're specific
   to this one band, not a shared spacing token. */
/* The card live wraps all of this in: article.rh_blog__post, background:#fff,
   no shadow or border-radius (measured, not assumed -- box-shadow: none) --
   838 wide, 60px clear of the map above (article top 468, margin-top 60px).

   flow-root, not the default: without it, .title-band's negative top
   margin two rules down would COLLAPSE into this element's own margin-top
   (60 positive + -60 negative collapses to 0), which pulls the white
   background itself up to 408 -- losing the effect entirely, since blue
   and white would then start at the same point instead of the blue visibly
   overlapping a white box that starts 60px lower. flow-root creates a new
   block formatting context that stops the collapse without clipping the
   band where it pokes out above the card (unlike overflow:hidden, which
   would cut that portion off).

   Named generically (.title-card, not .faq-card): the FAQ page and /news-2/
   both wrap their title band in exactly this shape, so this one rule serves
   both rather than a duplicated .news-card twin drifting out of sync. */
.title-card { background: #fff; margin-top: 6rem; display: flow-root; }
.title-card__body { padding: 2rem 3rem 1rem; }
/* Live's header is a DIRECT child of the white card, margin: -60px 30px 0 --
   not inset via a parent's padding, but by its own horizontal margin, with a
   negative top margin pulling it 60px above the card's own top edge (article
   top 468, header top 408). The 30px sides land it at 838-60=778 wide,
   matching .title-card__body's own 30px padding below it -- both arrive at
   the same 778px content width by different means, as live's own two
   elements do (header via margin, .rh_content via padding). */
.title-band {
    background: var(--fg-primary); min-height: 12rem;
    margin: -6rem 3rem 0;
}
/* h1 covers News's entry-title (live's own tag there); h2 covers FAQs',
   already shipped and unchanged. Same element shape either way. */
.title-band h1, .title-band h2 { color: #fff; margin: 0; padding: 2rem 2.5rem; font-size: 2.1rem; font-weight: 500; }

/* Recent Posts widget (WP_Widget_Recent_Posts via Elementor) on /news-2/,
   measured live: sits directly inside .title-card__body with no background/
   padding of its own -- the white card behind it is already there. Bullets
   are the browser's plain disc + 30px indent, not .link-list's date-row
   layout (no date renders per item on live). */
/* Single FAQ / News article / Partner pages: all three are the same
   RealHomes rh_blog__post template live's own theme uses, just with
   different content. News articles carry a real featured photo, so live
   overlays the title in a blue band on top of it; FAQs have no photo at
   all (their <img> 404s on live) and Partners' "photo" is a small logo,
   not a full-bleed hero shot -- both of those render as a plain centered
   title instead, confirmed against all three page types directly. */
.article-card { background: #fff; }
.article-card__media { position: relative; }
.article-card__media img { width: 100%; display: block; }
.article-card__caption {
    position: absolute; left: 0; bottom: 0; max-width: 85%;
    background: var(--fg-primary); color: #fff; padding: 2rem 2.5rem;
}
.article-card__caption h1 { margin: 0; font-size: 2.1rem; font-weight: 500; }
/* A category LISTING holds several cards, so each one's own title is an h2
   (only the archive page's "All Posts in Category: X" is the real h1) --
   otherwise the exact same overlay this rule already builds for h1. */
.article-card__caption h2 { margin: 0; font-size: 2.1rem; font-weight: 500; }
.article-card__caption h2 a { color: #fff; }
.article-card__caption .article-card__byline { color: #fff; opacity: .9; }
.article-card__byline { margin: .8rem 0 0; font-size: 1.4rem; color: var(--fg-body); }
.article-card__byline a { color: inherit; text-decoration: underline; }

/* Category archive listing card (build_category_archives() /
   blog_post_card() in pages_extra.py) -- live's rh_blog__post template.
   Measured directly against /category/uncategorized/: white background, no
   shadow (box-shadow: none, not just "none set" -- checked), 30px gap
   between cards, image+overlay reuses .article-card__media/__caption above. */
.rh_blog__post { background: #fff; box-shadow: none; margin-bottom: 3rem; }
.rh_blog__post .entry-summary { padding: 3rem; }
.rh_blog__post .entry-summary p { margin: 0 0 2rem; color: #808080; line-height: 1.6; }
.rh-btn.rh-btn-primary {
    display: inline-block; background: var(--fg-primary); color: #fff;
    padding: 1.5rem 2rem; font-size: 1.4rem; font-weight: 500;
    border-radius: var(--fg-radius);   /* square, matching the rest of the site */
}
.rh-btn.rh-btn-primary:hover { background: var(--fg-primary-dark); color: #fff; }
.article-card__body { padding: 2rem 3rem 3rem; }
.article-card--plain { padding: 3rem 3rem 0; text-align: center; }
.article-card--plain h1 { margin: 0 0 1rem; font-size: 2.8rem; font-weight: 700; color: var(--fg-heading); }
.article-card--plain .article-card__byline { margin-bottom: 2rem; }
.article-card--plain .article-card__logo { max-width: 18rem; margin: 0 0 2rem; }
.article-card--plain .article-card__body { text-align: left; padding: 0 3rem 3rem; }

.recent-posts__title { margin: 0 0 2rem; font-size: 1.385rem; font-weight: 700; color: #1a1a1a; }
.recent-posts ul { margin: 0 0 2rem; padding-left: 3rem; }
.recent-posts li { line-height: 3rem; }
.recent-posts a { color: #444; }

/* Live's tabs are plain underlined links, not filled pill buttons -- both
   active and inactive states measured identically (blue text, 2px solid
   blue underline); "active" only matters for which rows are shown, so the
   two are NOT visually distinguished beyond that on live either. */
.faq-tabs { margin: 0 0 2.5rem; }
.faq-tabs__btn {
    background: none; border: 0; border-bottom: 2px solid var(--fg-primary);
    color: var(--fg-primary); padding: 1rem 1.5rem;
    /* Same 30px-not-24px pattern as .faq summary just below -- live's row
       components run a 2.0 line-height ratio, not the site's 1.6 base. */
    font: inherit; font-size: 1.5rem; line-height: 3rem; cursor: pointer;
}

@media (max-width: 720px) {
    .page-hero--home { padding: 6rem 0; }
    .page-hero--home h1 { font-size: 2.8rem; }
}

/* ------------------------------------------------------ card hover overlay */
/* Ported from the live theme's .rh_overlay, values read from main.min.css:
     background rgba(primary, 0.7); transition all .4s ease-in-out
     contents: top 50%, translate(-50%,-50%), transition all .3s ease-in-out
     fadeIn-bottom: starts at top 80% and rises
     link: 1px solid #fff, 1.5rem 2rem, 1.4rem/500, inverts to white on hover

   One deliberate change: the original reveals this on :hover only. A phone has
   no hover, so there the overlay never appears and the image is not obviously
   tappable. Here the overlay IS the link and covers the photo, so a tap on the
   image navigates whether or not anything ever "hovers". */
.prop-card__overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 121, 184, .7);      /* --fg-primary #0079b8 at 70% */
    opacity: 0;
    transition: opacity .4s ease-in-out;
}

/* Keyboard users get it too — the original only handled the mouse.
   :focus-within, not :focus-visible on the overlay itself: the overlay is now
   aria-hidden with tabindex="-1" so it is NOT a second link to the same page
   for screen readers. Focusing the card's real title link reveals it instead,
   so the keyboard gets the same feedback without the duplicate. */
.prop-card:hover .prop-card__overlay,
.prop-card:focus-within .prop-card__overlay {
    opacity: 1;
}

.prop-card__cta {
    display: inline-block;
    padding: 1.5rem 2rem;
    border: 1px solid #fff;
    color: #fff;
    font-size: 1.4rem;
    font-weight: var(--fg-font-weight-medium);
    line-height: 1.5;
    /* the fadeIn-bottom lift: sits low, rises into place */
    transform: translateY(2.4rem);
    transition: transform .3s ease-in-out, background .2s linear, color .2s linear;
}
.prop-card:hover .prop-card__cta,
.prop-card:focus-within .prop-card__cta {
    transform: translateY(0);
}
.prop-card__overlay:hover .prop-card__cta {
    /* White-on-photo: inverts the white-bordered button over the dark
       image overlay. Not a page surface -- stays literal. */
    background: #fff;
    color: var(--fg-primary);
}

/* The favourite and compare buttons sit ON the overlay, so they need to stay
   above it or the overlay link would swallow every tap meant for them. */
.prop-card__actions { z-index: 2; }
.badge-featured { z-index: 2; }

@media (prefers-reduced-motion: reduce) {
    .prop-card__cta { transform: none; }
}

/* Belt and braces: even if an empty meta row ever reaches the page, it must
   not paint a border and padding around nothing. */
.prop-card__meta:empty, .detail-meta:empty { display: none; }

/* ------------------------------------------------------------- contact --- */
/* Same 108px search-bar-overlap clearance as .hero-search-body (see the
   comment there) and for the same reason: contact shares .page-hero--search
   with the archive pages, so this gets its own class rather than joining
   that rule's selector list. */
.container.contact-body { padding-top: 10.8rem; }
.contact-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    gap: var(--fg-space-2xl);
    align-items: start;
}

.contact-details { margin: 0 0 var(--fg-space-lg); display: grid; gap: var(--fg-space-md); }
.contact-details > div {
    display: grid; grid-template-columns: auto 1fr;
    gap: .4rem var(--fg-space-md); align-items: start;
}
.contact-details dt {
    grid-column: 1 / -1;
    display: flex; align-items: center; gap: .8rem;
    font-weight: var(--fg-font-weight-medium); color: var(--fg-heading);
    font-size: 1.4rem;
}
.contact-details dt svg { width: 1.8rem; height: 1.8rem; fill: var(--fg-primary); }
.contact-details dd { grid-column: 1 / -1; margin: 0 0 0 2.6rem; font-size: 1.5rem; }

.contact-map {
    width: 100%; height: 32rem; border: 0;
    border-radius: var(--fg-radius);
    /* Reserving the box keeps the map from shoving the page down when the
       iframe finally loads. */
    background: var(--fg-page-alt);
}

@media (max-width: 900px) {
    .contact-layout { grid-template-columns: 1fr; gap: var(--fg-space-xl); }
}

/* The real logo, from theme_sitelogo. The wordmark stays as a fallback for the
   case where that option is unset. */
.site-logo__img { display: block; height: 10rem; width: auto; }
@media (max-width: 720px) { .site-logo__img { height: 6rem; } }

/* ============================ home: contact-form-slider ==================== */
/* theme_homepage_module = 'contact-form-slider'. The header sits ON the slider
   on this page only; everywhere else it keeps its solid bar. */

.has-overlay-header .site-header {
    position: absolute;
    inset-inline: 0;
    top: 0;
    /* .site-header's own base rule is a solid var(--fg-header-bg) -- without
       an explicit override here that solid colour shows instead of the hero
       photo behind it, which is exactly backwards for an overlaid header. */
    background: transparent;
}
.has-overlay-header .site-header__inner { min-height: 14rem; }
/* This gradient was written for the home page's own hero-slider and then
   inherited, unverified, by every later has-overlay-header page (the
   property archives, then /faqs/). Measured live: the archive header and the
   FAQ header both carry NO gradient at all (background: none) -- only home's
   really does, and at a different value (.7, not .45). Scoped with :has() so
   the two don't drift back into one one-size-fits-all rule again. */
.has-overlay-header:has(.hero-slider) .site-header {
    background: linear-gradient(180deg, rgba(0,0,0,.7), rgba(0,0,0,0));
}

.hero-slider {
    position: relative;
    overflow: hidden;
    background: var(--fg-header-bg);
    /* Live: height:80vh with an 800px floor (assets/modern/styles/css/
       main.min.css, .inspiry_mod_header_variation_one .rh_cfos_slider
       .flexslider ul.slides .slide @min-width:1140px). Not a flat 800px --
       a tall viewport gets a taller hero than this floor. */
    height: 80vh;
    min-height: 80rem;
}
.hero-slider__track { list-style: none; margin: 0; padding: 0; }
.hero-slider__slide {
    position: absolute; inset: 0;
    opacity: 0;
    transition: opacity .6s ease-in-out;
}
.hero-slider__slide[data-current="true"] { opacity: 1; }
.hero-slider__slide img {
    width: 100%; height: 100%;
    object-fit: cover; object-position: center;
    display: block;
}
/* Per-slide caption (title / sub-text / Read More), rendered by
   pages_home.py's slide_desc(). Hidden by default and revealed only at
   1139px and below -- NOT a design choice here, a copy of live's own rule:
   realhomes/assets/modern/styles/css/main.min.css ships
   `.rh_cfos_slider .cfos_slide_visible_sm{display:none}` with a
   `@media(max-width:1139px){...display:block}` beneath it. Getting this
   backwards would put a white card on the desktop hero that live does not
   have -- and no desktop screenshot comparison would ever have flagged the
   opposite mistake either, which is why this rebuild shipped without the
   caption at all until now.

   Simplified deliberately: live carries roughly eight breakpoint variants of
   the wrap's inset/padding/max-width, and this keeps one bottom-anchored card.

   KNOWN DIVERGENCE, not a harmless one. In the 768-1139px band the card lands
   on the OPPOSITE SIDE of the hero from live (measured against live's own
   main.min.css by .devils-advocate check #38, band by band; not re-measured
   here). That is a placement difference, so an earlier version of this note
   excusing it with "sub_text and url are empty, so the title is the only line
   that renders today" was answering a question nobody asked -- where the card
   sits does not depend on what is written in it, and the content argument is
   weaker still now that both those fields do render (pages_home.py's
   slide_desc).

   Left as-is on purpose, with the reason stated rather than dressed up: it
   wants live measured properly across all eight variants first, which is a
   bigger job than the caption work this comment grew out of. */
.hero-slider__desc { display: none; }
@media (max-width: 1139px) {
    .hero-slider__desc { display: block; }
}
.hero-slider__desc-wrap {
    position: absolute; bottom: 3rem; right: 0; left: 0; margin: auto;
    width: 100%; max-width: 50rem;
    padding: 2rem; background: #fff;
    z-index: 2;
}
.hero-slider__desc-wrap h3 {
    margin: 0 0 1rem;
    font-size: 2.4rem; font-weight: 500; line-height: 1.5;
    color: var(--fg-heading);
}
.hero-slider__desc-wrap h3 .title { color: var(--fg-heading); }
.hero-slider__desc-wrap h3 .title:hover { color: var(--fg-primary); }
.hero-slider__desc-wrap .sub-text { margin: 0 0 1rem; line-height: 2; }
@media (max-width: 767px) {
    .hero-slider__desc-wrap { max-width: 45rem; padding: 1.6rem; bottom: 2rem; }
    .hero-slider__desc-wrap h3 { font-size: 1.8rem; }
}
/* Keeps the white nav legible over a bright photo without dimming the whole
   image — the header gradient above handles the top, this handles the edges. */
.hero-slider::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(180deg, rgba(0,0,0,.28) 0 18%, rgba(0,0,0,0) 45%);
}

/* Live's hero arrows are 38x38 sitting at opacity 0 until the slider is
   hovered -- which is why they appear nowhere in a screenshot of it, while
   ours were two permanent blue discs. Revealed on focus as well as hover, so
   a keyboard user can still see what they are operating; live only does hover
   and leaves them invisible-but-clickable. */
.hero-slider__arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    z-index: 3;
    display: inline-flex; align-items: center; justify-content: center;
    width: 3.8rem; height: 3.8rem;
    border: 0; border-radius: 50%;
    background: var(--fg-primary); color: #fff;
    cursor: pointer;
    opacity: 0;
    transition: background .15s ease-out, opacity .2s ease-out;
}
.hero-slider:hover .hero-slider__arrow,
.hero-slider__arrow:focus-visible { opacity: 1; }
.hero-slider__arrow:hover { background: var(--fg-primary-dark); }
.hero-slider__arrow svg { width: 2.4rem; height: 2.4rem; }
.hero-slider__arrow--prev { left: 2rem; }
.hero-slider__arrow--next { right: 2rem; }

.hero-slider__dots {
    position: absolute; left: 50%; bottom: 2rem; transform: translateX(-50%);
    z-index: 3; display: flex; gap: .8rem;
}
.hero-slider__dots button {
    width: 1.2rem; height: 1.2rem; padding: 0;
    border: 2px solid #fff; border-radius: 50%;
    background: transparent; cursor: pointer;
}
/* White-on-photo: the dots sit over the hero image. Stay literal. */
.hero-slider__dots button[aria-current="true"] { background: #fff; }

/* Above 1140 the panel floats over the hero photo; below it, live stacks the
   panel full-width underneath. Both come from this one wrapper. */
.hero-block { position: relative; }
.hero-cta-wrap {
    position: absolute; inset: 0; z-index: 2;
    /* Live anchors .rh_cfos_wrap with position:absolute; bottom:80px against
       its hero -- NOT centred, and NOT a fixed distance from the top. That
       80px held constant across every hero height tested (800, 960, 1120px,
       as the hero's own height grows on a taller viewport per the vh80 rule
       above), while a fixed padding-top -- what was here before, reverse-
       engineered from the single 800px case -- stays put as the hero grows
       and drifts away from the panel's real, bottom-anchored position. */
    display: flex; align-items: flex-end;
    padding-bottom: 8rem;
    pointer-events: none;               /* let the arrows stay clickable */
}
.hero-cta-wrap .container {
    display: flex; justify-content: flex-end; width: 100%;
}

/* ---- the "Let Us Call You!" panel ---- */
/* Live: the white panel is 400x457 and the blue phone tab is a 46x46 square
   sitting OUTSIDE its left edge, 30px down from the panel's top -- not a
   column inside the panel, which is what made ours 400 wide overall with a
   350px body. */
.hero-cta {
    position: relative;
    pointer-events: auto;
    width: 40rem;
}
.hero-cta__tab {
    position: absolute; left: -4.6rem; top: 3rem;
    display: flex; align-items: center; justify-content: center;
    width: 4.6rem; height: 4.6rem; padding: .8rem;
    background: var(--fg-primary); color: #fff;
    transition: background .25s ease-in-out;
}
/* Live glues the tab to the panel with a small triangle spike -- a border
   trick, not an image: a zero-width box whose left border is the only solid
   side, so it renders as a 12px arrowhead pointing into the panel. */
.hero-cta__tab::after {
    content: ""; display: block; position: absolute;
    left: 100%; top: 0;
    border-top: 2.3rem solid transparent;
    border-bottom: 2.3rem solid transparent;
    border-left: 1.2rem solid var(--fg-primary);
    transition: border-left-color .25s ease-in-out;
}
.hero-cta__tab svg {
    width: 2.4rem; height: 2.4rem; fill: currentColor;
    transition: transform .25s ease-in-out;
}
/* Live's only click/tap feedback on this widget: hovering (which a touch tap
   also triggers on most phones) spins and grows the phone icon -- no color
   change, since live's primary and secondary theme colors are the same blue. */
.hero-cta:hover .hero-cta__tab svg { transform: rotate(20deg) scale(1.03); }
.hero-cta__body {
    background: var(--fg-surface);
    padding: 3rem;
}
/* Every other h2 on the site is the shared 500-weight heading style
   (tokens.css). Live's .rh_cfos_cta_title is the one exception -- bold,
   700 -- so it is overridden here rather than in the shared rule. */
.hero-cta__body h2 { margin: 0 0 .5rem; font-size: 2.4rem; line-height: 2.9rem; font-weight: 700; }
.hero-cta__sub { margin: 0 0 2rem; font-size: 1.5rem; line-height: 1.8rem; }
.hero-cta .form-field { margin-bottom: 1.5rem; }
/* An empty error span still reserved 24px, making every field 66px tall against
   live's 40 and the panel 109px too long. It reappears the moment JS fills it. */
.hero-cta .field-error:empty,
.hero-cta .form-status:empty { display: none; }
/* .form-field input above sets min-height:4.4rem at the same specificity, and
   a min-height always beats a height -- so this must match that specificity and
   override min-height itself, or the panel stays 10px long. */
/* The dial-code select sat at 44 and made the number row 4px taller than the
   two above it; live's three text rows are all exactly 40. */
.hero-cta .phone-row,
.hero-cta .phone-row select { min-height: 4rem; height: 4rem; }
.hero-cta .form-field input, .hero-cta .form-field textarea {
    width: 100%;
    /* Live's .cfos_field is 40px tall, not 44. */
    min-height: 4rem; height: 4rem; padding: .8rem 1rem;
    border: 1px solid var(--fg-border); border-radius: var(--fg-radius);
    font: inherit; font-size: 1.5rem;
}
/* Live's textarea wrapper is 84px, and it carries the extra 15px that live's
   empty reCAPTCHA slot contributes before the button. */
.hero-cta .form-field textarea { height: 8.4rem; min-height: 8.4rem; resize: vertical; }
.hero-cta .form-field:has(textarea) { margin-bottom: 3rem; }
/* Live's submit is 46px, not the 44px touch-target default. */
.hero-cta .btn--primary { width: 100%; min-height: 4.6rem; height: 4.6rem; }

/* ---- the advance-search bar over the slider ---- */
/* Live paints no band here. The wrapper is transparent, the bar's own white
   fields sit half over the hero photo and half over the page background, and
   the page background is what shows through underneath -- there is no second
   image. Measured on the live site: .rh_prop_search is background-color
   rgba(0,0,0,0), and its form carries margin-top:-47.5px against a 96px bar. */
/* The property search bar. Live uses the same component on the home page and
   on every archive -- 3x331 fields plus a 94px advance toggle and a 154px
   Search button, 96px tall, overlapping whatever sits above it. Named
   prop-search, not home-search, because it is not the home page's alone. */
.prop-search {
    background: none;
    /* live's .rh_prop_search carries 47.5px beneath the bar -- but only where
       the bar overlaps a hero photo above 1140px. Below that the bar is a
       plain stacked block flush against the panel above and the section
       below, and this padding has to go or every page carrying this bar
       (home plus ~40 archives) gets a blank 47.5px gap that live does not. */
    padding: 0 0 4.75rem;
}
@media (max-width: 1139px) {
    .prop-search { padding-bottom: 0; }
}
/* Live's bar is 96px tall and starts at y=753 against an 800px hero -- it
   overlaps by 47, just under half its height. The columns are its own:
   three 331px fields, then a 94px advance toggle and a 154px Search button,
   1240px in total. Scoped to .prop-search because the archive page shares
   .search-panel and its bar is a different shape. */
.prop-search .search-panel {
    margin-top: -4.7rem;
    /* height, not min-height: the field padding made the bar 103px and pushed
       the "Advance Search" label 7px below where live puts it. */
    height: 9.6rem;
    grid-template-columns: repeat(3, 1fr) 24.8rem;
}
.prop-search .search-panel__field { padding: 2rem; justify-content: center; overflow: hidden; }
/* That overflow:hidden clips a plain <select>'s own box to the row's fixed
   height (the whole reason it's there) -- but Property Type's bootstrap-
   select needs its dropdown-menu to render BELOW the field, escaping that
   same box. Confirmed by measurement, not guessed: the map section behind
   the search bar was painting over an open dropdown that was actually
   sized and positioned correctly the whole time, just clipped invisible by
   this ancestor. Scoped to just the field that holds the picker. */
.prop-search .search-panel__field:has(.bootstrap-select) { overflow: visible; }
/* Live turns its field label white while this one's own panel is open,
   over the blue background the panel is about to sit on.
   Keyed on the WRAPPER's .open, not .dropdown-menu's: bootstrap-select bakes
   a permanent "open" class onto the menu at creation time regardless of
   shown/hidden state (measured: menu className is "dropdown-menu open" while
   the wrapper's own .open is false), so the earlier .dropdown-menu.open form
   of this selector matched ALWAYS -- painting every label white on the white
   panel, i.e. invisible, on every page. Same wrong premise that once left the
   menu itself stuck open; the visibility rule above was corrected for it and
   this one was not. */
.search-panel__field:has(.bootstrap-select.open) label { color: #fff; }
.prop-search .search-panel > *, .prop-search .search-panel__buttons > * { height: 9.6rem; }
.search-panel__buttons { display: flex; }
/* A shade darker than the Search button beside it, as live has it. */
.search-panel__advance {
    display: flex; align-items: center; justify-content: center;
    width: 9.4rem; background: #006fa9; color: #fff;
    transition: background .15s ease-out;
}
.search-panel__advance:hover { background: var(--fg-primary-dark); }
.search-panel__advance svg { width: 2.4rem; height: 2.4rem; fill: currentColor; }
.prop-search .search-panel__submit { width: 15.4rem; min-width: 0; }
.prop-search .search-panel__submit svg { width: 2rem; height: 2rem; fill: currentColor; }

/* The label and its curved arrow sit under the toggle, not centred. */
.prop-search__adv {
    margin: .5rem 0 0; text-align: right; padding-right: 15.4rem;
    display: flex; align-items: center; justify-content: flex-end; gap: .4rem;
}
.prop-search__adv .adv-arrow {
    width: 2.6rem; height: 2.6rem; fill: currentColor; opacity: .75;
    transform: scaleX(-1);
}
.prop-search__adv a {
    color: var(--fg-heading); font-size: 1.4rem; font-weight: 500;
    text-decoration: none;
}
.prop-search__adv a:hover { text-decoration: underline; }

/* ---- the "more options" half of the bar (advance_more_panel()), collapsed
   behind the magnifier-plus on every page, exactly as live does it.
   position+z-index so the expanded panel paints above whatever the bar
   overlaps -- the archive map and the hero photo both sit under it via
   negative margins, and without its own stacking the panel is later-painted
   normal-flow content that the map's later DOM position wins against.
   The white surface belongs to the panel, NOT this wrapper: the wrapper is
   full-bleed and painting it would put a white band across the hero photo
   the compact bar is supposed to float over. */
.adv-search { position: relative; z-index: 5; }
.adv-search__more { background: var(--fg-surface); }
.adv-search__more[hidden] { display: none; }
/* Those three hero variants float the bar itself up 47px out of a zeroed
   wrapper (.page-hero--faqs/--news/--search + .prop-search .search-panel).
   The panel has to travel with it or it stays put in normal flow and a 47px
   band of page background opens between the bar and the fields it just
   expanded. The home page needs none of this -- its bar is offset with a
   margin, so the panel already follows directly underneath. */
.page-hero--faqs + .prop-search .adv-search__more,
.page-hero--news + .prop-search .adv-search__more,
.page-hero--search + .prop-search .adv-search__more { position: relative; top: -4.7rem; }
.adv-search__grid {
    display: grid; grid-template-columns: repeat(4, 1fr);
    border-top: 1px solid var(--fg-border-light);
    border-left: 1px solid var(--fg-border-light);
}
.adv-search__field {
    display: flex; flex-direction: column; gap: .4rem;
    padding: 1.6rem 2rem;
    border-right: 1px solid var(--fg-border-light);
    border-bottom: 1px solid var(--fg-border-light);
}
.adv-search__field:nth-child(9),
.adv-search__field:nth-child(10) { grid-column: span 2; }
.adv-search__field label { font-size: 1.3rem; font-weight: 500; color: var(--fg-heading); }
.adv-search__unit { font-weight: 400; color: var(--fg-meta); }
.adv-search__field select, .adv-search__field input {
    appearance: none; border: 0; background: transparent;
    font: inherit; font-size: 1.5rem; color: var(--fg-body);
    min-height: 3rem; padding: 0; cursor: pointer;
}
.adv-search__field input { cursor: text; }
.adv-search__field input::placeholder { color: var(--fg-body); opacity: 1; }
@media (max-width: 900px) {
    .adv-search__grid { grid-template-columns: repeat(2, 1fr); }
    .adv-search__field:nth-child(9), .adv-search__field:nth-child(10) { grid-column: span 1; }
}

/* Collapsed by default; the toggle just below flips [hidden]. */
.adv-search__features {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
    gap: 1.2rem 2rem; padding: 2rem;
    border-left: 1px solid var(--fg-border-light);
    border-right: 1px solid var(--fg-border-light);
}
.adv-search__features[hidden] { display: none; }
.adv-search__feature { display: flex; align-items: center; gap: .8rem; font-size: 1.4rem; }
.adv-search__feature input { width: 1.6rem; height: 1.6rem; cursor: pointer; }
.adv-search__feature label { cursor: pointer; }
.adv-search__feature small { color: var(--fg-meta-icon); }

.adv-search__toggle {
    display: block; width: 100%; text-align: left;
    background: var(--fg-primary); color: #fff;
    border: 0; padding: 1.5rem 2rem; font: inherit; font-size: 1.5rem;
    cursor: pointer; transition: background .15s ease-out;
}
.adv-search__toggle:hover { background: var(--fg-primary-dark); }
.adv-search__toggle::before { content: "+ "; }
.adv-search__toggle[aria-expanded="true"]::before { content: "\2212 "; }

/* Superseded by the hero-block/.hero-cta-wrap breakpoint ladder further down
   this file, which now carries live's measured values at each of its three
   bands. .hero-slider__overlay no longer exists as an element -- the panel is
   .hero-cta-wrap, a sibling of the slider, not an absolutely positioned child
   of it -- so that rule matched nothing. The stray `.prop-search {
   padding-top: 4rem }` here was the source of a 40px gap this build never
   asked for: it stacked with the base rule's 47.5px BOTTOM padding (meant for
   the desktop overlap only) and inflated the tablet/mobile search block by
   87.5px on every page that carries this bar -- home and all ~40 archives. */
/* Dial code + number, side by side. The live form gets a flag dropdown from
   intl-tel-input at runtime; its server markup is a bare <input type="tel">.
   A native select does the same job with no third-party script, which keeps
   script-src in the CSP as tight as it already is. */
.phone-row { display: flex; gap: .8rem; }
.phone-row .dial-code {
    flex: 0 0 auto;
    max-width: 14rem;
    min-height: 4.4rem;
    padding: 0 .8rem;
    border: 1px solid var(--fg-border);
    border-radius: var(--fg-radius);
    background: var(--fg-surface);
    font: inherit; font-size: 1.4rem;
}
.phone-row input { flex: 1 1 auto; min-width: 0; }

/* reCAPTCHA v3 puts a floating badge bottom-right. Google permits hiding it
   only if the required attribution is shown instead, so it stays. */
.grecaptcha-badge { z-index: 30; }

/* ------------------------------------------------ property detail additions */
/* theme_display_property_breadcrumbs = true, theme_breadcrumbs_taxonomy =
   property-city — so the trail runs Home > City > Property, and doubles as the
   internal link from a listing to its location archive. */
.breadcrumb { margin-bottom: var(--fg-space-md); font-size: 1.4rem; }
.breadcrumb ol {
    list-style: none; margin: 0; padding: 0;
    display: flex; flex-wrap: wrap; align-items: center; gap: .4rem 1rem;
}
.breadcrumb li { display: flex; align-items: center; gap: 1rem; }
.breadcrumb li + li::before {
    content: "›"; color: var(--fg-meta-icon); margin-right: .2rem;
}
.breadcrumb a { color: var(--fg-primary); }
.breadcrumb [aria-current="page"] { color: var(--fg-body); }

.prop-idbar {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: var(--fg-space-md);
    padding: var(--fg-space-md) 0;
    border-bottom: 1px solid var(--fg-border-light);
}
.prop-ref { font-size: 1.4rem; color: var(--fg-body); }
.prop-ref strong { color: var(--fg-heading); font-weight: var(--fg-font-weight-medium); }
.badge-featured--inline { position: static; border-radius: var(--fg-radius); }
.prop-idbar__actions { margin-left: auto; display: flex; gap: .4rem; }
/* On the photo the buttons sit on a dark scrim; here they are on white, so
   they need the opposite treatment rather than white-on-white. */
.icon-btn--ghost {
    background: transparent;
    border: 1px solid var(--fg-border);
    color: var(--fg-meta);
}
.icon-btn--ghost:hover { background: var(--fg-tint); color: var(--fg-primary); }
.icon-btn--ghost[aria-pressed="true"] { color: var(--fg-favourite-hover); }

.prop-map {
    width: 100%; height: 38rem; border: 0;
    border-radius: var(--fg-radius);
    background: var(--fg-page-alt);   /* reserve the box; no layout shift */
}

.similar-props { margin-top: var(--fg-space-2xl); }

@media print {
    /* The print button is only useful if the printed page is worth having. */
    .site-header, .site-footer, .page-hero, .breadcrumb, .prop-idbar__actions,
    .prop-card__actions, aside, .prop-map, .similar-props, .skip-link,
    .grecaptcha-badge { display: none !important; }
    .prop-layout { grid-template-columns: 1fr; }
    body { color: #000; }
    a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .9em; }
}

/* ==================================================================
   The Elementor home body
   ------------------------------------------------------------------
   Nine sections that are NOT in the RealHomes theme: they live in
   postmeta _elementor_data on page 105, are extracted into
   build/data/home-sections.json and rendered by pages_home_sections.py.
   Backgrounds and vertical padding arrive as inline styles from that
   file, because they are per-section data; everything below is the
   shared look, measured on the live site at 1600x1000.
   ================================================================== */
/* Section padding comes from Elementor as three sets of values -- desktop,
   tablet (<=1024) and mobile (<=767) -- delivered as --sec-* custom properties
   on each <section>. Each breakpoint falls back to the one above it, so a
   section that only defines desktop values keeps them everywhere. This is why
   the padding is NOT an inline shorthand: inline would outrank these queries,
   which is what previously pinned 140/190/160px onto phones. */
.home-sec {
    position: relative;
    padding-block: var(--sec-pt, 0) var(--sec-pb, 0);
    padding-inline: var(--sec-pl, 0) var(--sec-pr, 0);
}
@media (max-width: 1024px) {
    .home-sec {
        padding-block: var(--sec-pt-t, var(--sec-pt, 0)) var(--sec-pb-t, var(--sec-pb, 0));
        padding-inline: var(--sec-pl-t, var(--sec-pl, 0)) var(--sec-pr-t, var(--sec-pr, 0));
    }
}
@media (max-width: 767px) {
    .home-sec {
        padding-block: var(--sec-pt-m, var(--sec-pt-t, var(--sec-pt, 0)))
                       var(--sec-pb-m, var(--sec-pb-t, var(--sec-pb, 0)));
        padding-inline: var(--sec-pl-m, var(--sec-pl-t, var(--sec-pl, 0)))
                        var(--sec-pr-m, var(--sec-pr-t, var(--sec-pr, 0)));
    }
}
.home-sec > .container,
.home-sec > .quote-band { position: relative; z-index: 3; }

/* ---- Elementor "tilt" shape divider -----------------------------
   Copied from the live DOM: .elementor-shape is absolute, full width,
   overflow:hidden, z-index 2, holding a viewBox="0 0 1000 100"
   preserveAspectRatio="none" SVG whose single path is the wedge. The
   height arrives inline (140px on every divider on this page). The
   1.3px of extra width is the live rule too: it hides the hairline a
   browser leaves at the right edge when the SVG is stretched. */
.tilt {
    position: absolute; left: 0; width: 100%;
    overflow: hidden; z-index: 2;
    line-height: 0; pointer-events: none;
}
.tilt--top { top: -1px; }
.tilt--bottom { bottom: -1px; transform: rotate(180deg); }
.tilt svg {
    display: block;
    width: calc(100% + 1.3px); height: 100%;
    transform: rotateY(180deg);       /* shape_divider_*_flip: yes */
}
.tilt--noflip svg { transform: none; }

/* ---- ere-section-title-widget ---------------------------------- */
/* Live's .re_section_head_elementor carries margin: 0 0 35px. Ours had none,
   so every one of the five title bands came out 15px short -- the description's
   own 20px is all that escaped by margin collapsing. Setting 35px here lets the
   two collapse to 35 and the band measures 147px, as live does. */
.sec-title { text-align: center; margin: 0 0 3.5rem; }
.sec-title__top {
    margin: 0;
    color: var(--fg-primary); font-size: 1.5rem; font-weight: 500; line-height: 1.8rem;
}
.sec-title h2 {
    margin: 0;
    color: var(--fg-heading); font-size: 3.6rem; font-weight: 300; line-height: 5.4rem;
}
.sec-title__desc {
    margin: 0 0 2rem;
    /* Live uses #808080, which measures 3.95:1 on white and fails AA. The
       project already settled on #767676 (4.54:1) for body text, so the
       colour match is the one place this page deliberately departs. */
    color: var(--fg-body); font-size: 1.5rem; line-height: 3rem;
}

/* ---- rhea-properties-widget (For Sale / For Rent) ---------------
   These two sections carry no padding of their own in Elementor; the
   space above them comes from the title section's 140px top padding. */
.home-sec--props { padding-bottom: var(--fg-space-2xl); }

.pager {
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
    gap: .8rem; margin-top: var(--fg-space-lg);
}
.pager__num, .pager__step {
    min-width: 4rem; min-height: 4rem; padding: 0 1.2rem;
    border: 1px solid var(--fg-border-light); background: var(--fg-surface);
    color: var(--fg-body); font: inherit; font-size: 1.5rem; cursor: pointer;
    transition: background .15s ease-out, color .15s ease-out;
}
.pager__num:hover, .pager__step:hover { background: var(--fg-tint); }
.pager__num[aria-current="true"] {
    background: var(--fg-primary); border-color: var(--fg-primary); color: #fff;
}
.pager__step[disabled] { opacity: .45; cursor: default; }
.pager__all { margin-left: var(--fg-space-md); font-size: 1.5rem; }

/* ---- ere-cta-widget ---------------------------------------------
   ere_cta_1_padding is 220px top and bottom, and it sits on the widget
   rather than the section, so it belongs here and not in the data.
   The band's background image (2023/08/parallax-and-header.jpg) is a
   404 on the live site and absent from the backup, so what a visitor
   sees is the bare rgba(0,0,0,.42) overlay over the page colour.
   Reproduced -- but the page colour is now --fg-page (grey), so this
   band renders a shade darker here than on live, which overlays white. */
.home-sec--cta {
    padding-block: 22rem;
    background: var(--fg-page) center/cover no-repeat;
    color: #fff; text-align: center;
}
/* Live's overlay is rgba(0,0,0,.42) over its background photo. That photo --
   2023/08/parallax-and-header.jpg -- is a 404 on the live site and absent from
   the backup, so the overlay lands on white and the band renders rgb(148,148,
   148). White text on that is 2.9:1, and live's own page fails the same way.
   Reproducing the failure is not worth it: .62 gives rgb(97,97,97), where the
   white text measures 5.6:1. The band's size and layout are unchanged. */
.cta-band__overlay { position: absolute; inset: 0; z-index: 1; background: rgba(0, 0, 0, .62); }
.cta-band__body { max-width: 95rem; margin-inline: auto; }
/* Live: .rh_cta__title is 15px/30px with 15px beneath, .rh_cta__quote has 25px
   beneath, and the button row is 53px tall -- 30+15+108+25+53 = the 231px the
   band measures. Ours was 24px/10px/30px/48px and so came out 11px short. */
.cta-band__top { margin: 0 0 1.5rem; font-size: 1.5rem; font-weight: 500; line-height: 3rem; }
.cta-band__quote {
    margin: 0 0 2.5rem;
    color: #fff;            /* h2 sets its own colour; inheriting is not enough */
    font-size: 3.6rem; font-weight: 400; line-height: 5.4rem;
}
.cta-band__btns { display: flex; justify-content: center; }
.cta-band__btn {
    display: inline-flex; align-items: center; justify-content: center;
    min-height: 5.3rem; padding: 0 2.4rem;
    color: #fff; font-size: 1.5rem; text-decoration: none;
    transition: background .15s ease-out;
}
.cta-band__btn--primary { background: var(--fg-primary); }
.cta-band__btn--primary:hover { background: var(--fg-primary-dark); }
/* A translucent WHITE pill lightens the band back to ~rgb(132,132,132) and
   drops its own white label to 3.1:1. Darkening instead keeps the secondary
   button reading as secondary while the label clears 4.5:1. */
.cta-band__btn--muted { background: rgba(0, 0, 0, .35); }
.cta-band__btn--muted:hover { background: rgba(0, 0, 0, .5); }

/* ---- ere-featured-properties-three-widget -----------------------
   One big photo with the property card laid over its left edge, and
   an arrow either side of the photo rather than on top of it. */
/* Live's .rhea_fp_thumbnail fills the whole 1240px container -- no side
   padding -- and holds the photo as a cover background sized by a padding-top
   ratio of 719.188/1240, i.e. 1240:719. Insetting by 56px for the arrows made
   the photo 1128x546 and the section 173px shorter than live. The arrows sit
   over the photo instead, as flexslider's do. */
.feat { position: relative; }
.feat__track { list-style: none; margin: 0; padding: 0; }
.feat__slide { position: relative; }
.feat__media img {
    display: block; width: 100%; height: auto;
    aspect-ratio: 1240 / 719; object-fit: cover; background: var(--fg-tint);
}
.feat__card {
    position: absolute; top: 50%; left: 0; transform: translateY(-50%);
    width: 40rem; max-width: calc(100% - 3.2rem);
    padding: 3.5rem 3.5rem 1.5rem; background: var(--fg-surface); box-shadow: var(--fg-shadow-raised);
}
.feat__card h3 {
    margin: 0 0 1.2rem;
    /* live: 30px/45px, measured on .rhea_fp2_title */
    font-size: 3rem; font-weight: 500; line-height: 4.5rem;
}
.feat__card h3 a { color: var(--fg-heading); text-decoration: none; }
.feat__card h3 a:hover { color: var(--fg-primary); }
.feat__where {
    display: flex; align-items: center; gap: .6rem;
    margin: 0 0 1.2rem; font-size: 1.4rem; color: var(--fg-heading);
}
.feat__where svg { width: 1.4rem; height: 1.4rem; fill: var(--fg-body); flex: 0 0 auto; }
.feat__added { margin: 0 0 1.2rem; font-size: 1.5rem; line-height: 3rem; color: var(--fg-body); }
.feat__added span { font-weight: 500; color: var(--fg-heading); }
.feat__meta {
    display: flex; flex-wrap: wrap; gap: var(--fg-space-md);
    margin-bottom: 1.2rem;
    line-height: 3rem;      /* live baseline; the meta row is 74px there */
}
.feat__price { margin: 0; font-size: 1.5rem; line-height: 3rem; }
.feat__price span { display: block; font-weight: 500; color: var(--fg-heading); }
.feat__price strong { font-size: 2rem; font-weight: 400; color: var(--fg-primary); }
.feat__agent {
    display: flex; align-items: center; gap: 1.2rem;
    margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--fg-border-light);
    font-size: 1.4rem;
}
.feat__agent img { width: 4.8rem; height: 4.8rem; object-fit: cover; }
.feat__agent a { color: var(--fg-heading); font-weight: 500; text-decoration: none; }
.feat__arrow {
    position: absolute; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; justify-content: center;
    width: 4.4rem; height: 4.4rem;
    border: 1px solid var(--fg-border-light); background: var(--fg-surface);
    color: var(--fg-primary); cursor: pointer;
}
.feat__arrow:hover { background: var(--fg-tint); }
.feat__arrow svg { width: 2.4rem; height: 2.4rem; }
/* Live keeps both arrows fully OUTSIDE the 1240px widget -- measured at
   wrap.x-60..wrap.x-20 and wrap.right+20..wrap.right+60 -- not layered on
   the photo. left:1.6rem/right:1.6rem put ours inside the padding gutter,
   which no longer exists now .feat carries no inset, so they landed on top
   of the card instead. There is room: .feat is centred in the viewport with
   equal margin either side (180px at 1600), the same margin live uses. */
.feat__arrow--prev { left: -6rem; }
.feat__arrow--next { right: -6rem; }

/* ---- ere-agents-widget ------------------------------------------
   .home-agent, not .agent-card: that name is already taken by the
   property detail sidebar earlier in this file. */
.agent-row {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: repeat(auto-fit, minmax(24rem, 29rem));
    justify-content: center; gap: var(--fg-space-lg);
}
.home-agent {
    background: var(--fg-surface); text-align: center;
    padding: 2.5rem 2rem;                 /* .rh_agent__details: 25px 20px */
    border: 1px solid var(--fg-border-light);
    /* 40px above for the photo's -40px pull, and 40px below: live's row is
       398px around a 318px card, i.e. the overhang mirrored underneath. */
    margin: 4rem 0;
}
/* Live's .rh_agent__thumbnail is 128x128 with margin-top:-40px, so the photo
   overlaps the top edge of the details box rather than sitting inside it, and
   .rh_agent__details carries the 25px/20px padding. Ours rendered a 256x137
   letterbox, which is both the wrong shape and 33px short. */
/* line-height:0 matters: without it the anchor is 135px tall around a 128px
   image, and the whole card comes out 7px over live's 318. */
.home-agent__photo { display: block; margin: -4rem auto 0; width: 12.8rem; line-height: 0; }
.home-agent__photo img {
    width: 12.8rem; height: 12.8rem; object-fit: cover; border-radius: 50%;
}
.home-agent h3 { margin: var(--fg-space-md) 0 1.2rem; font-size: 1.6rem; font-weight: 500; }
.home-agent h3 a { color: var(--fg-heading); text-decoration: none; }
.home-agent p { margin: 0 0 .8rem; font-size: 1.4rem; }
.home-agent__tel a { color: var(--fg-primary); text-decoration: none; }
.home-agent__mail a { color: var(--fg-body); text-decoration: none; word-break: break-word; }
.home-agent__count { margin-top: var(--fg-space-md); }
.home-agent__count strong {
    display: block; color: var(--fg-primary); font-size: 2.4rem; font-weight: 400;
}
.home-agent__count span { font-size: 1.3rem; font-weight: 500; color: var(--fg-heading); }

/* ---- ere-big-testimonial-widget ---------------------------------
   The section is #1EA69A with a 140px grey tilt above and below (all
   from the data). The blue block inside runs the full width of it and
   only the text is capped, at the live widget's 950px. */
.quote-band {
    background: #0079b8; padding: 10rem 6rem; color: #fff; text-align: center;
}
.quote { max-width: 95rem; margin: 0 auto; }
.quote blockquote {
    margin: 0 0 5rem; font-size: 3.6rem; font-weight: 400; line-height: 5.4rem;
}
/* Measured on live at 1440px: 30px, weight 500, white -- not the 15px/700
   this was set to. */
.quote figcaption { font-size: 3rem; font-weight: 500; line-height: 3.2rem; }
/* The oversized quotation marks. Every number here was measured in the browser
   against the live page at 1440px rather than guessed: the shape is a 205x141
   SVG rendered 190 wide, filled #00419e at 0.9 opacity, inset 13px from the
   blue block's side and overhanging it by 60px -- left one above, right one
   below and rotated 180deg. The section's 140px of padding is deeper than the
   60px overhang, so neither mark escapes the teal band. */
.quote-band { position: relative; }
.quote-mark {
    position: absolute; z-index: 1; opacity: .9;
    line-height: 0; pointer-events: none;
}
.quote-mark svg { width: 19rem; height: auto; fill: #00419e; }
.quote-mark--left { top: -6rem; left: 1.3rem; }
.quote-mark--right { bottom: -6rem; right: 1.3rem; transform: rotate(180deg); }

/* ---- ere-partners-widget ----------------------------------------
   .partner-strip is shared with /partners/; only the home band greys
   the logos out, as live does. */
.home-sec--partners .partner-strip {
    /* 19.6rem, not 16: the widest logo renders at 196px, and a 163px track
       let neighbouring logos overlap at 390 and 600px. This rule and not a
       .partner-strip--home one, because .home-sec--partners .partner-strip
       is (0,2,0) and would win against it anyway. */
    /* min() so the floor never exceeds the container: a hard 19.6rem forced a
       single column below 475px and stacked all four logos into a 984px band
       against live's 365px. The logos themselves stay capped at 196px. */
    /* 13.6rem floor: live runs two columns at 390px with the logos rendered
       136px wide. A 16rem floor left room for only one 320px track there. */
    grid-template-columns: repeat(auto-fit, minmax(min(13.6rem, 100%), 1fr));
    align-items: center;
}
.home-sec--partners .partner-strip img { filter: grayscale(1); opacity: .65; }
.home-sec--partners .partner-strip img:hover { filter: none; opacity: 1; }

/* 1024, not 900: this is Elementor's own tablet breakpoint, and live switches
   here -- at 1024 it drops to two property columns, stacks the featured card
   under its photo and cuts the CTA band's padding. Holding out until 900 left
   the page 1802px shorter than live across the 12 sections at exactly 1024. */
@media (max-width: 1024px) {
    /* Every divider on this page sets its _height_tablet to 0. */
    .tilt { display: none; }
    .home-sec--cta { padding-block: 6rem; }
    .feat { padding-inline: 0; }
    .feat__card {
        position: static; transform: none; width: auto; max-width: none;
        padding: 2rem; box-shadow: none; border: 1px solid var(--fg-border-light);
    }
    /* Hiding these stranded 2 of the 3 featured properties: site.js binds
       paging to the arrows alone, so there was no other way to reach them.
       8rem sits inside the photo at every width from 320 up (the photo is
       ~58% of the container's width tall). */
    .feat__arrow { top: 8rem; transform: none; }
    /* -6rem assumes .feat's own 5.6rem side gutter, which this same block
       zeroes two lines up. Left as -6rem, the arrow pushed 30px past the
       viewport edge on every width from 768 down to 1024 -- confirmed by
       measuring document.body.scrollWidth against clientWidth, not by
       reading the rule in isolation. */
    .feat__arrow--prev { left: .8rem; }
    .feat__arrow--next { right: .8rem; }
}

/* Below Elementor's mobile breakpoint. Measured on live at 390px and 768px:
   the section heading and the testimonial both stay 36px/54px at EVERY width
   -- only the CTA quote drops, to 26px/39px, and only the testimonial's
   padding shrinks, 100px/60px to 60px/30px. Shrinking the headings and the
   testimonial at <=1024 cost 18px on each of the five title bands and left
   the testimonial band well short. Live also keeps both quote marks visible
   at 390 (measured: 2 of them), so they are no longer hidden. */
@media (max-width: 767px) {
    .cta-band__quote { font-size: 2.6rem; line-height: 3.9rem; }
    .quote-band { padding: 6rem 3rem; }
    .quote-mark svg { width: 12rem; }
    .quote-mark--left { top: -3.5rem; }
    .quote-mark--right { bottom: -3.5rem; }
}

/* ---- featured-card favourite / compare -----------------------------
   Live draws these inside the card body under the price, in
   .rhea_wrapper_member_functions: a flex row of bare 24px icons filled
   #b3b3b3, 5px apart, 20px of space beneath, reddening on hover for the
   favourite and warming to orange for compare (elementor-styles.css:538-569).
   They are deliberately NOT the dark circular .icon-btn the ordinary property
   card uses -- that button belongs over a photo, and these sit on white.
   The padding is ours: it lifts a 24px icon to a ~40px pointer target
   without moving it, cancelled by an equal negative margin. */
/* One row, price left and icons right, matching live's .rhea_fp_sales_icons. */
.feat__sale {
    display: flex; align-items: flex-end; justify-content: space-between;
    gap: var(--fg-space-md); margin-bottom: 2rem;
}
.feat__actions {
    display: flex; align-items: center;
    margin: 0 -.8rem;
}
.feat__act {
    display: inline-flex; align-items: center; justify-content: center;
    padding: .8rem; background: none; border: 0; cursor: pointer;
    color: inherit; line-height: 0;
}
.feat__act svg { width: 2.4rem; height: 2.4rem; fill: #b3b3b3; transition: fill .15s ease-out; }
.feat__act--fav:hover svg,
.feat__act--fav[aria-pressed="true"] svg { fill: #ea3d3d; }
.feat__act--cmp:hover svg,
.feat__act--cmp[aria-pressed="true"] svg { fill: #ea723d; }
.feat__act:focus-visible { outline: 2px solid var(--fg-primary); outline-offset: 2px; }

/* ---- ere-partners-widget, home ------------------------------------
   Live applies no sizing of its own (elementor-styles.css:1921 only sets a
   hover transition); the logos render at the natural size of the
   `partners-logo` thumbnail, capped by the column. Measured in the browser
   at 1440px: ahb and MPD 196x145, UDC 196x134, Lusail 96x200 -- i.e. width
   capped at 196 with the aspect ratio kept, and no height cap at all. The
   shared .partner-strip box (16rem wide, 7rem tall) is half that, which is
   why the home strip needs its own rule rather than a change to the base. */
.home-sec--partners .partner-strip img {
    width: auto; height: auto;
    max-width: min(19.6rem, 100%); max-height: 20rem;
}
@media (max-width: 767px) {
    /* Live caps them at 136px wide here, not 196. */
    .home-sec--partners .partner-strip img { max-width: min(13.6rem, 100%); }
}
/* Live's .rh_partner is padding: 0 20px 20px with margin-bottom 12px, which is
   what makes its strip 232px tall against the 200px of the tallest logo. */
.home-sec--partners .partner-strip li { padding-bottom: 2rem; margin-bottom: 1.2rem; }

/* ---- rhea-properties-widget card ---------------------------------
   Live does NOT reuse the archive card on the home page. Measured at 1440px:

     archive  .rh_prop_card            393 wide, photo 393x295  (4:3)
     home     .rh_prop_card_elementor  383 wide, photo 353x236  (3:2)

   The home card is the narrower Elementor variant -- 383 outer with 15px
   gutters either side, so consecutive photos sit 30px apart and the row is
   3 x 383 = 1149px rather than our 1240px container. Its details box takes
   25px of padding against the archive card's 20px. Hence a modifier: the
   archive card is right where it is and must not move. */
/* CSS Grid was the wrong tool here, proven by an independent re-check: live's
   own widget is `display:flex; flex-wrap:wrap` with a 320px card floor and
   NO fixed track count. That produces a specific pattern grid cannot
   replicate with breakpoints -- measured directly against live at 25+ widths:

       viewport >= 1050    3 across, card fluid down to a 320 floor
       688 - 1049          2 across, the THIRD CARD wraps to its own row
                            and CENTERS under the pair above (not left-aligned)
        <  688              1 column, every card centered

   A grid's auto-fill can drop a track when the floor stops fitting, but it
   fills row-major and left-aligns the remainder -- it cannot center a lone
   wrapped item under the row above. flex-wrap does this natively, because
   that is genuinely how live is built, not an approximation of it. This
   also directly replaces an earlier, wrong claim written in this file: that
   live "holds 3 columns the whole way" down to 960px. It does not -- live's
   own cliff to 2 columns lands at 1050px, independently re-measured. The
   two numbers happened to be close by coincidence, not because the reasoning
   was right. */
.home-sec--props .prop-grid {
    display: flex; flex-wrap: wrap; justify-content: center;
    max-width: 111.9rem; margin-inline: auto;
    gap: 3rem;
}
/* shrink:0, not the default 1 -- with shrink allowed, three cards squeezed
   down BELOW the 310px floor to keep fitting on one row instead of wrapping,
   so the row never actually reached the width where it should give up a
   column. Refusing to shrink forces the wrap at the real threshold. */
/* No single basis satisfies both of live's own thresholds exactly -- solving
   both cliffs (3->2 at content 964-990, 2->1 at content 625-630) gives
   non-overlapping ranges for a constant basis+gap, meaning live's real
   gutter/padding isn't perfectly constant across the two regimes either.
   31rem matches the 3->2 cliff (964 < 3x310+60=990 <= 990, holding through
   1024-1050 exactly as live does); the 2->1 cliff lands ~15px of viewport
   later than live's (700 vs ~688) as the trade-off. The wider range was the
   one actually reported wrong. */
.prop-card--home { flex: 1 0 31rem; max-width: 35.3rem; }
.prop-card--home .prop-card__media img { aspect-ratio: 3 / 2; }
.prop-card--home .prop-card__body { padding: 2.5rem; }
.prop-card--home .prop-card__excerpt {
    font-size: 1.5rem; line-height: 2.25rem; margin-bottom: 2rem;
}
.prop-card--home .prop-card__foot { padding: 1.2rem 2.5rem; }
/* Live's details box is 314px tall: title 68 + excerpt 45(+20) + meta 79(+5)
   + price row 47. Ours ran 41px short across the excerpt gap and the meta
   row, which is the whole of the two property rows' height difference. */
.prop-card--home .prop-card__meta {
    padding-top: 2rem; padding-bottom: .9rem; margin-bottom: .5rem;
}
/* .rh_prop_card_elementor is 383x580 around a 353x550 card: 15px either side
   and 30px underneath. The side gutters are the grid's 3rem gap; this is the
   30px below. */
.prop-card--home { margin-bottom: 3rem; }

/* ------------------------------------------------- maps (Leaflet + OSM) --- */
/* The live site is on OpenStreetMap, not Google: ere_theme_map_type is absent
   from the database so every RealHomes path defaults to `openstreetmaps`, and
   there is no Google API key anywhere in the dump. Leaflet is self-hosted so
   script-src can stay 'self'. */

.prop-map, .contact-map {
    width: 100%;
    height: 38rem;
    background: var(--fg-page-alt);   /* reserve the box: no layout shift */
}
.contact-map { height: 32rem; }

.listing-map-wrap { background: var(--fg-page-alt); }
/* When a map follows the hero, the search panel must sit BELOW the map rather
   than being dragged up over it. */
.listing-map-wrap + .container .search-panel { margin-top: 0; }
#listing-map {
    width: 100%;
    height: 42rem;
    background: var(--fg-page-alt);
}

/* Leaflet paints its own panes; keep it under the sticky-ish header and the
   search panel, which overlaps the hero by 56px. */
.leaflet-container { z-index: 1; font: inherit; }
.leaflet-control-attribution { font-size: 1.1rem; }

/* The default marker is a blue teardrop PNG; this is an inline SVG pin in the
   brand colour so the map does not look like a different site. */
.osm-pin { background: none; border: 0; }
.osm-pin svg { display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }

.osm-popup-content { display: grid; gap: .6rem; min-width: 22rem; }
.osm-popup-content img {
    width: 100%; aspect-ratio: 11 / 7; object-fit: cover; display: block;
}
.osm-popup-title {
    color: var(--fg-heading); font-weight: var(--fg-font-weight-medium);
    font-size: 1.5rem; line-height: 1.3;
}
.osm-popup-title:hover { color: var(--fg-primary); }
.osm-popup-price { color: var(--fg-primary); font-size: 1.5rem; }
.leaflet-popup-content { margin: 1.2rem; }

/* Cluster bubbles in the brand blue rather than MarkerCluster's stock green /
   yellow / orange, which clash with #0079b8.

   Scoped under .leaflet-container deliberately: MarkerCluster.Default.css is
   injected by map_assets() AFTER site.css, so a bare .marker-cluster-small
   rule has equal specificity and loses on source order. */
.leaflet-container .marker-cluster-small,
.leaflet-container .marker-cluster-medium,
.leaflet-container .marker-cluster-large {
    background: rgba(0, 121, 184, .3);
}
.leaflet-container .marker-cluster-small div,
.leaflet-container .marker-cluster-medium div,
.leaflet-container .marker-cluster-large div {
    background: var(--fg-primary);
    color: #fff;
}
.leaflet-container .marker-cluster span { line-height: 3rem; }
.leaflet-container .marker-cluster div { font: 500 1.3rem/3rem var(--fg-font); }

@media (max-width: 720px) {
    #listing-map { height: 30rem; }
    .prop-map { height: 28rem; }
}

/* An earlier round read live's card as 320 OUTER / 290 INNER (its own 15px
   side padding) and shrank the whole card to 290 to match the inner number.
   That was the wrong number to constrain: max-width clamps the size flexbox
   uses for its OWN wrap decision, so at 1024 three 290px cards (930px) fit a
   964px row where live's real 320px cards (960px) do not -- our row stayed
   three-across where live had already wrapped to two-plus-one. Confirmed by
   re-measuring live properly scrolled: the outer footprint is 320 from 1024
   all the way down to 320, never 290. No override here now; the flex-basis
   above (31rem, shrink:0) is what decides both the size and the wrap. */

/* At 290px live's card title runs to three 27px lines and its meta row stacks
   into a 152px column; ours held a 15px title on two lines and kept the meta
   on one row, which is the whole of the two property rows' remaining gap on
   tablet and mobile. */
@media (max-width: 1024px) {
    /* Only below 1024: at 1440 the desktop title already matched live, and
       raising it there added 21px to each property row. */
    .prop-card--home .prop-card__title {
        font-size: 1.8rem; line-height: 2.7rem; margin-bottom: 1.5rem;
    }
    /* Live's meta stacks into a 152px column of three ~50px rows, and carries
       none of the desktop card's padding around it. */
    .prop-card--home .prop-card__meta {
        display: flex; flex-direction: column; gap: 0;
        padding-top: 0; padding-bottom: 0;
    }
    .prop-card--home .prop-card__meta .prop-meta__item {
        min-height: 5rem; display: flex; align-items: center;
        justify-content: space-between; width: 100%;
    }
}

/* Partners on tablet. Live lays these out as inline-block flow, so the logos
   GROW as fewer fit per row: measured 196x145 at 1440 (four up), 288x213 at
   1024 (two up) and 206x152 at 768. A grid cannot reproduce that scaling law
   exactly -- this is the one deliberately approximate rule on the page. Two
   columns with a 288px cap lands 1024 on live's numbers and brings 768 much
   closer than the 159px logos it had. */
@media (max-width: 1024px) and (min-width: 768px) {
    .home-sec--partners .partner-strip { grid-template-columns: repeat(2, 1fr); }
    .home-sec--partners .partner-strip img { max-width: min(28.8rem, 100%); max-height: 22rem; }
}
/* Live's logos are 206 wide at 768, not 288 -- a single 288 cap across the
   whole tablet range overshot that end by 85px. */
@media (max-width: 900px) and (min-width: 768px) {
    .home-sec--partners .partner-strip img { max-width: min(20.6rem, 100%); }
}

/* ---- the home search bar below the desktop layout -------------------
   BLOCKER FIX. `.prop-search .search-panel` above is (0,2,0) and sits later
   in the file than the generic `.search-panel {grid-template-columns:1fr}`
   mobile rule, so that rule never applied: at 320-390 the three fields
   computed to 40px each -- exactly their own padding -- and both the label
   and the <select> measured 0px behind `overflow:hidden`. The search was
   unusable on a phone, and the row's 388px min-content also pushed 68px of
   horizontal overflow at 320. Live stacks the same block. Scoped to the same
   selector so specificity and order both favour it. */
@media (max-width: 900px) {
    .prop-search .search-panel {
        grid-template-columns: 1fr;
        height: auto;
    }
    .prop-search .search-panel > *,
    .prop-search .search-panel__buttons > * { height: auto; }
    .prop-search .search-panel__field {
        overflow: visible;
        min-height: 6.4rem;
        border-right: 0; border-bottom: 1px solid var(--fg-border-light);
    }
    .search-panel__buttons { display: grid; grid-template-columns: 9.4rem 1fr; }
    .search-panel__advance { width: auto; min-height: 5.6rem; }
    .prop-search .search-panel__submit { width: auto; min-height: 5.6rem; }
    .prop-search__adv { padding-right: 0; justify-content: center; }
}

/* Live's header is not a fixed 140 at every width: it measures 130 at 1024
   and 768, and 169 at 390 where the logo and menu stack. */
@media (max-width: 1024px) {
    /* .has-overlay-header .site-header__inner is (0,2,0) and pins 14rem, so
       these have to match its specificity or nothing moves. The logo has to
       come down too: a 100px mark inside 20px padding is already 140, so the
       min-height alone could never reach live's 130. */
    .site-header__inner,
    .has-overlay-header .site-header__inner { min-height: 13rem; }
    .site-logo img, .site-logo__img { height: 9rem; }
}
@media (max-width: 767px) {
    .site-header__inner,
    .has-overlay-header .site-header__inner { min-height: 16.9rem; }
}

/* ================= property archive: live's two-column listing =============
   Measured against /property-status/for-sale/ at 1440: banner 1440x300 at y=0
   with the header floating over it, search bar 992x96 at y=253 overlapping the
   banner's foot, map 600 tall, then an 868px listing column beside a 372px
   sidebar. Rows are 838x205 with a 251x205 photo, 33px apart. */
/* Live's .rh_banner__cover is a FLAT rgba(0,0,0,.5); ours inherited the
   .55->.72 gradient .page-hero uses elsewhere and read visibly darker than the
   live banner. Only this banner changes: contact, FAQs, news and the agents
   page carry a 15px .page-hero__meta line that needs the deeper ramp, and
   this banner has no such line -- an eyebrow and a 36px h1, nothing smaller. */
.page-hero--search::after {
    background: rgba(0, 0, 0, .5);
}
.page-hero--search {
    min-height: 30rem;
    display: flex; flex-direction: column; justify-content: flex-end;
    padding-bottom: 8rem;
}
.page-hero--search .container { position: relative; }
/* Was a <p> BEFORE the <h1>; now a <span> INSIDE it -- live's own structure
   is one <h1> whose accessible name is "Property Type Office", not a heading
   plus an unrelated paragraph before it (confirmed directly: live nests
   <span class="tax-title"> inside its <h1>). display:block keeps the exact
   same visual line break this rule already produced as a separate paragraph;
   only the DOM nesting (and so the accessible name a screen reader announces
   for the heading) actually changed. */
.page-hero__eyebrow {
    display: block;
    margin: 0 0 .4rem; font-size: 1.5rem; line-height: 1.8rem; color: rgba(255,255,255,.85);
}
/* inline-block: live's h1 is shrink-to-fit (134px around "For Sale"). */
.page-hero--search h1, .page-hero--search h2 {
    /* weight 400, not 500: live's .rh_banner__title is regular. */
    font-size: 3.6rem; line-height: 4.32rem; font-weight: 400;
    margin: 0; display: inline-block;
}

/* Grid/list toggle, top right of the banner. */
/* The hero's .container is full-bleed with 100px of padding, not a 1240 box,
   so the offset is measured from the viewport edge: 1440 - 100 - 86 = 1254.
   The 5rem gap is what makes two 18px marks span live's 86px. */
/* Live's two 18px marks sit at x=1279 and x=1322 -- 25px apart, ending flush
   with the 1340 container edge. (.rh_view_type measures 86 wide because of its
   own padding; the icons themselves span 61.) */
.view-type { position: absolute; right: 10rem; bottom: .6rem; display: flex; gap: 2.5rem; }
.view-type__btn {
    background: none; border: 0; padding: 0; cursor: pointer; line-height: 0;
    /* Sampled from live's rendered pixels: the inactive mark is pure white and
       the active one is rgb(0,121,184) -- the primary. */
    color: #fff;
}
.view-type__btn svg { width: 1.8rem; height: 1.8rem; fill: currentColor; }
.view-type__btn[aria-pressed="true"] { color: var(--fg-primary); }
.view-type__btn:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* Live's listing section is padded 60px top / 0 bottom, not the 40/40 that
   `main > .container` hands every other page. Measured on live
   /property-status/for-sale/: #properties-listing paddingTop 60px,
   paddingBottom 0px, against our container's 40/40 -- which put our results
   bar 20px too high and left 40px of dead space above the footer.

   Scoped to the container that FOLLOWS the results map, because only the
   property listing pages have one (measured: .listing-map-wrap is present on
   fgr-properties, properties-search and every taxonomy archive, and absent
   from /faqs/). The FAQ and News pages reuse .listing-layout but have no map,
   so they keep the global 40/40 they were matched to live with.

   `main > .listing-map-wrap + .container` is (0,2,1) against the global rule's
   (0,1,1), so it wins on specificity without !important. */
main > .listing-map-wrap + .container { padding-block: 6rem 0; }

.listing-layout {
    /* The main column is fluid and the sidebar fixed: two hard-coded widths
       summing to 1240 overflowed by 60px at a 1280 viewport, where the
       container is narrower than 1240. At 1440 this still gives live's
       868 + 372 exactly. minmax(0,1fr) rather than 1fr so a long word in a
       card cannot push the track wider than its share. */
    display: grid; grid-template-columns: minmax(0, 1fr) 37.2rem;
    align-items: start;
}
.listing-main { padding: 0 3rem 8rem 0; }
.listing-side { padding: 0 0 8rem; }
/* 20 of the 44 archives have no featured property, so build_listing emits no
   sidebar -- and they were left with a 372px empty column beside the listing. */
.listing-layout:not(:has(.listing-side)) { grid-template-columns: 1fr; }
/* weight 400, measured on live's .Featured_Properties_Widget > h3.title --
   everything else about this heading already matched. */
.listing-side__title {
    margin: 0 0 2rem; font-size: 2.2rem; line-height: 2.64rem; font-weight: 400;
    color: var(--fg-heading);
}
.listing-side .prop-card { margin-bottom: 3rem; }

/* ---- the list row --------------------------------------------------- */
.prop-list { display: grid; gap: 3.3rem; }
/* Live has no gap property here: each card is an <article class="rh_list_card">
   with margin-top 13px and padding-bottom 20px, and the visual card is the
   <div class="rh_list_card__wrap"> inside it. Between two cards that sums to
   the 33px gap above, which already matched -- but it also puts 13px between
   the results bar and the FIRST card, which we had at 0 (measured: live's first
   card top y=1059 against ours at 1046, both toolbars ending at 1046).
   Adjacent-sibling scoped so the grid archives, which have no results bar
   directly above them, are untouched. */
.results-bar + .prop-list { margin-top: 1.3rem; }
.prop-card--list {
    display: grid; grid-template-columns: 25.1rem 1fr;
    background: var(--fg-surface); box-shadow: var(--fg-shadow-card);
    /* NO overflow:hidden here. It used to clip the media, but the media now
       clips itself (see .prop-card--list .prop-card__media below), and this
       rule sits ~1870 lines after the `overflow: visible` that lets the
       "Featured" ribbon hang 13px above the card. Both selectors are plain
       `.prop-card--list` at (0,1,0), so the later one won and sliced every
       list-card ribbon in half -- a bug the ribbon's own position check missed,
       because the badge was measured at exactly the right coordinates while
       being clipped out of sight. */
}
/* Live's row is exactly its thumbnail's height: a fixed 251x205 crop. */
.prop-card--list .prop-card__media { margin: 0; height: 20.5rem; }
.prop-card--list .prop-card__media picture { display: block; height: 100%; }
.prop-card--list .prop-card__media img {
    height: 100%; width: 100%; object-fit: cover; aspect-ratio: auto;
}
/* Live puts the 20/25 inset on the WRAPPER, not on the details column, and hangs
   the vertical rule off the details column's own border-right. Measured on live:
     .rh_list_card__details_wrap  587x205, padding 20px 25px
     .rh_list_card__details       349 wide, padding-right 20px,
                                  border-right 1px solid rgba(128,128,128,.2)
     .rh_list_card__priceLabel    188 wide, padding-left 20px
   25 + 349 + 1 + 188 + 25 = 588, i.e. the 587 wrapper to a rounding pixel.

   We had the inset on .list-card__details instead and the rule as a border-LEFT
   on the price column, which pushed the whole right-hand column 26px inward and
   put our divider at x=990 against live's x=964 (confirmed by sampling live's
   own card bitmap: a single #e6e6e6 pixel column at absolute x=964).
   rgba(128,128,128,.2) over white resolves to exactly that #e6e6e6, so the
   rendered colour was already right -- only its owner and position were wrong. */
.list-card__wrap { display: grid; grid-template-columns: 1fr 18.8rem; padding: 2rem 2.5rem; }
.list-card__details { padding: 0 2rem 0 0; min-width: 0; border-right: 1px solid rgba(128, 128, 128, .2); }
/* Live's .rh_list_card__details h3 is 18px/21.6px weight 700; ours was weight
   500 and otherwise identical, yet live's box measured 51px against our 43.19px.
   The difference is NOT on the h3 at all: the visible text is the <a> inside it,
   and live styles THAT at 17px/25.5px while ours simply inherited the h3's
   18px/21.6px. Two 25.5px line boxes are live's 51px exactly.
   (A first attempt "fixed" this by setting the h3's own line-height to 2.55rem
   from 51/2, which introduced a mismatch that had not existed -- the h3's
   line-height already agreed. Measure the element that carries the text.) */
.prop-card--list .prop-card__title { font-size: 1.8rem; line-height: 2.16rem; font-weight: 700; margin: 0 0 1rem; }
.prop-card--list .prop-card__title a { font-size: 1.7rem; line-height: 2.55rem; }
/* Live spaces the meta row off the EXCERPT's bottom margin being 0 and the meta
   wrapper carrying margin-top 20px (measured: excerpt y=1140 h=0 mb=0,
   .rh_list_card__meta_wrap y=1160 mt=20px). Ours put 12px under the excerpt and
   nothing above the meta, landing the row at 1152 -- 8px high. Splitting it
   live's way also holds for cards that DO have excerpt text, where the excerpt
   has real height and only the 20px above the meta should separate them. */
.prop-card--list .prop-card__excerpt { margin: 0; }
/* Live's meta items are CONTIGUOUS with `padding: 0 7px` each, and the row is
   pulled 7px left so the first label still lines up with the title. Measured
   item boxes on live: [609,687] [687,771] [771,870] -- touching, no gap -- with
   labels landing at 616, 694, 778. Ours used a 16px flex gap and no padding,
   which put our labels at 616, 696, 781: right for the first, 2-3px adrift for
   the rest, and it grew with each item. */
.prop-card--list .prop-card__meta { border: 0; padding: 0; margin: 2rem 0 0 -.7rem; gap: 0; }
.prop-card--list .prop-meta__item { padding: 0 .7rem; }
/* Live's price column is flex-column space-between: status+price sit at the TOP
   of the card and the agent byline is pushed to the BOTTOM (measured: price
   block y=1079 = card top + 20, author bottom y=1244 = card bottom - 20). We
   were centring all three as one group, which floated them into the middle.

   space-between is not usable directly here: live groups status+price inside a
   .rh_list_card__price div so it has exactly two flex children, while our markup
   has three siblings -- space-between would spread them top/middle/bottom.
   flex-start plus margin-top:auto on the byline gives live's result without
   changing the markup. The divider moved to .list-card__details's border-right
   above, so there is no border-left here any more, and the padding is
   left-only, matching live's 188px column with its 20px inner gutter. */
.list-card__price {
    /* gap 0: live spaces status from price with a 5px margin-bottom on the
       status, not a gap (measured status y=1079 h=16 mb=5, price y=1100). */
    display: flex; flex-direction: column; justify-content: flex-start; gap: 0;
    padding: 0 0 0 2rem;
}
/* Measured on live: status 13px/15.6px with margin-bottom 5px; price 22px/28.6px
   at weight 400. Ours were 13px/20.8px with no margin, and 20px/32px at 500 --
   a smaller, heavier price than live's. */
.list-card__price .prop-status {
    font-size: 1.3rem; line-height: 1.56rem; margin-bottom: .5rem; color: var(--fg-body);
}
.list-card__price .prop-price {
    font-size: 2.2rem; line-height: 2.86rem; font-weight: 400; color: var(--fg-primary);
}
/* Live renders this as two lines -- "By" then the agent's name -- because its
   <span class="author"> is display:block with margin-top 5px, both at 15.6px.
   15.6 + 5 + 15.6 = live's measured 36.19px block. Ours was one 20.8px line.

   DELIBERATE DIFFERENCE KEPT: live's agent name is a plain <span>; ours is an
   <a> to the agent's page. It is styled to look identical (same colour, weight
   and metrics) and only the cursor gives it away. Removing a working link purely
   to match live would be a functional regression, so it stays -- named here
   rather than silently reported as a match. */
.list-card__author { margin: auto 0 0; font-size: 1.3rem; line-height: 1.56rem; color: var(--fg-body); }
.list-card__author a {
    display: block; margin-top: .5rem; line-height: 1.56rem;
    color: var(--fg-heading); font-weight: 500; text-decoration: none;
}

/* ---- grid view: the same rows, restacked --------------------------- */
.prop-list--grid { grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr)); }
.prop-list--grid .prop-card--list { grid-template-columns: 1fr; }
.prop-list--grid .list-card__wrap { grid-template-columns: 1fr; }
/* The photo must be released from that fixed 205 in grid view, or a 4/3 image
   overflows it by 97px, lands on top of the title and makes the link
   unclickable -- the figure is overflow:visible. Higher specificity than the
   base rule, so this does not depend on where in the file it sits. */
.prop-list--grid .prop-card--list .prop-card__media { height: auto; }
.prop-list--grid .prop-card--list .prop-card__media img { aspect-ratio: 4 / 3; height: auto; }
.prop-list--grid .list-card__price {
    border-left: 0; border-top: 1px solid var(--fg-border-light);
    flex-direction: row; align-items: center; justify-content: space-between;
    padding: 1.2rem 2.5rem;
}
.prop-list--grid .list-card__author { margin: 0; }

@media (max-width: 1024px) {
    .listing-layout { grid-template-columns: 1fr; }
    .listing-main { padding: 0 0 4rem; }
}
@media (max-width: 767px) {
    .prop-card--list { grid-template-columns: 1fr; }
    .list-card__wrap { grid-template-columns: 1fr; }
    /* Same release on a phone, where the row stacks. */
    .prop-card--list .prop-card__media { height: auto; }
    .prop-card--list .prop-card__media img { aspect-ratio: 4 / 3; height: auto; }
    .list-card__price {
        border-left: 0; border-top: 1px solid var(--fg-border-light);
        padding: 1.6rem 2.5rem;
    }
    .view-type { position: static; margin-top: 1.2rem; }
}

/* ---- archive-specific tuning of the shared pieces ------------------- */
/* Live's archive banner is 300 tall with the search bar overlapping its foot
   by 47px, then a 600px map. Ours had the bar below a 420px map. */
/* Zero-height and out of flow: live's map starts at 300, immediately under the
   banner, with the search bar floating across the seam. Left in flow the bar
   pushed the map down to 397. */
.page-hero--search + .prop-search {
    height: 0; padding: 0; position: relative; z-index: 5;
}
/* top, not margin-top: a negative margin on a zero-height block still shifts
   what follows, and dragged the map up to 253 with it. */
.page-hero--search + .prop-search .search-panel {
    /* The shared rule already pulls the bar up 47 with a margin; here that
       margin must go, or the bar lands at 206 and drags the map to 253. */
    margin-top: 0; position: relative; top: -4.7rem;
}
.listing-map-wrap, #listing-map { height: 60rem; }
/* Several page types zero .prop-search's height so the bar can float over
   the hero/map seam (the rule two above, plus the --news/--faqs pair far
   earlier in this file). That is right while the bar is just the one
   floated row -- but the moment the magnifier-plus expands the extra
   fields, the wrapper needs real flow height again or the panel renders
   into a zero box and whatever follows (the map, the listing) paints over
   it. Keyed on the panel's own expanded state so it costs nothing while
   collapsed, and placed after every height:0 rule it has to beat -- they
   are the same specificity, so source order is the tiebreak. */
.prop-search:has(#adv-more:not([hidden])) { height: auto; }

/* Live's toggle is 86px wide -- two 18px marks 50px apart -- sitting at
   x=1254, i.e. inside the 1240 container, not flush to its right edge. */
/* The hero's .container is full-bleed with 100px of padding, not a 1240 box,
   so the offset is measured from the viewport edge: 1440 - 100 - 86 = 1254. */
/* Live's h1 is shrink-to-fit (134px around "For Sale"), not a full-width block. */

/* Live's row is exactly its thumbnail's height: a fixed 251x205 crop. */
/* head 838x86 = a 46px control over 40px of space; pager 838x60. */
.listing-main .results-bar {
    padding: 0 0 4rem; min-height: 8.6rem; align-items: center;
}
.listing-main .pagination { padding: 0; min-height: 6rem; align-content: center; }

/* =============== home hero: live's breakpoint ladder ======================
   Measured on live, width by width. It is NOT one tablet rule -- there are
   three bands under the desktop one, and ours had only a single 1024 switch,
   which is why the panel stayed small and high where live shows it full width
   and lower:

       >= 1140   header overlaid 140   hero 800 @0     panel 400 floating @263
       1024-1139 header solid    130   hero 630 @130   panel full width @760
        768-1023 header solid    130   hero 500 @130   panel full width @630
        <= 767   header solid    169   hero 400 @169   panel full width @569

   Live also drops the blue phone tab entirely below 1140 (measured 0x0). */
@media (max-width: 1139px) {
    .has-overlay-header .site-header {
        position: relative; background: var(--fg-header-bg);
    }
    .hero-cta-wrap {
        position: static; padding: 0; pointer-events: auto;
    }
    .hero-cta-wrap .container {
        max-width: none; padding: 0; justify-content: stretch;
    }
    .hero-cta { width: 100%; }
    .hero-cta__tab { display: none; }
    /* Live's submit is 90px wide here, not the full column. */
    .hero-cta .btn--primary { width: auto; min-width: 9rem; }
}
/* The bar stops overlapping and goes full-bleed once the panel is stacked:
   live's .rh_prop_search__fields spans the whole viewport at 980 and 1024 and
   starts immediately under the panel, where ours kept the desktop -47px pull
   and the container's 30px side padding. */
@media (max-width: 1139px) {
    /* Live hides this label-and-arrow entirely below 1140 (measured: 0x0,
       display:none) -- it exists only to sit under the floating toggle, which
       itself is gone below this width. Left visible it added a 31px tail
       after the search block on every page that carries this bar. */
    .home-search__adv, .prop-search__adv { display: none; }
    .prop-search .search-panel {
        margin-top: 0; height: auto;
    }
    .prop-search > .container { max-width: none; padding: 0; }
    .prop-search .search-panel > *,
    .prop-search .search-panel__buttons > * { height: auto; }
    /* height, not min-height: content (label + gap + select = 55px) is
       shorter than 84, so a min-height that also had to fight the shared
       2rem-padding rule at :870 landed on 96 instead. An explicit height
       centers the same content in exactly 84px, no clipping. Live's rows
       are 84px each and its button row 85: 84 + 85 = the 169 it measures at
       768, and 3x84 + 85 = the 336 it measures at 390. */
    .prop-search .search-panel__field { height: 8.4rem; padding: 1.6rem 2rem; }
    .prop-search .search-panel__buttons {
        grid-column: 1 / -1; height: 8.5rem;
    }
}
/* Live keeps the three fields side by side down to 768 and only stacks them
   below that. Ours stacked all four from 900, which made the block 462px tall
   against live's 169 -- the whole of the tablet page-height gap. */
@media (max-width: 1139px) and (min-width: 768px) {
    .prop-search .search-panel { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 767px) {
    .prop-search .search-panel { grid-template-columns: 1fr; }
}
/* Live's own breakpoint here is min-width:1140px (i.e. >=1140 gets the vh80
   rule above), so this needs to stop one pixel below that, at max-width:1139px
   -- not 1140, which the vh80 rule already claims exclusively at exactly 1140.
   This tier carries no floor on live (plain height:70vh, no min-height), so
   the base rule's 80rem floor must be reset to 0 or it would leak through as
   an 800px minimum on short viewports where 70vh is smaller than that. */
@media (max-width: 1139px) { .hero-slider { min-height: 0; height: 70vh; } }
@media (max-width: 1023px) { .hero-slider { height: 50rem; } }
@media (max-width: 767px)  { .hero-slider { height: 40rem; } }

/* Between the two existing breakpoints: at 1025-1139 the featured carousel
   keeps its full desktop layout (feat__card floating, 5.6rem side padding),
   but -6rem still assumes a 1240px-wide container -- at anything narrower it
   pushes the arrow past the viewport edge by up to 30px. Confirmed by
   measuring document.body.scrollWidth against clientWidth at 1050-1100. */
@media (max-width: 1139px) and (min-width: 1025px) {
    .feat__arrow--prev { left: .8rem; }
    .feat__arrow--next { right: .8rem; }
}

/* ---- Featured sidebar card body, measured against live's .rh_prop_card--block
   at a 1440px content width. Live's own numbers:
     .rh_prop_card__details   padding 25px (ours had 20px)
     h3                       15px/18px w700, margin-bottom 15px
     h3 > a                   19px/28.5px w500 #1a1a1a  (ours 18px/22.5px w500)
     meta wrapper             NO border, no top margin
     .rh_prop_card__priceLabel  a plain block: status ABOVE price, both flush
                              left, no rule above them
     status                   13px/15.6px w500 #1a1a1a, 4px above the price
     price                    22px w400 #0079b8
   Ours drew two horizontal rules (above the meta row and above the price row)
   that live does not have at all, and laid status/price out as a
   space-between row with the price right-aligned instead of stacked. */
.listing-side .prop-card__body { padding: 2.5rem; gap: 0; }
.listing-side .prop-card__title { font-size: 1.5rem; line-height: 1.8rem; font-weight: 700; margin: 0 0 1.5rem; }
.listing-side .prop-card__title a { font-size: 1.9rem; line-height: 2.85rem; font-weight: 500; color: var(--fg-heading); }
.listing-side .prop-card__meta { border-top: 0; padding-top: 0; margin-top: 0; }
.listing-side .prop-card__foot { display: block; border-top: 0; padding: 0 2.5rem 2.5rem; }
/* Both are <span>s, so making the FOOT a block is not enough to stack them --
   live's .rh_prop_card__status and .rh_prop_card__price are each display:block
   at the full 322px content width, which is what puts the price on its own line
   under the status. */
.listing-side .prop-status {
    display: block; font-size: 1.3rem; line-height: 1.56rem; font-weight: 500;
    color: var(--fg-heading); margin-bottom: .4rem;
}
.listing-side .prop-price { display: block; font-size: 2.2rem; line-height: normal; font-weight: 400; }

/* ---- Pagination, measured against live's .rh_pagination__btn at 1440px:
     button   40x40, border-radius 5000px (a pill), NO border, and a soft
              rgba(0,0,0,.05) 0 0 5px 2px shadow -- the same card shadow the
              property cards use, not an outline
     spacing  margin 0 10px per button (so 20px between two), margin-top 10px
     type     14px/16.8px weight 500
     current  background #0079b8, white
     next     background #fff, colour #808080, padding 0 15px
   Ours were 44x44 with a 1px border, no shadow, an 8px flex gap and 15px type.

   The 44px was the AAA touch-target size (WCAG 2.5.5). 40px still clears the
   AA minimum of 24x24 (2.5.8) comfortably, so this follows live without
   dropping below AA -- the same trade already recorded for the card action
   icons above.

   Scoped to .listing-main so any other pager in the site keeps its own size. */
.listing-main .pagination { gap: 0; }
.listing-main .pagination button {
    min-width: 4rem; min-height: 4rem; width: 4rem; height: 4rem;
    margin: 1rem; padding: 0;
    border: 0; border-radius: 5000px;
    box-shadow: var(--fg-shadow-card);
    font-size: 1.4rem; line-height: 1.68rem; font-weight: 500;
    color: var(--fg-body);
}
.listing-main .pagination button[aria-current="page"] { background: var(--fg-primary); color: #fff; }
/* Next/Previous are wider pills with 15px of side padding. */
.listing-main .pagination button[aria-label="Next page"],
.listing-main .pagination button[aria-label="Previous page"] {
    width: auto; padding: 0 1.5rem;
}
/* NAMED SUBSTITUTION: live draws the two chevrons with Font Awesome glyphs
   (i.fas.fa-angle-right on Next, i.fas.fa-angle-left on Previous). This rebuild
   ships no icon font -- the same decision already recorded for bootstrap-select's
   tickIcon -- so both are drawn in CSS from a rotated border corner. Two
   substitutions, not one. */
.listing-main .pagination button[aria-label="Next page"]::after,
.listing-main .pagination button[aria-label="Previous page"]::before {
    content: ""; width: .5rem; height: .5rem; flex: none;
    border-top: 2px solid currentColor; border-right: 2px solid currentColor;
}
.listing-main .pagination button[aria-label="Next page"]::after {
    margin-left: .7rem; transform: rotate(45deg);
}
.listing-main .pagination button[aria-label="Previous page"]::before {
    margin-right: .7rem; transform: rotate(-135deg);
}
