/*
 * lore-listings.css v1.2.0
 *
 * Chunk E Phase 4 — listings index map + cards rail + desktop overlay
 * + mobile bottom-sheet + Map/List mode toggle.
 *
 * v1.2.0 (Chunk E Phase 4) polish-12 (mtime-versioned, no rename; ADR-100):
 *   Street address line under the card title. The title slot (legacy class
 *   .lore-listings-card__address, data-fill="title") now carries Salesforce
 *   Headline__c; the actual street address renders beneath it in a NEW
 *   .lore-listings-card__location line, composed client-side by the JS
 *   orchestrator (street + ", " + city/state/zip display) into the
 *   [data-address-full] marker. Secondary/muted/single-line ellipsis;
 *   mirrors the single-listing hero address sub-line one tier smaller.
 *
 * v1.2.0 (Chunk E Phase 4) polish-11 (mtime-versioned, no rename):
 *   Search bar charcoal in device dark mode. The search uses Google's
 *   gmp-place-autocomplete web component, which follows the system color
 *   scheme through its shadow DOM — so on a device in dark mode it rendered
 *   charcoal while the rest of the (light-only) site stayed white. Pinned the
 *   search to color-scheme:light so it renders light regardless of device
 *   appearance. CSS-only; no markup/JS change.
 *
 * v1.2.0 (Chunk E Phase 4) polish-10 (mtime-versioned, no rename):
 *   Lock internal scrolling at peek state. Operator surfaced the real
 *   root cause of the "tap-the-bar-opens-listing" bug that polish-8
 *   and polish-9 partially addressed: `.lore-listings-sheet__content`
 *   had `overflow-y: auto` unconditionally, so at peek state the user
 *   could scroll the card content upward within the visible area —
 *   pushing the card photo's top edge right up against (or under) the
 *   sheet header. A subsequent tap on the handle then landed on BOTH
 *   the handle's tap zone AND the card photo's tap target, depending
 *   on coordinates. iOS Safari resolves to whichever element receives
 *   the touch event, which was sometimes the card, opening the
 *   listing in a new tab.
 *
 *   Polish-8 (handle click suppression) defeats accidental opens
 *   when the synthesized click fires on the handle. Polish-9 (24px
 *   symmetric padding) widens the rest position. Neither helps when
 *   the user has scrolled the content area to bring the card flush
 *   under the handle — which is the actual failure mode.
 *
 *   Fix: at peek state, `body.sheet-peek .lore-listings-sheet__content
 *   { overflow-y: hidden; }`. The card is locked to whatever fits in
 *   the visible peek viewport (30vh). To see more, the user drags or
 *   tap-cycles to mid state — where overflow:auto is restored so
 *   cards that exceed the 85vh ceiling can still be scrolled within
 *   the sheet.
 *
 *   Companion JS change (lore-listings-bottomsheet-v1.0.0.js v2.4.5):
 *   on transition into peek, the bottom-sheet module resets
 *   contentEl.scrollTop = 0. This handles the mid → peek edge case
 *   where the user had scrolled at mid (tall card hitting the 85vh
 *   cap) and then dragged back to peek — without the reset, scrollTop
 *   would persist its non-zero value, and the peek state would show
 *   a partial mid-view stuck at the top with overflow:hidden,
 *   instead of the card-from-top view that peek should always show.
 *
 *   Net effect: peek state is now visually frozen — card top sits
 *   flush against the content padding-top, never drifts up into the
 *   header area. The thumb-on-handle motion can no longer accidentally
 *   target a card photo because the card photo is no longer in the
 *   handle's neighborhood.
 *
 * v1.2.0 (Chunk E Phase 4) polish-9 (mtime-versioned, no rename):
 *   Symmetric vertical padding on the sheet content area. Operator
 *   feedback after polish-8: with the handle-click suppression in
 *   place, accidental taps from the handle itself no longer open
 *   the listing — but a thumb pad reaching for the visible pill can
 *   still drift slightly into the card photo area, registering a
 *   tap on the card (not the handle) and opening the listing in a
 *   new tab. Polish-9 widens the gap between the visible pill and
 *   the card photo by bumping content padding-top from --space-3
 *   (12px) to 24px AND padding-bottom from --space-4 (16px) to 24px
 *   so both gaps are visually equal. Horizontal padding unchanged.
 *
 *   Why 24px (not --space-4 = 16px): operator's literal request
 *   was "make the distance the same." Equalizing at the larger
 *   value (16px on top instead of 12px) only adds 4px of breathing
 *   room — not enough to clear a thumb pad. Equalizing at 24px
 *   delivers BOTH the visual symmetry the operator asked for AND
 *   a meaningful increase in finger-pad clearance. Pill-bottom to
 *   card-photo-top gap grows from ~35px to ~47px (computed: 22px
 *   below pill in header + 1px border + 24px content padding-top).
 *
 *   No JS changes needed: computeMidHeightPx() in lore-listings-
 *   bottomsheet-v1.0.0.js uses contentEl.scrollHeight, which
 *   includes its own padding, so the mid-state height auto-grows
 *   by the new padding delta (20px total: +12 top, +8 bottom). The
 *   85vh ceiling cap clamps any runaway. Drag math and tap
 *   classification are unaffected.
 *
 *   Side effect: sheet is slightly taller at mid state by ~20px.
 *   On most cards this is imperceptible (well under the 85vh cap).
 *   On the tallest cards that already hit the cap pre-polish-9,
 *   the cap continues to apply — content scrolls within the sheet
 *   as before.
 *
 * v1.2.0 (Chunk E Phase 4) polish-8 (mtime-versioned, no rename):
 *   Two coupled refinements addressing operator feedback after polish-7:
 *
 *   1. Mode toggle pill stays visible at sheet mid state. Pre-polish-8,
 *      the body.sheet-mid rule faded the pill to opacity:0 + pointer-
 *      events:none. That was correct when the pill was bottom-right
 *      (polish-1 through polish-3) because the sheet rising overlapped
 *      the pill area entirely. Polish-4 moved the pill to top-right,
 *      where the overlap with the sheet at mid is much smaller (~12px
 *      on typical viewports, larger on smaller phones). The fade rule
 *      was retained at polish-4 for visual cleanliness but operator
 *      flagged it as a real usability cost: when reading a card at
 *      mid state, the toggle is unreachable — user must dismiss the
 *      sheet, switch mode, re-open. Polish-8 keeps the pill visible
 *      and tappable at all sheet states.
 *
 *      Companion change: pill z-index bumped 150 → 250 so it sits
 *      ABOVE the sheet (z:200) instead of below. Without this, the
 *      pill would be hidden behind the sheet at mid even with opacity
 *      fade removed. The pill now floats over the sheet's top edge
 *      area — matches Google Maps' floating-control-over-bottom-sheet
 *      pattern.
 *
 *   2. Companion JS change (lore-listings-bottomsheet-v1.0.0.js):
 *      added click event suppression on the drag handle. Pre-polish-8,
 *      tapping the handle would sometimes synthesize a click event
 *      whose coordinates landed inside the card content area (an iOS
 *      Safari touch→click coordinate-drift quirk), opening the
 *      listing's <a> wrapper in a new tab. Polish-8 attaches a click
 *      listener on the handle that always preventDefault() +
 *      stopPropagation(). Independent of CSS but bundled in polish-8
 *      since both fixes target the same operator feedback round.
 *
 * v1.2.0 (Chunk E Phase 4) polish-7 (mtime-versioned, no rename):
 *   iOS-Safari-specific fix for the list→map scroll-reset bug that
 *   polish-5 and polish-6 attempted to fix in JavaScript.
 *
 *   Background: polish-4 added the body :has() lock. Polish-5 added
 *   window.scrollTo(0, 0) inside setMode between classList.remove and
 *   classList.add — failed because iOS Safari doesn't synchronously
 *   reflow CSS in that window. Polish-6 moved scrollTo to the click
 *   handler before setMode is called — works on desktop and Android
 *   but operator confirmed it STILL fails on iOS Safari when fully
 *   scrolled to the footer.
 *
 *   Root cause: iOS Safari's combination of touch event momentum,
 *   visual viewport vs layout viewport accounting, and lazy scrollTop
 *   clamping on style-change reflow means window.scrollTo issued
 *   from inside a click handler is unreliable when the user has
 *   scrolled deep into the document.
 *
 *   Polish-7 fix: don't fight the JS scroll API — change the layout
 *   so there's no invalid scrollTop to clamp. When body has .mode-map,
 *   hide the site footer (display: none on #colophon / .site-footer).
 *   Body content height shrinks to exactly header + page = 100dvh,
 *   which equals the viewport. Maximum scrollable area = 0. The
 *   browser's natural scroll-clamp-on-reflow then forces scrollTop
 *   to 0 — and iOS Safari DOES handle this correctly because it's
 *   a basic layout response, not a scroll API call.
 *
 *   Side effect: footer is unreachable in mode-map on mobile. Users
 *   wanting to see footer info (copyright, KW East Bay branding,
 *   Quick Links, contact) must toggle to List mode first. Acceptable
 *   tradeoff — footer info isn't relevant during map interaction.
 *
 *   The polish-4 body :has() lock (overflow:hidden + height:100dvh)
 *   is RETAINED as redundant safety. If the footer selectors don't
 *   match for some reason (theme update, custom markup), the lock
 *   still prevents overflow.
 *
 *   The polish-6 JS scrollTo in the click handler is also RETAINED.
 *   On desktop and Android it does work, contributing to instant
 *   visual reset without waiting for the reflow-clamp pass.
 *
 *   Selectors targeted: #colophon and .site-footer cover GeneratePress
 *   defaults (the parent theme this site uses). If the actual footer
 *   uses different selectors, add them here.
 *
 * v1.2.0 (Chunk E Phase 4) polish-4 (mtime-versioned, no rename):
 *   Two coupled fixes addressing operator browser-verify feedback
 *   after polish-3:
 *
 *   1. Body scroll lock via :has() instead of JS-toggled body class.
 *      Pre-polish-4 the body lock was applied by JS in setMode() via
 *      `body.lore-listings-map-locked` toggle. On hard refresh there
 *      was a race window: HTML painted with the body unlocked → user
 *      saw the cabernet footer briefly before JS caught up and
 *      applied the lock. The lock only "took" visually after the
 *      first sheet-state transition forced a layout pass.
 *
 *      Fix: use :has() to watch the server-rendered .mode-map class
 *      on the page directly. archive-listing.php server-renders
 *      `<main class="lore-listings-page mode-map">` as the default,
 *      so :has() picks it up on first paint — body locks before any
 *      JS executes, footer never gets a chance to paint. The
 *      orchestrator JS still toggles .mode-map ↔ .mode-list on user
 *      switch; :has() follows along automatically. The body class
 *      mechanism (.lore-listings-map-locked) is retired; orchestrator
 *      polish-4 removes the JS toggle. Architectural cleanup: one
 *      source of truth (the page's mode class) instead of two.
 *
 *      Browser support note: :has() is universally supported in
 *      current mobile browsers — Safari 15.4+ (March 2022), Chrome
 *      105+ (August 2022), Firefox 121+ (December 2023). Our minimum
 *      mobile-browser support floor is set by the underlying Bitnami
 *      WordPress stack and is well below these thresholds.
 *
 *   2. Mode toggle pill repositioned bottom-right → top-right of the
 *      map area. Pre-polish-4 the pill was bottom-right with `bottom:
 *      var(--space-4); right: var(--space-4)`. Two problems:
 *
 *        - At peek state (sheet 30vh), the pill sits INSIDE the sheet
 *          area visually (bottom of sheet is at viewport bottom; pill
 *          16px above viewport bottom is overlapped). Operator
 *          couldn't easily tap Map/List with a card open.
 *        - The Google Maps zoom controls live at bottom-right by
 *          default. The pill was floating ABOVE the zoom controls
 *          and partially covering them.
 *
 *      Fix: move pill to top-right with `top: calc(--lore-header-
 *      height + --space-4); right: var(--space-4)`. The pill sits
 *      ~96px from viewport top (below site header, above the map).
 *      Convention-aligned: Google Maps, Apple Maps, Zillow, Redfin
 *      all put secondary map controls at top-right. The site
 *      hamburger sits in a different visual plane (site header) so
 *      there's no real conflict with the pill.
 *
 *      Bonus: at mid sheet state the pill still overlaps the sheet
 *      top by ~12px on most viewports. The existing `body.sheet-mid`
 *      fade-out rule from the main Phase 4 ship handles this — pill
 *      fades to opacity:0 + pointer-events:none when the sheet is at
 *      mid. Behavior preserved.
 *
 * v1.2.0 (Chunk E Phase 4) polish-3 (mtime-versioned, no rename):
 *   Single-issue fix for handle tap-zone bleed into the card area.
 *
 *   Diagnosis: pre-polish-3 the .lore-listings-sheet__header was 32px
 *   tall and the handle (transparent 80×44 tap zone, anchored top:0
 *   with horizontal-only translate) extended 12px BELOW the header
 *   bottom into the card content area. A touch in that 12px strip
 *   just below the visible pill registered as a handle tap (cycling
 *   peek↔mid) instead of as a card tap (opening the listing in a new
 *   tab). Operator reported the bar feeling "very sensitive" with
 *   accidental cycle-triggers when aiming at the card photo.
 *
 *   Fix: increase header height 32px → 48px, then center the handle
 *   vertically inside it via top:50% + translate(-50%,-50%) (was
 *   top:0 + translateX(-50%)). Net result:
 *     - 44px handle tap zone fits cleanly inside 48px header
 *       (2px gap top + 2px gap bottom; zero bleed).
 *     - Visible pill (4px tall, vertically centered in the handle)
 *       now sits at ~22px from the top of the header and ~22px above
 *       the card photo — actual visual breathing room, not just a
 *       bug-fix.
 *
 *   JS coupling: assets/js/lore-listings-bottomsheet-v1.0.0.js has a
 *   `headerPx = 48` constant in computeMidHeightPx() that mirrors
 *   this CSS height. Changing the header height here requires
 *   updating that constant — they MUST stay in sync, otherwise the
 *   computed mid-state height will be off by the delta. The constant
 *   is documented with a back-pointer to this rule.
 *
 * v1.2.0 (Chunk E Phase 4) polish-2 (mtime-versioned, no rename):
 *   Two coupled refinements, both centered on the mid state:
 *
 *   1. Fit-to-content mid state. Pre-polish, the sheet at mid was a
 *      fixed 70vh — typically left ~100-150px of white space below
 *      the card's content because real cards don't fill 70vh on
 *      most viewports. JS now computes the natural content height
 *      (sheet header + contentEl.scrollHeight) and sets it as an
 *      inline pixel height when transitioning to mid. The CSS class
 *      `.lore-listings-sheet--mid { height: 70vh; }` is retained as
 *      a defensive fallback for the (unreachable in practice) case
 *      where JS hasn't set inline height yet. A new `max-height:
 *      85vh` cap on the base sheet rule defends against runaway
 *      heights — at most 15vh of map stays visible above the sheet
 *      no matter what the content size is.
 *
 *   2. Tap-the-handle cycle (CSS-side: no change). The tap-vs-drag
 *      distinction is purely JS — pointer movement threshold (<10px)
 *      + duration (<500ms) classifies a pointer interaction as tap.
 *      Visual affordances unchanged.
 *
 * v1.2.0 (Chunk E Phase 4) polish-1 (mtime-versioned, no rename):
 *   Two coupled refinements landing together after operator browser-
 *   verify on staging:
 *
 *   1. Lock body scroll in mode-map on mobile. Pre-polish, scrolling
 *      up on the mode-map page revealed the dark cabernet footer
 *      underneath — confusing UX given the map fills the visible
 *      viewport area and the user expects map-only behavior à la
 *      Google Maps app. Fix: a new body class `.lore-listings-map-
 *      locked` (managed by the orchestrator's setMode) gets overflow:
 *      hidden + height:100dvh under @media (max-width: 991.98px). The
 *      JS adds the class on map mode and removes on list mode; the
 *      CSS @media gate makes the visual effect mobile-only. The
 *      `.lore-listings-page.mode-map` `min-height: 600px` rule is
 *      also removed — it was a defense against tiny landscape
 *      viewports but conflicts with the new scroll-lock model
 *      (100dvh covers that case correctly via dynamic-viewport units).
 *
 *   2. Remove the X close button from the bottom-sheet header.
 *      Operator design call: drag handle + tap-on-map-basemap are
 *      sufficient dismissal affordances; the X adds clutter without
 *      adding capability. Matches the Google Maps app pattern.
 *      `.lore-listings-sheet__close` + its `:hover`, `:focus`,
 *      `:focus:hover`, `:focus-visible`, and `svg` size rules all
 *      removed. The sheet header now contains only the drag handle.
 *      ADR-079 :focus override pattern still applies to any future
 *      button reintroduced here.
 *
 * v1.2.0 (Chunk E Phase 4): mobile bottom-sheet + Map/List mode toggle.
 *   Three new sections at the END of the stylesheet covering:
 *
 *   1. Mobile mode classes on .lore-listings-page. Scoped to <992px
 *      via @media (max-width: 991.98px). Default (no mode class) keeps
 *      the Phase 2 stacked layout intact for backward compat / no-JS
 *      fallback rendering. With JS booted, the orchestrator applies
 *      .mode-map (default) or .mode-list based on sessionStorage.
 *      Map mode: map column fills the page height, rail hidden.
 *      List mode: map hidden, rail visible at full mobile width.
 *
 *   2. Bottom sheet (.lore-listings-sheet). Mobile-only (display:none
 *      at ≥992px). Position:fixed at viewport bottom, three height
 *      states via class modifiers (--hidden 0, --peek 30vh, --mid 70vh)
 *      per SCOPE-CHUNK-E §8.1. 300ms ease-out transition per §8.2.
 *      is-dragging class suppresses the transition during active
 *      pointer drag for 1:1 finger tracking (the bottom-sheet JS
 *      module owns the inline height during drag).
 *
 *      Sheet structure: __header (handle + close) + __content
 *      (scrollable card-clone area). Handle is a small 36×4px pill
 *      visually (operator design call 2026-05-28 Phase 4 architecture
 *      lock: "small handle only, not the whole sheet top") with a
 *      larger transparent tap zone (~80×44px) for thumb ergonomics.
 *      The visible pill is drawn via ::before pseudo-element so the
 *      parent element's larger box is the tap target without growing
 *      the visible pill.
 *
 *      Close button mirrors the overlay's close (44×44 sand circle,
 *      24×24 inline SVG X glyph). Full ADR-079 :focus override
 *      treatment to defeat the GeneratePress parent-theme global
 *      button:focus rule that would otherwise paint the button dark
 *      whenever it receives programmatic focus.
 *
 *   3. Map/List toggle pill (.lore-listings-mode-toggle). Floating
 *      bottom-right, segmented two-button control. Mobile-only via
 *      the same @media gate. Active state is harbor-blue per brand
 *      palette. Auto-hides at sheet mid state (body.sheet-mid
 *      reaction) — sheet content is the user's focus; toggle returns
 *      when sheet drags back to peek or hidden.
 *
 *      Both buttons get full ADR-079 :focus override treatment plus
 *      active-state preservation across all hover/focus combinations
 *      (active button stays harbor-blue regardless of state).
 *
 *   No pre-existing rules touched. Phase 4 additions are purely
 *   additive at the end of the file. Desktop layout (≥992px) is
 *   visually identical to Phase 3 ship.
 *
 *   See §mobile-modes + §bottom-sheet + §mode-toggle at file end,
 *   plus SCOPE-CHUNK-E §11 Phase 4 + §8 + ADR-078 (architectural
 *   cousin: overlay shares the slide-surface pattern) + ADR-079
 *   (button :focus override pattern, applied here to the sheet
 *   close button + both mode-toggle buttons per convention #90).
 *
 * v1.1.0 (Chunk E Phase 3) polish-1 (mtime-versioned, no rename):
 *   Three coupled refinements landing together on top of v1.0.0 main:
 *
 *   1. Panel surface white instead of linen. Phase 3 main ship had
 *      both the overlay panel and its header on `var(--color-linen)`
 *      to "stay brand-consistent with the listings index." But the
 *      injected single-listing content renders against white in its
 *      canonical standalone form (listing.css line 727:
 *      `.lore-listing__hero { background: var(--color-white); }`).
 *      Using linen in the overlay therefore created a tonal mismatch
 *      between standalone /listing/<slug> and overlay-injected
 *      variants of the same content. Polish-1 resolves: white
 *      panel + white header. Listings INDEX surface keeps linen
 *      (browse canvas); detail/document surfaces are white. Principled
 *      split between "browse" and "document" surface treatments.
 *
 *   2. Close button uses inline SVG (matches the open-external
 *      button structure). Earlier polish iterations attempted to
 *      draw the X via ::before/::after pseudo-elements copied from
 *      .lore-inquiry-modal__close. DevTools confirmed identical
 *      computed styles between the close and open-external buttons,
 *      but the pseudo-element X rendered unreliably in the
 *      overlay's flex-container context. Switched to inline SVG
 *      for structural symmetry with the (working) open-external
 *      button; both buttons now byte-identical except for icon
 *      glyph path.
 *
 *      44×44 touch target, sand-tinted circle (var(--color-border-
 *      soft)) at rest, darkens to var(--color-border) on hover/
 *      active. The inline <svg> in archive-listing.php carries the
 *      X path; CSS just sizes it to 24×24 and lets it render.
 *      Close-button-in-circle convention worth promoting to canonical
 *      tracked as a Phase 3 follow-up in ADR-073.
 *
 *   3. Click-outside-on-map close affordance. Operator design call
 *      2026-05-28: clicking blank area of the map should close the
 *      overlay (standard operation for slide-in cards on maps).
 *      Wired via a new lore-map:click CustomEvent dispatched by the
 *      map module on basemap clicks; the overlay controller listens
 *      and closes. The map's gmp-click on AdvancedMarkerElement is
 *      consumed by its own handler (no propagation), so clicking a
 *      marker while overlay-open SWAPS content via that marker's
 *      open-on-click intercept — does NOT close-then-reopen.
 *
 *   4. Pattern B card navigation + "Open in new tab" affordance.
 *      Card click in the rail now opens /listing/<slug> in a new
 *      tab (target="_blank") rather than the overlay. Markers
 *      on the map still open the overlay (the "compare while
 *      browsing" intent). Cards represent the "inspect deeply"
 *      intent — full page in a new tab matches the multi-tab
 *      research workflow users naturally do on portals.
 *      Additionally, the overlay header gains an "open in new tab"
 *      button (left of the close X) so users who DID open via map-
 *      marker can also escape into the new-tab workflow. Clicking
 *      the new-tab button does NOT close the overlay — the new tab
 *      opens but the overlay stays for the user's continuing browse
 *      session.
 *
 *   The four changes ship together because (1) the close-button
 *   pattern in (2) only works visually with the white panel from
 *   (1); they're tonally coupled. (3) and (4) are independent but
 *   land in the same polish round because they surfaced in the same
 *   browser-verify pass and the deploy pipeline cost is the same.
 *
 * v1.1.0 (Chunk E Phase 3): desktop overlay slide-in.
 *   Adds a new §overlay block at the end of the stylesheet covering the
 *   .lore-listings-overlay container + its slide-in animation + scroll
 *   container + close button + the ADR-031 sticky-CTA-inside-overlay
 *   override. Desktop-only at ≥992px (display:none below); mobile gets
 *   hard-nav per Phase 3 lock until Phase 4 lands the bottom-sheet.
 *
 *   ONE pre-existing rule touched: .lore-listings-page in the ≥992px
 *   media query gains `position: relative` so the overlay can absolute-
 *   position against it. Without this, position:absolute would resolve
 *   to the viewport and the overlay would cover the page header too.
 *   No visual change to non-overlay state.
 *
 *   See §overlay block + SCOPE-CHUNK-E §11 Phase 3 + ADR-073.
 *
 * v1.0.9 (Chunk E Phase 2 polish 12): visual hierarchy swap on card body.
 *   Operator visual call: title should be primary, price secondary.
 *   Polish-7's prior direction had price at fs-xl display Larken (the
 *   visual anchor) and title at fs-sm muted body (supporting info);
 *   polish-12 reverses.
 *
 *   Title (`.lore-listings-card__address` — class name kept for backward
 *   compat, semantically the Portal Display Name slot per polish-4):
 *     font-family: var(--font-body)    → var(--font-display)   (Larken serif)
 *     font-size:   var(--fs-sm) (14px) → var(--fs-xl) (22px)
 *     color:       --color-text-muted  → --color-text          (dark)
 *     margin-bot:  var(--space-3)      → var(--space-1)        (tight to price)
 *     2-line clamp preserved (Portal Display Names carry curated context).
 *
 *   Price (`.lore-listings-card__price`):
 *     font-family: var(--font-display) → var(--font-body)      (Articulat sans)
 *     font-size:   var(--fs-xl) (22px) → var(--fs-lg) (20px)   (one tier down)
 *     color:       --color-text        → --color-text          (unchanged)
 *     margin-bot:  var(--space-2)      → var(--space-3)        (separates metrics)
 *
 *   Hierarchy: title is the listing's identity (display Larken anchor);
 *   price is supporting info one tier below (body Articulat at fs-lg,
 *   dark text — still clearly visible as a fast-scan anchor, just not
 *   the visual lead). Source order in the partial also swapped: title
 *   before price (see listing-card.php @since 2.2.6).
 *
 *   Display Larken serif font reserved for primary anchors per ADR-012
 *   brand discipline; secondary surfaces stay in body Articulat. The
 *   font-family swap follows the hierarchy swap by construction.
 *
 *   Markers + status pills NOT changed (their visual role is independent
 *   of card body hierarchy). Metric strip NOT changed (still fs-xs body
 *   under the new hierarchy — sits below the price as supporting data).
 *
 * v1.0.8 (Chunk E Phase 2 polish 11): marker / pill visual parity.
 *   Operator visual call: map markers and card status pills should
 *   feel like the SAME element at the same scale, side-by-side. After
 *   polish-10 bumped the card pill padding to 7px 16px, the markers
 *   (still at 4px 12px) felt noticeably smaller than pills.
 *
 *   Single change: marker padding 4px 12px → 7px 16px. Font-size
 *   (fs-sm), weight (fw-medium), and border-radius (pill) were already
 *   matched to the card pill. Letter-spacing, text-transform, and
 *   shadow remain marker-specific (numeric text + map-backdrop reasons
 *   documented in marker rule comment).
 *
 *   The map now reads with chunkier markers — slightly more presence
 *   over street labels. Watch for marker overlap at dense viewports;
 *   may need Google Maps clustering tuning later if it becomes a
 *   problem at scale.
 *
 * v1.0.7 (Chunk E Phase 2 polish 10): pill optical density bump.
 *   Card status pill (.lore-listings-card__status-pill) gets larger
 *   glyphs at the same Medium weight to compensate for the synthesized-
 *   Medium thinness (true Articulat Medium cut is pending license per
 *   BACKLOG §3.1). Single-listing pill (.lore-listing__status in
 *   listing.css) gets the same bump in this ship via remote sed — both
 *   pills share the canonical contract per polish-8 alignment.
 *
 *     font-size: var(--fs-xs)   → var(--fs-sm)    (12px → 14px)
 *     padding:   6px 14px       → 7px 16px        (proportional)
 *
 *   No weight change (stays at --fw-medium); we deliberately do NOT
 *   bump to --fw-bold because the bold cut is also synthesized today
 *   and would render fuzzier, not crisper. Bigger glyphs at Medium
 *   weight = better optical density at category-label role.
 *
 *   When the Articulat Medium license lands (BACKLOG §3.1), the pill
 *   may want to drop back to fs-xs / 6px 14px since true Medium will
 *   carry sufficient weight at the smaller size. Re-evaluate at that
 *   time. The pill scale here is calibrated against synthesized
 *   Medium, not the brand-book target.
 *
 *   Markers (.lore-listings-marker) NOT changed — already at fs-sm,
 *   their role + scale on the map is distinct from card pills sitting
 *   over hero imagery.
 *
 * v1.0.6 (Chunk E Phase 2 polish 8): canonical status visual language.
 *   Establishes the brand-guideline-aligned 4-state status palette
 *   across BOTH the card pill AND the map markers. Mirrors the
 *   .lore-listing__status contract from listing.css on the single-
 *   listing detail page (which polish-8 also updates for --sold and
 *   --off_market parity).
 *
 *   Card status pill (new — replaces .lore-listings-card__status-badge):
 *     - .lore-listings-card__status-pill positioned hero top-left
 *     - Typography mirrors .lore-listing__status: padding 6px 14px,
 *       border-radius var(--radius-pill), fs-xs, fw-medium, lh-tight,
 *       ls-loose, uppercase, white-space nowrap.
 *     - 4 modifier classes for color (see palette below).
 *     - data-toggle="present:status_label" gracefully hides empty.
 *
 *   Marker palette (overhaul):
 *     - Base style → harbor blue + linen (was: cabernet + linen).
 *       Default visual "available looking" since most listings are.
 *     - --available → harbor blue + linen (explicit, matches base)
 *     - --pending   → sand + dark text (unchanged from earlier)
 *     - --sold      → scarlet + linen (was: text-muted grey + linen)
 *     - --off_market → bay mist + cabernet (NEW — exclusive accent)
 *     - Hover state: unified cabernet+linen pulse for all statuses
 *       EXCEPT off_market (which keeps bay-mist text on cabernet bg
 *       to preserve its exclusive signature).
 *
 *   Brand reference (LORE_Guidelines_V1.pdf):
 *     - Page 15: 6-color palette (Scarlet, Cabernet, Linen, Sand,
 *       Harbor Blue, Bay Mist) — no greens, no off-palette deviations.
 *     - Page 17: "Linen on Harbor Blue", "Linen on Scarlet", "Cabernet
 *       on Bay Mist", "Cabernet on Sand" all sanctioned for body copy.
 *       Card pill text uses these exact sanctioned pairings.
 *     - Pages 4, 18, 19, 24: Bay Mist as the "accent pop" / signature
 *       brand-merchandise color — justifies its use for "exclusive
 *       opportunity" off-market listings.
 *
 *   Semantic intent of color choices:
 *     - Available (harbor blue): calm, ready, on-market.
 *     - Pending (sand): warm caution, in-progress.
 *     - Sold (scarlet): loud, delivered, track record — LORE's
 *       loudest tone for the "we sold this" credibility signal.
 *       Polish-8 shifts SOLD from cabernet (quietest red) → scarlet
 *       (loudest red) for this exact reason; the single-listing
 *       pill is updated to match.
 *     - Off-market (bay mist): exclusive, curated, an invitation —
 *       sits OUTSIDE the active-state ladder.
 *
 * v1.0.5 (Chunk E Phase 2 polish 7): "Apple clean" pass. Operator
 * call after polish-6: not yet hitting the intended feel. Step back
 * from density-tightening; lean into whitespace and hierarchy.
 *
 *   - Map / rail basis: 55/45 → 50/50. Co-equal surfaces. Forward-
 *     looking for the upcoming view-mode toggle (Redfin-style
 *     map-only / split / list-only), where the 50/50 baseline reads
 *     as "this is one of three balanced layouts."
 *   - Rail max-width: 700px → 900px. On ultrawide monitors this caps
 *     so the rail doesn't dominate; on standard 1440-1700px monitors
 *     the rail can grow to its 50% basis, giving cards meaningfully
 *     more horizontal room (~360-380px wide vs ~330px in polish-6).
 *   - Card body padding: space-2 (8px) → space-4 (16px). Generous
 *     internal padding is what makes cards feel premium rather than
 *     packed. Same call applies to grid gap below.
 *   - Card grid gaps: vertical (rows) space-2 → space-4 (8 → 16px);
 *     horizontal (columns) space-2 → space-3 (8 → 12px). More
 *     whitespace between cards reads as "considered."
 *   - Price font: fs-lg (18px) → fs-xl (22px). The price is what
 *     buyers scan for; it should be the visual anchor of the card.
 *   - Title font: fs-xs (12px) → fs-sm (14px). Better readability
 *     at the wider card width. 2-line clamp unchanged.
 *   - Card border-radius: var(--radius-lg) [12px] → 16px hardcoded.
 *     No --radius-xl token exists in tokens.css; 16px deviation
 *     is a one-off for the listings card. If a second consumer wants
 *     the same radius later, promote to tokens. Apple-product cards
 *     use ~16-20px radius for the "premium object" feel.
 *
 *   Hero aspect (3/2), border-top divider removal, metric strip
 *   restructure (CAP · GRM · PPU · PSF with present:&lt;field&gt;
 *   toggles), and CSS-generated separators all carry over from
 *   polish-6 unchanged.
 *
 *   Density trade-off: at typical viewport (~720-800px rail
 *   visible), this configuration shows ~2 cards per column +
 *   half-of-a-third teasing below the fold. That's fewer listings
 *   visible than polish-6's ~3-per-column. Operator-confirmed
 *   acceptable trade: feel over density.
 *
 * v1.0.4 (Chunk E Phase 2 polish 6): hero ratio + metric strip refresh.
 *   - Hero aspect: 16/9 → 3/2. Hero now occupies ~67% of card height
 *     vs ~61% previously. Matches Zillow card density closer than
 *     polish-5's 16/9. Operator spec: "want the hero image bigger."
 *   - Metrics inline strip: removed border-top divider above. The
 *     visual unit between title and metrics is now seamless rather
 *     than partitioned. Removed padding-top that paired with the
 *     border. The previous separator was excess structure.
 *   - Metric set change: was units · cap · grm · year_built; now
 *     CAP · GRM · PPU · PSF. Operator spec — pricing economics.
 *   - Separator approach changed: manual <span class="metric-sep">·</span>
 *     elements gone from markup, replaced with CSS-generated content
 *     via .lore-listings-card__metric-pair + .lore-listings-card__
 *     metric-pair::before. Combined with :not([hidden]) on both sides
 *     of the adjacent-sibling combinator, this means: a missing
 *     metric drops out cleanly along with its separator without any
 *     JS-side DOM manipulation. JS toggles [hidden]; CSS handles the
 *     visual rest.
 *
 * v1.0.3 (Chunk E Phase 2 polish 5): final density + ratio pass.
 *   - Map / rail basis: 58/42 → 55/45 (rail claims slightly more of
 *     the desktop split, approximating Zillow's reference ratio).
 *   - Rail max-width: 640px → 700px (room for 2 columns of cards
 *     with bigger horizontal breathing per card).
 *   - Card body padding: space-3 (12px) → space-2 (8px). Tighter.
 *   - Hero aspect: 16/10 → 16/9. Shaves ~17px of vertical card
 *     height — combined with denser gaps this lands ~3 cards per
 *     column visible without scroll at typical desktop viewport
 *     heights (~720-800px rail visible area).
 *   - Address slot binding (now title field per polish-4): font
 *     drops from fs-sm to fs-xs, removes the white-space:nowrap +
 *     text-overflow:ellipsis 1-line rule, replaces with a 2-line
 *     clamp via -webkit-line-clamp. Long Portal Display Names like
 *     "Channing Way Opportunity | Owner Occupy One + Rent Two"
 *     wrap to 2 lines instead of truncating mid-word. If the title
 *     still exceeds 2 lines, the third line ellipsises — cards
 *     keep matching heights in the grid.
 *   - Card grid gaps: vertical (rows) and horizontal (columns)
 *     both space-3 → space-2 (12px → 8px). Reduces visual
 *     whitespace between cards, increases effective card density
 *     without growing card dimensions.
 *
 * v1.0.2 (Chunk E Phase 2 polish 3): 2-column card grid + density tighten.
 *   - Rail width: was max-width 480px on desktop (single-column card stack);
 *     became max-width 640px with the grid splitting into 2 columns.
 *     (v1.0.3 expanded further to 700px.) Map basis 60% → 58% to absorb
 *     the wider rail. Operator spec: "scale cards back by ~30%, get
 *     two columns side by side, more listings populated in rail."
 *   - Cards grid: was `flex-direction: column` (single stack); now CSS
 *     Grid `grid-template-columns: repeat(2, 1fr)` on desktop, stays
 *     single-column on mobile (< 992px). Gap unchanged.
 *   - Card density: hero aspect 16/9 → 16/10 (v1.0.3 reverts to 16/9);
 *     body padding space-4 → space-3 (v1.0.3 drops to space-2); price
 *     font fs-xl → fs-lg (v1.0.3 unchanged); metrics restructured from
 *     a 3-column stacked dt/dd grid to a single inline row matching
 *     Zillow pattern — same data, ~60% the vertical footprint.
 *   - FEATURED eyebrow capability removed entirely (operator spec; not
 *     used in production listings).
 *
 * v1.0.1 (Chunk E Phase 2 polish 2): GP cage-breakout per ADR-048.
 *   Body-class scoped rule declaring explicit width:100% + max-width:none
 *   on both #page and .lore-listings-page — same shape auth pages use.
 *   See in-file comment block for the diagnosis + cross-references.
 *
 * Loaded on /listings/ only via the conditional lore_listings_enqueue
 * function in theme/functions.php (priority 20, gates on
 * is_post_type_archive('listing')). Depends on lore-listing (Chunk D's
 * single-listing stylesheet) per convention #11 linear dep chain — that
 * dependency exists not because we reuse rules from listing.css but to
 * preserve the cascade order: lore-tokens → ... → lore-listing →
 * lore-listings. Token references throughout are var(--...) from
 * tokens.css v1.1.0.
 *
 * Layout: mobile-first per convention. Mobile stacks map above cards;
 * desktop splits map left / cards rail right (rail = 2-column grid).
 * Phase 4 will add the mobile bottom-sheet flow; until then mobile is
 * the simple stack.
 *
 * BEM root: .lore-listings-page (the outer <main>) + .lore-listings-card
 * (each card). The two share the lore-listings- prefix but are siblings,
 * not parent/child.
 *
 * Cross-references:
 *   - SCOPE-CHUNK-E-v1.0.0.md §6 (card spec) + §7 (marker spec) + §1.10
 *     (proprioception)
 *   - tokens.css v1.1.0 (color, type, space, radius variables)
 *   - assets/js/lore-listings-v1.0.0.js (orchestrator + marker rendering)
 *   - ADR-048 (cage-breakout pattern — auth pages precedent; this
 *     stylesheet is the second consumer site of the same pattern)
 *   - convention #22 (cascade defense)
 */


/* ──────────────────────────────────────────────────────────────────────
 * GeneratePress cage breakout (ADR-048)
 *
 * The CPT archive body carries `post-type-archive` + `post-type-archive-
 * listing` classes (WP-core convention). Without this rule, the GP
 * `#page.grid-container { max-width: 1200px }` clamps the map+rail
 * column to 1200px centered, AND because GP also applies `display:
 * flex` on <body>, the inner <main> shrinks to min-content width
 * inside #page leaving large empty zones on both sides. The fix
 * declares explicit width:100% + max-width:none on BOTH the GP
 * wrapper AND our <main> — matching the auth.css pattern verbatim.
 * ─────────────────────────────────────────────────────────────────── */

body.post-type-archive-listing #page,
body.post-type-archive-listing .lore-listings-page {
    width: 100%;
    max-width: none;
    margin: 0;
}


/* ──────────────────────────────────────────────────────────────────────
 * Page layout
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    background: var(--color-linen);
}

@media (min-width: 992px) {
    .lore-listings-page {
        flex-direction: row;
        height: calc(100vh - var(--lore-header-height, 80px));
        min-height: 600px;
        /* v1.1.0 (Phase 3): the .lore-listings-overlay absolute-positions
           against this — needs an explicit positioning context. Adding
           here in the desktop media query because the overlay is desktop-
           only (display:none below 992px). */
        position: relative;
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * Map column
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-map-column {
    position: relative;
    width: 100%;
    height: 50vh;
    min-height: 320px;
    background: var(--color-surface-muted);
}

@media (min-width: 992px) {
    .lore-listings-map-column {
        flex: 1 1 50%;
        height: 100%;
        min-height: 0;
    }
}

.lore-listings-map {
    width: 100%;
    height: 100%;
    background: var(--color-surface-muted);
}


/* ──────────────────────────────────────────────────────────────────────
 * Cards rail
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-rail {
    width: 100%;
    background: var(--color-linen);
    padding: var(--space-4);
    overflow-y: auto;
    flex: 1 1 auto;
}

@media (min-width: 992px) {
    .lore-listings-rail {
        flex: 0 0 50%;
        max-width: 900px;
        height: 100%;
        padding: var(--space-6);
        border-left: 1px solid var(--color-border-soft);
    }
}

.lore-listings-rail__header {
    /* Search Phase A: was a flex row (title | count); now a block container
       for the stacked search bar (field over count). Title retired (§3.63). */
    display: block;
    margin-bottom: var(--space-4);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--color-border-soft);
}

.lore-listings-rail__count-loading {
    font-style: italic;
}

/* ──────────────────────────────────────────────────────────────────────
 * Search bar (Search Phase A) — partials/listing-search-bar.php
 *
 * Sits in the rail header, replacing the prior "Listings" title. Hosts the
 * Google Places (New) PlaceAutocompleteElement (mounted by
 * lore-listings-search-v1.0.0.js) and the rehomed result count. The no-JS
 * fallback <input> and the live element share resting looks so the bar does
 * not visibly reflow when the element swaps in. The web component renders its
 * own input inside a shadow root, so only the host is sizable here; deeper
 * theming via its exposed CSS parts is a polish-round item.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-search {
    display: block;
}

.lore-listings-search__field {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--space-3);
}

.lore-listings-search__ac {
    flex: 1 1 auto;
    min-width: 0;
}

.lore-listings-search__input {
    width: 100%;
    box-sizing: border-box;
    font-family: var(--font-body);
    font-size: 16px; /* literal: 16px avoids iOS focus-zoom on the search input */
    color: var(--color-text);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    padding: var(--space-3) var(--space-4);
}

.lore-listings-search__input::placeholder {
    color: var(--color-text-muted);
}

.lore-listings-search__input:focus {
    outline: none;
    border-color: var(--color-text);
}

.lore-listings-search__ac gmp-place-autocomplete,
.lore-listings-search__pac {
    display: block;
    width: 100%;
}

/* polish-11: pin the search to light. The Google Place Autocomplete web
   component (and the no-JS fallback input) follow the device color scheme
   through their shadow DOM, so on a device in dark mode the search rendered
   charcoal against the otherwise light-only site. color-scheme:light forces
   light regardless of device appearance. (color-scheme inherits through the
   shadow boundary, so the host element / ancestor is enough.) */
.lore-listings-search,
.lore-listings-search__input,
.lore-listings-search__ac gmp-place-autocomplete {
    color-scheme: light;
}

.lore-listings-search__count {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-text-muted);
    margin: var(--space-3) 0 0;
}


/* ──────────────────────────────────────────────────────────────────────
 * Advanced filters (Search Phase B1) — partials/listing-filters.php
 *
 * The "Filters" button lives in the search field row; the panel drops down
 * beneath the field (inline expand — it pushes the cards grid down rather
 * than overlaying it, so nothing is occluded). Collapsed by default via the
 * `hidden` attribute, toggled by lore-listings-filters-v1.0.0.js.
 *
 * No sliders: price + units are dual min/max fields (partial entry → range),
 * CAP a single minimum, status multi-select chips. Selected chips take the
 * harbor-blue (available) brand accent; the footer's primary action takes
 * cabernet. The Sold chip is masked in the partial until sold listings load.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-search__filters-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-text);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    padding: var(--space-3) var(--space-4);
    cursor: pointer;
    white-space: nowrap;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
    transition: border-color 120ms ease, background-color 120ms ease;
}

/* :hover/:focus/:active restate the resting white background + text color.
   This defeats GeneratePress's parent-theme global button:focus / button:active
   rule (generate-style-inline-css sets background:#3f4047; color:#fff on every
   <button> site-wide) which otherwise renders this button charcoal on click
   until focus leaves. Same root cause + fix as .lore-listings-overlay__close
   (see its :focus note). Keyboard focus indication stays on :focus-visible. */
.lore-listings-search__filters-btn:hover,
.lore-listings-search__filters-btn:focus,
.lore-listings-search__filters-btn:active,
.lore-listings-search__filters-btn[aria-expanded="true"] {
    background: var(--color-white, #fff);
    color: var(--color-text);
    border-color: var(--color-text);
}

.lore-listings-search__filters-btn:focus-visible {
    outline: none;
    border-color: var(--color-text);
    box-shadow: 0 0 0 2px var(--color-harbor-blue);
}

/* Active-filter count badge inside the toggle. */
.lore-listings-filters__badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    font-size: var(--fs-xs);
    line-height: 1;
    color: var(--color-white, #fff);
    background: var(--color-harbor-blue);
    border-radius: var(--radius-round);
}

.lore-listings-filters__badge[hidden] {
    display: none;
}

.lore-listings-filters {
    margin: var(--space-3) 0 0;
    padding: var(--space-4);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border-soft);
    border-radius: var(--radius-lg);
}

.lore-listings-filters[hidden] {
    display: none;
}

.lore-listings-filters__group {
    margin-bottom: var(--space-4);
}

.lore-listings-filters__group:last-of-type {
    margin-bottom: 0;
}

.lore-listings-filters__group--split {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-4);
}

.lore-listings-filters__label {
    margin: 0 0 var(--space-2);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-text-muted);
}

/* Status chips */
.lore-listings-filters__chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
}

.lore-listings-filters__chip {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-text);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    padding: var(--space-2) var(--space-4);
    cursor: pointer;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
    transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}

/* Deselected chip across :hover/:focus/:active — restate the resting white
   fill + text color so GeneratePress's global button:focus / button:active
   (background:#3f4047; color:#fff) can't render the chip charcoal between
   click and blur. The reported dark flash on deselect was exactly this; same
   fix as .lore-listings-overlay__close. */
.lore-listings-filters__chip:hover,
.lore-listings-filters__chip:focus,
.lore-listings-filters__chip:active {
    color: var(--color-text);
    background: var(--color-white, #fff);
    border-color: var(--color-text);
}

/* Selected chip wins in every interaction state (harbor-blue fill). Higher
   specificity than both the deselected override above and GeneratePress's
   button:focus, so a selected chip stays branded on click / hover / focus. */
.lore-listings-filters__chip[aria-pressed="true"],
.lore-listings-filters__chip[aria-pressed="true"]:hover,
.lore-listings-filters__chip[aria-pressed="true"]:focus,
.lore-listings-filters__chip[aria-pressed="true"]:active {
    color: var(--color-white, #fff);
    background: var(--color-harbor-blue);
    border-color: var(--color-harbor-blue);
}

.lore-listings-filters__chip:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--color-harbor-blue);
}

/* Dual min/max rows + CAP */
.lore-listings-filters__row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.lore-listings-filters__input {
    width: 100%;
    box-sizing: border-box;
    font-family: var(--font-body);
    font-size: 16px; /* 16px: avoids iOS focus-zoom, matching the search input */
    color: var(--color-text);
    background: var(--color-white, #fff);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    padding: var(--space-3) var(--space-3);
}

.lore-listings-filters__input::placeholder {
    color: var(--color-text-muted);
}

.lore-listings-filters__input:focus {
    outline: none;
    border-color: var(--color-text);
}

.lore-listings-filters__dash {
    flex: 0 0 auto;
    color: var(--color-text-muted);
}

.lore-listings-filters__capwrap {
    position: relative;
}

.lore-listings-filters__input--cap {
    padding-right: calc(var(--space-4) + 8px);
}

.lore-listings-filters__suffix {
    position: absolute;
    right: var(--space-3);
    top: 50%;
    transform: translateY(-50%);
    color: var(--color-text-muted);
    pointer-events: none;
}

/* Footer */
.lore-listings-filters__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--color-border-soft);
}

.lore-listings-filters__reset {
    border: none;
    background: none;
    padding: 0;
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-text-muted);
    text-decoration: underline;
    cursor: pointer;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
}

/* Keep the link-style reset transparent on focus/active too (GeneratePress
   would otherwise give this <button> a charcoal fill on click). */
.lore-listings-filters__reset:hover,
.lore-listings-filters__reset:focus,
.lore-listings-filters__reset:active,
.lore-listings-filters__reset:focus-visible {
    background: none;
    color: var(--color-text);
}

.lore-listings-filters__apply {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    color: var(--color-white, #fff);
    background: var(--color-cabernet);
    border: 1px solid var(--color-cabernet);
    border-radius: var(--radius-pill);
    padding: var(--space-3) var(--space-6);
    cursor: pointer;
    white-space: nowrap;
    appearance: none;
    -webkit-tap-highlight-color: transparent;
}

/* Hover (incl. hover-while-focused) → scarlet. :focus:hover is restated so it
   beats the :focus/:active cabernet rule below when both apply. */
.lore-listings-filters__apply:hover,
.lore-listings-filters__apply:focus:hover {
    background: var(--color-scarlet);
    border-color: var(--color-scarlet);
    color: var(--color-white, #fff);
}

/* :focus/:active restate cabernet so GeneratePress's button:focus charcoal
   can't override the brand fill between click and blur. */
.lore-listings-filters__apply:focus,
.lore-listings-filters__apply:active {
    background: var(--color-cabernet);
    border-color: var(--color-cabernet);
    color: var(--color-white, #fff);
}

.lore-listings-filters__apply:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--color-harbor-blue);
}

@media (prefers-reduced-motion: reduce) {
    .lore-listings-search__filters-btn,
    .lore-listings-filters__chip {
        transition: none;
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * Cards grid
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-rail__grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--space-3);
}

/*
 * Two-column card grid on desktop. Below 992px the mobile single-column
 * stack remains (matches the rail's vertical-scroll behavior on touch).
 * The 1fr 1fr (not minmax) reflects that rail max-width is already
 * clamped to 900px — at the clamp ceiling, 2 cards × ~380-400px + gap
 * fits comfortably; below the clamp, cards shrink proportionally.
 *
 * v1.0.5 gap split into asymmetric values: row-gap space-4 (16px) for
 * generous vertical breathing between rows, column-gap space-3 (12px)
 * for tighter between columns. The visual hierarchy reads as rows-of-
 * pairs rather than a uniform grid, which suits the natural scrolling
 * direction. Previous polish-3 → polish-6 used symmetric space-2 (8px)
 * which packed cards too tightly.
 */
@media (min-width: 992px) {
    .lore-listings-rail__grid {
        grid-template-columns: 1fr 1fr;
        row-gap: var(--space-4);
        column-gap: var(--space-3);
    }
}

/* Empty / error states */

.lore-listings-rail__empty,
.lore-listings-rail__error {
    padding: var(--space-6) var(--space-4);
    text-align: center;
    color: var(--color-text-muted);
    font-family: var(--font-body);
    font-size: var(--fs-base);
}

.lore-listings-rail__empty-headline {
    font-family: var(--font-display);
    font-size: var(--fs-lg);
    color: var(--color-text);
    margin: 0 0 var(--space-2);
}

.lore-listings-rail__clear {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    color: var(--color-cabernet);
    text-decoration: underline;
    cursor: pointer;
    font: inherit;
}

.lore-listings-rail__clear:hover,
.lore-listings-rail__clear:focus-visible {
    color: var(--color-scarlet);
}


/* ──────────────────────────────────────────────────────────────────────
 * Card
 *
 * One card per listing. Single <a> wrapping a hero image + body. At
 * v2.2.2 polish-3 the FEATURED eyebrow was removed (operator spec —
 * capability unused in production). The card root is now hero + body
 * with no preamble.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-card {
    position: relative;
    display: block;
    background: var(--color-white);
    /* 16px hardcoded — no --radius-xl token in tokens.css; the radius
       set there caps at --radius-lg (12px). This card uses a more
       generous radius for the "premium object" feel per polish-7
       Apple-clean direction. If a second consumer wants the same
       radius later, promote to a --radius-xl token. */
    border-radius: 16px;
    overflow: hidden;
    box-shadow: var(--shadow-popup);
    transition: box-shadow 150ms ease-out, transform 150ms ease-out;
}

.lore-listings-card:hover,
.lore-listings-card.is-hovered {
    box-shadow: var(--shadow-card);
    transform: translateY(-2px);
}

.lore-listings-card__link {
    display: block;
    color: inherit;
    text-decoration: none;
}

.lore-listings-card__link:hover,
.lore-listings-card__link:focus-visible {
    color: inherit;
    text-decoration: none;
}


/* Hero image */

.lore-listings-card__hero {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 2;
    background-color: var(--color-sand);
    overflow: hidden;
}

.lore-listings-card__hero-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.lore-listings-card__hero-img[src=""],
.lore-listings-card__hero-img:not([src]) {
    visibility: hidden;
}

/*
 * Card status pill (polish-8). Mirrors the .lore-listing__status
 * contract on the single-listing detail page (assets/css/listing.css)
 * 1:1 except for the absolute positioning (top-left of hero). Same
 * typography, same padding, same radius. Modifier classes carry the
 * brand-guideline-aligned color per status.
 *
 * The single-listing pill is also updated in polish-8 to align its
 * --sold (cabernet → scarlet) and --off_market (border-soft grey →
 * bay-mist + cabernet) per brand guidelines. The two consumer
 * surfaces (detail page + listings index card) share a canonical
 * status visual language from polish-8 forward.
 *
 * Previously: .lore-listings-card__status-badge — shown only for
 * non-available statuses, single cabernet color regardless of state.
 * Polish-8 retires the badge in favor of the pill above.
 */

.lore-listings-card__status-pill {
    position: absolute;
    top: var(--space-3);
    left: var(--space-3);
    /* Polish-10: padding 6px 14px → 7px 16px and font-size fs-xs → fs-sm.
       Pill glyphs at fs-xs (12px) read thin over hero imagery — particularly
       under synthesized Medium weight (Articulat Medium license is pending,
       see BACKLOG §3.1). fs-sm (14px) gives crisper optical density at this
       category-label role; padding bumps proportionally so the pill chrome
       doesn't feel cramped around the larger glyphs.

       Diverges from .lore-listing__status (single-listing pill) by exactly
       the same amount — both pills track the same canonical contract per
       polish-8 cross-surface alignment. listing.css gets the same bump in
       this ship via remote sed. */
    padding: 7px 16px;
    border-radius: var(--radius-pill);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    line-height: var(--lh-tight);
    letter-spacing: var(--ls-loose);
    text-transform: uppercase;
    white-space: nowrap;
    z-index: 2;
}

.lore-listings-card__status-pill--available {
    background: var(--color-harbor-blue);
    color: var(--color-white);
}

.lore-listings-card__status-pill--pending {
    background: var(--color-sand);
    color: var(--color-text);
}

.lore-listings-card__status-pill--sold {
    background: var(--color-scarlet);
    color: var(--color-linen);
}

.lore-listings-card__status-pill--off_market {
    background: var(--color-bay-mist);
    color: var(--color-cabernet);
}

/*
 * "Opens in new tab" affordance hint (Phase 3 polish-1, Pattern B companion).
 *
 * Small ↗ icon in the top-right of every card hero. Symmetric to the
 * status pill's top-left position. Always visible (not hover-only) so
 * mobile users see it too. Purely visual — the wrapping <a> already has
 * target="_blank" set by the orchestrator JS; this icon just communicates
 * that fact discoverably.
 *
 * Translucent dark backdrop circle (rgba 0,0,0,0.45) for readability against
 * any hero photo — works equally well over bright skies, dark roof tiles,
 * mid-tone foliage. White stroke arrow SVG.
 *
 * 28×28 outer circle + 14×14 SVG inside. Smaller than the overlay header
 * buttons (44×44) because the card hero already carries the status pill
 * top-left and the photo-count badge bottom-right — visual real estate is
 * scarcer. The hint is intentionally low-emphasis: noticed by users who
 * scan-and-learn-the-convention, but doesn't compete with the listing
 * imagery for primary attention.
 */
.lore-listings-card__external-hint {
    position: absolute;
    top: var(--space-3);
    right: var(--space-3);
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-round);
    background: rgba(0, 0, 0, 0.45);
    color: var(--color-white);
    pointer-events: none;
    z-index: 2;
    transition: background 150ms ease-out, transform 150ms ease-out;
}

/* Card hover lifts the hint slightly and darkens the backdrop — subtle
   affordance feedback echoing the card's existing hover lift. */
.lore-listings-card:hover .lore-listings-card__external-hint,
.lore-listings-card.is-hovered .lore-listings-card__external-hint {
    background: rgba(0, 0, 0, 0.65);
    transform: scale(1.08);
}

@media (prefers-reduced-motion: reduce) {
    .lore-listings-card__external-hint {
        transition: none;
    }
    .lore-listings-card:hover .lore-listings-card__external-hint,
    .lore-listings-card.is-hovered .lore-listings-card__external-hint {
        transform: none;
    }
}

/*
 * Sold-state mask. Applied to the card root by JS when payload.price_masked
 * === true. The card visually downplays (lighter background, muted text)
 * to signal that this listing is not currently buyable at the displayed
 * price (because price is masked-out).
 */
.lore-listings-card--masked .lore-listings-card__hero {
    filter: grayscale(0.3);
}

.lore-listings-card--masked .lore-listings-card__price {
    color: var(--color-text-muted);
}


/* Card body
 *
 * Polish-7: body padding space-2 → space-4 (8 → 16px). Generous
 * internal whitespace is what makes the card feel premium. Inverts
 * polish-3/polish-5/polish-6 density tightening direction.
 */

.lore-listings-card__body {
    padding: var(--space-4);
}

.lore-listings-card__price {
    /* Polish-12: visual hierarchy swap. Price drops from PRIMARY anchor
       (display Larken serif at fs-xl, set at polish-7) to SECONDARY
       supporting info (body Articulat sans at fs-lg). Stays dark text +
       regular weight + tight letter-spacing — secondary doesn't mean
       muted or invisible, it means one tier below title. fs-lg (20px)
       is two ticks below the new fs-xl (22px) title; price reads as
       supporting info to title's identity, but is still clearly visible
       as a fast-scan anchor for buyers comparing listings. Bottom margin
       grows space-2 → space-3 (8px → 12px) for separation from the
       metric strip below — the title + price block reads as a tied
       identity unit; metric strip reads as supporting data below it.
       Display Larken font reserved for primary anchors per brand
       discipline (ADR-012); secondary surfaces stay in body Articulat. */
    font-family: var(--font-body);
    font-size: var(--fs-lg);
    font-weight: var(--fw-regular);
    color: var(--color-text);
    letter-spacing: var(--ls-tight);
    margin: 0 0 var(--space-3);
    line-height: var(--lh-tight);
}

.lore-listings-card__address {
    /* Polish-12: visual hierarchy swap. Title (semantically Portal
       Display Name per polish-4; class name kept as `__address` for
       backward compat) promotes from SECONDARY (body Articulat at
       fs-sm muted, set at polish-3+5+7) to PRIMARY anchor (display
       Larken serif at fs-xl, dark text). Title is the listing's
       identity — what the operator curates as the buyer-facing name
       ("Channing Way Opportunity"). Polish-7's previous direction
       had price as primary anchor; polish-12 corrects to title-
       primary, price-secondary per operator visual call. Bottom
       margin drops space-3 → space-1 (12px → 4px): title and price
       now read as a tied identity unit, so the gap between them
       tightens. Larken serif gives the title weight + character
       beyond what fs-xl alone provides — display fonts are reserved
       for primary anchors per ADR-012 brand discipline. */
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-weight: var(--fw-regular);
    color: var(--color-text);
    margin: 0 0 var(--space-1);
    line-height: var(--lh-tight);
    letter-spacing: var(--ls-tight);
    /* v1.0.3 polish-5 / v1.0.9 polish-12: 2-line clamp preserved. Portal
       Display Names like "Channing Way Opportunity | Owner Occupy One +
       Rent Two" carry operator-curated context — truncating to one line
       loses signal. At the new fs-xl Larken treatment, 2-line titles do
       take more vertical space (~60-70px tall) than the prior fs-sm
       body treatment, but the card grid's auto-rows absorb the variance
       and title is now the primary anchor — worth the height cost. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    /* Modern equivalent (Safari/Firefox both honor -webkit-* for now;
       this is the standards-track property for future portability). */
    line-clamp: 2;
}

/* Street address line under the title (ADR-100). The title slot above
   (legacy class `__address`) carries the Headline__c; THIS line carries the
   actual street address, composed by the JS orchestrator into the
   [data-address-full] node. Secondary + muted + single-line ellipsis so the
   dense 2-col card stays compact. Title margin-bottom (--space-1) tucks this
   close under the title as its address; --space-2 below separates it from
   the price. Mirrors the single-listing hero address line, one tier smaller. */
.lore-listings-card__location {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    font-weight: var(--fw-regular);
    color: var(--color-text-muted);
    margin: 0 0 var(--space-2);
    line-height: var(--lh-snug);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Inline metric strip (polish-3 — replaces the old dt/dd grid).
 *
 * Renders horizontally: "3 units · 4.6% cap · 11.7 grm · 1931". The
 * separator characters (·) are their own spans so they can be hidden
 * via CSS for narrow cards (under 240px the strip drops separators
 * and wraps). Metric labels are lowercase to match Zillow density;
 * the value is body weight, the label is muted.
 */
.lore-listings-card__metrics-inline {
    font-family: var(--font-body);
    font-size: var(--fs-xs);
    color: var(--color-text);
    margin: 0;
    line-height: var(--lh-snug);
}

.lore-listings-card__metric-pair {
    white-space: nowrap;
}

.lore-listings-card__metric-label {
    color: var(--color-text-muted);
}

/*
 * Generated separator (polish-6). Replaces the static
 * .lore-listings-card__metric-sep spans that lived between metric-pairs
 * in earlier polish rounds. The ::before runs on every metric-pair
 * EXCEPT the first VISIBLE one, achieved by :not([hidden]) on both
 * sides of the adjacent-sibling combinator. When a metric-pair is
 * hidden via data-toggle="present:<field>" (because the listing has
 * that metric null), neither the pair NOR the separator renders.
 *
 * Why this beats DOM mutation in JS: keeps the markup declarative
 * (the template doesn't know about visibility logic); keeps the JS
 * toggle pipeline single-purpose (it sets [hidden], CSS handles the
 * rest); and the rule is one place where the "what's a separator"
 * decision lives.
 */
.lore-listings-card__metric-pair:not([hidden]) + .lore-listings-card__metric-pair:not([hidden])::before {
    content: " · ";
    color: var(--color-text-muted);
    margin: 0 var(--space-1);
}

.lore-listings-card__agent {
    font-family: var(--font-body);
    font-size: var(--fs-xs);
    color: var(--color-text-muted);
    margin: var(--space-2) 0 0;
    padding-top: var(--space-2);
    border-top: 1px solid var(--color-border-soft);
}


/* ──────────────────────────────────────────────────────────────────────
 * Skeleton loading state
 *
 * Mirrors the real card structure with placeholder grays. JS removes
 * these from the DOM when real cards are inserted; CSS-only animation
 * during the loading window.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-card--skeleton {
    background: var(--color-white);
    pointer-events: none;
}

.lore-listings-card--skeleton .lore-listings-card__hero {
    background: linear-gradient(
        90deg,
        var(--color-border-soft) 0%,
        var(--color-border) 50%,
        var(--color-border-soft) 100%
    );
    background-size: 200% 100%;
    animation: lore-listings-skeleton-shimmer 1.5s ease-in-out infinite;
}

.lore-listings-card--skeleton .lore-listings-card__body {
    padding: var(--space-4);
}

.lore-listings-card__line {
    height: 12px;
    background: var(--color-border-soft);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-2);
}

.lore-listings-card__line--w70 { width: 70%; }
.lore-listings-card__line--w50 { width: 50%; }
.lore-listings-card__line--w40 { width: 40%; }

@keyframes lore-listings-skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
    .lore-listings-card--skeleton .lore-listings-card__hero {
        animation: none;
    }
    .lore-listings-card {
        transition: none;
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * Map markers
 *
 * Markers render via Google Maps AdvancedMarkerElement with HTML content
 * — we control the markup, Maps positions/clusters/handles events. The
 * marker shape is a price-pill: rounded background, brand color, short
 * price text. Hover state coupled to the matching card via JS (the
 * "proprioception" pattern per SCOPE §1.10).
 * ─────────────────────────────────────────────────────────────────── */

/*
 * Marker base. Polish-8: the base style is now AVAILABLE (harbor blue
 * background, linen text). Previously base was cabernet, which meant
 * cabernet markers carried "this is a LORE listing" semantics. The
 * polish-8 shift is intentional: on the MAP, the primary visual color
 * should signal "what's actionable" (available now), and harbor blue
 * is the canonical AVAILABLE color per brand guidelines + the existing
 * .lore-listing__status--available pill on the single-listing detail
 * page. Brand-cabernet still carries through everywhere else (LORE
 * wordmark, eyebrow photo overlays, card price text, hover states).
 *
 * The base rule also serves as fallback for listings with empty/unknown
 * status — they render harbor-blue-ish ("available looking"), which is
 * the right default until source data corrects.
 */
.lore-listings-marker {
    display: inline-flex;
    align-items: center;
    /* Polish-11: padding bumped from var(--space-1) var(--space-3)
       (4px 12px) → 7px 16px to match .lore-listings-card__status-pill
       polish-10 chrome. Visual parity between map markers and card
       pills: same font-size (fs-sm), same weight (medium), same
       border-radius (pill), same padding (7px 16px). Markers retain
       their shadow (sit on busy map background) — pills don't need
       one (sit on photos). Numeric price text doesn't take the pill's
       letter-spacing or uppercase transform.

       Side effect to watch: at dense viewports with many nearby
       listings, slightly larger markers overlap more and can occlude
       street labels. Google Maps clustering threshold may need tuning
       in a future round if this becomes visible at scale. Filing for
       BACKLOG observation. */
    padding: 7px 16px;
    background: var(--color-harbor-blue);
    color: var(--color-linen);
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    font-weight: var(--fw-medium);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-popup);
    white-space: nowrap;
    cursor: pointer;
    transition: transform 150ms ease-out, box-shadow 150ms ease-out, background 150ms ease-out;
}

/*
 * Hover/coupled-hover state. Single unified hover behavior across all
 * statuses: the marker pulses cabernet (brand-primary-dark). Brand
 * cabernet returns on engagement — markers carry status info at rest
 * and brand on interaction. Scale + shadow lift signal "active."
 */
.lore-listings-marker:hover,
.lore-listings-marker.is-hovered {
    background: var(--color-cabernet);
    color: var(--color-linen);
    transform: scale(1.08);
    box-shadow: var(--shadow-card);
    z-index: 10;
}

/*
 * Status palette per brand guidelines page 15-18 (LORE_Guidelines_V1.pdf).
 * Each status maps to a palette color with semantic intent:
 *
 *   --available  → Harbor Blue + Linen.  Calm, ready, on-market.
 *                  Sanctioned pair "Linen on Harbor Blue" per page 17.
 *                  Same as .lore-listing__status--available on detail page.
 *
 *   --pending    → Sand + dark text.    Warm caution, in-progress.
 *                  Same as .lore-listing__status--pending on detail page.
 *
 *   --sold       → Scarlet + Linen.     Loud, delivered, track-record.
 *                  Sanctioned pair "Linen on Scarlet" per page 17.
 *                  Polish-8 CHANGES this from cabernet/muted-grey of
 *                  earlier polish rounds; brings the markers in line
 *                  with the .lore-listing__status--sold pill (which
 *                  polish-8 ALSO migrates from cabernet → scarlet for
 *                  the same loud-track-record-signal reason).
 *
 *   --off_market → Bay Mist + Cabernet. "Exclusive opportunity" pop.
 *                  Bay Mist is the brand's signature accent pop color
 *                  per guidelines pages 4, 18, 19, 24. Off-market
 *                  listings aren't a public state — they're a curated
 *                  invitation. Bay Mist sits OUTSIDE the active-state
 *                  ladder (available/pending/sold) intentionally.
 *
 * All four are explicit modifier classes (rather than relying on
 * absence-of-modifier for "available"). The JS map module applies
 * the class explicitly per listing.status — see lore-listings-map-
 * v1.0.0.js buildMarkerElement.
 */

.lore-listings-marker--available {
    background: var(--color-harbor-blue);
    color: var(--color-linen);
}

.lore-listings-marker--pending {
    background: var(--color-sand);
    color: var(--color-text);
}

.lore-listings-marker--pending:hover,
.lore-listings-marker--pending.is-hovered {
    /* Inherits .lore-listings-marker:hover (cabernet+linen); listed
       here explicitly to make the hover color shift documented at
       each status level rather than spread across cascading rules. */
    background: var(--color-cabernet);
    color: var(--color-linen);
}

.lore-listings-marker--sold {
    background: var(--color-scarlet);
    color: var(--color-linen);
}

.lore-listings-marker--off_market {
    background: var(--color-bay-mist);
    color: var(--color-cabernet);
}

.lore-listings-marker--off_market:hover,
.lore-listings-marker--off_market.is-hovered {
    /* On-hover, off-market markers shift to cabernet+bay-mist text
       (inverted from base) — preserves the bay-mist accent in the
       text role rather than dropping it. Differs from other statuses'
       hover (which all go cabernet+linen) to keep off-market's
       "exclusive" visual signature even on engagement. */
    background: var(--color-cabernet);
    color: var(--color-bay-mist);
}


/* ──────────────────────────────────────────────────────────────────────
 * Off-viewport proprioception fade
 *
 * SCOPE §7.3 — cards corresponding to markers outside the current map
 * viewport get a subtle fade. The JS module tracks which listings are
 * in the bbox and toggles the .is-off-viewport class.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-card.is-off-viewport {
    opacity: 0.5;
    transition: opacity 200ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
    .lore-listings-card.is-off-viewport {
        transition: none;
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * Chunk E Phase 3 — Desktop overlay slide-in
 *
 * Slides in over the .lore-listings-rail from the right edge when a
 * card or marker is clicked. Renders the full single-listing template
 * inside the scroll container (AJAX-fetched + injected by the overlay
 * controller — assets/js/lore-listings-overlay-v1.0.0.js).
 *
 * Layout shape (Shape A from Phase 3 plan): the overlay is position:
 * absolute over the .lore-listings-page parent at the rail's right-half
 * position. The rail stays in flex flow underneath; the overlay covers
 * it during the slide-in but doesn't displace it. The map (left half)
 * is unaffected positionally and stays fully interactive.
 *
 * Mobile bypass per SCOPE §1.2 Phase 3 lock: display:none below the
 * 992px desktop breakpoint. The overlay controller also gates open()
 * on a matchMedia check; if either layer fails, the click falls
 * through to native /listing/<slug> navigation.
 *
 * Animation per SCOPE §9.2: transform translateX(100%) → translateX(0)
 * over 250ms ease-out. CSS-transform animation only (not View
 * Transitions API at this ship — VT API is for cross-document; this
 * is same-document). View Transitions could be a polish-round
 * candidate if browser support widens further.
 *
 * Sticky-CTA-inside-overlay override (ADR-031): when body has
 * .overlay-open, the floating inquiry CTA (which is position:fixed at
 * bottom-right of viewport by default) becomes sticky inside the
 * overlay scroll container instead. The CTA was already positioned
 * absolutely against its inquiry-root parent; we leverage that here.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-overlay {
    /* Mobile default: not used. The controller falls through to
       native nav. display:none keeps the element entirely out of
       the layout below the desktop breakpoint. */
    display: none;
}

@media (min-width: 992px) {

    .lore-listings-overlay {
        position: absolute;
        top: 0;
        right: 0;
        /* Match the .lore-listings-rail width — 50% of the
           .lore-listings-page parent. The map (left half) stays
           visible at its left:0 position. */
        width: 50%;
        height: 100%;
        /* White panel surface — matches the standalone single-listing
           rendering. listing.css line 727: .lore-listing__hero {
           background: var(--color-white); }. The injected single-
           listing content expects white throughout; using linen here
           would create a tonal mismatch between standalone /listing/
           <slug> and overlay-injected variants. The brand linen stays
           on the listings INDEX surface (the cards rail + map page
           background), which is the browse canvas. White = document
           surfaces (detail pages, overlay-as-document); linen =
           browse surfaces (index, search). Principled split. */
        background: var(--color-white);
        z-index: 100;

        /* Slide-in start state: translated fully off-screen to the
           right. The --open class moves it to translateX(0). */
        transform: translateX(100%);
        transition: transform 250ms ease-out;

        /* Layout the close-button header + scroll container as a
           vertical flex column. Header sticks at top; scroll
           container fills remaining space. */
        display: flex;
        flex-direction: column;

        /* Subtle left edge to delineate from the map underneath
           during the brief slide-in moment. */
        border-left: 1px solid var(--color-border-soft);
        box-shadow: -8px 0 24px rgba(0, 0, 0, 0.08);
    }

    .lore-listings-overlay[hidden] {
        /* The HTML5 hidden attribute would normally apply display:none,
           but we override display above for the breakpoint match. When
           the controller toggles `hidden`, we want the element fully
           removed from interaction + tab order. */
        display: none;
    }

    .lore-listings-overlay--open {
        transform: translateX(0);
    }

    .lore-listings-overlay__header {
        flex: 0 0 auto;
        display: flex;
        justify-content: flex-end;
        align-items: center;
        gap: var(--space-2);
        padding: var(--space-3) var(--space-4);
        border-bottom: 1px solid var(--color-border-soft);
        background: var(--color-white);
    }

    .lore-listings-overlay__close {
        /* Inline-SVG close button (polish-1, symmetry-fix with the
           open-external <a>). 44×44 touch target, sand-tinted circle
           at rest (var(--color-border-soft)), darkens to var(--color-
           border) on hover/active. The X glyph is drawn by an inline
           <svg> in archive-listing.php; CSS rule below sizes it to
           24×24 to match the open-external pattern.
           
           Earlier iterations attempted ::before/::after pseudo-element
           X drawing copied from .lore-inquiry-modal__close. DevTools
           confirmed identical computed styles between this button and
           the working open-external <a> button, but the pseudo-element
           X rendered unreliably in the overlay's flex-container
           context. Switched to inline SVG for structural symmetry —
           both buttons now structurally identical except for the
           icon path. Close-button-in-circle convention worth promoting
           to canonical tracked as a Phase 3 follow-up in ADR-073. */
        position: relative;
        appearance: none;
        width: 44px;
        height: 44px;
        padding: 0;
        margin: 0;
        border: none;
        border-radius: var(--radius-round);
        background: var(--color-border-soft);
        color: var(--color-text);
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        -webkit-tap-highlight-color: transparent;
        transition: color 0.15s ease, background 0.15s ease;
    }

    .lore-listings-overlay__close:hover {
        background: var(--color-border);
        /* Restate color explicitly. The close button is a <button>
           element, and some user-agent stylesheets apply default
           color transitions on :hover that flip text color (most
           visibly: the SVG's stroke="currentColor" rendering white
           instead of the inherited --color-text). The open-external
           <a> button doesn't have this UA default — but we apply the
           same explicit `color` on its hover rule below for symmetry.
           
           Note: :active was previously combined with :hover here.
           Removed because programmatic .focus() calls (closeBtn.focus()
           in the overlay open() flow) trigger :active in some browsers
           (Chrome especially), which made the X button render in the
           darkened hover state on initial overlay-open until the user
           clicked elsewhere. Pure :hover (cursor-over only) is the
           safer rule — no styling on click-press is fine; the click
           action itself completes too fast for press-feedback to add
           value, and focus() no longer triggers any darkened state. */
        color: var(--color-text);
    }

    /* :focus override — defeats GeneratePress's parent-theme global
       `button:focus` rule (generate-style-inline-css) which sets
       background:#3f4047 and color:#ffffff on every <button> on the
       site. Without this override, the close button renders in the
       dark-with-white-X state on initial overlay-open (because the
       overlay's open() flow calls closeBtn.focus() for keyboard
       a11y, which triggers the GeneratePress :focus styling), and
       only resets to normal when the user clicks elsewhere and
       focus leaves the button.
       
       This rule restates the resting-state background and color
       explicitly when the button is :focus. Keyboard focus indication
       continues to come from :focus-visible below (cabernet outline).
       Mouse-click focus shows no visual change. */
    .lore-listings-overlay__close:focus {
        background: var(--color-border-soft);
        color: var(--color-text);
    }
    /* When BOTH :focus AND :hover apply (cursor over a focused
       button), the :hover darkening should win — restate it here
       to make the cascade order explicit. */
    .lore-listings-overlay__close:focus:hover {
        background: var(--color-border);
        color: var(--color-text);
    }

    .lore-listings-overlay__close:focus-visible {
        outline: 2px solid var(--color-cabernet);
        outline-offset: 2px;
    }

    /* Inline SVG X glyph. Earlier polish iterations tried hiding the
       SVG and drawing the X via ::before/::after pseudo-elements
       (copying the .lore-inquiry-modal__close pattern from
       listing.css). The pseudo-elements failed to render reliably
       in this context — DevTools confirmed identical computed styles
       between this button and the working .lore-listings-overlay__
       open-external <a> button, but the X glyph was invisible
       across multiple deploys. Root cause was never fully
       isolated; possibilities included pseudo-element interaction
       with the parent flex layout, or `appearance: none` on
       <button> behaving differently than on <a>.
       
       Symmetry-fix: make the close button structurally identical
       to the open-external — both use a visible inline SVG, sized
       at 24×24. Eliminates the divergence that was producing the
       bug. The pre-existing inline <svg> in archive-listing.php is
       unchanged; this rule just makes it visible at the right size. */
    .lore-listings-overlay__close svg {
        width: 24px;
        height: 24px;
    }

    /* Open-in-new-tab affordance (polish-1, Pattern B companion).
       Sits left of the close button in the header cluster. Same
       44×44 sand circle as the close button so the two read as a
       visually-unified control pair, but the icon is the up-right
       arrow (rendered via the inline SVG — kept visible because it's
       a single glyph stroke that scales cleanly at this size; no
       pseudo-element draw needed).
       
       <a> element with href + target="_blank" — native browser-
       intercepted, so cmd-click + middle-click + drag-to-other-
       window all work without JS involvement. The JS controller
       only updates the href on each overlay open; the click itself
       is the browser's job. */
    .lore-listings-overlay__open-external {
        position: relative;
        width: 44px;
        height: 44px;
        padding: 0;
        margin: 0;
        border: none;
        border-radius: var(--radius-round);
        background: var(--color-border-soft);
        color: var(--color-text);
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        -webkit-tap-highlight-color: transparent;
        text-decoration: none;
        transition: color 0.15s ease, background 0.15s ease;
    }

    .lore-listings-overlay__open-external:hover {
        background: var(--color-border);
        /* Mirror the close button's :hover styling for visual symmetry.
           No :active here either — see the close button's notes for
           the full reasoning (programmatic focus() triggering :active
           in Chrome). The <a> element doesn't have the same focus
           issue, but keeping both buttons structurally identical means
           no surprises if future changes move focus to either one. */
        color: var(--color-text);
        text-decoration: none;
    }

    .lore-listings-overlay__open-external:focus-visible {
        outline: 2px solid var(--color-cabernet);
        outline-offset: 2px;
    }

    .lore-listings-overlay__open-external svg {
        width: 20px;
        height: 20px;
    }

    .lore-listings-overlay__scroll {
        flex: 1 1 auto;
        overflow-y: auto;
        /* The injected <main class="lore-listing"> brings its own
           padding via listing.css; we don't add padding here so the
           single-listing template renders identically to the
           standalone /listing/<slug> view. */
    }

    .lore-listings-overlay__loading,
    .lore-listings-overlay__error {
        padding: var(--space-6) var(--space-4);
        text-align: center;
        font-family: var(--font-body);
        color: var(--color-text-muted);
    }

    .lore-listings-overlay__error a {
        color: var(--color-cabernet);
    }

    /* ── ADR-031 sticky-inside-overlay floating CTA ──────────────────
     *
     * The .lore-inquiry-cta is position:fixed by default (anchored to
     * viewport bottom-right). When the overlay is open, we want the
     * CTA to stick inside the overlay's scroll container instead so
     * it never escapes the panel.
     *
     * The injected inquiry-cta partial lives at the bottom of the
     * scroll container (appended after the <main>). Position:sticky
     * with bottom:0 keeps it pinned to the bottom of the overlay
     * viewport as the user scrolls the listing content.
     *
     * Right-alignment preserved via margin-left:auto + a flex-row
     * wrapper, OR by keeping the CTA absolutely positioned within the
     * scroll container. Going with `position: fixed` overridden ONLY
     * by the right offset — the CTA stays bottom-right of the
     * VIEWPORT but constrained to the overlay's horizontal range
     * by the right offset matching the overlay's right edge (50%
     * → right: <something compatible>).
     *
     * Actually the simpler shape: keep .lore-inquiry-cta as
     * position:fixed but tweak its `right` offset so it sits within
     * the right 50% (the overlay area) instead of viewport-edge. The
     * inquiry-cta partial already has position:fixed; we just nudge
     * `right` when body.overlay-open is set so the CTA stays inside
     * the overlay column visually.
     *
     * Defensive: the inquiry-cta partial may not always render (no-
     * agent listings cause the partial to bail). When it doesn't,
     * this rule has nothing to apply to — no impact.
     * ───────────────────────────────────────────────────────────── */

    body.overlay-open .lore-inquiry-cta {
        /* The CTA stays position:fixed (its base behavior from
           listing.css). We just confirm bottom-right placement.
           No horizontal offset change needed — the viewport-bottom-
           right of the overlay column IS the same coordinate as
           viewport-bottom-right of the whole page when the overlay
           covers the right half. The CTA visually sits at the
           bottom-right corner of the overlay, which is exactly
           where the user expects it. */
        position: fixed;
        bottom: var(--space-4);
        right: var(--space-4);
        z-index: 110; /* Above .lore-listings-overlay (z:100) */
    }

    /* Reduced-motion users: skip the slide; just appear. */
    @media (prefers-reduced-motion: reduce) {
        .lore-listings-overlay {
            transition: none;
        }
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * §mobile-modes — Phase 4 mobile mode toggle classes
 *
 * Scoped to <992px via @media (max-width: 991.98px). Default (no mode
 * class) preserves the Phase 2 stacked layout (map 50vh + rail below)
 * — this is the no-JS fallback. With JS booted, the orchestrator
 * applies .mode-map or .mode-list to .lore-listings-page based on
 * sessionStorage (default mode-map for first-time visitors). The two
 * mode classes are mutually exclusive at the orchestrator layer.
 *
 * Desktop (≥992px) is unaffected — the existing 50/50 split-view
 * rules in the earlier @media (min-width: 992px) blocks still apply.
 * The mode classes are no-ops on desktop because the selectors below
 * are scoped to the mobile media query.
 * ─────────────────────────────────────────────────────────────────── */

@media (max-width: 991.98px) {

    /* Map mode — fullscreen map, rail hidden, bottom-sheet active.
       Polish-1: dropped `min-height: 600px` from the v1.2.0 main
       ship. The min-height was a defense against tiny landscape
       viewports but conflicted with the scroll-lock model added in
       polish-1 — when the page is taller than the locked viewport,
       there's no scroll path to reach the bottom of the page. Using
       100dvh (dynamic viewport height) alongside 100vh gives correct
       behavior on iOS Safari where the URL bar collapse/expand
       changes the visible area. */
    .lore-listings-page.mode-map {
        height: calc(100vh - var(--lore-header-height, 80px));
        height: calc(100dvh - var(--lore-header-height, 80px));
    }
    .lore-listings-page.mode-map .lore-listings-map-column {
        height: 100%;
        min-height: 0;
    }
    /* Search Phase B2 — mobile map-mode search/filter surface.
       List mode keeps the search + filters in the rail header (unchanged). Map
       mode used to hide the whole rail, leaving no way to search/filter without
       switching to list. Instead of a separate floating partial + bottom sheet
       (the abandoned §map-search approach, now removed), we reuse the SAME rail
       header the list view already renders: float just its header over the top
       of the map and hide the cards grid / empty / error / count (the map is the
       result surface here). No markup, JS, partial, or archive change — this
       restyles the live Phase-A / B1 elements only.

       Pinned below the site header. Sits clear of the Map/List toggle pill
       (fixed, top-right, z-250) via the reserved padding-top so the two never
       collide. The container is click-through (pointer-events:none) so the map
       stays pannable around the bar; only the header's controls re-enable it. */
    .lore-listings-page.mode-map .lore-listings-rail {
        display: block;
        position: fixed;
        top: var(--lore-header-height, 80px);
        left: 0;
        right: 0;
        z-index: 160;            /* above the map; below marker-sheet (200) + toggle pill (250) */
        height: auto;
        max-height: calc(100dvh - var(--lore-header-height, 80px));
        margin: 0;
        padding: var(--space-3);
        padding-top: 64px;       /* clear the fixed Map/List pill floating above */
        background: transparent;  /* map shows around the floating controls */
        border: none;
        overflow: visible;
        flex: none;
        pointer-events: none;
    }

    /* Only the header (search field + Filters button + the panel it opens) is
       interactive; pointer-events:auto cascades to the panel. Drop the list-mode
       divider — it isn't wanted floating over the map. */
    .lore-listings-page.mode-map .lore-listings-rail__header {
        pointer-events: auto;
        margin: 0;
        padding: 0;
        border-bottom: none;
    }

    /* The map is the result surface in map mode — hide the list-only pieces. */
    .lore-listings-page.mode-map .lore-listings-rail__grid,
    .lore-listings-page.mode-map .lore-listings-rail__empty,
    .lore-listings-page.mode-map .lore-listings-rail__error,
    .lore-listings-page.mode-map .lore-listings-search__count {
        display: none;
    }

    /* An open Filters panel can be taller than the visible map, so cap it and
       let it scroll internally; lift it off the map with a shadow. (The panel
       keeps its own white card background + border from the base rule.) */
    .lore-listings-page.mode-map .lore-listings-filters {
        max-height: calc(100dvh - var(--lore-header-height, 80px) - 140px);
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
    }

    /* Polish-4 — body scroll lock for mode-map via :has(). The page's
       .mode-map class is server-rendered by archive-listing.php as the
       default, so this rule fires on FIRST PAINT before any JS runs —
       no race window during which the footer can be visible on a hard
       refresh. The orchestrator JS toggles .mode-map ↔ .mode-list on
       user mode switch; :has() follows along automatically.
       
       Retired the polish-1 `body.lore-listings-map-locked` mechanism
       (JS-toggled class on body). Architectural cleanup: one source of
       truth for the lock state (the page's mode class) instead of two
       (page mode class + body mirror class kept in sync by JS).
       
       100dvh tracks the dynamic visible area on iOS Safari where the
       URL bar can collapse/expand. 100vh fallback for older browsers.
       position: fixed on body is NOT used — it interacts poorly with
       iOS keyboard / system-UI elements; overflow: hidden + height-
       lock is sufficient.
       
       :has() browser support: Safari 15.4+ (Mar 2022), Chrome 105+
       (Aug 2022), Firefox 121+ (Dec 2023). Well above our floor. */
    body:has(.lore-listings-page.mode-map) {
        overflow: hidden;
        height: 100vh;
        height: 100dvh;
    }

    /* Polish-7 — hide the site footer in mode-map. The body lock
       above (overflow:hidden + height:100dvh) prevents overflow but
       does NOT reliably clamp scrollTop on iOS Safari when style
       changes while user is deeply scrolled. Hiding the footer
       collapses body content height to header + page = 100dvh
       exactly, which means maxScrollable = 0 — the browser's
       natural scroll-clamp pass forces scrollTop to 0 even on iOS
       Safari (which handles layout-based scroll clamping correctly,
       unlike the scroll API).
       
       Selectors target GeneratePress defaults (#colophon is the
       footer ID, .site-footer is its class). Additional selectors
       can be added here if the theme is changed or extended. The
       parent theme audit (convention #90) covers any future
       reorganization of the footer markup. */
    body:has(.lore-listings-page.mode-map) #colophon,
    body:has(.lore-listings-page.mode-map) .site-footer {
        display: none;
    }

    /* List mode — map hidden, rail fills the mobile viewport. The
       rail retains its single-column 1fr grid from the earlier rule;
       no additional grid changes needed here. */
    .lore-listings-page.mode-list .lore-listings-map-column {
        display: none;
    }
    .lore-listings-page.mode-list .lore-listings-rail {
        /* Rail already has width:100% in the base rule. Nothing more
           needed — it takes the full mobile viewport naturally. */
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * §bottom-sheet — Phase 4 mobile bottom-sheet
 *
 * Mobile-only (display:none at ≥992px). Position:fixed at viewport
 * bottom, three height states via BEM modifier classes per SCOPE-
 * CHUNK-E §8.1:
 *   --hidden   height 0     (collapsed; no content visible)
 *   --peek     height 30vh  (default state on marker tap)
 *   --mid      height 70vh  (drag up reveals full card content)
 *
 * Transition: 300ms ease-out per SCOPE §8.2. is-dragging class
 * suppresses the transition during active pointer drag so the sheet
 * tracks the finger 1:1 (the bottom-sheet JS module owns the inline
 * `height` style during drag; on release, the inline style is
 * cleared and the class-based height value transitions to.
 *
 * z-index ordering at mobile:
 *   100  .lore-listings-overlay  (defined upthread, but display:none
 *                                 at mobile so not visually relevant)
 *   150  .lore-listings-mode-toggle  (defined below)
 *   200  .lore-listings-sheet         (this section)
 * Sheet sits above the toggle so the sheet header can occlude the
 * toggle when the sheet rises. Toggle's own opacity-fade rule
 * (body.sheet-mid below) handles the visual transition.
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-sheet {
    /* Desktop default — hidden. The @media (max-width) block below
       reveals it for mobile. Using display:none here keeps the sheet
       entirely out of the tab order + layout calculations on desktop. */
    display: none;
}

@media (max-width: 991.98px) {

    .lore-listings-sheet {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 200;

        background: var(--color-white);
        border-top-left-radius: var(--radius-lg);
        border-top-right-radius: var(--radius-lg);
        box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.12);

        /* Vertical flex: header on top, scrollable content below. */
        display: flex;
        flex-direction: column;

        /* Default state — hidden. Class modifiers below override. */
        height: 0;
        /* Polish-2 — ceiling cap so even if JS computes a runaway mid
           height (or the content is taller than viewport), at least
           15vh of map stays visible. Belt-and-braces against the
           computeMidHeight() math in the bottom-sheet JS. */
        max-height: 85vh;
        overflow: hidden;

        /* Smooth state transition. is-dragging suppresses this for
           1:1 pointer tracking during a drag. */
        transition: height 300ms ease-out;

        /* iOS Safari: prevent overscroll bouncing past the sheet
           bounds when scrolling its content. */
        overscroll-behavior: contain;
    }

    .lore-listings-sheet--hidden {
        height: 0;
    }
    .lore-listings-sheet--peek {
        height: 30vh;
    }
    .lore-listings-sheet--mid {
        /* Polish-2: JS owns the actual mid-state height. The bottom-
           sheet module's computeMidHeight() measures content size on
           transition to mid + sets inline pixel height; the 70vh here
           is a defensive fallback for the (unreachable in practice)
           case where JS hasn't set inline height before this class is
           applied. The max-height: 85vh on the base sheet rule clamps
           any too-large value. */
        height: 70vh;
    }
    .lore-listings-sheet.is-dragging {
        /* Drag-time: JS sets inline `height` directly. The transition
           must be off so the sheet tracks the finger 1:1 rather than
           lagging 300ms behind. */
        transition: none;
    }

    /* ── Header — drag handle only (X close removed at polish-1) ── */

    .lore-listings-sheet__header {
        flex: 0 0 auto;
        position: relative;
        height: 48px;
        border-bottom: 1px solid var(--color-border-soft);
        /* Polish-3: bumped 32 → 48px so the handle's 44px tap zone
           fits fully inside the header with no bleed into the card
           content area below. Pre-polish-3 the 44px tap zone (anchored
           top:0) extended 12px past a 32px header bottom — accidental
           taps on the card photo's top edge triggered the handle's
           peek↔mid cycle instead of opening the listing. The taller
           header also gives ~22px visual breathing room between the
           pill and the card photo. JS coupling: computeMidHeightPx()
           in lore-listings-bottomsheet-v1.0.0.js mirrors this 48px
           value; they must stay in sync. */
    }

    /* Drag handle — small visible pill (36×4) centered, with a larger
       transparent tap zone (~80×44) for thumb ergonomics per operator
       Phase 4 lock 2026-05-28. The visible pill is drawn via ::before
       so the parent element's box can be larger than the visible
       affordance without making the gray bar wider.
       
       cursor: grab on desktop emulation; touch-action: none prevents
       iOS Safari from intercepting the gesture as a system pull-to-
       refresh. */
    .lore-listings-sheet__handle {
        position: absolute;
        /* Polish-3: was `top: 0; transform: translateX(-50%)` — handle
           was top-anchored, extending 12px below the header into the
           card area. Now `top: 50% + translate(-50%, -50%)` centers the
           handle's 44px tap zone vertically inside the 48px header,
           leaving 2px of safe margin top and bottom. No tap-zone bleed
           into the card content area. The visible ::before pill is
           centered inside the handle, so this also visually centers it
           inside the header. */
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 80px;
        height: 44px;
        background: transparent;
        border: none;
        padding: 0;
        cursor: grab;
        touch-action: none;
        -webkit-tap-highlight-color: transparent;
    }
    .lore-listings-sheet__handle:active {
        cursor: grabbing;
    }
    .lore-listings-sheet__handle::before {
        content: '';
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        width: 36px;
        height: 4px;
        border-radius: var(--radius-pill);
        background: var(--color-border);
        transition: background 0.15s ease;
    }
    .lore-listings-sheet__handle:hover::before {
        background: var(--color-text-muted);
    }

    /* Polish-1: the .lore-listings-sheet__close button was removed.
       Drag handle + tap-on-map-basemap are sufficient dismissal
       affordances; the X added clutter without capability. If a
       future polish reintroduces a close button here, apply the
       canonical ADR-079 :focus override pattern (resting-state
       restated on :focus + hover-state restated on :focus:hover)
       so GeneratePress's global button:focus rule doesn't paint
       it dark on programmatic focus. The .lore-listings-overlay__
       close rules upthread are the reference pattern. */

    /* ── Content — scrollable card-clone area ───────────────────── */

    .lore-listings-sheet__content {
        flex: 1 1 auto;
        overflow-y: auto;
        /* Polish-9: was `var(--space-3) var(--space-4) var(--space-4)`
           (top=12, sides=16, bottom=16) — top gap from visible pill to
           card photo felt too tight to operator (thumb pad reaching
           for the pill would graze the card photo and trigger card
           click). Bumped to 24px top + 24px bottom (symmetric) with
           sides unchanged. Pill-bottom to card-top gap grows from
           ~35px to ~47px while the bottom whitespace also matches
           the top — visual symmetry the operator explicitly asked
           for. computeMidHeightPx auto-handles the new height via
           scrollHeight measurement; no JS changes needed. */
        padding: 24px var(--space-4);

        /* iOS Safari momentum scrolling + overscroll containment so
           scrolling past content bounds doesn't pull the page. */
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
    }

    /* Polish-10 — lock internal scrolling at peek state. Without this,
       the user could scroll the card up within the content area so
       the photo's top edge sat flush under (or grazing) the sheet
       header, and a subsequent handle tap would catch both the handle
       tap zone AND the card photo at once. iOS Safari then routed the
       touch to the card, opening the listing in a new tab. With
       overflow:hidden at peek, the card stays anchored to the top of
       the content area regardless of swipe-in-content gestures. Mid
       state restores overflow:auto via the default rule above, so
       tall cards that hit the 85vh ceiling can still be scrolled
       within the sheet. The companion JS change resets scrollTop=0
       on transition into peek to cover the mid → peek edge case
       where the user had scrolled at mid and then dragged back to
       peek (would otherwise show stale scroll offset locked in). */
    body.sheet-peek .lore-listings-sheet__content {
        overflow-y: hidden;
    }

    /* The cloned card from the orchestrator's rail cache lands here.
       Default card styles apply unchanged; this rule just ensures the
       card doesn't exceed the sheet's horizontal bounds. */
    .lore-listings-sheet__content .lore-listings-card {
        max-width: 100%;
    }

    /* Reduced-motion users: skip the height-transition animation
       between sheet states. Drag tracking continues to work (it's
       inline-style-driven, not transition-driven). */
    @media (prefers-reduced-motion: reduce) {
        .lore-listings-sheet {
            transition: none;
        }
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * §mode-toggle — Phase 4 Map/List floating toggle pill
 *
 * Mobile-only segmented-control pill at viewport bottom-right per
 * SCOPE-CHUNK-E §8.3. Two buttons (Map / List) sharing a single
 * white pill background; active button gets harbor-blue fill +
 * white text per brand palette.
 *
 * Visibility: visible at sheet states hidden + peek; auto-hides via
 * opacity at sheet mid state (body.sheet-mid). The user at mid is
 * focused on the listing content; the toggle becomes available again
 * when they drag back to peek or hidden.
 *
 * z-index: 250 — above the sheet (z:200) so the pill stays visible and
 * tappable when the sheet rises to mid. Pre-polish-8 this was z:150
 * (below the sheet) with a body.sheet-mid fade-out rule; polish-8
 * retired the fade and bumped z-index so the pill floats over the
 * sheet's top edge area at mid, matching Google Maps' floating-
 * control-over-bottom-sheet pattern.
 *
 * Both buttons get the full ADR-079 :focus override treatment per
 * convention #90. The is-active button preserves its harbor-blue
 * fill across all :hover / :focus / :focus:hover combinations
 * (active state is the dominant visual signal — never flips on
 * incidental interaction).
 * ─────────────────────────────────────────────────────────────────── */

.lore-listings-mode-toggle {
    /* Hidden on desktop; mobile-only. */
    display: none;
}

@media (max-width: 991.98px) {

    .lore-listings-mode-toggle {
        display: flex;
        position: fixed;
        /* Polish-4 — was `bottom: var(--space-4); right: var(--space-4)`
           which sat the pill INSIDE the sheet area at peek state (sheet
           bottom = viewport bottom, pill 16px above bottom = overlapped)
           and on top of the Google Maps zoom controls. Top-right places
           the pill ~96px from viewport top: below the site header, above
           the map column, clear of the sheet's footprint at peek, and
           clear of the map's bottom-right zoom controls. */
        top: calc(var(--lore-header-height, 80px) + var(--space-4));
        right: var(--space-4);
        /* Polish-8 — z-index bumped 150 → 250 (above sheet z:200) so the
           pill stays visible AND tappable when the sheet rises to mid.
           Pre-polish-8 the pill was hidden behind the sheet at mid via
           the opacity-fade rule below (now retired). Operator surfaced
           this as a usability cost: when studying a card at mid state,
           the toggle is unreachable — user must dismiss the sheet to
           switch modes. With z:250 the pill floats over the sheet's
           top edge area on small viewports, matching Google Maps'
           floating-control-over-bottom-sheet pattern. */
        z-index: 250;

        background: var(--color-white);
        border-radius: var(--radius-pill);
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
        padding: var(--space-1);
        gap: var(--space-1);

        /* Polish-8 — opacity stays at 1 across all sheet states; the
           body.sheet-mid fade rule was retired (see commentary on the
           z-index change above). The transition is kept for any future
           opacity changes (e.g., visibility tied to a different signal)
           but does not currently fire. */
        opacity: 1;
        transition: opacity 200ms ease-out;
    }

    /* Polish-8 — body.sheet-mid fade rule retired. The pill now stays
       visible at all sheet states (z-index 250 above sheet z:200).
       The polish-2 rationale (pill needed to fade because sheet rose
       over it bottom-right) no longer applies after polish-4 moved
       the pill to top-right and polish-8 raised it above the sheet
       in z-stack. The body.sheet-mid class is still set by the
       bottom-sheet module on state transitions for any future
       sheet-state-driven CSS, but nothing currently consumes it. */

    .lore-listings-mode-toggle__btn {
        appearance: none;
        background: transparent;
        border: none;
        padding: var(--space-2) var(--space-3);
        border-radius: var(--radius-pill);

        font-family: var(--font-body);
        font-size: var(--fs-sm);
        font-weight: var(--fw-medium);
        color: var(--color-text-muted);

        cursor: pointer;
        -webkit-tap-highlight-color: transparent;
        transition: background 0.15s ease, color 0.15s ease;
    }
    /* Inactive segment — hover / active fill cabernet (mirrors the Sign-in
       button), and by RESTATING the background this defeats GeneratePress's
       global button:hover / :active charcoal (#3f4047) bleed. The prior rule
       only restated `color`, so GP's charcoal background still showed through on
       hover — that was the charcoal the operator saw. The .is-active rules below
       keep the SELECTED segment teal across every state (higher specificity). */
    .lore-listings-mode-toggle__btn:hover,
    .lore-listings-mode-toggle__btn:active,
    .lore-listings-mode-toggle__btn:focus:hover {
        background: var(--color-cabernet);
        color: var(--color-white);
    }
    /* ADR-079 — resting focus (not hovered): restate the transparent resting
       look so a focused-but-not-hovered segment never goes charcoal either. */
    .lore-listings-mode-toggle__btn:focus {
        background: transparent;
        color: var(--color-text-muted);
    }
    .lore-listings-mode-toggle__btn:focus-visible {
        outline: 2px solid var(--color-cabernet);
        outline-offset: 2px;
    }

    /* Active button — harbor-blue fill, white text. Preserved across
       all :hover / :focus / :active / :focus:hover combinations so the
       selected state stays visually dominant (and never flips to the
       cabernet hover/active fill above). */
    .lore-listings-mode-toggle__btn.is-active,
    .lore-listings-mode-toggle__btn.is-active:hover,
    .lore-listings-mode-toggle__btn.is-active:focus,
    .lore-listings-mode-toggle__btn.is-active:active,
    .lore-listings-mode-toggle__btn.is-active:focus:hover {
        background: var(--color-harbor-blue);
        color: var(--color-white);
    }
}


/* ──────────────────────────────────────────────────────────────────────
 * §mobile top-area fixes (Search Phase B2 — fix 1)
 *
 * Two issues from operator review of the mobile listings header:
 *
 *  (1) The fixed Map/List toggle pill (top-right) overlapped the right end of
 *      the search bar in LIST mode — and the inline Filters button lives at
 *      that right end, so it ended up sitting UNDER the pill (occluded). Map
 *      mode already clears the pill (the floated rail header starts 64px down);
 *      this gives LIST mode the same 64px clearance so the search field +
 *      Filters button sit cleanly below the pill in both views (consistent).
 *
 *  (2) The toggle pill + the map-mode floating rail are position:fixed (anchored
 *      to the viewport), so the WordPress admin bar — itself fixed at the very
 *      top, pushing page content down — left them mis-pinned for logged-in
 *      ADMINS. Regular users are unaffected: functions.php suppresses
 *      #wpadminbar for logged-in non-admins (show_admin_bar filter), and
 *      logged-out visitors never get it. These rules keep the admin-side preview
 *      accurate and make the fixed positioning robust. WP admin bar height:
 *      32px at >=783px, 46px at <=782px (fixed at the top in both bands).
 * ─────────────────────────────────────────────────────────────────── */

@media (max-width: 991.98px) {

    /* (1) List mode — clear the fixed toggle pill AND line the search bar up with
       the map view. The map-mode search is position:fixed and anchored to
       var(--lore-header-height) (80px fallback); the list-mode search flows under
       the REAL rendered header, which is a bit shorter than 80px, so a flat 64px
       clearance left it riding ~10-15px higher than the map search (grazing the
       pill). Anchoring the clearance to the same ~80px reference the map/toggle
       use brings the two search bars onto the same line and clears the pill in
       both views. (The rail's own padding-top already contributes
       var(--space-4), so the header makes up the remainder.) */
    .lore-listings-page.mode-list .lore-listings-rail__header {
        padding-top: calc(80px - var(--space-4));
    }

    /* (2) Admin bar present (32px in this width band) — drop the fixed controls
       by the admin-bar height so they stay pinned to the real header bottom. */
    body.admin-bar .lore-listings-mode-toggle {
        top: calc(var(--lore-header-height, 80px) + var(--space-4) + 32px);
    }
    body.admin-bar .lore-listings-page.mode-map .lore-listings-rail {
        top: calc(var(--lore-header-height, 80px) + 32px);
    }
    body.admin-bar .lore-listings-page.mode-map {
        height: calc(100vh - var(--lore-header-height, 80px) - 32px);
        height: calc(100dvh - var(--lore-header-height, 80px) - 32px);
    }
}

@media (max-width: 782px) {

    /* The WP admin bar grows to 46px at phone widths. */
    body.admin-bar .lore-listings-mode-toggle {
        top: calc(var(--lore-header-height, 80px) + var(--space-4) + 46px);
    }
    body.admin-bar .lore-listings-page.mode-map .lore-listings-rail {
        top: calc(var(--lore-header-height, 80px) + 46px);
    }
    body.admin-bar .lore-listings-page.mode-map {
        height: calc(100vh - var(--lore-header-height, 80px) - 46px);
        height: calc(100dvh - var(--lore-header-height, 80px) - 46px);
    }
}
