/* ==========================================================================
   QS Property Showcase

   The single stylesheet for this module. Per spec section 1 there are no inline
   styles and no <style> blocks in views, and every colour / radius / font
   decision below is a token. A designer restyling for a client should only ever
   need to touch the :root block -- the rules underneath reference tokens only.

   All classes are prefixed qs- so nothing collides with the site theme.
   Bootstrap 5.2 is loaded by the skin; this file only adds what Bootstrap
   utilities cannot express.
   ========================================================================== */

:root {
    /* Colour */
    --qs-primary: #ab955a;
    --qs-primary-hover: #c2ae74;
    --qs-primary-contrast: #1f2328;
    --qs-primary-strong: #75642f;
    --qs-primary-wash: #f3efe4;
    --qs-surface: #ffffff;
    --qs-surface-sunken: #f7f5ef;
    --qs-control-bg: #fcfbf8;
    --qs-ink: #373b38;
    --qs-ink-soft: #626963;
    --qs-line: #e2dccd;

    /* Feedback. Separate from the listing status colours below: those label a property,
       these label an outcome of something the visitor just did. */
    --qs-success: #1a7f4b;
    --qs-success-bg: #e8f6ee;
    --qs-success-line: #b7e0c8;
    --qs-danger: #b42318;
    --qs-danger-bg: #fdeceb;
    --qs-danger-line: #f5c2c0;

    /* Status */
    --qs-sold-bg: #b42318;
    --qs-sold-ink: #ffffff;
    --qs-pending-bg: #b54708;
    --qs-pending-ink: #ffffff;
    --qs-draft-bg: #656d76;
    --qs-draft-ink: #ffffff;

    /* Shape */
    --qs-radius: 8px;
    --qs-radius-sm: 4px;
    --qs-shadow: 0 1px 3px rgba(55, 59, 56, .10);

    /* Rhythm */
    --qs-gap: 20px;
    --qs-gap-sm: 10px;

    /* Motion. Understated and quick: the card reacts immediately, the image drifts.
       Every value here is a token so a designer can retime the whole module from one
       block. Only transform and opacity are animated, plus box-shadow for depth --
       nothing that triggers layout. */
    --qs-ease-out: cubic-bezier(0.2, 0, 0, 1);
    --qs-dur-hover: 180ms;   /* card lift */
    --qs-dur-image: 400ms;   /* image drift, deliberately slower than the lift */
    --qs-dur-enter: 320ms;   /* card entering the grid */
    --qs-dur-leave: 150ms;   /* card leaving the grid */
    --qs-dur-move: 320ms;    /* remaining cards settling into new positions */
    --qs-dur-sync: 150ms;    /* card <-> pin highlight */
    --qs-dur-imgfade: 250ms; /* lazy image reveal */
    --qs-dur-bounce: 640ms;  /* one hop of the active pin */
    --qs-ease-bounce: cubic-bezier(0.33, 0, 0.33, 1);
    --qs-pin-rise: -7px;     /* how far the active pin hops */
    --qs-pin-scale: 1.06;
    --qs-stagger: 30ms;      /* per-card cascade, capped at --qs-stagger-cap cards */
    --qs-lift: -3px;
    --qs-image-zoom: 1.04;
    --qs-enter-rise: 10px;
    --qs-shadow-raised: 0 10px 28px rgba(55, 59, 56, .15);
    --qs-ring: 0 0 0 2px var(--qs-primary);
    /* For focus rings on a filled control, where the ring needs a gap to stay visible. */
    --qs-ring-offset: 0 0 0 2px var(--qs-surface), 0 0 0 4px var(--qs-primary);

    /* Layout.

       --qs-nav-h and --qs-filters-h are MEASURED AT RUNTIME and written onto the module root
       by module.js, so the values here are only what applies before the first measurement (and
       in the no-JS case). Editing them is not how you retune the header offset -- the script
       reads the real header, including a collapsed one and DNN's control panel bar.

       --qs-nav-h  : whatever fixed/sticky chrome occupies the top of the viewport. 0 when the
                     site header simply scrolls away.
       --qs-filters-h : the module's own sticky filter bar, which sits directly below it. */
    --qs-nav-h: 64px;
    --qs-filters-h: 0px;
    --qs-list-col: 60%;
    --qs-map-col: 40%;
    --qs-card-ratio: 3 / 2;

    /* Detail page split, as flex-grow ratios -- 6 / 4 is 60 / 40. See .qs-detail-grid for
       why these are ratios and not percentages. */
    --qs-gallery-col: 6;
    --qs-info-col: 4;

    /* Detail thumbnails per row. A count, not a minimum width, because the thumbnails
       should be a fraction of the gallery rather than an absolute size -- at 150px they
       stayed 150px whether the column was 600px or 1400px wide, which is what made them
       look like a contact sheet under a full-width hero. Three across puts each at a third
       of the gallery, in the spirit of the reference design without making a listing with a
       dozen photographs unreasonably tall. Mobile drops to two. */
    --qs-thumb-cols: 3;

    /* Ceiling on the detail hero at desktop widths, so a tall or portrait image cannot push
       the thumbnails off the first screen. Not applied on mobile, where the column is narrow
       enough that the image never gets near it. */
    --qs-hero-max-h: 812px;

    /* Height of the mobile Filters/Map bar. The slider sits above it, so both read this
       one value rather than each carrying its own hand-tuned offset that can drift. */
    --qs-mobilebar-h: 60px;
    --qs-sheet-max-h: 78vh;
    --qs-dur-sheet: 260ms;
    --qs-scrim: rgba(0, 0, 0, .45);
}

.qs-showcase {
    color: var(--qs-ink);
    background: var(--qs-surface);
    font-family: "Manrope", system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* Bootstrap 5.2 comes from the skin. Keep its layout and accessibility behaviour, but
   close every route by which the stock blue can leak into this module. */
.qs-showcase .form-control,
.qs-showcase .form-select,
.qs-showcase .btn {
    font-family: inherit;
}

.qs-showcase .form-control,
.qs-showcase .form-select {
    color: var(--qs-ink);
    background-color: var(--qs-control-bg);
    border-color: var(--qs-line);
}

.qs-showcase .form-control:focus,
.qs-showcase .form-select:focus {
    color: var(--qs-ink);
    background-color: var(--qs-surface);
    border-color: var(--qs-primary);
    box-shadow: 0 0 0 .2rem rgba(171, 149, 90, .18);
}

.qs-showcase .form-check-input:focus {
    border-color: var(--qs-primary);
    box-shadow: 0 0 0 .2rem rgba(171, 149, 90, .18);
}

.qs-showcase .form-check-input:checked {
    background-color: var(--qs-primary);
    border-color: var(--qs-primary);
}

.qs-showcase .btn-link {
    color: var(--qs-primary-strong);
}

.qs-showcase .btn-link:hover,
.qs-showcase .btn-link:focus {
    color: var(--qs-ink);
}

.qs-showcase .btn-outline-secondary {
    color: var(--qs-ink);
    border-color: #9da39e;
}

.qs-showcase .btn-outline-secondary:hover,
.qs-showcase .btn-outline-secondary:focus,
.qs-showcase .btn-outline-secondary:active,
.qs-showcase .btn-outline-secondary.active {
    color: var(--qs-primary-contrast);
    background-color: var(--qs-primary);
    border-color: var(--qs-primary);
}

.qs-showcase .btn-light,
.qs-showcase .text-bg-light {
    color: var(--qs-ink) !important;
    background-color: var(--qs-primary-wash) !important;
    border-color: var(--qs-line) !important;
}

.qs-showcase .btn-close:focus {
    box-shadow: 0 0 0 .2rem rgba(171, 149, 90, .18);
}

/* ---------- Admin toolbar ---------- */

.qs-adminbar {
    display: flex;
    align-items: center;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap-sm) var(--qs-gap);
    background: var(--qs-surface-sunken);
    border-bottom: 1px solid var(--qs-line);
}

/* ---------- Filter bar ---------- */

.qs-filters {
    position: sticky;
    top: var(--qs-nav-h);
    z-index: 20;
    background: var(--qs-surface);
    border-bottom: 1px solid var(--qs-line);
}

.qs-filters-inner {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap-sm) var(--qs-gap);
}

.qs-filter-label {
    font-size: .8125rem;
    color: var(--qs-ink-soft);
}

/* A .qs-field is one label + control pair, shared between the desktop bar and the mobile
   sheet. In the bar it collapses to an inline pair sized to its content; the sheet restyles
   it below. The explicit width: auto replaces Bootstrap's .w-auto, which cannot be used
   here because the sheet needs the opposite and would have to fight its !important. */
.qs-filters-inner .qs-field {
    display: flex;
    align-items: center;
    gap: var(--qs-gap-sm);
}

.qs-filters-inner .qs-field .form-select {
    width: auto;
}

.qs-count {
    font-size: .8125rem;
    color: var(--qs-ink-soft);
    white-space: nowrap;
}

/* ---------- 60 / 40 split ---------- */

.qs-split {
    display: flex;
    align-items: flex-start;
}

.qs-list-col {
    flex: 0 0 var(--qs-list-col);
    max-width: var(--qs-list-col);
    padding: var(--qs-gap);
}

.qs-map-col {
    flex: 0 0 var(--qs-map-col);
    max-width: var(--qs-map-col);
    position: sticky;
    top: var(--qs-nav-h);
    height: calc(100vh - var(--qs-nav-h));
}

.qs-map {
    width: 100%;
    height: 100%;
    background: var(--qs-surface-sunken);
}

/* ---------- Cards ---------- */

.qs-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--qs-gap);
    /* Containing block for cards that are pulled out of flow while leaving. */
    position: relative;
}

.qs-card {
    position: relative;
    background: var(--qs-surface);
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius);
    overflow: hidden;
    box-shadow: var(--qs-shadow);
    /* Hovering a pin scrolls its card into view. The browser considers an element "in view"
       as soon as it is inside the viewport, which it can be while sitting underneath the
       fixed header and the sticky filter bar -- so the card has to reserve room for both. */
    scroll-margin-top: calc(var(--qs-nav-h) + var(--qs-filters-h) + var(--qs-gap-sm));
    transition:
        transform var(--qs-dur-hover) var(--qs-ease-out),
        box-shadow var(--qs-dur-hover) var(--qs-ease-out);
}

/* The lift is pointer-only. Keyboard focus gets the ring below but no displacement,
   which would otherwise shift the element a user is tracking with the caret. */
.qs-card:hover {
    transform: translateY(var(--qs-lift));
    box-shadow: var(--qs-shadow-raised);
}

/* Highlight is a shadow ring rather than a border-colour swap: it transitions on the
   compositor, and changing border-colour alone cannot be animated without also
   animating a paint-only property. */
.qs-card:hover,
.qs-card:focus-within,
.qs-card.qs-card-active {
    box-shadow: var(--qs-ring), var(--qs-shadow-raised);
}

.qs-card.qs-card-active {
    transition:
        transform var(--qs-dur-hover) var(--qs-ease-out),
        box-shadow var(--qs-dur-sync) var(--qs-ease-out);
}

.qs-card-draft {
    opacity: .6;
}

.qs-card-link {
    display: block;
    color: inherit;
    text-decoration: none;
}

.qs-card-media {
    position: relative;
    aspect-ratio: var(--qs-card-ratio);
    background: var(--qs-surface-sunken);
    /* Required, not decorative: without it the zoomed image spills over the card body
       and covers the title. The card's own overflow clips at the card edge, not here. */
    overflow: hidden;
}

.qs-card-media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    /* Starts hidden and is revealed on load, so lazy images do not pop in mid-scroll.
       The class is added on both load and error, so a broken image still becomes
       visible rather than staying invisible forever. */
    opacity: 0;
    transition:
        opacity var(--qs-dur-imgfade) var(--qs-ease-out),
        transform var(--qs-dur-image) var(--qs-ease-out);
}

.qs-card-media img.qs-img-ready {
    opacity: 1;
}

/* The image drifts slowly while the card lifts quickly -- the difference in duration is
   what makes it read as depth rather than as one moving object. */
.qs-card:hover .qs-card-media img.qs-img-ready {
    transform: scale(var(--qs-image-zoom));
}

.qs-card-media-empty {
    width: 100%;
    height: 100%;
    background: var(--qs-surface-sunken);
}

/* Land-partner identity travels with the property without competing with its status badge.
   The white plaque keeps transparent and dark logos readable against every photograph. */
.qs-card-developer-logo {
    position: absolute;
    right: 10px;
    bottom: 10px;
    z-index: 2;
    display: flex;
    width: 116px;
    height: 42px;
    align-items: center;
    justify-content: center;
    padding: 5px 8px;
    overflow: hidden;
    border: 1px solid rgba(55, 59, 56, .14);
    border-radius: 7px;
    background: rgba(255, 255, 255, .95);
    box-shadow: 0 3px 12px rgba(55, 59, 56, .16);
}

.qs-card-media .qs-card-developer-logo-image {
    display: block;
    width: auto;
    height: auto;
    max-width: 100px;
    max-height: 30px;
    object-fit: contain;
    opacity: 1;
    transform: none;
    transition: none;
}

.qs-card-body {
    padding: var(--qs-gap-sm) var(--qs-gap-sm) var(--qs-gap);
}

.qs-card-title {
    margin: 0 0 5px;
    color: var(--qs-ink);
    font-size: clamp(1.25rem, 1.15rem + .35vw, 1.45rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -.015em;
}

.qs-card-address {
    font-size: .875rem;
    color: var(--qs-ink-soft);
    margin: 0 0 var(--qs-gap-sm);
    line-height: 1.4;
}

.qs-card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: var(--qs-gap-sm);
}

.qs-card-tags .badge,
.qs-info .badge {
    padding: .38em .65em;
    border: 1px solid var(--qs-line);
    border-radius: 999px;
    font-weight: 500;
}

.qs-card-tags .badge {
    background: var(--qs-primary-wash) !important;
    color: var(--qs-ink) !important;
}

.qs-card-specs {
    display: flex;
    gap: 0;
    list-style: none;
    padding: 0;
    margin: 0 0 var(--qs-gap-sm);
    font-size: .875rem;
    color: var(--qs-ink-soft);
    font-variant-numeric: tabular-nums;
}

.qs-card-specs li + li::before {
    content: "\2022";
    margin: 0 8px;
    color: var(--qs-primary);
}

.qs-card-specs strong {
    color: var(--qs-ink);
    font-weight: 700;
}

.qs-card-price {
    font-size: 1.1875rem;
    font-weight: 700;
    color: var(--qs-primary-strong);
    margin: 0;
    font-variant-numeric: tabular-nums;
}

/* ---------- Reorder mode (editors) ---------- */

.qs-reorder-hint {
    margin: var(--qs-gap-sm) var(--qs-gap) 0;
}

.qs-cards-reordering .qs-card {
    cursor: grab;
}

.qs-cards-reordering .qs-card:active {
    cursor: grabbing;
}

/* Whole-card links swallow the drag gesture, so suppress them while reordering. */
.qs-cards-reordering .qs-card-link {
    pointer-events: none;
}

.qs-card-order {
    position: absolute;
    top: var(--qs-gap-sm);
    left: var(--qs-gap-sm);
    z-index: 3;
    min-width: 26px;
    height: 26px;
    padding: 0 6px;
    border-radius: 999px;
    background: var(--qs-primary);
    color: var(--qs-primary-contrast);
    font-size: .8125rem;
    font-weight: 700;
    line-height: 26px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.qs-card-edit {
    position: absolute;
    top: var(--qs-gap-sm);
    right: var(--qs-gap-sm);
    z-index: 2;
    border-radius: var(--qs-radius-sm);
}

/* ---------- Status badges ----------
   Status is conveyed by the badge text, never by colour alone. */

.qs-badge {
    position: absolute;
    top: var(--qs-gap-sm);
    left: var(--qs-gap-sm);
    padding: 3px 8px;
    border-radius: var(--qs-radius-sm);
    font-size: .75rem;
    font-weight: 700;
    letter-spacing: .04em;
}

.qs-badge-sold {
    background: var(--qs-sold-bg);
    color: var(--qs-sold-ink);
}

.qs-badge-pending {
    background: var(--qs-pending-bg);
    color: var(--qs-pending-ink);
}

.qs-badge-draft {
    top: auto;
    bottom: var(--qs-gap-sm);
    background: var(--qs-draft-bg);
    color: var(--qs-draft-ink);
}

.qs-empty {
    color: var(--qs-ink-soft);
    padding: var(--qs-gap);
}

/* ---------- Grid transitions (filtering, and the initial cascade) ----------
   Vue's TransitionGroup adds these classes. Entering cards rise and fade in with a
   per-card delay; leaving cards fade out and are taken out of flow so the survivors can
   animate to their new positions instead of snapping. */

.qs-card-enter-from {
    opacity: 0;
    transform: translateY(var(--qs-enter-rise));
}

/* Doubled class deliberately: .qs-card.qs-card-active also sets a transition, and would
   otherwise outrank a single-class rule here -- so a card that was hovered when it got
   filtered out would keep the hover timing and never fade. */
.qs-card.qs-card-enter-active {
    transition:
        opacity var(--qs-dur-enter) var(--qs-ease-out),
        transform var(--qs-dur-enter) var(--qs-ease-out);
}

.qs-card-leave-to {
    opacity: 0;
}

.qs-card.qs-card-leave-active {
    transition: opacity var(--qs-dur-leave) var(--qs-ease-out);
    /* Out of flow immediately, so the remaining cards' new positions -- and therefore the
       move animation below -- are calculated against the final layout. The element's own
       box is pinned in JS first (see onCardBeforeLeave); a grid child that goes absolute
       would otherwise jump to the container origin before fading. */
    position: absolute;
    /* Cannot be hovered or focused while it is on its way out. */
    pointer-events: none;
    z-index: 0;
}

.qs-card-move {
    transition: transform var(--qs-dur-move) var(--qs-ease-out);
}

/* Stagger. Done with nth-child rather than a bound inline delay so every timing value
   stays in this file; the cascade therefore follows visual position, which is what you
   want when only some cards re-enter after a filter change. Capped so a long list does
   not end up with a visibly late tail. */
.qs-card.qs-card-enter-active:nth-child(1) { transition-delay: 0ms; }
.qs-card.qs-card-enter-active:nth-child(2) { transition-delay: calc(var(--qs-stagger) * 1); }
.qs-card.qs-card-enter-active:nth-child(3) { transition-delay: calc(var(--qs-stagger) * 2); }
.qs-card.qs-card-enter-active:nth-child(4) { transition-delay: calc(var(--qs-stagger) * 3); }
.qs-card.qs-card-enter-active:nth-child(5) { transition-delay: calc(var(--qs-stagger) * 4); }
.qs-card.qs-card-enter-active:nth-child(6) { transition-delay: calc(var(--qs-stagger) * 5); }
.qs-card.qs-card-enter-active:nth-child(7) { transition-delay: calc(var(--qs-stagger) * 6); }
.qs-card.qs-card-enter-active:nth-child(8) { transition-delay: calc(var(--qs-stagger) * 7); }
.qs-card.qs-card-enter-active:nth-child(9) { transition-delay: calc(var(--qs-stagger) * 8); }
.qs-card.qs-card-enter-active:nth-child(n+10) { transition-delay: calc(var(--qs-stagger) * 9); }

/* ---------- Map pins ----------
   Only used by the AdvancedMarkerElement path; legacy markers are styled by the
   Maps API itself. */

.qs-pin {
    transition: transform var(--qs-dur-sync) var(--qs-ease-out);
    padding: 4px 8px;
    border-radius: 999px;
    background: var(--qs-primary);
    color: var(--qs-primary-contrast);
    font-size: .75rem;
    font-weight: 700;
    white-space: nowrap;
    box-shadow: var(--qs-shadow);
    border: 2px solid var(--qs-surface);
}

.qs-pin-sold {
    background: var(--qs-sold-bg);
    color: var(--qs-sold-ink);
}

.qs-pin-draft {
    background: var(--qs-draft-bg);
    color: var(--qs-draft-ink);
}

/* The active pin hops continuously while its card is hovered. This mirrors the legacy
   google.maps.Animation.BOUNCE that advanced markers do not provide -- AdvancedMarkerElement
   has no setAnimation, so the motion has to live here on the pin's own content element.
   Kept a little shorter and shallower than Google's default so it reads as a nudge. */
.qs-pin-active {
    z-index: 2;
    animation: qs-pin-bounce var(--qs-dur-bounce) var(--qs-ease-bounce) infinite;
}

@keyframes qs-pin-bounce {
    0%, 100% { transform: translateY(0) scale(var(--qs-pin-scale)); }
    45%      { transform: translateY(var(--qs-pin-rise)) scale(var(--qs-pin-scale)); }
}

.qs-map-error {
    padding: var(--qs-gap-sm);
    margin: 0;
    font-size: .875rem;
    color: var(--qs-ink-soft);
}

.qs-noscript-list {
    padding: var(--qs-gap);
}

/* ---------- Modals ----------
   Hand-rolled rather than Bootstrap's JS modal: the module ships no Bootstrap
   bundle of its own, and the theme's copy may or may not include the JS. */

.qs-modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1050;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: var(--qs-gap);
    background: rgba(0, 0, 0, .5);
    overflow-y: auto;
}

.qs-modal {
    width: 100%;
    max-width: 720px;
    margin: auto;
    background: var(--qs-surface);
    border-radius: var(--qs-radius);
    box-shadow: 0 10px 40px rgba(0, 0, 0, .3);
}

.qs-modal-sm {
    max-width: 460px;
}

.qs-modal-md {
    max-width: 680px;
}

.qs-modal-xl {
    max-width: 1140px;
}

.qs-property-editor-modal {
    display: flex;
    max-height: calc(100vh - (var(--qs-gap) * 2));
    flex-direction: column;
    overflow: hidden;
}

.qs-property-editor-modal .qs-modal-head,
.qs-property-editor-modal .qs-modal-foot {
    flex: 0 0 auto;
    background: var(--qs-surface);
}

.qs-property-editor-modal .qs-modal-body {
    min-height: 0;
    overflow-y: auto;
}

.qs-modal-head,
.qs-modal-foot {
    display: flex;
    align-items: center;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap-sm) var(--qs-gap);
}

.qs-modal-head {
    border-bottom: 1px solid var(--qs-line);
}

.qs-modal-head h2 {
    font-size: 1.125rem;
    margin: 0;
    flex: 1 1 auto;
}

.qs-modal-foot {
    border-top: 1px solid var(--qs-line);
    justify-content: flex-end;
}

.qs-modal-body {
    padding: var(--qs-gap);
}

.qs-editor-section {
    padding: 18px;
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius);
    background: var(--qs-surface);
}

.qs-editor-section + .qs-editor-section,
.qs-editor-columns,
.qs-editor-images {
    margin-top: var(--qs-gap);
}

.qs-editor-columns {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--qs-gap);
}

.qs-editor-columns .qs-editor-section {
    margin-top: 0;
}

.qs-editor-section-title {
    margin: 0 0 14px;
    color: var(--qs-ink);
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
}

.qs-property-editor-modal .form-label {
    margin-bottom: 5px;
    color: var(--qs-ink);
    font-size: .82rem;
    font-weight: 600;
}

.qs-delete-property {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

@media (max-width: 767.98px) {
    .qs-modal-backdrop {
        padding: 8px;
    }

    .qs-property-editor-modal {
        max-height: calc(100vh - 16px);
    }

    .qs-editor-columns {
        grid-template-columns: 1fr;
    }

    .qs-editor-section {
        padding: 14px;
    }

    .qs-property-editor-modal .qs-modal-foot {
        flex-wrap: wrap;
    }

    .qs-delete-property {
        width: 100%;
        margin-right: 0 !important;
        justify-content: center;
    }
}

/* ---------- Inquiry outcome messages ----------
   Two states share one shape: a coloured glyph carrying the meaning at a glance, and text
   carrying it properly. The glyphs are inline SVG in the script and inherit currentColor,
   so recolouring is a token change here and nothing else.

   Icons are aria-hidden throughout -- the wording has to stand alone for a screen reader,
   which is why neither message relies on the icon to say whether it worked. */

.qs-alert {
    display: flex;
    align-items: flex-start;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap-sm) var(--qs-gap-sm);
    margin-bottom: var(--qs-gap);
    border: 1px solid var(--qs-danger-line);
    border-radius: var(--qs-radius);
    background: var(--qs-danger-bg);
}

.qs-alert-icon {
    display: flex;
    flex: none;
    color: var(--qs-danger);
}

.qs-alert-glyph {
    width: 20px;
    height: 20px;
}

.qs-alert-body {
    min-width: 0;
}

.qs-alert-title {
    margin: 0;
    font-size: .9375rem;
    font-weight: 600;
    color: var(--qs-danger);
}

.qs-alert-line {
    margin: 2px 0 0;
    font-size: .875rem;
    color: var(--qs-ink);
}

/* Several validation failures are several lines. Joining them into one sentence -- the old
   behaviour -- made three separate corrections read as one instruction. */
.qs-alert-list {
    margin: 4px 0 0;
    padding-left: 1.1rem;
    font-size: .875rem;
    color: var(--qs-ink);
}

.qs-alert-list li + li {
    margin-top: 2px;
}

/* ---------- Sent confirmation ----------
   Replaces the form outright, so it is laid out as a small panel in its own right rather
   than as a bar pinned above one. */

.qs-result {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: var(--qs-gap) var(--qs-gap-sm) var(--qs-gap-sm);
}

.qs-result-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--qs-success-bg);
    border: 1px solid var(--qs-success-line);
    color: var(--qs-success);
    animation: qs-result-in var(--qs-dur-enter) var(--qs-ease-out) both;
}

.qs-result-glyph {
    width: 26px;
    height: 26px;
}

.qs-result-title {
    margin: var(--qs-gap-sm) 0 0;
    font-size: 1.125rem;
    font-weight: 600;
}

.qs-result-text {
    margin: 6px 0 0;
    max-width: 34ch;
    font-size: .9375rem;
    line-height: 1.5;
    color: var(--qs-ink-soft);
}

.qs-result-done {
    margin-top: var(--qs-gap);
    min-width: 140px;
}

/* Settles rather than bounces: the badge is a receipt, not a celebration. */
@keyframes qs-result-in {
    from { opacity: 0; transform: scale(.85); }
    to   { opacity: 1; transform: scale(1); }
}

/* ---------- Primary buttons ----------
   Bootstrap's .btn-primary ships white text, but skins routinely restyle it and some leave
   the label dark on a saturated fill -- which is what made "Back to all properties" hard to
   read until it was hovered. Scoped to the module so the fix never reaches the rest of the
   site, and specific enough (0,2,0) to beat a plain .btn-primary override in a skin. */

.qs-showcase .btn-primary {
    background-color: var(--qs-primary);
    border-color: var(--qs-primary);
    color: var(--qs-primary-contrast);
}

.qs-showcase .btn-primary:hover,
.qs-showcase .btn-primary:focus,
.qs-showcase .btn-primary:active {
    background-color: var(--qs-primary-hover);
    border-color: var(--qs-primary-hover);
    color: var(--qs-primary-contrast);
}

.qs-showcase .btn-primary:focus-visible {
    outline: none;
    box-shadow: var(--qs-ring-offset);
}

/* ---------- Property not available ----------
   The one screen a visitor reaches by accident -- a stale link, a sold home, a shared URL
   that outlived the listing. It gets the same treatment as the inquiry confirmation rather
   than being left as bare Bootstrap: something went wrong, so the page should still feel
   like part of the site. */

.qs-notfound {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    max-width: 32rem;
    margin: 0 auto;
    padding: calc(var(--qs-gap) * 2.5) var(--qs-gap) calc(var(--qs-gap) * 3);
}

.qs-notfound-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    border-radius: 50%;
    background: var(--qs-surface-sunken);
    border: 1px solid var(--qs-line);
    color: var(--qs-ink-soft);
}

.qs-notfound-icon svg {
    width: 34px;
    height: 34px;
}

.qs-notfound-title {
    margin: var(--qs-gap) 0 0;
    font-size: 1.5rem;
    line-height: 1.25;
}

.qs-notfound-text {
    margin: var(--qs-gap-sm) 0 0;
    max-width: 34ch;
    color: var(--qs-ink-soft);
    line-height: 1.5;
}

.qs-notfound-action {
    margin-top: calc(var(--qs-gap) * 1.25);
    padding-inline: var(--qs-gap);
}

/* ---------- Send spinner ---------- */

.qs-spinner {
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-right: 8px;
    vertical-align: -2px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: qs-spin 700ms linear infinite;
}

@keyframes qs-spin {
    to { transform: rotate(360deg); }
}

.qs-tax-list {
    list-style: none;
    padding: 0;
    margin: 0 0 var(--qs-gap);
}

.qs-tax-list li,
.qs-tax-add {
    display: flex;
    gap: var(--qs-gap-sm);
    align-items: center;
    margin-bottom: var(--qs-gap-sm);
}

.qs-tax-list .qs-tax-item {
    display: block;
    padding-bottom: var(--qs-gap-sm);
    border-bottom: 1px solid var(--qs-line);
}

.qs-tax-row,
.qs-tax-logo-controls {
    display: flex;
    align-items: center;
    gap: var(--qs-gap-sm);
}

.qs-tax-order {
    display: grid;
    grid-template-columns: 24px 28px 28px;
    flex: 0 0 auto;
    align-items: center;
    gap: 3px;
}

.qs-tax-position {
    color: var(--qs-ink-soft);
    font-size: .75rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.qs-tax-move {
    display: inline-flex;
    width: 28px;
    height: 28px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius-sm);
    background: var(--qs-surface);
    color: var(--qs-ink);
    font-size: .95rem;
    line-height: 1;
    cursor: pointer;
}

.qs-tax-move:hover:not(:disabled),
.qs-tax-move:focus-visible {
    border-color: var(--qs-primary);
    background: var(--qs-primary-wash);
}

.qs-tax-move:focus-visible {
    outline: 0;
    box-shadow: var(--qs-ring);
}

.qs-tax-move:disabled {
    opacity: .35;
    cursor: default;
}

.qs-tax-row .form-control {
    min-width: 0;
}

.qs-tax-logo-controls {
    flex-wrap: wrap;
    padding-top: 8px;
}

.qs-tax-logo-preview {
    display: flex;
    width: 96px;
    height: 42px;
    flex: 0 0 96px;
    align-items: center;
    justify-content: center;
    padding: 4px 7px;
    overflow: hidden;
    border: 1px solid var(--qs-line);
    border-radius: 6px;
    background: #fff;
    color: var(--qs-ink-soft);
    font-size: .7rem;
}

.qs-tax-logo-preview img {
    display: block;
    max-width: 80px;
    max-height: 30px;
    object-fit: contain;
}

.qs-tax-logo-button {
    margin: 0;
    cursor: pointer;
}

.qs-tax-logo-controls small {
    color: var(--qs-ink-soft);
}

.qs-tax-logo-error {
    margin: 6px 0 0 104px;
    color: var(--bs-danger, #842029);
    font-size: .8rem;
}

.qs-tax-order-error {
    margin: 0 0 var(--qs-gap);
    color: var(--qs-danger);
    font-size: .85rem;
}

/* ---------- Honeypot ----------
   Kept out of sight without display:none, which some bots skip. Real visitors
   never see or tab into it; anything in it means a bot. */

.qs-hp {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* ---------- Image manager ---------- */

.qs-section-title {
    font-size: 1rem;
    margin: 0 0 var(--qs-gap-sm);
}

.qs-dropzone {
    display: flex;
    min-height: 118px;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: var(--qs-gap);
    border: 2px dashed var(--qs-line);
    border-radius: var(--qs-radius);
    background: var(--qs-surface-sunken);
    color: var(--qs-ink);
    text-align: left;
    cursor: pointer;
    transition: border-color 150ms ease, background-color 150ms ease, box-shadow 150ms ease;
}

.qs-dropzone:hover,
.qs-dropzone:focus-within,
.qs-dropzone-active {
    border-color: var(--qs-primary);
    background: var(--qs-primary-wash);
    box-shadow: inset 0 0 0 1px var(--qs-primary);
}

.qs-dropzone-busy {
    cursor: progress;
    opacity: .72;
}

.qs-dropzone-icon {
    display: inline-flex;
    width: 48px;
    height: 48px;
    flex: 0 0 48px;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--qs-primary);
    color: var(--qs-primary-contrast);
    font-size: 1.6rem;
    font-weight: 700;
    line-height: 1;
}

.qs-dropzone-copy {
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.qs-dropzone-copy strong {
    font-size: 1rem;
}

.qs-dropzone-copy small {
    color: var(--qs-ink-soft);
}

.qs-image-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: var(--qs-gap-sm);
    list-style: none;
    padding: 0;
    margin: var(--qs-gap-sm) 0 0;
}

.qs-image-item {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: var(--qs-gap-sm);
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius-sm);
    background: var(--qs-surface);
    cursor: grab;
}

.qs-image-item:active {
    cursor: grabbing;
}

.qs-image-primary {
    border-color: var(--qs-primary);
    box-shadow: inset 0 0 0 1px var(--qs-primary);
}

.qs-image-item img {
    width: 100%;
    aspect-ratio: var(--qs-card-ratio);
    object-fit: cover;
    border-radius: var(--qs-radius-sm);
    display: block;
    pointer-events: none;
}

.qs-image-flag {
    position: absolute;
    top: calc(var(--qs-gap-sm) + 4px);
    left: calc(var(--qs-gap-sm) + 4px);
    padding: 2px 6px;
    border-radius: var(--qs-radius-sm);
    background: var(--qs-primary);
    color: var(--qs-primary-contrast);
    font-size: .6875rem;
    font-weight: 700;
}

.qs-image-actions {
    display: flex;
    gap: 4px;
}

.qs-image-actions .btn {
    flex: 1 1 auto;
    font-size: .75rem;
}

/* ---------- Mobile bottom bar: Filters | Map ----------
   One fixed element rather than two floating ones. The left segment describes what it
   opens; the right absorbs what used to be a separate centred map pill. Hidden at desktop
   widths, switched on in the mobile media query at the foot of this file. */

.qs-mobilebar {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 30;
    /* The inset keeps the bar clear of the iPhone home indicator; it resolves to 0 on
       every device without one, so no media query is needed. */
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--qs-surface);
    border-top: 1px solid var(--qs-line);
    box-shadow: 0 -2px 12px rgba(0, 0, 0, .10);
}

.qs-mb-btn {
    display: flex;
    align-items: center;
    gap: var(--qs-gap-sm);
    min-height: var(--qs-mobilebar-h);
    padding: var(--qs-gap-sm) var(--qs-gap);
    border: 0;
    background: none;
    color: var(--qs-ink);
    text-align: left;
}

/* Filters takes the slack so its summary line has room; Map stays at its content width. */
.qs-mb-filters {
    flex: 1 1 auto;
    min-width: 0;
}

.qs-mb-map {
    flex: none;
    flex-direction: column;
    gap: 2px;
    justify-content: center;
    border-left: 1px solid var(--qs-line);
    color: var(--qs-primary);
}

.qs-mb-icon {
    font-size: 1.125rem;
    line-height: 1;
}

.qs-mb-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.qs-mb-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: .9375rem;
    font-weight: 600;
    line-height: 1.2;
}

.qs-mb-map .qs-mb-title {
    font-size: .8125rem;
}

/* One line only. The summary can name a land partner and a price band at once, and wrapping it
   would change the bar's height as filters change. */
.qs-mb-sub {
    font-size: .75rem;
    color: var(--qs-ink-soft);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.qs-mb-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--qs-primary);
    color: var(--qs-primary-contrast);
    font-size: .6875rem;
    font-weight: 700;
}

/* ---------- Mobile filter sheet ----------
   Slides up over a scrim. Header and footer are pinned so the two actions stay reachable
   however long the body gets; only the middle scrolls. */

.qs-sheet-backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
    display: flex;
    align-items: flex-end;
    background: var(--qs-scrim);
}

.qs-sheet {
    display: flex;
    flex-direction: column;
    width: 100%;
    max-height: var(--qs-sheet-max-h);
    background: var(--qs-surface);
    border-radius: var(--qs-radius) var(--qs-radius) 0 0;
    box-shadow: var(--qs-shadow-raised);
}

.qs-sheet-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap);
    border-bottom: 1px solid var(--qs-line);
}

.qs-sheet-title {
    margin: 0;
    font-size: 1.0625rem;
    font-weight: 600;
}

.qs-sheet-sub {
    margin: 2px 0 0;
    font-size: .8125rem;
    color: var(--qs-ink-soft);
}

.qs-sheet-body {
    flex: 1 1 auto;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    padding: var(--qs-gap);
    display: flex;
    flex-direction: column;
    gap: var(--qs-gap);
}

.qs-sheet-group {
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius);
    padding: var(--qs-gap-sm) var(--qs-gap);
}

.qs-sheet-group-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--qs-gap-sm);
    min-height: 34px;
}

.qs-sheet-group-title {
    margin: 0;
    font-size: .9375rem;
    font-weight: 600;
}

/* Stacked label/control rows -- the opposite of the same .qs-field in the desktop bar. */
.qs-sheet-fields {
    display: flex;
    flex-direction: column;
}

.qs-sheet-fields .qs-field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--qs-gap);
    padding: var(--qs-gap-sm) 0;
    border-top: 1px solid var(--qs-line);
}

.qs-sheet-fields .qs-field:first-child {
    border-top: 0;
}

.qs-sheet-fields .qs-field .form-select {
    width: auto;
    min-width: 45%;
}

.qs-sheet-fields .qs-field-check {
    justify-content: flex-start;
}

.qs-sheet-foot {
    display: flex;
    gap: var(--qs-gap-sm);
    padding: var(--qs-gap);
    padding-bottom: calc(var(--qs-gap) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--qs-line);
}

/* The count button is the primary action, so it takes the width. */
.qs-sheet-go {
    flex: 1 1 auto;
}

/* ---------- Property type chips ----------
   Wrapping, unlike the desktop btn-group. Types get their full label on a narrow screen
   because the sheet gives them the room the filter bar could not. */

.qs-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--qs-gap-sm);
    padding-bottom: var(--qs-gap-sm);
}

.qs-chip {
    padding: 7px 14px;
    border: 1px solid var(--qs-line);
    border-radius: 999px;
    background: var(--qs-surface);
    color: var(--qs-ink);
    font-size: .875rem;
    line-height: 1.2;
    transition: background var(--qs-dur-hover) var(--qs-ease-out),
                border-color var(--qs-dur-hover) var(--qs-ease-out);
}

.qs-chip-on {
    background: var(--qs-primary);
    border-color: var(--qs-primary);
    color: var(--qs-primary-contrast);
}

.qs-chip:focus-visible {
    outline: none;
    box-shadow: var(--qs-ring);
}

/* Locks the page behind the sheet. Set on <body> because that is what scrolls. */
.qs-noscroll {
    overflow: hidden;
}

/* ---------- Sheet transition ---------- */

.qs-sheet-enter-active,
.qs-sheet-leave-active {
    transition: opacity var(--qs-dur-sheet) var(--qs-ease-out);
}

.qs-sheet-enter-active .qs-sheet,
.qs-sheet-leave-active .qs-sheet {
    transition: transform var(--qs-dur-sheet) var(--qs-ease-out);
}

.qs-sheet-enter-from,
.qs-sheet-leave-to {
    opacity: 0;
}

.qs-sheet-enter-from .qs-sheet,
.qs-sheet-leave-to .qs-sheet {
    transform: translateY(100%);
}

/* ---------- Mobile card slider ----------
   Raised by tapping a pin while the full-screen map is open. Snap points make the
   swipe land card-by-card, which is what drives the pin highlight and map pan. */

.qs-slider {
    display: none;
}

.qs-s-card {
    flex: none;
    width: 270px;
    scroll-snap-align: center;
    display: flex;
    gap: var(--qs-gap-sm);
    align-items: center;
    padding: var(--qs-gap-sm);
    background: var(--qs-surface);
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius);
    box-shadow: var(--qs-shadow);
    cursor: pointer;
}

.qs-s-card-active {
    border-color: var(--qs-primary);
}

.qs-s-card img,
.qs-s-card .qs-card-media-empty {
    width: 72px;
    height: 54px;
    object-fit: cover;
    border-radius: var(--qs-radius-sm);
    flex: none;
}

.qs-s-body {
    min-width: 0;
}

.qs-s-title,
.qs-s-price {
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.qs-s-title {
    font-size: .875rem;
    font-weight: 600;
}

.qs-s-price {
    font-size: .8125rem;
    color: var(--qs-ink-soft);
}

/* ---------- Detail ---------- */

.qs-detail {
    padding: var(--qs-gap);
}

.qs-back {
    display: inline-block;
    margin-bottom: var(--qs-gap-sm);
    color: var(--qs-ink-soft);
    text-decoration: none;
}

/* Gallery / information split.

   Expressed as flex-grow ratios rather than percentages because the columns are separated
   by a gap: 60% + 40% + gap overflows, and compensating with calc() means the gap value is
   duplicated in three places. Ratios divide whatever is left after the gap, so the split
   stays honest whatever --qs-gap becomes. 6 / 4 is 60 / 40. */
.qs-detail-grid {
    display: flex;
    align-items: flex-start;
    gap: var(--qs-gap);
}

.qs-gallery {
    flex: var(--qs-gallery-col) 1 0;
    min-width: 0;
}

.qs-detail-side {
    flex: var(--qs-info-col) 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--qs-gap);
}

.qs-hero {
    position: relative;
    border-radius: var(--qs-radius);
    overflow: hidden;
    background: var(--qs-surface-sunken);
}

.qs-hero img {
    width: 100%;
    height: auto;
    display: block;
}

/* Capped hero. Above the mobile breakpoint the gallery column is wide enough that a tall
   image would run past a screen on its own, taking the thumbnails with it.

   object-fit: contain means the whole image stays visible and is letterboxed against the
   hero's sunken background rather than being cropped -- for a property listing, showing the
   entire elevation matters more than filling the box. */
@media (min-width: 861px) {
    .qs-hero img {
        max-height: var(--qs-hero-max-h);
        object-fit: contain;
    }
}

/* Corner flags on the detail hero. Distinct from .qs-badge, which carries sale
   status: these describe the image, not the property. */

.qs-flag {
    position: absolute;
    bottom: var(--qs-gap-sm);
    padding: 3px 8px;
    border-radius: var(--qs-radius-sm);
    background: rgba(0, 0, 0, .62);
    color: #fff;
    font-size: .75rem;
    font-weight: 600;
    letter-spacing: .02em;
}

.qs-flag-rendering {
    left: var(--qs-gap-sm);
}

.qs-flag-count {
    right: var(--qs-gap-sm);
    font-variant-numeric: tabular-nums;
}

.qs-caption {
    margin: var(--qs-gap-sm) 0 0;
    font-size: .875rem;
    color: var(--qs-ink-soft);
    max-width: 70ch;
}

/* A wrapping grid rather than a scrolling strip of chips. At this size the thumbnails are
   worth looking at rather than merely clicking, which is the point of the reference design
   -- and a grid means no horizontal scroll to discover.

   A fixed column count rather than auto-fill: auto-fill sizes tracks from the container and
   keeps the empty ones, so a gallery with three photographs in a wide column got nine
   tracks and three minimum-width thumbnails. A count makes every thumbnail the same
   fraction of the gallery however many there are. */
.qs-thumbs {
    display: grid;
    grid-template-columns: repeat(var(--qs-thumb-cols), 1fr);
    gap: var(--qs-gap-sm);
    margin-top: var(--qs-gap-sm);
}

.qs-thumb {
    padding: 0;
    border: 2px solid transparent;
    border-radius: var(--qs-radius);
    overflow: hidden;
    background: none;
    cursor: pointer;
    transition: border-color var(--qs-dur-hover) var(--qs-ease-out),
                transform var(--qs-dur-hover) var(--qs-ease-out);
}

.qs-thumb:hover {
    transform: translateY(var(--qs-lift));
}

.qs-thumb.qs-thumb-active,
.qs-thumb:focus-visible {
    border-color: var(--qs-primary);
}

.qs-thumb img {
    width: 100%;
    aspect-ratio: var(--qs-card-ratio);
    object-fit: cover;
    display: block;
}

/* ---------- Detail sidebar cards ----------
   Two cards in one column: the property, then the enquiry. They share a shell so the pair
   reads as a set rather than two unrelated boxes. */

.qs-info,
.qs-inquiry-card {
    padding: var(--qs-gap);
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius);
    background: var(--qs-surface);
}

.qs-info-price {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--qs-primary-strong);
    margin: 0 0 var(--qs-gap-sm);
}

.qs-info-kicker {
    margin: 0 0 5px;
    color: var(--qs-primary-strong);
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
}

.qs-info-title {
    font-size: 1.375rem;
    line-height: 1.25;
    margin: 0 0 4px;
}

.qs-info-address {
    color: var(--qs-ink-soft);
    margin: 0 0 var(--qs-gap-sm);
}

/* Equal columns with hairline separators, rather than three labels floated next to each
   other. Reads as a spec block at a glance, which is how it is actually used. */
.qs-specs-grid {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    margin: var(--qs-gap) 0 0;
    padding: var(--qs-gap-sm) 0;
    border-top: 1px solid var(--qs-line);
    border-bottom: 1px solid var(--qs-line);
}

.qs-specs-grid > div + div {
    padding-left: var(--qs-gap-sm);
    border-left: 1px solid var(--qs-line);
}

.qs-specs-grid dt {
    font-size: .75rem;
    color: var(--qs-ink-soft);
    font-weight: 400;
    text-transform: uppercase;
    letter-spacing: .04em;
}

.qs-specs-grid dd {
    margin: 2px 0 0;
    font-size: 1.0625rem;
    font-weight: 600;
}

/* Description and land partner, moved in from the full-width band that used to sit under the
   gallery. Each is a titled block inside the card. */
.qs-info-section {
    margin-top: var(--qs-gap);
}

.qs-info-heading {
    margin: 0 0 6px;
    font-size: .75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--qs-ink-soft);
}

.qs-info-section p {
    margin: 0 0 var(--qs-gap-sm);
    line-height: 1.6;
}

.qs-info-section p:last-child {
    margin-bottom: 0;
}

.qs-opportunity-lead {
    padding-left: 12px;
    border-left: 3px solid var(--qs-primary);
    color: var(--qs-ink);
    font-weight: 500;
}

.qs-info-value {
    font-weight: 600;
}

.qs-developer-brand {
    display: flex;
    min-height: 70px;
    align-items: center;
    justify-content: center;
    margin-bottom: 8px;
    padding: 10px 14px;
    overflow: hidden;
    border: 1px solid var(--qs-line);
    border-radius: 8px;
    background: #fff;
}

.qs-developer-logo {
    display: block;
    width: auto;
    height: auto;
    max-width: 190px;
    max-height: 58px;
    object-fit: contain;
}

/* ---------- Inquiry card ----------
   The form used to be a modal. Inline it is a peer of the property card, so it carries a
   heading of its own -- without one, a bare stack of fields under a price reads as part of
   the listing rather than as something addressed to a person. */

.qs-inquiry-head {
    margin-bottom: var(--qs-gap);
}

.qs-inquiry-title {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 600;
}

.qs-inquiry-sub {
    margin: 4px 0 0;
    font-size: .875rem;
    color: var(--qs-ink-soft);
    line-height: 1.45;
}

.qs-inquiry-form {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.qs-dream-brief {
    min-width: 0;
    margin: 2px 0 4px;
    padding: 12px;
    border: 1px solid var(--qs-line);
    border-radius: var(--qs-radius-sm);
    background: var(--qs-surface-sunken);
}

.qs-dream-legend {
    float: none;
    width: auto;
    margin: 0 0 9px;
    padding: 0;
    color: var(--qs-ink);
    font-size: .8rem;
    font-weight: 700;
    letter-spacing: .045em;
    text-transform: uppercase;
}

.qs-dream-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}

.qs-select-field {
    position: relative;
}

.qs-select-field .form-select {
    min-height: 50px;
    padding: 1.25rem 2rem .3rem 2.55rem;
    border-radius: var(--qs-radius-sm);
    background-color: var(--qs-surface);
    color: var(--qs-ink);
    font-size: .85rem;
    line-height: 1.15;
}

.qs-select-field .form-label {
    position: absolute;
    z-index: 1;
    top: .42rem;
    left: 2.55rem;
    max-width: calc(100% - 3.2rem);
    margin: 0;
    color: var(--qs-primary-strong);
    font-size: .62rem;
    font-weight: 600;
    letter-spacing: .03em;
    line-height: 1;
    pointer-events: none;
}

.qs-select-field .qs-field-icon {
    top: 50%;
}

/* Container-driven, not viewport-driven: the card is a fraction of the page, so Bootstrap's
   col-sm-* would put these side by side in a narrow column on a wide screen. auto-fit
   collapses them to one per row when the card itself is narrow. */
.qs-field-pair {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: var(--qs-gap-sm);
}

.qs-float-field {
    position: relative;
}

.qs-float-field .form-control {
    min-height: 48px;
    padding: 1.18rem .8rem .28rem 2.65rem;
    border-radius: var(--qs-radius-sm);
    font-size: .9rem;
    line-height: 1.2;
}

.qs-float-field textarea.form-control {
    min-height: 108px;
    padding-top: 1.45rem;
    line-height: 1.45;
    resize: vertical;
}

.qs-float-field .form-label {
    position: absolute;
    z-index: 1;
    top: 50%;
    left: 2.65rem;
    max-width: calc(100% - 3.4rem);
    margin: 0;
    color: var(--qs-ink-soft);
    font-size: .875rem;
    font-weight: 500;
    line-height: 1;
    pointer-events: none;
    transform: translateY(-50%);
    transform-origin: left top;
    transition:
        color var(--qs-dur-hover) var(--qs-ease-out),
        font-size var(--qs-dur-hover) var(--qs-ease-out),
        top var(--qs-dur-hover) var(--qs-ease-out),
        transform var(--qs-dur-hover) var(--qs-ease-out);
}

.qs-float-area .form-label {
    top: 1.05rem;
    transform: none;
}

.qs-float-field .form-control:focus ~ .form-label,
.qs-float-field .form-control:not(:placeholder-shown) ~ .form-label,
.qs-float-field .form-control:-webkit-autofill ~ .form-label {
    top: .42rem;
    color: var(--qs-primary-strong);
    font-size: .65rem;
    letter-spacing: .035em;
    transform: none;
}

.qs-field-icon {
    position: absolute;
    z-index: 2;
    top: 50%;
    left: .82rem;
    width: 18px;
    height: 18px;
    color: var(--qs-primary-strong);
    pointer-events: none;
    transform: translateY(-50%);
}

.qs-field-icon svg {
    display: block;
    width: 100%;
    height: 100%;
}

.qs-float-area .qs-field-icon {
    top: 1.05rem;
    transform: none;
}

.qs-float-field .form-control.is-invalid {
    padding-right: 2.25rem;
}

.qs-float-field .form-control.is-invalid ~ .form-label,
.qs-float-field .form-control.is-invalid ~ .qs-field-icon {
    color: var(--qs-danger);
}

.qs-form-actions {
    margin-top: 4px;
}

.qs-form-actions .btn {
    width: 100%;
}

@media (max-width: 460px) {
    .qs-dream-grid {
        grid-template-columns: 1fr;
    }
}

/* ---------- Mobile: list first, map behind a toggle ---------- */

@media (max-width: 860px) {
    .qs-split {
        display: block;
    }

    .qs-list-col,
    .qs-map-col {
        flex: none;
        max-width: none;
        width: 100%;
    }

    .qs-map-col {
        display: none;
        position: fixed;
        inset: var(--qs-nav-h) 0 0 0;
        height: auto;
        z-index: 25;
    }

    .qs-showcase.qs-map-open .qs-map-col {
        display: block;
    }

    .qs-mobilebar {
        display: flex;
    }

    .qs-slider {
        display: flex;
        position: absolute;
        left: 0;
        right: 0;
        /* Clears the Filters/Map bar, which is fixed to the bottom of the viewport. Reads
           the bar's own height token so the two cannot drift apart. */
        bottom: calc(var(--qs-mobilebar-h) + env(safe-area-inset-bottom, 0px) + var(--qs-gap-sm));
        gap: var(--qs-gap-sm);
        padding: 6px var(--qs-gap-sm);
        overflow-x: auto;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        z-index: 26;
    }

    .qs-slider::-webkit-scrollbar {
        display: none;
    }

    /* The bar is fixed, so the last card would otherwise sit under it and be unreadable. */
    .qs-list-col {
        padding-bottom: calc(var(--qs-mobilebar-h) + env(safe-area-inset-bottom, 0px) + var(--qs-gap));
    }

    .qs-detail-grid {
        display: block;
    }

    /* Stacked, the sidebar follows the gallery. Its own cards keep their gap; this is the
       gap between the gallery and the pair. */
    .qs-detail-side {
        margin-top: var(--qs-gap);
    }

    /* Three across a phone would be back to thumbnails you cannot read. */
    .qs-thumbs {
        --qs-thumb-cols: 2;
    }
}

/* ---------- Reduced motion ----------
   Everything that moves is switched off; everything that only changes opacity is kept,
   because a cross-fade is not vestibular motion and losing it would make filtering feel
   like content teleporting. Images still fade in, so they do not pop during scroll.

   The stagger is zeroed too: a cascade is motion even when each step is a fade. */
@media (prefers-reduced-motion: reduce) {
    .qs-card,
    .qs-thumb,
    .qs-pin,
    .qs-card-media img {
        transition-duration: 1ms;
    }

    .qs-card:hover,
    .qs-thumb:hover {
        transform: none;
    }

    .qs-card:hover .qs-card-media img.qs-img-ready {
        transform: none;
    }

    /* The hop is the one piece of motion here that is purely decorative repetition, so it
       goes entirely. The pin still reads as selected via scale and stacking order. */
    .qs-pin-active {
        transform: scale(var(--qs-pin-scale));
        animation: none;
    }

    /* Keep the fade, drop the rise. */
    .qs-card-enter-from {
        transform: none;
    }

    .qs-card.qs-card-enter-active,
    .qs-card.qs-card-leave-active {
        transition: opacity var(--qs-dur-leave) linear;
    }

    .qs-card.qs-card-enter-active {
        transition-delay: 0ms !important;
    }

    .qs-card-move {
        transition: none;
    }

    .qs-card-media img {
        transition: opacity var(--qs-dur-imgfade) linear;
    }

    /* The sheet cross-fades instead of sliding. A full-height translate is the largest
       movement in the module, so it is the last thing that should survive here. */
    .qs-sheet-enter-from .qs-sheet,
    .qs-sheet-leave-to .qs-sheet {
        transform: none;
    }

    .qs-chip {
        transition: none;
    }

    /* The confirmation badge appears without scaling in. The send spinner is kept: it is
       not decoration, it is the only signal that the request is still in flight. */
    .qs-result-icon {
        animation: none;
    }
}
