body {
    margin: 0;
    padding: 0;
    font-family: 'Outfit', Arial, sans-serif;
    background-color: var(--color-bg);
    color: var(--color-text);
    overflow: hidden;
}

/* ───── Type tab bar (primary navigation) ───── */
.type-tabs {
    display: flex;
    align-items: center;
    gap: 28px;
    padding: 0 28px;
    /* Gray baseline as an inset shadow (not border-bottom): the active tab's 2px
       black underline (a child) then paints ON TOP of it at the same row, so the
       selected tab shows a full 2px black line. A border-bottom would get clipped
       to 1px by the mobile horizontal-scroll container (.type-tabs-scroll). Inset
       shadow works the same in every viewport — no per-breakpoint split. */
    box-shadow: inset 0 -1px 0 var(--color-divider);
    background: var(--color-bg);
    /* Sticky just below the sticky site header. Header height comes from
       partials/header.html: 72px on desktop (≥ 744), 56px on mobile (≤ 743). */
    position: sticky;
    top: 72px;
    /* 950 = ABOVE the coming-soon overlay (900, showComingSoon in map-page.js)
       so the mode-selector dropdown — the one dropdown that lives IN this bar and
       overflows DOWN into the .split area the overlay covers — stays clickable on
       a locked type page; still BELOW the sticky header wrapper (1000,
       header.html). The bar is the stacking context the mode menu lives in, so it
       must clear the overlay as a whole. (The filter/region dropdowns are in
       .results, not here, so they're unaffected.) Was 30 — see CLAUDE.md map
       stack. */
    z-index: 950;
}
/* Inner container holding the type-tab anchors. On desktop it's a plain
   flex row; on mobile (see @media block below) it becomes the horizontal
   scroll container so the mode toggle outside stays fixed at the left. */
.type-tabs-scroll {
    display: flex;
    align-items: center;
    gap: 28px;
    flex: 1;
    min-width: 0;
}
@media (max-width: 743px) {
    .type-tabs { top: 56px; }
}

/* ── Mode selector: leftmost in the tab bar ──
   Black pill (current mode label) + caret on a circle, CONCENTRIC with the
   pill's right cap — exact mirror of the filter-chip caret geometry: pill
   height 32 (cap r16) − caret circle 26 (r13) = 3px ring = 2px inset + 1px
   border → padding-right 2. The WHOLE pill opens a dropdown of all modes
   (Travel/Beauty/Interests/Serendipity); the caret circle is a visual
   affordance, not a separate target. Serendipity mirrors markup/JS inline. */
.mode-selector {
    position: relative;
    flex-shrink: 0;
    margin-right: 4px;
}
.mode-toggle {
    display: inline-flex;
    align-items: center;
    /* label→caret gap 3px = caret→pill-edge ring (2 inset + 1 border) — the
       caret circle sits with equal 3px breathing room on both sides. */
    gap: 3px;
    height: 32px;
    box-sizing: border-box;
    padding: 0 2px 0 14px;
    background: var(--color-surface-dark);
    border: 1px solid var(--color-text);
    border-radius: 999px;
    font-family: inherit;
    font-size: 15px;
    font-weight: 400;
    color: white;
    cursor: pointer;
    /* color transition dropped with the white-inversion hover — text stays
       white in every state now. */
    transition: background-color 0.3s ease, transform 0.15s ease;
    flex-shrink: 0;
}
/* Hover: subtle lighten (CLAUDE.md color-mix derivation) — the pill keeps
   its black identity. The old white inversion was the binary toggle's
   cross-fade affordance; with a dropdown it said nothing. The menu below is
   dark too, so pill + menu read as ONE dark "mode" object (mode = the
   superordinate switch above all types/categories — deliberate visual
   hierarchy: black menu = switch worlds, white menus = filter within). */
.mode-toggle:hover {
    background: color-mix(in srgb, var(--color-surface-dark), #fff 12%);
}
.mode-toggle:active { transform: scale(0.97); }
.mode-toggle-label-wrap {
    display: grid;
    grid-template-areas: "stack";
    align-items: center;
    flex: 0 0 auto;
    height: 15px;
    line-height: 15px;
    white-space: nowrap;
}
.mode-toggle-label {
    grid-area: stack;
    text-align: center;
    line-height: inherit;
}
/* Caret circle — invisible at rest, fill appears on ITS OWN hover only
   (exactly the filter-chip caret behavior). */
.mode-caret-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    border-radius: 50%;
    transition: background-color 0.12s ease;
}
.mode-caret-btn:hover { background: rgba(255, 255, 255, 0.2); }
.mode-caret {
    width: 8px;
    height: 5px;
    fill: currentColor;
    transition: transform 0.2s ease;
}
.mode-selector.open .mode-caret { transform: rotate(180deg); }
/* Dropdown — DARK on purpose, the site's only dark menu: mode is the
   superordinate concept, so its menu inherits the pill's black (one dark
   object). Do NOT "fix" this back to white — white menus mean in-page
   filtering. Same z-20 slot as .filter-dropdown-menu; anchors not buttons. */
.mode-menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 20;
    flex-direction: column;
    min-width: 180px;
    padding: 6px;
    background: var(--color-surface-dark);
    border: 1px solid var(--color-text);
    /* Radius = the pill's effective corner (height 32 / 2 = 16 = --radius-md),
       so the dark pill + dark menu read as one object with matching roundedness.
       Items nest concentrically: 16 window − 6 padding = 10px inner. */
    border-radius: var(--radius-md);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
}
.mode-selector.open .mode-menu { display: flex; }
.mode-menu a {
    display: block;
    padding: 8px 12px;
    /* Concentric with the 16px window at 6px padding (raw, like the planning-nav
       pill — an intentional concentric value, not a token). */
    border-radius: 10px;
    font-size: 14px;
    color: white;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.12s ease;
}
/* Same alpha as the caret-circle hover — one hover gray across the selector. */
.mode-menu a:hover { background: rgba(255, 255, 255, 0.2); }
/* Current mode = inverted item (mirror image of the white menus' dark
   active). Declared after :hover so it wins while hovered. */
.mode-menu a.active { background: white; color: var(--color-text); }

.type-tab {
    position: relative;
    padding: 14px 0 14px;
    font-family: inherit;
    font-size: 15px;
    font-weight: 500;
    color: var(--color-text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    transition: color 0.15s ease, border-color 0.15s ease;
    /* No margin-bottom overlap needed: the gray baseline is the parent's inset
       shadow, which this 2px border paints over directly (child > parent shadow). */
    /* Keep multi-word labels (e.g., "Medical Services") on a single line */
    white-space: nowrap;
}
.type-tab:hover:not([data-locked="1"]) { color: var(--color-text); }   /* locked(잠금) 탭은 호버 무반응 — 비활성=호버OFF 규칙 */
.type-tab.active {
    color: var(--color-text);
    border-bottom-color: var(--color-text);
    font-weight: 600;
}

/* ───── Split layout ───── */
.split {
    display: flex;
    height: calc(100vh - 72px - 49px); /* header 72px, tabs ~49px */
}

.results {
    flex: 1 1 50%;
    overflow-y: auto;
    padding: 20px 28px 60px;
    box-sizing: border-box;
}

.results-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 18px;
    gap: 12px;
}

.results-title-group {
    display: flex;
    align-items: center;
    gap: 6px;
    position: relative;
    flex-wrap: wrap;
}

.results-title {
    font-size: 22px;
    font-weight: 600;
    margin: 0 0 0 -13px;   /* negative left cancels the pill padding → text stays flush-left */
    /* Acts as a "show all of Korea" reset: pill-shaped hover matching the
       region dropdown pills. */
    padding: 3px 13px;
    border-radius: 999px;
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.results-title:hover { background: var(--color-surface); }

.results-count {
    display: none;
}

/* ───── Title-bar region dropdown ───── */
.results-title-chevron {
    color: var(--color-text-disabled);
    font-size: 22px;
    font-weight: 300;
    line-height: 1;
    user-select: none;
}
/* The chevron sitting right after the raw "in Korea" title needs an extra
   left push because the pill to its right has 14px internal padding on its
   left side — without this the chevron looks glued to "Korea". The second
   chevron (between two pills) already sits symmetrically between them. */
/* Title now carries its own 14px pill padding, so the chevron no longer needs
   the extra push it required when the title text sat flush to its box edge. */
.results-title + .results-title-chevron { margin-left: 0; }

.region-dropdown-btn {
    display: inline-flex;
    align-items: center;
    padding: 3px 13px;
    border: none;
    background: transparent;
    font-family: inherit;
    font-size: 22px;
    font-weight: 600;
    color: var(--color-text);
    cursor: pointer;
    border-radius: 999px;
    transition: background-color 0.15s ease;
}
.region-dropdown-btn:hover { background: var(--color-surface); }

.region-dropdown-label.is-placeholder {
    color: var(--color-text-disabled);
    font-weight: 400;
}

.region-dropdown-caret {
    width: 10px;
    height: 6px;
    margin-left: 8px;
    fill: var(--color-text);            /* matches selected label color */
    flex-shrink: 0;
    transform: translateY(2px);   /* nudge down to lowercase x-height midline */
    transition: fill 0.15s ease;
}
/* When the label is in placeholder state, fade the triangle to match */
.region-dropdown-btn:has(.region-dropdown-label.is-placeholder) .region-dropdown-caret {
    fill: var(--color-text-disabled);
}

.pill-dropdown {
    position: relative;
    display: inline-flex;
    align-items: center;
}
.pill-dropdown[hidden],
.results-title-chevron[hidden] { display: none; }

.region-dropdown-menu {
    position: absolute;
    top: 100%;
    left: 0;
    margin-top: 6px;
    min-width: 180px;
    background: white;
    border: 1px solid var(--color-divider);
    /* Window radius = the title pill's effective corner (~16.8 → snapped to
       --radius-md 16) — same concentric rule as the chip/mode menus. */
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px -6px rgba(0,0,0,0.15);
    padding: 6px;
    display: none;
    /* Page-content popover, same stack slot as the filter-chip dropdown (20):
       under the sticky type tabs (30) / header (1000), above cards + the map
       pane (z 0 containment). See the map-view exception in CLAUDE.md. */
    z-index: 20;
}
.region-dropdown-menu.open { display: block; }

.region-dropdown-menu button {
    display: block;
    width: 100%;
    text-align: left;
    border: none;
    background: transparent;
    padding: 9px 12px;
    /* Concentric with the 16px window at 6px padding (raw — see CLAUDE.md
       radius exceptions). */
    border-radius: 10px;
    font-family: inherit;
    font-size: 14px;
    color: var(--color-text);
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.region-dropdown-menu button:hover { background: var(--color-surface); }
.region-dropdown-menu button.active { background: var(--color-surface-dark); color: white; }

.filter-bar {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 20px;
}

/* ───── Filter modal: trigger button + centered dialog ───── */
.filter-modal-trigger {
    height: 32px;
    box-sizing: border-box;
    padding: 0 14px;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    line-height: 1;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: white;
    font-family: inherit;
    font-size: 13px;
    color: var(--color-text);
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.filter-modal-trigger:hover { border-color: var(--color-text); }
.filter-modal-trigger svg { width: 14px; height: 14px; }
/* has-active = a counted filter is on → black pill + count badge. Counts features
   always; on vp1/2 also region/category (the trigger is the only filter UI there).
   See updateFilterTrigger() in map-page.js. */
.filter-modal-trigger.has-active {
    border-color: var(--color-text);
    background: var(--color-surface-dark);
    color: var(--color-bg);
    /* Seat the count badge so its right semicircle (r=9) is concentric with
       the pill's right end (r=16): edge gap = 16-9 = 7px → padding-right 6px
       once the 1px border is accounted for. */
    padding-right: 6px;
}
.filter-modal-trigger-count { display: none; }
.filter-modal-trigger.has-active .filter-modal-trigger-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--color-bg);
    color: var(--color-text);
    font-size: 11px;
    font-weight: 600;
    cursor: pointer;
}
/* Badge shows the count by default; on hover it becomes an × to signal that
   clicking it clears the filters. */
.filter-modal-trigger-count .ft-cnt-x { display: none; width: 12px; height: 12px; }
.filter-modal-trigger.has-active .filter-modal-trigger-count:hover .ft-cnt-num { display: none; }
.filter-modal-trigger.has-active .filter-modal-trigger-count:hover .ft-cnt-x { display: block; }

/* Matches the shared sheet-modal pattern (report / inquiry / guide sheets):
   centered card that slides up from below with a fading backdrop, rather
   than a hard display:none/block swap. Keeps the .open class as the JS hook. */
.filter-modal {
    position: fixed;
    inset: 0;
    z-index: var(--z-overlay-modal);
    visibility: hidden;
    pointer-events: none;
    /* Delay visibility:hidden until the dialog finishes sliding out */
    transition: visibility 0s linear 0.36s;
}
.filter-modal.open {
    visibility: visible;
    pointer-events: auto;
    transition: visibility 0s linear 0s;
}
.filter-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    transition: opacity 0.3s ease;
}
.filter-modal.open .filter-modal-backdrop { opacity: 1; }
.filter-modal-dialog {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) translateY(100vh);
    width: min(480px, calc(100vw - 32px));
    /* Cap to 75% of the visible viewport (dvh, vh fallback) so a long filter
       list never dominates the screen; the body scrolls past this, head/foot
       stay put. */
    max-height: 75vh;
    max-height: 75dvh;
    display: flex;
    flex-direction: column;
    background: var(--color-bg);
    border-radius: var(--radius-2xl);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.25);
    overflow: hidden;
    transition: transform 0.36s cubic-bezier(0.32, 0.72, 0, 1);
}
.filter-modal.open .filter-modal-dialog { transform: translate(-50%, -50%); }
.filter-modal-head {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 18px 20px;
    border-bottom: 1px solid var(--color-divider);
    flex-shrink: 0;   /* stays put; only the body scrolls */
}
.filter-modal-title { font-size: 16px; font-weight: 600; color: var(--color-text); }
/* Close button pulled out of the flow into the dialog's top-right corner so
   it's concentric with the rounded corner (matches Botox/report sheets):
   close radius 18 + top/right 12 inset reads as one curve with the 28px
   panel corner. Removing it from flex also lets the title center cleanly. */
.filter-modal-close {
    position: absolute;
    top: 12px;
    right: 12px;
    width: 36px; height: 36px;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; background: var(--color-surface); border-radius: 50%;
    color: var(--color-text-secondary); cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}
.filter-modal-close:hover { background: var(--color-divider); color: var(--color-text); }
.filter-modal-close svg { width: 18px; height: 18px; }
.filter-modal-body { padding: 4px 20px 12px; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
.filter-modal-group { padding: 16px 0; }
.filter-modal-group-title { font-size: 14px; font-weight: 600; color: var(--color-text); margin-bottom: 12px; }
.filter-modal-options { display: flex; flex-wrap: wrap; gap: 8px; }
.filter-modal-option {
    padding: 8px 14px;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: var(--color-bg);
    font-family: inherit; font-size: 13px; color: var(--color-text);
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.filter-modal-option:hover { border-color: var(--color-text); }
.filter-modal-option.active { background: var(--color-surface-dark); border-color: var(--color-text); color: var(--color-bg); }

/* Tier option — 3-dot rating glyph before the label (mirrors the detail
   page's .place-rating dots: teal = filled, gray = empty). */
.filter-modal-option--tier {
    display: inline-flex;
    align-items: center;
    gap: 7px;
}
.tier-dots { display: inline-flex; gap: 3px; }
.tier-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--color-divider);
}
.tier-dot.is-on { background: var(--color-brand); }
/* On the active (dark) chip, keep teal dots vivid but lift the empty dots so
   they stay visible against the dark fill. */
.filter-modal-option--tier.active .tier-dot { background: rgba(255, 255, 255, 0.3); }
.filter-modal-option--tier.active .tier-dot.is-on { background: var(--color-brand); }
.filter-modal-foot {
    display: flex; align-items: center; justify-content: space-between;
    /* Right gap 16 (not 20) so the Show-results button's bottom-right corner is
       concentric with the dialog's 28px corner: gap = 28 − 12(btn radius) = 16.
       Left stays 20 to keep "Clear all" aligned with the body content. */
    padding: 16px 16px 16px 20px;
    border-top: 1px solid var(--color-divider);
    flex-shrink: 0;   /* stays put; only the body scrolls */
}
.filter-modal-clear {
    border: none; background: transparent;
    font-family: inherit; font-size: 14px; color: var(--color-text);
    text-decoration: underline; cursor: pointer;
}
/* Nothing selected → no filters to clear, so disable the button. */
.filter-modal-clear:disabled {
    color: var(--color-text-disabled);
    text-decoration: none;
    cursor: default;
}
.filter-modal-apply {
    padding: 11px 20px;
    border: none; border-radius: var(--radius-sm);
    background: var(--color-surface-dark); color: var(--color-bg);
    font-family: inherit; font-size: 14px; font-weight: 500;
    cursor: pointer;
    transition: background-color 0.15s ease;
}
.filter-modal-apply:hover:not(:disabled) { background: var(--color-surface-dark); }
.filter-modal-apply:disabled { background: var(--color-text-disabled); cursor: not-allowed; }
body.filter-modal-lock { overflow: hidden; }

/* Modal section tabs (Region/Categories/Features) — underline style, sitting
   below the "Filters" title. Hidden by default (desktop shows Features only);
   revealed on vp1/2. Panels show one at a time via .is-active (desktop:
   openFilterModal forces the Features panel). */
.filter-modal-tabs {
    display: none;
    align-items: center;
    border-bottom: 1px solid var(--color-divider);
    flex-shrink: 0;   /* fixed row; only the body below scrolls */
}
.filter-modal-tab {
    flex: 1;                    /* three equal thirds */
    display: flex;
    justify-content: center;    /* center the label within its third */
    border: none;
    background: none;
    padding: 0;
    font-family: inherit;
    cursor: pointer;
}
/* The underline lives on the label so it spans only the text width, not the
   whole third. */
.filter-modal-tab-label {
    padding: 12px 2px;
    margin-bottom: -1px;        /* sit the indicator on the row's bottom border */
    font-size: 14px; font-weight: 500;
    color: var(--color-text-muted);
    border-bottom: 2px solid transparent;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.filter-modal-tab.active .filter-modal-tab-label {
    color: var(--color-text);
    border-bottom-color: var(--color-text);
}
.filter-modal-panel { display: none; }
.filter-modal-panel.is-active { display: block; }
/* Region/Categories chip rows need the same top breathing room the facet
   groups have (those carry it via .filter-modal-group padding). */
.filter-modal-panel[data-tab="region"] .filter-modal-options,
.filter-modal-panel[data-tab="categories"] .filter-modal-options { padding: 16px 0; }

/* ── Viewport 1/2: radical map-first chrome — only the floating Filters button
   remains; the category chips, region selector and title are gone (title kept
   in the DOM, visually-hidden, for SEO/a11y). All of region/category/features
   filtering moves into the Filters modal's 3 tabs. ── */
@media (max-width: 949px) {
    /* Filter bar shows only the Filters trigger (chips hidden).
       Map pages only — /serendipity's browse chips must stay visible. */
    body:has(.map-pane) .filter-bar > *:not(.filter-modal-trigger) { display: none; }
    body:has(.map-pane) .filter-bar { top: 8px; }
    /* It floats over the map → lift it with a soft shadow. */
    .filter-modal-trigger { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }
    /* Region/area pills + the count gone; title visually-hidden (SEO keeps H1). */
    .results-title-group .pill-dropdown,
    .results-title-chevron,
    .results-count { display: none; }
    .results-title {
        position: absolute; width: 1px; height: 1px;
        padding: 0; margin: -1px; overflow: hidden;
        clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
    /* Modal: keep the "Filters" title; show the underline tabs below it. The
       tab row owns the divider, so drop the head's own border to avoid a
       double line. */
    .filter-modal-head { border-bottom: none; }
    .filter-modal-tabs { display: flex; }
}

.filter-chip {
    /* Fixed pill height so caret circles (26px) sit with a clean 2px inset
       inside the 1px border (total concentric ring radius diff = 3px).
       Height stays constant across font/browser line-height defaults. */
    height: 32px;
    box-sizing: border-box;
    padding: 0 14px;
    display: inline-flex;
    align-items: center;
    line-height: 1;
    border: 1px solid var(--color-border);
    border-radius: 999px;
    background: white;
    font-family: inherit;
    font-size: 13px;
    color: var(--color-text);
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}
.filter-chip:hover { border-color: var(--color-text); }
.filter-chip.active {
    background: var(--color-surface-dark);
    color: white;
    border-color: var(--color-text);
}

/* ── Hierarchical filter dropdown (major + subs) ── */
.filter-dropdown { position: relative; }
.filter-chip--dropdown {
    /* padding-right: 2px positions the caret's right edge concentric with the
       chip's right cap (cap radius 16 − caret radius 13 = 3px ring = 2 inset
       + 1 border). */
    gap: 0;
    padding-right: 2px;
}
.filter-chip-label {
    display: inline-flex;
    align-items: center;
    /* 2px gap from label text to caret circle. */
    padding-right: 2px;
}
/* Caret-btn — 26px circle, concentric with the chip's right cap. */
.filter-chip-caret-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    cursor: pointer;
    border-radius: 50%;
    transition: background-color 0.12s ease;
}
.filter-chip-caret-btn:hover { background: rgba(0, 0, 0, 0.08); }
.filter-chip--dropdown.active .filter-chip-caret-btn:hover { background: rgba(255, 255, 255, 0.2); }
.filter-chip-caret {
    width: 8px;
    height: 5px;
    fill: currentColor;
}
.filter-dropdown-menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    /* Filter-bar popover: sits UNDER the sticky chrome — type tabs (z 30) and
       header (1000, header.html) — so scrolling tucks it behind the tab bar, but
       above the page content: cards (unpositioned) and the map pane (z 0, its own
       stacking context that contains Leaflet's internal 200–700 panes). This is a
       bespoke map-page stack value, not a token. */
    z-index: 20;
    flex-direction: column;
    min-width: 180px;
    padding: 6px;
    background: white;
    border: 1px solid var(--color-divider);
    /* Window radius = the chip's effective corner (height 32 / 2 = 16 =
       --radius-md) — same rule as the mode-selector menu. Items nest
       concentrically: 16 window − 6 padding = 10px inner. */
    border-radius: var(--radius-md);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
.filter-dropdown.open .filter-dropdown-menu { display: flex; }
.filter-dropdown-menu button {
    appearance: none;
    border: none;
    background: transparent;
    text-align: left;
    padding: 8px 12px;
    /* Concentric with the 16px window at 6px padding (raw — see CLAUDE.md
       radius exceptions). */
    border-radius: 10px;
    font-family: inherit;
    font-size: 14px;
    color: var(--color-text);
    cursor: pointer;
    transition: background-color 0.12s ease;
}
.filter-dropdown-menu button:hover { background: var(--color-surface); }
.filter-dropdown-menu button.active { background: var(--color-surface-dark); color: white; }

/* ───── Listing cards (matches profile/saved card style) ───── */
.listings-grid {
    /* Card grid columns per viewport — see project_map_viewport_breakpoints.md:
       뷰포트6 (≥ 1920):    3 cols
       뷰포트5 (1440–1919): 2 cols
       뷰포트4 (1128–1439): 2 cols
       뷰포트3 (950–1127):  1 col
       뷰포트2 (744–949):   2 cols (mobile-mode + Map View toggle)
       뷰포트1 (≤ 743):     1 col (mobile-mode + Map View toggle) */
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 28px 18px;
}
@media (max-width: 1919px) {
    .listings-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 1127px) {
    .listings-grid { grid-template-columns: 1fr; }
}
@media (max-width: 949px) {
    .listings-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 743px) {
    .listings-grid { grid-template-columns: 1fr; }
}

/* .place-card / .place-card-* styles live in /partials/place-card.css
   (loaded by each /t/<type>/ page) — keep this file free of card-specific
   visuals so the card stays reusable elsewhere. */

/* ───── Map fullscreen control ───── */
.map-fullscreen-btn {
    width: 36px;
    height: 36px;
    background: white;
    border: none;
    /* Concentric with the map's top-right corner: map radius 22 − gap 10 = 12. */
    border-radius: var(--radius-sm);
    box-shadow: 0 1px 3px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.06);
    display: flex !important;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: var(--color-text);
    padding: 0;
    transition: background-color 0.15s ease, transform 0.15s ease;
}
.map-fullscreen-btn:hover {
    background: var(--color-surface);
    transform: scale(1.04);
}
.map-fullscreen-btn svg { width: 18px; height: 18px; }

/* Map View close button (X) — only shown on viewports 1/2 while
   body.show-map is active. Mirrors the white pill styling of the expand
   button so the corner control reads as a single visual family. */
.map-close-btn {
    width: 36px;
    height: 36px;
    background: white;
    border: none;
    border-radius: var(--radius-sm);
    box-shadow: 0 1px 3px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.06);
    display: none;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: var(--color-text);
    padding: 0;
    transition: background-color 0.15s ease, transform 0.15s ease;
}
.map-close-btn:hover {
    background: var(--color-surface);
    transform: scale(1.04);
}
.map-close-btn svg { width: 18px; height: 18px; }

/* Expand mode — when the expand button is clicked, cards collapse and the
   map takes over the .split row in-page. Toggled by `body.map-expanded`
   (NOT the browser Fullscreen API). Overrides the per-viewport split
   ratios since .results is removed from the flow. */
body.map-expanded .results { display: none; }
body.map-expanded .map-pane {
    flex: 1 1 100% !important;
    min-width: 0 !important;
    max-width: none !important;
    /* Mirror the right-side 16px so the map has symmetric horizontal padding */
    padding-left: 16px;
}

/* ───── Pagination (matches profile/saved pattern) ───── */
.map-pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    margin-top: 32px;
    padding-bottom: 8px;
}
.map-pagination:empty { display: none; }

.map-page-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 32px;
    padding: 0 6px;
    border: none;
    background: transparent;
    color: var(--color-text);
    font-family: inherit;
    font-size: 14px;
    border-radius: 50%;
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}
.map-page-btn:hover:not(:disabled) { background: var(--color-surface); }
.map-page-btn:disabled {
    color: var(--color-text-disabled);
    cursor: default;
    background: transparent;
}
.map-page-btn.active {
    background: var(--color-surface-dark);
    color: white;
    font-weight: 500;
}
.map-page-btn.active:hover { background: var(--color-surface-dark); }
.map-page-btn--arrow { width: 32px; }
.map-page-btn--arrow svg {
    width: 14px;
    height: 14px;
}
.map-page-ellipsis {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 32px;
    height: 32px;
    color: var(--color-text-muted);
}

/* ───── Map pane ───── */
.map-pane {
    flex: 1 1 50%;
    position: relative;
    min-width: 360px;
    padding: 12px 16px 16px 0;
    box-sizing: border-box;
}
#map {
    width: 100%;
    height: 100%;
    border-radius: var(--radius-lg);
    overflow: hidden;
    background: #eef0f2;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 4px 12px -6px rgba(0,0,0,0.08);
}

/* Leaflet's control corners default to z-index 1000, which would poke the
   expand/close buttons through the site header's mega-menu (backdrop 999,
   content 1000/1001). Drop them to 800 — above map popups/markers (≤700)
   but below the header stack — matching the project's "below 1000" rule.
   Scoped with #map for higher specificity than Leaflet's own .leaflet-top
   rule (leaflet.css loads after this file). */
#map .leaflet-top,
#map .leaflet-bottom {
    z-index: 800;
}

/* Split ratio: 1:1 by default (뷰포트3, 5, 6); 60:40 cards:map in 뷰포트4 only.
   뷰포트1/2 (≤ 949) ignore this since the split collapses to a stacked layout
   with the map fixed-overlaid behind the Map View toggle.
   Placed AFTER the base .results / .map-pane rules so it wins on cascade. */
@media (max-width: 1439px) and (min-width: 1128px) {
    .results  { flex: 60 1 0; }
    .map-pane { flex: 40 1 0; }
}

.price-pin {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: white;
    border: 1.5px solid rgba(0, 0, 0, 0.15);
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
    cursor: pointer;
    transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}
/* Sponsored default: white fill (from base) + teal ring. */
.price-pin--sponsored {
    border-color: var(--color-brand);
}
.price-pin:hover,
.price-pin.is-active {
    background: var(--color-surface-dark);
}
/* Sponsored selected/hover: teal fill, gray border (same as the others). After
   the generic active rule so it overrides (same specificity); the gray also
   resets the teal default-border above. */
.price-pin--sponsored.is-active,
.price-pin--sponsored:hover {
    background: var(--color-brand);
    border-color: rgba(0, 0, 0, 0.15);
}

/* ───── Pin hover label (desktop + mouse) — black chip, white title only ─────
   Shown above a pin on mouse hover as a quick skim; clicking the pin hides it
   and opens the full popup. Overrides Leaflet's default white tooltip. */
.leaflet-tooltip.pin-tip {
    background: var(--color-surface-dark);
    color: #fff;
    border: none;
    border-radius: var(--radius-pill);
    padding: 5px 12px;
    font-family: 'Outfit', sans-serif;
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
/* No tail — the pill floats clean above the pin. */
.leaflet-tooltip.pin-tip::before { display: none; }

/* ───── In-map InfoWindow popup (mirrors left listing card) ───── */
/* The popup content is the shared place-card (V2). Give the Leaflet wrapper the
   same white surface the mobile strip puts behind its cards (.listings-grid > *:
   white bg, 2xl radius, 8px pad, soft shadow) so the card reads cleanly over the
   map and matches the strip — the inner image's 20px (lg) radius stays concentric
   (28 − 8 pad = 20). */
.place-popup .leaflet-popup-content-wrapper {
    background: var(--color-bg);
    border: none;
    border-radius: var(--radius-2xl);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
    padding: 8px;
    overflow: visible;
    transition: transform 0.3s ease;
}
/* Hovering the popup grows the WHOLE card (scale the wrapper). Leaflet's position
   translate lives on the parent .leaflet-popup, so scaling the wrapper is safe.
   Cancel the card's own image-scale hover so the image holds still and only the
   popup as a whole grows (matches the pre-unification popup). */
.place-popup .leaflet-popup-content-wrapper:hover { transform: scale(1.02); }
.place-popup .place-card--v2:hover .place-card-img,
.place-popup .place-card--v2.is-hovered .place-card-img {
    transform: none;
    box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
}
.place-popup .leaflet-popup-content {
    margin: 0;
    line-height: normal;
    /* Pin the site font — the card text has no font-family of its own, so inside
       a Leaflet popup it would inherit .leaflet-container's "Helvetica Neue". */
    font-family: 'Outfit', Arial, sans-serif;
    /* width is set per-viewport by showPopup() via Leaflet min/maxWidth */
}
/* Hide Leaflet's default tail + close button — popup floats clean over the pin */
.place-popup .leaflet-popup-tip-container { display: none; }
.place-popup .leaflet-popup-close-button { display: none; }

/* Match the mobile strip card's compact text (the popup IS the same card):
   - kill Leaflet's default <p> margin (it outranks .place-card-title{margin:0}
     and would pad the title top/bottom by ~1.3em);
   - single-line title with no 2-line height reservation;
   - tighter eyebrow/category top padding (8 vs base 12). Mirrors .listings-grid. */
.place-popup .leaflet-popup-content p { margin: 0; }
.place-popup .place-card-title {
    min-height: 0;
    -webkit-line-clamp: 1;
}
.place-popup .place-card--v2 .place-card-eyebrow,
.place-popup .place-card--v2 .place-card-ad,
.place-popup .place-card--v2 .place-card-category { padding-top: 8px; }

/* ───── Mobile mode (뷰포트1 + 뷰포트2, ≤ 949px) — cards-default + Map button.
   The map collapses into a fixed-overlay revealed by the floating toggle.
   Card column count is set by the .listings-grid media rules above.
   Viewport 2 (744–949) keeps the desktop-style scroll containment so cards
   never bleed under the sticky header; viewport 1 (≤ 743) lets the body
   scroll naturally for native-phone feel. ───── */
.map-toggle-btn { display: none; }

@media (max-width: 949px) {
    .type-tabs {
        /* gap is the boundary between the toggle area (.mode-toggle) and
           the tab area (.type-tabs-scroll). Shrinking it moves the scroll
           container's left edge closer to the pill. */
        gap: 4px;
        padding: 0 0 0 8px;
        /* No overflow on the outer bar — the toggle stays fixed at the left,
           and only the inner .type-tabs-scroll scrolls horizontally. */
        overflow: hidden;
    }
    .type-tabs-scroll {
        flex: 1;
        min-width: 0;
        overflow-x: auto;
        overflow-y: hidden;
        touch-action: pan-x;
        overscroll-behavior-x: contain;
        display: flex;
        align-items: center;
        gap: 20px;
        padding-right: 16px;
        /* Hide the horizontal scrollbar on mobile — scroll still works via
           touch/swipe, but the bar itself isn't visible. */
        scrollbar-width: none; /* Firefox */
        -ms-overflow-style: none; /* legacy Edge/IE */
    }
    .type-tabs-scroll::-webkit-scrollbar { display: none; } /* Chrome/Safari */
    /* Blur veil pinned to the scroll container's left edge via position:sticky.
       Lives as the first flex item (width 8px = replaces the previous
       padding-left). At scroll=0 it sits where the first tab would start,
       blurring nothing (no content behind). As tabs scroll left, they pass
       behind this sticky veil and get blurred — strongest at the left edge,
       fading out across 8px via mask-image. position:absolute didn't work
       here because in WebKit/Blink, absolute children of an overflow:auto
       container scroll with the content rather than staying at the padding box. */
    .type-tabs-scroll::before {
        content: '';
        position: sticky;
        left: 0;
        width: 16px;
        align-self: stretch;
        flex-shrink: 0;
        /* Cancel the parent's 20px flex gap between the mask and the first
           tab — tab-to-tab gap stays 20px. */
        margin-right: -20px;
        /* Solid-white veil that fades from opaque at the left edge to fully
           transparent at the right via mask. Tabs scrolling behind appear
           to fade into the toggle boundary (opacity drops to 0 at the left). */
        background: var(--color-bg);
        mask-image: linear-gradient(to right, #000, transparent);
        -webkit-mask-image: linear-gradient(to right, #000, transparent);
        pointer-events: none;
        z-index: 1;
    }
    /* Region selector → a single horizontal line that scrolls/swipes, instead
       of wrapping to multiple rows (saves vertical space for the map). */
    .results-header { flex-wrap: nowrap; margin-bottom: 0; }  /* header is hidden in map-first mode → no trailing gap */
    .results-title-group {
        flex: 1 1 auto;
        min-width: 0;                   /* let it scroll internally, not overflow the page */
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: visible;
        touch-action: pan-x;
        overscroll-behavior-x: contain;
        scrollbar-width: none;          /* Firefox */
        -ms-overflow-style: none;       /* legacy Edge/IE */
    }
    .results-title-group::-webkit-scrollbar { display: none; } /* Chrome/Safari */
    .results-title-group > * { flex-shrink: 0; }  /* never squash the title/pills */
    /* Drop the negative margin so the title's hover pill starts at the 8px
       content edge (like the filter chips), with the text inset by its padding. */
    .results-title { margin-left: 0; }
    /* The dropdown menu would be clipped by the scroller — float it via fixed
       positioning (JS sets top/left from the button on open). */
    .results-title-group .region-dropdown-menu { position: fixed; margin-top: 0; }

    /* Filter bar — single line that scrolls horizontally instead of wrapping
       to a second row. Chips keep their natural width; scrollbar hidden.
       body:has(.map-pane) scopes this to REAL map pages — /serendipity borrows
       .filter-bar (browse chips) without a map and keeps normal wrapping flow. */
    body:has(.map-pane) .filter-bar {
        flex-wrap: nowrap;
        /* Only the Filters button shows here (chips hidden) so no scroll is
           needed — overflow:visible lets the button's drop shadow render
           instead of being clipped by a scroll box. */
        overflow: visible;
        /* Float the button over the top of the map (max out map height). Pulled
           out of flow → .results keeps only the title row and the map grows up. */
        position: absolute;
        left: 8px;
        right: 8px;
        /* Trigger sits top-RIGHT so it clears the type tabs (Travel / …) that
           occupy the top-left of the map. */
        justify-content: flex-end;
        margin-bottom: 0;
    }
    body:has(.map-pane) .filter-bar::-webkit-scrollbar { display: none; } /* Chrome/Safari */
    body:has(.map-pane) .filter-bar > * { flex-shrink: 0; }  /* never squash a chip */
    /* The chip dropdown would be clipped by the scrolling filter bar — float it
       via fixed positioning (JS sets top/left from the chip on open). The mode
       menu gets the same treatment: .type-tabs is overflow:hidden ≤949.
       .filter-dropdown-menu is map-page-only (serendipity's inline JS doesn't
       position a fixed menu); .mode-menu stays UNGATED — serendipity's own
       mode-selector script relies on it and sets top/left itself. */
    body:has(.map-pane) .filter-dropdown-menu { position: fixed; }
    .mode-menu { position: fixed; }

    /* Viewports 1/2 are map-first: the map fills the column and the filter bar
       floats over its top. z-index:0 makes the map its own stacking context so
       Leaflet's internal panes (z 200–700) stay contained and don't paint over
       the floated filter bar (.results z-index:5). */
    .map-pane {
        flex: 1 1 auto;
        min-height: 0;
        width: 100%;
        padding: 0;
        background: white;
        min-width: 0;
        z-index: 0;
    }
    #map { border-radius: 0; box-shadow: none; height: 100%; }

    /* Map/List toggle + mobile close button are gone — the map is always shown. */
    .map-toggle-btn,
    .map-close-btn { display: none !important; }
}

/* 뷰포트3+ (≥ 950px): Airbnb-style scroll. The LEFT card list scrolls the
   DOCUMENT (not an inner div) while the map is position:sticky and stays
   pinned beside it. On iOS/iPadOS Safari only a real document scroll collapses
   the address bar — a body with overflow:hidden never lets it minimize. dvh
   keeps the map filling the live visible area before AND after the bar hides.
   (vp1/2 keep their own body{overflow:hidden} + fixed map below, untouched.) */
@media (min-width: 950px) {
    body { overflow: visible; }
    /* The shared header bar is a semi-transparent frosted pill; now that the
       page scrolls, cards would show through it. Give the sticky header strip
       an opaque background so it masks the list scrolling underneath. (Scoped
       to vp3+ — vp1/2 don't scroll the body, so they keep the frosted look.) */
    #site-header { background: var(--color-bg); }
    .split { height: auto; align-items: flex-start; }
    .results { overflow-y: visible; }
    .map-pane {
        position: sticky;
        top: calc(72px + 49px);                /* pin below the sticky header + tabs */
        align-self: flex-start;
        height: calc(100vh - 72px - 49px);     /* fallback for browsers without dvh */
        height: calc(100dvh - 72px - 49px);    /* fills the live visible area */
        /* Own stacking context (same as the vp1/2 rule): contains Leaflet's
           internal panes (z 200–700) + controls (800) so they can't leak into
           the root stack and paint over low-z page popovers (filter dropdown 20). */
        z-index: 0;
    }
}

/* 뷰포트1 + 뷰포트2 (≤ 949px): desktop-style scroll containment so cards
   never bleed under the sticky header. Body itself does not scroll —
   .results owns its own scroll viewport. The Map View overlay sits under
   the tabs (not over them); JS sets --map-overlay-top from the tabs'
   bottom edge on toggle, so the value is correct for both viewports. */
@media (max-width: 949px) {
    body { overflow: hidden; }
    .split {
        display: flex;
        flex-direction: column;
    }
    /* Results shrinks to just the title row; the filter bar is pulled out of
       flow and floated over the map (below), so the map gains that height.
       position:relative + z-index makes the floated bar stack above the map. */
    .results {
        flex: 0 0 auto;
        overflow: visible;
        padding: 0 0 0 8px;   /* left like .type-tabs; no top so the map sits flush under the tabs */
        position: relative;
        z-index: 5;
    }
    .map-fullscreen-btn { display: none !important; }
    /* Pagination is a desktop paged-list control — the mobile swipe strip shows
       the current page's cards; pan the map to load the next set. */
    .map-pagination { display: none !important; }

    /* Card list → horizontal swipe strip docked over the bottom of the map.
       One card per view (next peeks), snaps to center; pin↔card sync in JS.
       Every rule in this section is gated by body:has(.map-pane) — the strip is
       a MAP-page feature. /serendipity borrows .listings-grid for its service
       grid without a map and must keep a normal document-flow grid. */
    body:has(.map-pane) .listings-grid {
        position: fixed;
        left: 0;
        right: 0;
        /* Box extends to the screen bottom (bottom:0) with a 16px bottom padding,
           so cards stay 16px up (unchanged) but their drop-shadow renders in that
           bottom gap — over the Leaflet attribution — instead of being clipped by
           the scroller's overflow. */
        bottom: 0;
        z-index: 850;                 /* above the map, below the header (1000) */
        display: flex;
        grid-template-columns: none;  /* cancel the desktop grid rule */
        gap: 12px;
        margin: 0;
        /* Center-peek carousel: card width capped at 360, side padding =
           (viewport − card)/2 so EVERY card (incl. first/last) snaps to center.
           At the ends the empty peek (map showing through) signals start/end. */
        --card-w: min(calc(100vw - 96px), 360px);   /* 96 = 2×(sliver 36 + gap 12) → 36px peek each side */
        padding: 0 calc((100vw - var(--card-w)) / 2);
        padding-top: 8px;             /* room for the top shadow (box grows up, stays transparent) */
        padding-bottom: 16px;         /* room for the card shadow over the bottom gap */
        overflow-x: auto;
        overflow-y: visible;
        scroll-snap-type: x mandatory;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        transition: transform 0.3s ease;
    }
    /* Tapping empty map slides the strip down off-screen (marker range expands). */
    body.cards-hidden:has(.map-pane) .listings-grid { transform: translateY(calc(100% + 24px)); }
    body:has(.map-pane) .listings-grid::-webkit-scrollbar { display: none; }
    body:has(.map-pane) .listings-grid > * {
        flex: 0 0 var(--card-w);
        scroll-snap-align: center;
        /* Solid surface so card text stays readable over the map. */
        background: var(--color-bg);
        /* Outer 28 (2xl) − inner image 20 (lg) = 8px inset → concentric corners. */
        border-radius: var(--radius-2xl);
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
        padding: 8px;
        box-sizing: border-box;
    }
    /* Strip pager — circular prev/next at the strip ends. Overrides the card-slot
       look above (full-width white card) with a small centred white circle. Still
       a scroll-snap target (inherited) so a swipe lands on it. */
    body:has(.map-pane) .listings-grid > .strip-pager {
        flex: 0 0 auto;
        align-self: center;
        width: 44px;
        height: 44px;
        padding: 0;
        border: none;
        border-radius: var(--radius-circle);
        background: var(--color-bg);
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        color: var(--color-text);
    }
    body:has(.map-pane) .listings-grid > .strip-pager svg { width: 22px; height: 22px; }
    /* Compact the strip card so the map stays the dominant element:
       shorter image + single-line title. */
    body:has(.map-pane) .listings-grid .place-card-img { aspect-ratio: 4 / 3; }
    body:has(.map-pane) .listings-grid .place-card-title {
        min-height: 0;
        -webkit-line-clamp: 1;
        padding-bottom: 0;
    }
    body:has(.map-pane) .listings-grid .place-card--v2 .place-card-eyebrow,
    body:has(.map-pane) .listings-grid .place-card--v2 .place-card-ad,
    body:has(.map-pane) .listings-grid .place-card--v2 .place-card-category { padding-top: 8px; }

    /* Touch viewports (1/2): no hover effect at all — :hover sticks after a tap.
       Keep the image's resting transform AND shadow (cancel the zoom + shadow
       change). */
    body:has(.map-pane) .listings-grid .place-card--v2:hover .place-card-img,
    body:has(.map-pane) .listings-grid .place-card--v2.is-hovered .place-card-img {
        transform: none;
        box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
    }
    /* Card surface shadow: no change on hover/tap-hold (V2:hover would drop it),
       but a tighter "pressed" shadow on :active — smaller blur looks pushed-in. */
    body:has(.map-pane) .listings-grid .place-card--v2 { transition: box-shadow 0.15s ease; }
    body:has(.map-pane) .listings-grid .place-card--v2:hover,
    body:has(.map-pane) .listings-grid .place-card--v2.is-hovered {
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
    }
    body:has(.map-pane) .listings-grid .place-card--v2:active {
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
    }
}

/* Header height differs per viewport, so reserve a matching amount for
   .split — header + tabs (49px). */
@media (max-width: 743px) {
    /* Viewport 1: mobile header 56px + tabs 49px = 105px.
       dvh (not vh) so the map+strip fit the LIVE visible area — iOS Safari's
       toolbar can't collapse here (no document scroll), so vh would size to the
       toolbar-hidden height and push the bottom behind the toolbar. */
    .split { height: calc(100vh - 105px); }
    .split { height: calc(100dvh - 105px); }
    .results { padding: 0 0 0 8px; }
}
@media (min-width: 744px) and (max-width: 949px) {
    /* Viewport 2: desktop header 72px + tabs 49px = 121px. dvh so it fits the
       live visible area (see vp1 note above). */
    .split { height: calc(100vh - 121px); }
    .split { height: calc(100dvh - 121px); }
}

/* Airbnb-style loading pill — a white capsule with three dots that pulse in
   sequence (grow/shrink), centered at the top of the map. Shown while the map
   is moving (pan/zoom) or on a filter fit, then fades out.
   z-index is local to the Leaflet stack (panes 200–700, controls 800). */
.map-loading {
    position: absolute;
    top: 16px;
    left: 50%;
    transform: translateX(-50%) translateY(-6px);
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 9px 15px;
    background: var(--color-bg);
    border-radius: var(--radius-pill);
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10), 0 8px 24px -8px rgba(0, 0, 0, 0.22);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s ease;
    z-index: 660;
}
.map-loading.is-active {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}
.map-loading span {
    width: 4px;
    height: 4px;
    border-radius: var(--radius-circle);
    background: var(--color-text-muted);
    animation: map-loading-dot 1s ease-in-out infinite;
}
.map-loading span:nth-child(2) { animation-delay: 0.16s; }
.map-loading span:nth-child(3) { animation-delay: 0.32s; }
@keyframes map-loading-dot {
    0%, 100% { transform: scale(0.6); opacity: 0.4; }
    40%      { transform: scale(1.3);  opacity: 1; }
}

/* Empty state — shown when no places fall inside the current map view
   (Airbnb-style "search as I move the map"). Spans the whole grid. */
.map-empty {
    grid-column: 1 / -1;
    margin: 40px 0;
    text-align: center;
    color: var(--color-text-muted);
    font-size: 15px;
    line-height: 1.6;
}
