/* Vivly Living — Developments listing (Phase 4c, spec 2026-07-28).
   Four blocks: filter band (ported), grid, empty state, sticky-zone props.
   Template: template-developments.php. No Figma frame for this page
   (derived page, CLAUDE.md) — filter-band styling ported from the
   development single's dev-plots.css filter section (dev page IS on
   Figma), renamed .vl-plot-* -> .vl-listing-*. */

/* ── Block 1: Filter band — PORTED from css/dev-plots.css lines 39-746
   (the filter-bar rules only; the plot-card rules in that file are out of
   scope here — cards come from card-development.css instead).

   Two deliberate differences from the source, both required by this page's
   layout (no Figma frame; brief 2026-07-28):

   (a) Stuck pin offset: source pins beneath the dev/plot page's sticky
       ANCHOR bar (`top: var(--vl-dev-anchor-h)`). This listing page has no
       anchor bar — just the sitewide FIXED header — so the pin offset is
       `top: var(--vl-header-h)` (82px, variables.css) instead.

   (b) No heading shares this row (the dev single's "Available homes" H2
       shares the desktop row with its filter form — see dev-plots.css's
       comment on .vl-dev-plots__heading / .vl-plot-filters' -120px pull-up
       margin). This page's filter band has no heading to share a row with
       or pull up beside at ANY width, so:
         - the source's "≥1478px side-by-side" variant (heading left,
           filters right, flex-grow slide-left ::before) is DROPPED
           entirely — the band is always the source's "≤1477 stacked"
           layout: opaque cream band, controls left-aligned, no slide.
         - the source's ::before slide spacer (and its .is-stuck
           counterpart) is likewise omitted — it only ever existed to
           animate the row-sharing handoff; with no heading, there is
           nothing to slide clear of.
         - the ≤1024 toggle wrapper's `margin-top: calc(-1 * (heading
           height + 40px))` pull-up (source: pulls the toggle row up beside
           the "Available homes" heading) is dropped for the SAME reason —
           extending (b)'s logic to this breakpoint, since that calc's sole
           purpose is heading-row alignment. Flag to Liam: brief named only
           the ≥1478 variant explicitly; this ≤1024 pull-up was cut by the
           same "no heading" reasoning since porting it literally would
           pull the toggle ~110px up past this page's own 89px/60px top
           section padding, into the hero above. */

.vl-listing-filterbar {
    /* Desktop: no box of its own — the form is the sticky element directly
       in the container flow (source: .vl-plot-filterbar, unchanged). */
    display: contents;
}

.vl-listing-filterbar__toggle {
    /* Hidden on desktop; ≤1024 block below turns it on. */
    display: none;
    align-items: center;
    gap: 15px; /* matches the Sections toggle gap (source parity) */
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    color: var(--vl-black); /* node fill is literally #000000 */
    font-family: inherit;
    font-size: var(--vl-text-nav);
    font-weight: var(--vl-weight-regular);
    line-height: 1;
}

.vl-listing-filterbar__toggle svg {
    display: block;
    stroke: var(--vl-ink); /* icon family: header burger/X, Sections toggle */
    stroke-width: 2;
    stroke-linecap: round;
}

.vl-listing-filters {
    display: flex;
    flex-wrap: wrap; /* mobile/tablet fallback */
    align-items: center;
    gap: 30px;
    row-gap: 16px;
    /* Source's stacked-layout override (≤1477) baked in unconditionally per
       difference (b) above: opaque band, no pull-up, no slide. */
    /* Padding is ALWAYS 16px, and the negative top margin cancels it visually
       at rest — Liam live review 2026-07-28, third pass. Why not padding-only-
       when-stuck (the obvious reading of "no padding at rest"): that changed
       the box height at the moment of sticking, so the controls jumped 16px
       down and the pin engaged a beat "late" — Liam: "can it start at the same
       point as that padding top". With the padding permanent, the box geometry
       never changes: the sticky pin engages exactly when the PADDED top edge
       meets the header, the controls hold their position through the
       transition, and the visible stuck band still gets the dev page's 16px.
       The negative margin keeps the at-rest controls where zero padding put
       them (the band is invisible against the matching section background at
       rest, so only the controls' position is observable).
       Bottom: 54 + 16px padding = the 70px gap above the card grid;
       .vl-listing-grid's own margin-top is matched to 54 for the collapse
       (adjacent siblings collapse to max(54,54)=54, +16 padding = 70 visual;
       both display:contents parents hand these elements up to be direct
       .vl-container children in normal flow). KEEP THE THREE NUMBERS IN STEP:
       padding 16 / margin-top -16 / margin-bottom 54 + grid margin-top 54. */
    margin: -16px 0 54px;
    padding-block: 16px;
    position: sticky;
    top: var(--vl-header-h); /* (a) fixed header, no anchor bar on this page */
    z-index: 50; /* SAME layer as the fixed header (header.css .vl-nav) — Liam
                    live review 2026-07-28: the two read as one band of chrome,
                    and this lifts the bar above the sticky Contact tab (40),
                    which previously painted across it. They never overlap
                    spatially (header at top, bar pinned beneath it), so equal
                    z-index is safe; still below the nav drawer (60)/overlay
                    (55) so the off-canvas menu covers everything. */
    background: var(--vl-cream); /* always opaque — no heading row beneath to
                                     show through (difference b) */
    transition: background-color 0.25s ease;
}

/* This page is the FIRST sticky-pinned-under-the-fixed-header element on
   the site (final review, 2026-07-28): js/nav.js hides the fixed header
   (.vl-nav--hidden, translateY(-100%)) on scroll-down past 80px, but the
   pin above stays anchored at --vl-header-h regardless — leaving cards
   scrolling visibly through the now-empty 0..header-height strip above a
   detached-looking floating band. Collapse the pin to 0 in step whenever
   the header is hidden. :has() precedent: css/dev-anchor.css's
   shadow-suppression rules (`.vl-dev-anchor.is-stuck:has(~ ...)`). JS half
   of this fix: js/dev-listing-filters.js's stuckCheck() reads the header's
   hidden state at check time so `.is-stuck` still flips at the right
   line. */
body:has(.vl-nav--hidden) .vl-listing-filters {
    top: 0;
}

/* Stuck (pinned under the fixed header): full-bleed opaque band + chrome
   shadow, edge to edge like the header above (source: .vl-plot-filters.is-stuck,
   full-bleed mechanic ported verbatim; the ::before slide-left half of that
   source block is omitted per difference (b) — nothing to slide left of). */
.vl-listing-filters.is-stuck {
    box-shadow: var(--vl-shadow-chrome);
    /* Full-bleed, tab-lane aware (Liam ruling 2026-07-28 — sticky-contact.css
       reserves an extra --vl-tab-lane of RIGHT padding on zone sections so
       content clears the sticky Contact tab). That makes the container
       off-centre in the viewport, and the plain `calc(50% - 50vw)` trick
       measures from the container's own centre — so the band would overhang
       the left edge by half the lane and stop half a lane short on the
       right. Shifting each side by half the lane lands it flush on both.
       The token is 0px above 1680, where the collapse is exact: both
       calcs reduce to the original symmetric `calc(50% - 50vw)`. */
    margin-left:   calc(50% - 50vw + var(--vl-tab-lane, 0px) / 2);
    margin-right:  calc(50% - 50vw - var(--vl-tab-lane, 0px) / 2);
    padding-left:  calc(50vw - 50% - var(--vl-tab-lane, 0px) / 2);
    padding-right: calc(50vw - 50% + var(--vl-tab-lane, 0px) / 2);
    /* No padding-block here: it now lives permanently on the base rule (see
       its comment) so the box height can't change at the moment of sticking. */
    /* Clip the shadow's UPWARD bleed flush at the top edge (Liam live review
       2026-07-28: a grey line was visible above the stuck bar).
       --vl-shadow-chrome is `0 4px 16px` — only 4px of y-offset against a
       16px blur, so ~12px spills above the box and drew a line across the
       seam with the header. Sides/bottom stay open (-30px) so the band's own
       shadow still falls on the content below. Same fix, same values, as
       dev-anchor.css:318 / dev-plots.css:714 and this file's own dropdown
       panel rule. */
    clip-path: inset(0 -30px -30px -30px);
}


/* Shadow handoff, header → stuck filter bar (Liam live review 2026-07-28,
   the same rule he set for the dev page's anchor/filter pair on 2026-07-23:
   the BOTTOM-MOST stuck bar carries the shadow, never the seam between two).
   Here the upper bar is the FIXED HEADER, which carries --vl-shadow-chrome
   unconditionally (header.css) — so suppress it while this bar is stuck
   beneath it. Precedent for the :has() form: dev-anchor.css's own
   `.vl-dev-anchor.is-stuck:has(~ … .vl-plot-filters.is-stuck)` rules.
   Scoped safely: dev-listing.css is enqueued ONLY on this template, so no
   other page's header is touched. Both selectors cover the two breakpoint
   modes — the form is the sticky element ≥1025, the wrapper ≤1024. */
body:has(.vl-listing-filters.is-stuck) .vl-nav,
body:has(.vl-listing-filterbar.is-stuck) .vl-nav {
    box-shadow: none;
}

.vl-listing-filters__reset {
    padding: 0;
    border: 0;
    background: none;
    color: var(--vl-black); /* node fill is literally #000000 */
    font: inherit;
    font-size: var(--vl-text-nav); /* 18px */
    font-weight: var(--vl-weight-regular);
    line-height: 22px;
    text-decoration: underline;
    cursor: pointer;
}

.vl-listing-filters__check {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 8px; /* node-derived: label left 1007 - box right (949+50=999) */
    color: var(--vl-black);
    font-size: var(--vl-text-nav);
    font-weight: var(--vl-weight-regular);
    line-height: 22px;
    cursor: pointer;
}

/* Real checkbox sits transparent over the visual box (accessible, keyboard
   + screen-reader operable) rather than styling the native control directly. */
.vl-listing-filters__check-input {
    position: absolute;
    inset: 0;
    width: 50px;
    height: 50px;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.vl-listing-filters__box {
    position: relative;
    flex-shrink: 0;
    width: 50px; /* node 3:876: 50x50 square */
    height: 50px;
    border: 1px solid var(--vl-ink); /* node fill #32383b */
    border-radius: var(--vl-radius-field); /* 10px, exact node match */
    background: var(--vl-cream); /* node fill #eee9de */
}

.vl-listing-filters__check-input:checked + .vl-listing-filters__box {
    background: var(--vl-ink);
}

.vl-listing-filters__check-input:checked + .vl-listing-filters__box::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 45%;
    width: 22px;
    height: 12px;
    border-left: 3px solid var(--vl-white);
    border-bottom: 3px solid var(--vl-white);
    transform: translate(-50%, -50%) rotate(-45deg);
}

.vl-listing-filters__check-input:focus-visible + .vl-listing-filters__box {
    outline: 2px solid var(--vl-cornflower);
    outline-offset: 2px;
}

.vl-listing-filters__selects {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px; /* node-derived: 1350-1340, 1560-1550, both exact */
}

.vl-listing-filters__select {
    position: relative;
}

/* A select with a real value chosen reads black, not placeholder-grey —
   .has-value toggled by dev-listing-filters.js's sibling dev-filters.js
   convention (source parity comment). */
.vl-listing-filters__select select.has-value {
    color: var(--vl-black);
}

.vl-listing-filters__select select {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    width: 200px; /* node 3:856/3:869/3:873 */
    height: 50px;
    border: 1px solid var(--vl-ink);
    border-radius: var(--vl-radius-field); /* 10px, exact node match */
    background: var(--vl-cream);
    padding: 0 40px 0 20px; /* left: node-derived 1160-1140; right: clears the chevron */
    color: var(--vl-grey); /* node text fill #90918c — the "placeholder" state */
    font: inherit;
    font-size: var(--vl-text-nav); /* 18px */
    font-weight: var(--vl-weight-regular);
    line-height: 22px;
    cursor: pointer;
}

.vl-listing-filters__select select:focus-visible {
    outline: 2px solid var(--vl-cornflower);
    outline-offset: 2px;
}

/* Chevron: node vector 21x11, positioned ~9px in from the box's right edge,
   vertically centred (source parity — see dev-plots.css for the full node
   metadata trail). */
.vl-listing-filters__select::after {
    content: '';
    position: absolute;
    top: 50%;
    right: 9px;
    width: 21px;
    height: 11px;
    transform: translateY(-50%);
    background-color: var(--vl-ink);
    -webkit-mask: url("../assets/icon-chevron-down.svg") no-repeat center / contain;
    mask: url("../assets/icon-chevron-down.svg") no-repeat center / contain;
    pointer-events: none;
    /* Vendor sets .ts-control { z-index: 1 }, lifting it above this ::after
       chevron once Tom Select mounts (the enquiry buyer-position lesson,
       commit 4695055). */
    z-index: 2;
}

/* ── Tom Select control skin (filter-dropdowns spec 2026-07-31): the
   .ts-control adopts EXACTLY the native select construction above; the
   PANEL comes from the shared css/ts-dropdown.css (vl-ts scope, mounted by
   js/ts-filters.js). The native select is NOT pre-hidden — no-JS keeps the
   working native control (spec §4); TS hides it itself on mount, so the
   brief native flash before footer JS runs is accepted. Selector depth
   (.ts-wrapper ancestor) deliberately matches the vendor's own
   `.ts-wrapper.single.input-active .ts-control` (0,3,0) — ours loads later,
   so ties resolve our way and the click-to-open state can't flash the
   vendor's white ground over the cream. */
.vl-listing-filters__select .ts-wrapper {
    width: 200px; /* = the native select's width above */
}

.vl-listing-filters__select .ts-wrapper .ts-control {
    width: 100%;
    height: 50px;
    min-height: 50px;
    align-items: center;
    flex-wrap: nowrap;
    border: 1px solid var(--vl-ink);
    border-radius: var(--vl-radius-field); /* 10px */
    background: var(--vl-cream);
    padding: 0 40px 0 20px; /* right clears the chevron, as the native select */
    font-size: var(--vl-text-nav); /* 18px */
    font-weight: var(--vl-weight-regular);
    line-height: 22px;
    cursor: pointer;
    box-shadow: none;
}

/* Selected-value text: black with a real value, placeholder-grey on the ""
   option — the native has-value convention, now pure CSS off the TS item's
   own data-value (the filter scripts' class toggling keeps running on the
   hidden native select, harmlessly). */
.vl-listing-filters__select .ts-wrapper .ts-control .item {
    color: var(--vl-black);
}

.vl-listing-filters__select .ts-wrapper .ts-control .item[data-value=""] {
    color: var(--vl-grey); /* node text fill #90918c — the placeholder state */
}

.vl-listing-filters__select .ts-wrapper.focus .ts-control {
    outline: 2px solid var(--vl-cornflower);
    outline-offset: 2px;
    box-shadow: none;
}

/* ── Tablet / mobile: filters collapse behind the "Filters" toggle
   (source: dev-plots.css's `@media (max-width: 1024px)` block, ported —
   tablet = 768-1024 inclusive, CLAUDE.md). Two open modes, one DOM:
     · bar unstuck -> form in flow under the toggle row, pushes the grid down
     · bar stuck   -> form is an absolute dropdown panel under the pinned bar
   dev-listing-filters.js toggles .is-open AND .is-stuck on
   .vl-listing-filterbar (the WRAPPER) at this width — NOT on the form
   (Task 5 review finding) — so every rule below targets the bar's classes,
   matching the source's own ≤1024 convention (which already keyed off the
   bar, not the form, at this width). */
@media (max-width: 1024px) {
    .vl-listing-filterbar {
        /* DELIBERATELY NO `margin-top` here. Source (dev-plots.css .vl-plot-filterbar)
           sets `margin-top: calc(-1 * (var(--vl-text-display) + 40px))` to pull
           this row up beside the "Available homes" heading it shares. This page
           has NO heading in the filter row at any width (confirmed against
           template-developments.php) — porting that calc literally would pull
           the toggle ~110px upward, past this page's own 89px (tablet) / 60px
           (mobile) top section padding, overlapping the hero section above.
           Dropped for the same "no heading" reason as difference (b) above;
           flagged to Liam since the brief's text named only the ≥1478 desktop
           variant explicitly, not this ≤1024 pull-up. */
        display: flex;
        flex-wrap: wrap; /* row 1 = the toggle alone (no spacer element in
                             this port's DOM — difference (b) above dropped
                             the heading this row shared with in the
                             source); the open form wraps to row 2. */
        position: sticky;
        top: var(--vl-header-h); /* (a) pins under the fixed header */
        z-index: 50; /* same layer as the header — see the desktop rule's note
                        (Liam live review 2026-07-28); also lifts the pinned
                        Filters row and its dropdown panel above the sticky
                        Contact tab (40). */
        margin-bottom: 50px; /* Liam live review 2026-07-28: 50px below the
                                 Filters row on tablet AND mobile. Collapses
                                 with .vl-listing-grid's margin-top to
                                 whichever is larger — so that is set to 50
                                 too (below), giving exactly 50, not a double
                                 gap and not the larger of two values. */
        pointer-events: none; /* row has no heading to overlay in THIS page,
                                  but click-through is harmless and keeps
                                  parity with the source's toggle-row
                                  mechanics below */
        transition: background-color 0.25s ease;
    }

    /* Auto-hiding header (final review, 2026-07-28) — see the desktop
       .vl-listing-filters rule above for the full explanation; same fix,
       this breakpoint's sticky element (the wrapper, not the form). */
    body:has(.vl-nav--hidden) .vl-listing-filterbar {
        top: 0;
    }

    .vl-listing-filterbar__toggle {
        display: inline-flex;
        pointer-events: auto;
    }

    /* Pinned: full-bleed cream band + chrome shadow (source parity). */
    .vl-listing-filterbar.is-stuck {
        background: var(--vl-cream);
        box-shadow: var(--vl-shadow-chrome);
        /* Tab-lane aware full-bleed — same correction and reasoning as the
           desktop .vl-listing-filters.is-stuck rule above. */
        margin-left:   calc(50% - 50vw + var(--vl-tab-lane, 0px) / 2);
        margin-right:  calc(50% - 50vw - var(--vl-tab-lane, 0px) / 2);
        padding-left:  calc(50vw - 50% - var(--vl-tab-lane, 0px) / 2);
        padding-right: calc(50vw - 50% + var(--vl-tab-lane, 0px) / 2);
        padding-block: 15px; /* breathing room around the pinned Filters row */
        pointer-events: auto;
        /* Clip the shadow's UPWARD bleed (Liam live review 2026-07-28: still
           visible above the bar on tablet/mobile after the desktop fix —
           --vl-shadow-chrome is `0 4px 16px`, so ~12px spills above the box
           and paints over the header, which this bar sits on top of at the
           same z-index). The source (dev-plots.css) only ever clipped the
           dropdown PANEL, never the bar, so this case was never covered.
           ⚠️ Bottom is -100vh, not the panel's -30px: this element CONTAINS
           the absolutely-positioned dropdown panel, and a -30px bottom would
           clip the open panel to 30px. 100vh can never be too small since the
           panel is bounded by the viewport. */
        clip-path: inset(0 -30px -100vh -30px);
    }

    .vl-listing-filters {
        display: none;
        position: static; /* the WRAPPER is the sticky element at this width */
        width: 100%; /* full-width flex item — wraps below the toggle row */
        margin: 0;
        padding: 40px 0 0;
        gap: 16px;
        row-gap: 20px; /* checkbox-row <-> selects-row */
        pointer-events: auto;
    }

    .vl-listing-filterbar.is-open .vl-listing-filters {
        display: flex;
    }

    /* Dropdown mode: floats over the grid instead of pushing it. The stuck
       wrapper is full-bleed, so left/right 0 span the viewport; --vl-edge
       restores the content gutters inside the panel. */
    .vl-listing-filterbar.is-stuck .vl-listing-filters {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        background: var(--vl-cream);
        box-shadow: var(--vl-shadow-chrome);
        clip-path: inset(0 -30px -30px -30px); /* shadow blur clipped flush at
                                                   the top edge only (source
                                                   parity) */
        padding: 20px var(--vl-edge) 24px;
    }

    /* Reset moves to the right-hand end of the checkbox row. These `order`
       values are what preserve this tablet/mobile layout after Liam's
       2026-07-28 DOM move (the checkbox now sits LAST in the form, for the
       desktop row): checkbox keeps the default order 0, so the visual result
       here is unchanged — row 1 = checkbox … reset, row 2 = full-width
       selects. Don't drop these declarations assuming DOM order suffices. */
    .vl-listing-filters__reset {
        order: 1;
        margin-left: auto;
    }

    .vl-listing-filters__selects {
        order: 2;
        width: 100%;
        gap: 10px;
    }

    /* The three selects share the full row width equally at this width; the
       767px block below stacks them instead (flex: 1 is inert in a column). */
    .vl-listing-filters__select {
        flex: 1;
    }

    .vl-listing-filters__select select {
        width: 100%;
    }
}

@media (max-width: 767px) {
    .vl-listing-filters__select select {
        width: 100%;
    }

    .vl-listing-filters__select .ts-wrapper {
        width: 100%; /* tracks the native select's own rule above */
    }

    .vl-listing-filters__selects {
        width: 100%;
        flex-direction: column;
        align-items: stretch;
    }
}

/* Source's `@media (max-width: 500px)` block (dev-plots.css ~752-769) tightens
   the toggle-row's own gap-to-grid from 40px to 30px on small phones (Liam,
   2026-07-24), plus a heading margin and an `::before` display:none — both
   moot here (no heading, no `::before` spacer in this port). The 40->30
   tightening itself is DELIBERATELY NOT carried over: `.vl-listing-filterbar`'s
   margin-bottom (40px, set in the ≤1024 block above) collapses with
   `.vl-listing-grid`'s own margin-top, which is ALSO 40px at this width
   (block 2, ≤767 override) — adjacent-sibling collapse takes the larger of
   the two margins, so a 500px-only drop to 30px on the filterbar side alone
   would be fully absorbed by the grid's 40px and produce no visible change.
   Porting it would just be dead CSS; flagging the omission explicitly per
   review round 1's ask rather than silently dropping it. */

/* ── Block 2: Listing grid — flat, NO snap carousel at any width (spec: a
   listing must show everything; the ≤1280 carousel ruling covers teaser
   rows only). */
.vl-listing {
    padding-block: 89px 100px; /* template-family band paddings (plot-intro/morehomes reads) */
}

.vl-listing-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 30px; /* card-grid gap, node-read lineage (card-development.css) */
    margin-top: 54px; /* filter band -> grid: 54 collapses with the bar's own
                          54 margin-bottom, + the bar's 16px bottom padding =
                          the dev page's 70px gap. Keep in step with the three
                          numbers listed on .vl-listing-filters. */
}

/* Four across on wide desktop (client request via Liam, 2026-08-25). The base
   above stays THREE, and this only takes over from 1600px up, deliberately:
   the dev card is DESIGNED at 513px wide (Figma node 36:13, 513x342 — the
   source of the media box's aspect ratio), and four in a 1600px container is
   377px, 26% under that. The media box scales (aspect-ratio), but the 30px
   title and 30px body padding do not, so names wrap sooner the narrower this
   gets. Holding 4-up to >=1600 keeps the card at 357-377px, which is about
   the narrowest the SAME card already ships at elsewhere (380px at the top of
   the 3-up band); letting 4-up run down to 1281 would have put it at 277px,
   narrower than anywhere it renders today.
   Safe to sit above the max-width blocks below — a min-width query cannot
   overlap them — but it is placed here, immediately after the base rule, per
   this file's own ordering warning (media queries add no specificity, so a
   later same-specificity base rule wins regardless of the query).
   NOT applied to the plot grid, the related-developments row or the Home
   showcase: those keep 3-up. */
/* ⚠️ THE 4-UP RULE LIVED HERE AND WAS REMOVED 2026-08-26, one day after it
   shipped — not because it was wrong, but because the container scale changed
   underneath it. It read:

       @media (min-width: 1600px) {
           .vl-listing-grid { grid-template-columns: repeat(4, 1fr); }
       }

   At the designed 1600px container that gave 377px cards, already 26% under
   the card's 513px design width (Figma node 36:13) and the reason it was gated
   to wide desktop rather than applied outright. At the 1280px container set in
   variables.css it would give **297px** — narrower than the 277px figure that
   was explicitly rejected when the rule was written, so it cannot stay.

   RESTORE THIS RULE if --vl-container ever goes back to 1600px. It is named in
   the REVERT SWITCH block in variables.css for exactly that reason. */

/* Equal card heights across a row (Liam live review 2026-07-28). The GRID
   ITEM here is .vl-listing-item — the filter wrapper this page adds — not
   the card, so the grid's default align-items:stretch stopped at the
   wrapper and each .vl-dev-card sized to its OWN content: a development
   whose name wraps to two lines grew a taller body, and the media boxes and
   CTA rows stopped lining up across the row. Passing the stretched height
   down fixes it at the root, so the card's existing internals do their job
   (.vl-dev-card__link is height:100% and .vl-dev-card__body is flex:1 —
   they just had an auto-height parent to resolve against).
   NOTE for future pages: the plot grid, related-developments row and
   more-homes row all place cards as DIRECT grid children, so they stretch
   correctly and need nothing — this is only needed where a wrapper sits
   between the grid and the card. */
.vl-listing-item {
    display: flex;
}

.vl-listing-item > .vl-dev-card {
    flex: 1;
}

.vl-listing-item[hidden] {
    display: none; /* higher specificity than the flex rule above — keep it after */
}

/* ── Tablet + mobile gaps around the Filters row = 50px (Liam live review
   2026-07-28, measured off the live render). ABOVE = this section's own top
   padding (was 89px inherited from desktop at tablet, 60px at mobile);
   BELOW = the grid's margin-top, matched to the filterbar's 50px
   margin-bottom so the collapse resolves to exactly 50 rather than the
   larger of two values.
   ⚠️ PLACEMENT IS LOAD-BEARING: this block must sit AFTER the base
   `.vl-listing` / `.vl-listing-grid` rules above. Media queries add no
   specificity, so an identical-specificity base rule later in the file wins
   regardless of the query — which is exactly what silently killed the first
   attempt at this (the override was written into the ≤1024 filter-band block
   further up, and the base 89px/54px rules below simply beat it).
   ⚠️ --vl-sticky-wait must track the first section's top padding (CLAUDE.md)
   — its 50px override is in the sticky-zone block at the foot of this file. */
@media (max-width: 1024px) {
    .vl-listing {
        padding-top: 50px;
    }

    .vl-listing-grid {
        margin-top: 50px;
    }
}

@media (max-width: 1280px) {
    .vl-listing-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* Single column from 899 down — column parity with the plot grid
   (dev-plots.css: 3 cols → ≤1280 two → ≤899 one), so "large mobile"
   (768–899) behaves the same on both pages (Liam live review 2026-07-28).
   Was ≤767 here, which left large-mobile widths on a cramped 2-up. */
@media (max-width: 899px) {
    .vl-listing-grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 767px) {
    /* Filters row → cards drops 50px → 30px on MOBILE only (Liam live review
       2026-07-28). Both sides of the collapsing pair move together so the
       result is exactly 30, not the larger of the two.
       On "when not stuck": this margin only ever reads as a visible gap in
       the UNSTUCK state — while the bar is pinned, its flow space (and so
       this margin) stays back at its original scroll position and the cards
       simply pass beneath the floating band, whose own breathing room is its
       15px padding-block instead. So no .is-stuck-scoped selector is needed
       to satisfy that requirement. */
    .vl-listing-filterbar {
        margin-bottom: 30px;
    }

    .vl-listing-grid {
        margin-top: 30px;
    }

    .vl-listing {
        /* Top stays the 50px set in the ≤1024 block (Liam, 2026-07-28);
           bottom keeps the sitewide 60px mobile section-padding convention. */
        padding-bottom: 60px;
    }
}

/* ── Block 3: Zero-results state ──────────────────────────────────────────
   Structure and wording adopted from Renmore's our-homes zero state (Liam
   live review 2026-07-28): message, sub-line, "View all developments" CTA,
   then the fallback cards. The block sits BEFORE the grid in the DOM so it
   reads above the fallback cards, and carries `hidden` whenever anything
   matched — so it occupies no space in the normal case and the filter
   bar's margin still collapses straight onto the grid. */
.vl-listing-empty {
    margin: 0;
    text-align: center;
}

.vl-listing-empty__msg {
    margin: 0;
    font-size: var(--vl-text-card-title); /* 24px — a notch above body so the message carries */
    font-weight: var(--vl-weight-semibold);
    line-height: 1.2;
}

.vl-listing-empty__sub {
    margin: 16px 0 0;
    font-size: var(--vl-text-body);
    font-weight: var(--vl-weight-regular);
}

/* (Renmore's "View all developments" CTA is deliberately not ported — Liam,
   2026-07-28: the fallback already shows ALL current developments, so the
   button would reset to the set already on screen; the filter bar's own
   Reset link clears the controls.) */

/* Hard-empty (no developments in HBP at all) — Renmore's own string. */
.vl-listing-none {
    margin: 0;
    text-align: center;
    font-size: var(--vl-text-body);
}

/* .vl-visually-hidden promoted to css/base.css (final review, 2026-07-30 —
   template-updates.php became the second consumer). Rule lives there now. */

/* ── Block 4: Sticky-zone props (CLAUDE.md rule — wait = first section's
   top padding, stop = last section's bottom padding; this page has ONE
   section in the zone).
   NOTE the `-php` suffix below is REQUIRED, not a typo to "tidy away" —
   CORRECTED, final review 2026-07-28: an earlier version of this comment
   claimed the non-`-php` form was "entirely inert" on this page. It isn't
   — WP core's get_body_class() emits BOTH classes here: the basename
   without its extension (`page-template-template-developments`) AND the
   literal stored template slug with '.' -> '-' (`page-template-template-
   developments-php`, from `template-developments.php`), so either
   selector currently matches. Keep the `-php` form anyway: it's the
   exact, unmodified sanitize_html_class( str_replace( '.', '-',
   get_page_template_slug() ) ) transform WP core itself derives the class
   from, so it can't be silently dropped by a future WP change to the
   basename-only class the way a "tidied" selector could. ── */
body.page-template-template-developments-php .vl-sticky-zone {
    --vl-sticky-wait: 89px;
    --vl-sticky-stop: 100px;
}

/* ≤1024: the section's top padding became 50px (Liam, 2026-07-28), and the
   rule is that --vl-sticky-wait tracks it. */
@media (max-width: 1024px) {
    body.page-template-template-developments-php .vl-sticky-zone {
        --vl-sticky-wait: 50px;
    }
}

@media (max-width: 767px) {
    body.page-template-template-developments-php .vl-sticky-zone {
        --vl-sticky-wait: 50px; /* = the ≤1024 top padding, unchanged at mobile */
        --vl-sticky-stop: 60px;
    }
}

/* ── Sticky Contact tab vs the stuck filter bar (Liam live review 2026-07-28)
   This is the only page where both exist (dev/plot singles disable the tab),
   and they collide in exactly ONE state: header VISIBLE and filter bar STUCK.
   Geometry: the stuck bar is 82px tall (50px control pills + 2×16px padding)
   pinned at --vl-header-h (82px), so it owns 82–164px; the tab's default
   --vl-sticky-top is 100px, putting its top 64px behind the band (the bar is
   z-index 50, above the tab's 40).
   Scrolling DOWN is already fine: nav.js hides the header, the bar's pin
   collapses to top:0 (the :has(.vl-nav--hidden) rule above) so it owns
   0–82px and clears the tab entirely. So the drop is applied ONLY while the
   header is on screen — Liam's call: "only drop it lower on scroll up".
   180px = the band's 164px bottom + a 16px gap. Two selectors for the two
   breakpoint modes (form is the sticky element ≥1025, wrapper ≤1024). */
body.page-template-template-developments-php:has(.vl-listing-filters.is-stuck):not(:has(.vl-nav--hidden)) .vl-sticky-zone,
body.page-template-template-developments-php:has(.vl-listing-filterbar.is-stuck):not(:has(.vl-nav--hidden)) .vl-sticky-zone {
    --vl-sticky-top: 180px;
}

/* Slide rather than jump, timed to the header's own 0.3s slide (header.css)
   so the tab travels down as the header travels in. Restates the base
   background-color transition (sticky-contact.css) because this is a
   shorthand; scoped to this template so no other page's tab is affected. */
body.page-template-template-developments-php .vl-sticky-contact {
    transition: background-color 0.2s ease, top 0.3s ease;
}
