/* ===================================================================
   SEARCH — Field Guide's real product root (not a reference page).
   Pulls only from tokens.css, same portability rule components/*.css
   follows — no dependency on field-guide.css, which is reference-kit
   chrome only (see its own file header). This page's only chrome is
   the Masthead component (primary variant) plus the map controls
   defined below.

   Layout is a single-screen app shell, not a scrolling document: html/
   body are pinned to the viewport height with no overflow, so the map
   frame below the masthead is a true full-bleed remaining-viewport
   region — no max-width, no center column, unlike every reference page.
   =================================================================== */

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: var(--color-surface);
  color: var(--color-ink);
  font-family: var(--font-ui);
}

.search-shell {
  display: flex;
  flex-direction: column;
  height: 100%;
}

/* flex:1 alone lets a flex child's content overflow its box in some
   browsers unless min-height:0 overrides the default min-height:auto —
   without it the map frame can grow taller than the space actually
   left under the masthead. */
.map-frame {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  background: var(--color-surface);
  overflow: hidden;
}

/* MapLibre owns everything inside #map (canvas, its own control
   markup) — all fill/boundary colors now live in the cloned style
   object search.js builds from OpenFreeMap's Liberty style, not in
   this stylesheet. See search.js and the Reference doc for the
   layer-keep/color-mapping rationale. */
#map {
  width: 100%;
  height: 100%;
}

/* MapLibre's required attribution control (bottom-left, so it never
   collides with the icon stack's own bottom-right position) restyled
   to sit quietly against this system's tokens instead of its library-
   default white-on-white chrome — content and the OpenFreeMap/
   OpenMapTiles/OSM attribution text itself are untouched, only its
   surface. --color-faint against --color-surface throughout (not
   --color-ink-secondary — this control is meant to read as quieter
   than the map's own labels, the same restrained-token treatment the
   boundary lines use, not a UI element competing for attention).

   Offset matches the icon stack's and filter card's own var(--space-6):
   the library's default 10px control margin is overridden below, at
   the same 24px every other piece of floating UI on this frame uses.
   At that offset the control's bottom-left corner lands exactly on the
   filter card's own bottom-left corner (card: left/bottom both
   var(--space-6) — see the .card-filter rule), so with the card open
   the attribution control sits fully underneath it (the card's
   z-index: 5 beats the library's own z-index: 2 on its bottom-left
   controls layer) rather than peeking out past the card's edge. With
   the card collapsed to its chip there's nothing left to sit under, so
   it's fully visible again — the recoloring below is what keeps that
   state looking deliberate rather than like a leftover default.

   Every override below is scoped through #map (the map container these
   controls are actually injected into, confirmed directly in the live
   DOM) rather than the bare library class names used at first pass —
   needed because MapLibre's own compact-mode rule
   (`.maplibregl-ctrl-attrib.maplibregl-compact`) carries two classes
   against this file's one, and CSS specificity is resolved by that
   count, not stylesheet order, so the bare single-class version lost
   silently (measured background/color still the library's white/black
   defaults, confirmed live, not assumed). #map adds an ID to the
   selector, which outranks any number of classes without reaching for
   !important — this codebase doesn't use it elsewhere, no reason to
   start here. */
#map .maplibregl-ctrl-attrib {
  background: var(--color-surface);
  color: var(--color-faint);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  border-radius: var(--radius-card);
}

#map .maplibregl-ctrl-attrib a {
  color: var(--color-faint);
}

/* The collapsed "i" trigger is a library-drawn background-image glyph,
   not text or an inline SVG — its color is baked into the image data,
   not readable via `color`. Swapped for the same shape (identical SVG
   path, copied verbatim from the library's own default) applied as a
   mask instead of an image, so its fill comes from background-color
   and tracks --color-faint (and this token's own light/dark values)
   the way an inline icon would, rather than a fixed baked-in black. The
   circular --color-surface backdrop is the button's own background,
   separate from the masked glyph layered on top via ::before — kept
   deliberately, since "icon ... against --color-surface" describes two
   layers, not one recolored image. */
#map .maplibregl-ctrl-attrib-button {
  filter: none;
  background-color: var(--color-surface);
  background-image: none;
}

#map .maplibregl-ctrl-attrib-button::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--color-faint);
  -webkit-mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: 20px;
  mask-size: 20px;
}

#map .maplibregl-ctrl-bottom-left .maplibregl-ctrl {
  margin: 0 0 var(--space-6) var(--space-6);
}

/* ---- Profile menu (Reference doc) — the masthead's own reserved
   profile icon (primary variant) is no longer inert; clicking it opens
   this dropdown. Built and positioned by search.js's initMenuProfile()
   — see that function's own header comment for why positioning is
   measured live (getBoundingClientRect()) rather than derived from a
   calc() of masthead spacing tokens.

   Ink-filled, matching the masthead bar's own --color-ink/--color-surface
   treatment exactly — not a hardcoded dark value. Checked directly,
   not assumed: the masthead bar's own background genuinely inverts
   with --color-ink between themes (dark in light mode, light in dark
   mode — confirmed live via getComputedStyle in both themes), so a
   hardcoded "always dark" dropdown would visually mismatch the real
   bar it's flush against the instant the page is in dark mode. Reusing
   the same reactive tokens the bar itself uses is what keeps the two
   visually continuous in both themes, and is the only reading of "the
   masthead's own dark treatment" that actually holds up against the
   live bar's real behavior.

   Shape/scale — same corner radius and shadow formula
   .card-filter__panel already established (Section 20): --radius-card,
   and the identical box-shadow value (var(--space-1) offset,
   var(--space-2) blur, the same color-mix(...) formula), not a new
   floating-surface shadow invented for a second surface. */
.menu-profile {
  position: absolute;
  top: 100%;
  /* right is set inline by positionMenu() (search.js) */
  z-index: 20;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 200px; /* deliberate literal, same category as .card-filter's own 224px (Section 20) — no spacing-scale rung targets "how wide a dropdown menu gets" */
  padding: var(--space-2);
  /* Square on top, rounded on bottom (Reference doc) — the dropdown
     sits flush against the masthead's own bottom edge (top:100%, see
     the header comment above), so a rounded top edge showed a visible
     gap/curve where two surfaces that are supposed to read as
     continuous meet. Only the top two corners change; bottom keeps
     .card-filter__panel's own --radius-card, unchanged. */
  border-radius: 0 0 var(--radius-card) var(--radius-card);
  background: var(--color-ink);
  box-shadow: 0 var(--space-1) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
  /* Clips the shadow's own blur (var(--space-2), larger than its
     var(--space-1) downward offset) at exactly this box's top edge, so
     it can't bleed upward onto the masthead bar this dropdown is
     flush against (Reference doc) — left/right/bottom are pushed out
     generously so nothing is clipped on the sides that are actually
     supposed to show the shadow. A boundary this precise is more
     reliable than re-tuning the offset/blur numbers themselves, which
     would still leave some bleed unless overcorrected well past what
     .card-filter__panel's own shadow formula (Section 20) actually
     calls for. */
  clip-path: inset(0 -100vw -100vw -100vw);
}

.menu-profile[hidden] {
  display: none;
}

/* Four inert nav items — real, focusable <button>s with a visible
   hover/focus state and no click handler, matching .controls-map-btn's
   own "styled and focusable but no handler" treatment (this file,
   below) rather than a new inertness convention for this one menu. */
.menu-profile-item {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-tag);
  background: none;
  color: var(--color-surface);
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  font-weight: var(--w-medium);
  text-align: left;
  cursor: pointer;
}

.menu-profile-item:hover,
.menu-profile-item:focus-visible {
  background: color-mix(in srgb, var(--color-surface) 15%, transparent);
}

.menu-profile-item:focus-visible {
  outline: var(--border-width-active) solid var(--color-surface);
  outline-offset: -2px;
}

.menu-profile-divider {
  height: var(--border-width-hairline);
  margin: var(--space-1) 0;
  background: var(--color-surface);
  opacity: 0.35; /* the exact .masthead-divider formula (masthead.css) — same ink-bar context, not a new value */
}

/* The one fully functional item — styled distinctly from the four flat
   nav rows above it (a compact pill, not a full-width row) since it's
   a control, not a navigation item. Values are the exact ones
   masthead.css's own .theme-toggle rule already uses for the design-
   system pages' toggle (border/radius-pill/background/color/font-size/
   hover treatment) — reused because this dropdown sits in the same
   ink-bar visual context that toggle was designed for; only the
   layout properties differ (no margin-left:auto — that assumes a
   horizontal masthead row, not this vertical dropdown stack). Named
   .theme-toggle--in-menu — a real BEM modifier of .theme-toggle
   (naming-convention-plan.md, Resolve Item 6) — rather than composed
   with .theme-toggle itself in the DOM, since .theme-toggle's own rule
   (masthead.css) has no :focus-visible treatment and a different
   border-radius token than this dropdown-mounted version already
   carries; kept as its own complete ruleset here rather than risk a
   visual regression reconciling those two small pre-existing
   differences as part of this rename. */
.theme-toggle--in-menu {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-pill);
  background: var(--color-surface);
  color: var(--color-ink-secondary);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-medium);
  line-height: 1;
  cursor: pointer;
}

.theme-toggle--in-menu:hover {
  border-color: var(--color-ink-secondary);
  color: var(--color-ink);
}

.theme-toggle--in-menu:focus-visible {
  outline: var(--border-width-active) solid var(--color-surface);
  outline-offset: 2px;
}

/* ---- Icon stack — zoom/pan chrome floating over the map ----
   One rounded card holding four stacked icon buttons, divided by
   hairlines — the toolbar-card pattern, not four independent floating
   buttons. Single consumer today (this page only), so — per NAMING.md's
   own scoped-vs-shared guidance — it stays page-local CSS here rather
   than a components/ extraction; if a second page needs this stack,
   that's the trigger to extract it, same path tag-source and masthead
   both took. */

.controls-map {
  position: absolute;
  right: var(--space-6);
  bottom: var(--space-6);
  z-index: 5;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  overflow: hidden;
  /* Literal, not tokenized: this is the system's first floating-card-
     over-content use, so there's no --shadow-* scale yet to draw from —
     same "fine to leave un-tokenized" call NAMING.md makes for a
     single-consumer value, until a second consumer justifies a real
     token. */
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}

.controls-map-btn {
  box-sizing: border-box;
  width: var(--space-8);
  height: var(--space-8);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-bottom: var(--border-width-hairline) solid var(--color-hairline);
  background: var(--color-surface);
  color: var(--color-ink-secondary);
  cursor: pointer;
}

.controls-map-btn:last-child {
  border-bottom: none;
}

.controls-map-btn:hover {
  background: var(--color-surface-sunken);
  color: var(--color-ink);
}

.controls-map-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: -2px;
}

/* Icon-stack glyphs are vendored Lucide SVGs (icons/icons.js), injected
   by search.js from a data-icon attribute — not this project's own
   square/diamond/circle/hourglass shape vocabulary. That vocabulary is
   reserved exclusively for Data Provenance jurisdiction meaning (see
   Reference doc); a generic toolbar icon must never borrow it, so these
   are a real, unrelated icon set rather than a fifth shape. Sizing only
   here — color comes from stroke="currentColor" on the vendored markup
   itself, inheriting .controls-map-btn's color the same way every other
   icon in this system already does for hover/focus state. */
.controls-map-icon {
  width: var(--space-5);
  height: var(--space-5);
}

.controls-map-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- Filter card — floating search/filter panel ----
   Visual language derived from Card-datapoint (components/card-datapoint.css),
   not copied verbatim: --color-surface body, hairline border + --radius-card,
   --color-surface-sunken header strips — generalized here to cover every
   collapsible section's own header, not just one card-level title bar,
   since this card holds five independent sections instead of
   Card-datapoint's single disclosure. Single consumer today, so — same
   NAMING.md scoped-vs-shared call as .controls-map above — this stays
   page-local CSS rather than a components/ extraction.

   Positioning: 24px is var(--space-6) exactly, not a coincidence — used
   directly rather than a literal pixel value, and matching the icon
   stack's own var(--space-6) right/bottom offset (Section 20 shipped
   this at var(--space-8)/48px; tightened to match the icon stack rather
   than the two floating elements sitting at two different distances
   from the same frame edge — see Reference doc). Width is fluid
   (`--card-filter-width`, clamp() below), not a fixed value; its
   original 248px floor is unchanged and still where all of that
   article's own reasoning (232/240/256px comparison, the 272px right-
   edge coincidence with the old 48px icon-stack offset) applies — see
   Reference doc for the full clamp() rationale, including the chosen
   400px ceiling. top+bottom (not top+height) is what makes the height
   "durable on reflow": the card's height is never computed or
   hardcoded, it falls out of the map frame's own height automatically,
   at any viewport size, without a resize listener.

   --card-filter-width is declared once, here, and read by both this
   rule and .card-filter__chip below (a custom property set on this
   element is visible to its own descendants, and the chip lives inside
   this wrapper in the real DOM — see search.html) — the two were never
   allowed to specify their own separate clamp() expressions, precisely
   so they can't drift apart from each other the way two independently-
   maintained literals eventually could. */

.card-filter {
  --card-filter-width: clamp(248px, 25vw, 400px);
  position: absolute;
  top: var(--space-6);
  left: var(--space-6);
  bottom: var(--space-6);
  width: var(--card-filter-width);
  z-index: 5;
}

/* Collapsed: the wrapper itself shrinks to fit just the chip — the
   full-height box only exists while the panel is actually showing. */
.card-filter.card-filter--collapsed {
  bottom: auto;
  width: auto;
}

.card-filter__panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--color-surface);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  overflow: hidden;
  /* --color-faint, not --color-hairline — tried both by eye against the
     map's own detail underneath; hairline read as barely-there next to
     live basemap content, faint gave the card genuine lift without
     competing with it. 4px/8px offset+blur are var(--space-1)/
     var(--space-2) — the task's "8px soft shadow" already lines up with
     an existing rung, so no literal value was needed for those two. */
  box-shadow: 0 var(--space-1) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
}

.card-filter.card-filter--collapsed .card-filter__panel {
  display: none;
}

/* Collapsed-state chip — hidden by default, shown only via .card-filter--collapsed.
   Sections underneath keep their real DOM <details open> state the
   entire time this is hidden; nothing here touches it, which is what
   makes "restore prior state on re-expand" free rather than a separate
   piece of state to track. */
/* Collapsed state reads as the expanded header bar sitting alone, not a
   separate smaller control — same width as the card (var(--card-filter-width),
   explicit since a <button>'s auto width doesn't shrink-to-fit a
   containing block the way .card-filter__panel's plain block width does;
   box-sizing border-box so that width is the actual outer width
   including this element's own border, matching the panel exactly —
   reading the same custom property .card-filter itself declares, not a
   second clamp() literal, is what keeps the two locked together at
   every viewport size rather than just at the two endpoints someone
   happened to test), same padding/background/typography as
   .card-filter__topbar and .card-filter__title below, and --radius-card
   (not --radius-pill) on all four corners since nothing renders beneath
   it in this state — only border, shadow, and radius-card are new here,
   since a standalone floating element needs its own edge treatment the
   way the topbar strip itself never did living inside the already-
   bordered panel. */
.card-filter__chip {
  display: none;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  box-sizing: border-box;
  width: var(--card-filter-width);
  padding: var(--space-3) var(--space-4);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
  color: var(--color-ink);
  font-family: var(--font-ui);
  cursor: pointer;
  box-shadow: 0 var(--space-1) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
}

.card-filter.card-filter--collapsed .card-filter__chip {
  display: inline-flex;
}

.card-filter__chip:hover {
  background: var(--color-surface);
}

.card-filter__chip-label {
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.card-filter__chip-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-6);
  height: var(--space-6);
  color: var(--color-ink-secondary);
}

.card-filter__chip-icon svg {
  display: block;
  width: var(--space-4);
  height: var(--space-4);
}

.card-filter__topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-sunken);
  border-bottom: var(--border-width-hairline) solid var(--color-hairline);
  flex-shrink: 0;
}

.card-filter__title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  color: var(--color-ink);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.card-filter__collapse-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-6);
  height: var(--space-6);
  padding: 0;
  border: none;
  border-radius: var(--radius-tag);
  background: transparent;
  color: var(--color-ink-secondary);
  cursor: pointer;
}

.card-filter__collapse-btn:hover {
  background: var(--color-surface);
  color: var(--color-ink);
}

.card-filter__collapse-icon {
  width: var(--space-4);
  height: var(--space-4);
}

.card-filter__collapse-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* Scrollable section stack — the flex sibling that actually grows/
   shrinks; footer below stays put because it isn't part of this box.
   That's the entire "sticky footer within the card" mechanism, plain
   flexbox rather than position:sticky. */
.card-filter__scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.filter-section {
  border-bottom: var(--border-width-hairline) solid var(--color-hairline);
}

.filter-section:last-child {
  border-bottom: none;
}

/* Same disclosure pattern as .card-datapoint-disclosure-summary (native
   <details>/<summary>, marker hidden, chevron rotates on [open]) — see
   card-datapoint.css for the original. The one deliberate departure:
   this summary sits on --color-surface-sunken, not the card's own
   --color-surface, so each section reads as its own header "strip"
   stacked in the card, since a five-section card needs that repeated
   per section rather than once at the top the way Card-datapoint's
   single disclosure does. */
.filter-section-summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-sunken);
  cursor: pointer;
  list-style: none;
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  font-weight: var(--w-medium);
  color: var(--color-ink);
}

.filter-section-summary::-webkit-details-marker {
  display: none;
}

.filter-section-summary::marker {
  content: '';
}

.filter-section-summary:hover {
  background: var(--color-hairline);
}

/* Real Lucide chevron-right glyph (icons/icons.js), filled in by each
   page's own initIcons() sweep — CSS-icon audit finding #4
   (Reference/css-icon-audit.md), replacing the ‘›’ text character this
   used to be. Same width/height + inner svg pattern every other icon
   on this site uses (.controls-map-icon, .results-command-icon). */
.filter-section-chevron {
  display: inline-flex;
  flex-shrink: 0;
  width: var(--ui-h4-size);
  height: var(--ui-h4-size);
  color: var(--color-ink);
  transition: transform 0.15s ease;
}

.filter-section-chevron svg {
  display: block;
  width: 100%;
  height: 100%;
}

.filter-section[open] > .filter-section-summary .filter-section-chevron {
  transform: rotate(90deg);
}

.filter-section-title {
  flex: 1;
  min-width: 0;
}

/* Category's live "N selected" caption (Section 26) — empty text
   renders as a zero-width inline element, which is the entire "no
   caption when nothing is selected" mechanism, no conditional display
   toggle needed. .filter-section-title's own flex:1 already pushes
   this to the row's trailing edge, so no extra margin is needed here
   either. Date and Data Provenance don't have one yet — added only
   where a section actually populates it, not spec'd onto every summary
   ahead of need. */
.filter-section-count {
  flex-shrink: 0;
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  color: var(--color-muted);
}

/* Date and Data Provenance are still deliberately empty this pass
   (padding stays so an open-but-empty section still visibly differs
   from a collapsed one) — AI assisted search, Description or keywords,
   and Category are populated below.

   Left/right inset is set once here, at the section-container level
   (not the card level — .card-filter__topbar and .card-filter__footer
   are their own chrome, not "section content," and keep their own
   padding) — var(--space-2), 8px, confirmed against the scale in
   tokens.css (--space-1 is 4px, --space-2 is 8px). Every element inside
   a section body (inputs, captions, whatever a future section adds)
   fills this box at width:100% with no left/right padding of its own,
   so the 8px inset applies card-wide to all current and future section
   content automatically, from this one declaration, rather than
   needing to be re-specified per element. Top/bottom (var(--space-2)/
   var(--space-5)) are unchanged from Section 20 — this pass only
   touches left/right. */
.filter-section-body {
  padding: var(--space-2) var(--space-2) var(--space-5);
}

/* Shared base for every text input on the card, current and future —
   both fields below apply this plus their own modifier class for
   field-specific sizing, the same base+modifier split .card-filter__btn
   already uses for the footer buttons. Border and focus ring are card-
   wide rules here, not per-input: hairline border, var(--color-ink-
   secondary) on focus — AI assisted search originally shipped with a
   heavier var(--border-width-active) border and a darker var(--color-
   ink) focus ring meant to read as the card's "crown jewel" input, but
   that visual distinction is gone now — every text input on this card
   reads identically, and any future one inherits the same treatment
   automatically instead of needing its own border/focus declaration. */
.filter-text-input {
  box-sizing: border-box;
  width: 100%;
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  color: var(--color-ink);
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
}

/* Two separate rules, deliberately — ::placeholder only ever applies
   while the field is empty; real typed content always falls through to
   .filter-text-input's own `color: var(--color-ink)` above, the
   browser's native behavior, not something this rule needs to
   duplicate or guard against. --color-muted (not --color-faint, the
   prior value) — legible as "helper text," not so faint it reads as
   disabled. .keyword-input-hint below gets the same token for the same
   reason: one "helper/placeholder text" color across the card, not two
   near-identical grays that happen to diverge. */
.filter-text-input::placeholder {
  color: var(--color-muted);
}

.filter-text-input:focus {
  outline: none;
  border-color: var(--color-ink-secondary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-ink-secondary) 12%, transparent);
}

/* AI assisted search's remaining distinction from the plain keyword
   field is structural, not a border/color treatment: multi-line
   (`rows="4"` — bumped from `3` per the Reference doc's own submission-
   behavior pass, still the browser's own font-metric-accurate sizing,
   not a guessed min-height literal; scrolling and this stage's own
   resize:vertical still apply once typed content exceeds four lines)
   and a leading sparkles icon (vendored solely for this field — see
   icons/icons.js) that no other input on the card gets. */
.ai-search-field {
  position: relative;
}

.ai-search-field-icon {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  width: var(--space-4);
  height: var(--space-4);
  color: var(--color-ink-secondary);
  pointer-events: none;
}

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

/* Right/bottom padding widened past the plain var(--space-3) every
   other edge of this field uses, to clear the embedded submit button
   below — sized to that button's own 24px (var(--space-6)) square plus
   its own var(--space-3) inset from the textarea's edge (matching the
   button's own corrected, resize-handle-clearing inset), so typed text
   wraps before ever running under the button rather than behind it. */
.ai-search-input {
  padding: var(--space-3) calc(var(--space-3) + var(--space-6) + var(--space-2)) calc(var(--space-3) + var(--space-6) + var(--space-2)) calc(var(--space-3) + var(--space-4) + var(--space-2));
  line-height: 1.5;
  resize: vertical;
}

/* ---- Embedded submit button (Reference doc) — the chat-input pattern
   the task named directly (Claude's own send button): a filled,
   circular, ink-colored control sitting inside the textarea's own
   bottom-right corner, not a separate control below it. Ink-fill +
   --color-surface icon is this card's own established "primary action"
   treatment (.card-filter__btn--primary, a checked .checkbox-tristate,
   .date-mode-btn.is-active) — reused here for the same reason: this is
   the section's one primary action. Disabled state reuses
   .card-filter__btn--primary:disabled's own exact formula (sunken
   background, --color-faint, not-allowed cursor) rather than a new
   disabled treatment, so "primary action, currently unavailable" reads
   identically everywhere on this card it appears. */
/* Square, var(--radius-card) — the same corner-radius token this
   card's own real buttons already use (.card-filter__btn, the search
   modal's own "Done" control), not the fully-round 50% this shipped
   with initially, and not --radius-tag either (that one's reserved for
   smaller checkbox/toggle-scale controls — .checkbox-tristate, .date-
   mode-btn — a different scale than this button's own). Inset from the
   corner increased from var(--space-2) to var(--space-3) so the square
   shape clears the textarea's own native resize handle: a circle at
   the same var(--space-2) inset left a visible gap around the grip in
   every corner, but a square's own corner reaches further into that
   same corner than a circle's curve ever did, and did overlap the
   handle at the original inset — confirmed live, not assumed, before
   choosing the new value. */
.ai-search-submit-btn {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  padding: 0;
  border: none;
  border-radius: var(--radius-card);
  background: var(--color-ink);
  color: var(--color-surface);
  cursor: pointer;
}

.ai-search-submit-btn:hover:not(:disabled) {
  background: var(--color-ink-secondary);
}

.ai-search-submit-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

.ai-search-submit-btn:disabled {
  cursor: not-allowed;
  background: var(--color-surface-sunken);
  color: var(--color-faint);
}

.ai-search-submit-icon {
  display: flex;
  width: var(--space-4);
  height: var(--space-4);
  pointer-events: none;
}

.ai-search-submit-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- Query pill + disclaimer (Reference doc) ----
   Fill/radius/font now matches .region-tier-summary-pill's own ink-
   colored "N selected" summary-pill treatment exactly (background:
   var(--color-ink), color: var(--color-surface), border-radius:
   var(--radius-pill), font-size: var(--ui-caption-size), font-weight:
   var(--w-semibold), no border) — compared directly against that
   rule's own values, not approximated. Only the fill/radius/font
   changed; the layout/sizing behavior that makes this pill different
   from a tag-sized one is untouched: still width:100%/box-sizing:
   border-box (a full-width bar, not .region-pill/.dp-pill's own small
   inline-flex/max-width:168px tag shape — this represents one long
   free-text string, not a short label), still truncates via the same
   overflow/text-overflow/white-space trio on .ai-query-pill-text.
   Margin lives on the pill itself, not the wrapper — #ai-query-pill-
   wrap needs no :empty override the way #region-pills/.dp-pills did,
   since an actually-empty wrapper (no pill rendered) has no margin of
   its own to collapse in the first place.

   This pill's own dismiss button now shares .btn-dismiss (Phase 5 batch
   3) with Data Provenance's and Region's — see that block's own
   definition, below the pill rules, for the consolidation and the one
   real, deliberate exception this pill still needs. */
.ai-query-pill-wrap:empty {
  margin-top: 0;
}

.ai-query-pill {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  margin-top: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--color-ink);
}

.ai-query-pill-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  color: var(--color-surface);
}

/* Shared dismiss-button block (Phase 5 batch 3) — consolidates what
   were three separately-declared, near-identical "×" dismiss buttons
   (.ai-query-pill-remove here, .dp-pill-remove, .region-pill-remove,
   both below) into one real canonical treatment, past NAMING.md's own
   "duplicate at a 2nd consumer, extract at a 3rd" threshold
   (naming-convention-plan.md, Resolve Item 7). Base rule below is the
   shape EVERY consumer shares verbatim (size, padding, border-radius,
   transparent background, font-size, line-height, cursor) plus the
   color pairing Data Provenance's and Region's pills need — those two
   were already 100% byte-identical, genuine drift-free duplication.

   One real, deliberate exception, confirmed against live token values
   before merging rather than assumed away: this pill (.ai-query-pill)
   fills with --color-ink, not the light --color-surface-sunken tray
   Data Provenance's/Region's pills use — the one ink-filled, always-
   inverted surface on this card (same treatment the masthead bar
   uses), which flips tone opposite the page in both themes. The base
   .btn-dismiss color pairing below (--color-ink-secondary text,
   --color-hairline hover) is tuned for a pill that's the SAME tone as
   the page — applied here unchanged it would read as low-to-invisible
   ink-on-ink contrast, not a cosmetic nit. The scoped override just
   below re-declares only the two color-bearing properties for this one
   real, contrast-driven case — not a third full rule block, and not a
   BEM modifier (this is a real per-background necessity, not a named
   visual variant) — so there is still exactly one shape/behavior
   definition for every dismiss button on this page, per the task's own
   "one canonical treatment, applied consistently everywhere" goal. */
.btn-dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--color-ink-secondary);
  line-height: 1;
  cursor: pointer;
}

/* Real Lucide x glyph (icons/icons.js), injected directly into this
   button (no wrapper span — the button's own box already IS the
   icon's box, same as the '×' character it replaces) — CSS-icon audit
   finding #7 (Reference/css-icon-audit.md). Sized to var(--space-3),
   matching the old font-size the '×' character rendered at, not the
   full var(--space-4) button box — filling 100% here would sit
   visibly larger/heavier than the glyph it's replacing. */
.btn-dismiss svg {
  display: block;
  width: var(--space-3);
  height: var(--space-3);
}

.btn-dismiss:hover {
  background: var(--color-hairline);
  color: var(--color-ink);
}

.btn-dismiss:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 1px;
}

.ai-query-pill .btn-dismiss {
  color: var(--color-surface);
}

.ai-query-pill .btn-dismiss:hover {
  background: color-mix(in srgb, var(--color-surface) 25%, transparent);
  color: var(--color-surface);
}

/* Same caption formula .keyword-input-hint already established
   (margin-top var(--space-2), --ui-caption-size, --color-muted) —
   reused directly rather than a new helper-text treatment invented for
   one more caption. font-style:italic is the one deliberate departure
   from that shared formula — size/weight/color all stay exactly as
   .keyword-input-hint's own, only the style axis changes, so this one
   caption reads as a disclaimer/caveat specifically without becoming a
   second, differently-weighted helper-text treatment. */
.ai-query-disclaimer {
  margin: var(--space-2) 0 0;
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-style: italic;
  color: var(--color-muted);
}

/* Description or keywords: the plain sibling — single line, no icon,
   no border/focus overrides of its own anymore (both now come from
   .filter-text-input). The instruction to enter multiple comma-
   separated keywords is a caption below the field, not folded into the
   placeholder — the placeholder stays a short, concrete example of
   what to type, the caption carries the "how" instead of cramming both
   into the same line at this field's width. */
.keyword-input {
  padding: var(--space-2) var(--space-3);
}

.keyword-input-hint {
  margin: var(--space-2) 0 0;
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  color: var(--color-muted);
}

/* ---- Category tree — tri-state multi-select (Section 26) ----
   Markup is built entirely by category-tree.js from a literal data
   spec, not hand-authored per row (43 checkboxes across 9 top-level
   groups) — this file only styles the resulting .category-* classes,
   the same division of labor .card-filter__* already has with
   search.js. No left/right padding on any rule below — .filter-
   section-body's own 8px inset (above) already reaches every row, per
   Section 25's "set once at the container, not per element" rule; the
   only horizontal spacing added here is .category-children's own
   indent, deliberately relative to that inherited inset rather than
   stacking a second one on top of it. */
.category-node + .category-node {
  margin-top: var(--space-2);
}

.category-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
}

.category-label {
  flex: 1;
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  color: var(--color-ink);
  cursor: pointer;
}

/* Real <input type="checkbox">, appearance:none and rebuilt from
   tokens — same call this card already made for the native <details>
   marker and the collapsed chip, rather than leaving browser-default
   checkbox chrome inconsistent across platforms/themes. Checked and
   indeterminate share one filled treatment; the mark drawn inside each
   (a rotated-border checkmark, a flat dash) is a bespoke small shape at
   this size, same category as the disclosure summary's literal `›`
   character elsewhere on this card — not something a spacing/color
   token governs.

   Its own standalone block (Phase 5 batch 3), not an element of either
   consumer — this exact class is shared verbatim between
   card-filter__category (category-tree.js) and
   card-filter__data-provenance (data-provenance.js)'s own blanket
   toggles, and strict BEM requires an element belong to exactly one
   block. Promoted rather than duplicated into two near-identical rule
   blocks, the same call already made for tag-source/tag-confidence
   (naming-convention-plan.md, Block row 12). */
.checkbox-tristate {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  flex-shrink: 0;
  width: var(--space-4);
  height: var(--space-4);
  margin: 0;
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-tag);
  background: var(--color-surface);
  cursor: pointer;
}

.checkbox-tristate:checked,
.checkbox-tristate:indeterminate {
  background: var(--color-ink);
  border-color: var(--color-ink);
}

.checkbox-tristate:checked:not(:indeterminate)::after {
  content: '';
  position: absolute;
  left: 4px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--color-surface);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.checkbox-tristate:indeterminate::after {
  content: '';
  position: absolute;
  left: 3px;
  top: 6px;
  width: 8px;
  height: 2px;
  background: var(--color-surface);
}

.checkbox-tristate:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

/* Expand/collapse is its own control, deliberately not the native
   <details>/<summary> pattern the rest of this card uses — nesting
   <details> here would put a checkbox inside <summary>, and a click on
   any descendant of <summary> toggles the details' own open state by
   default, which would make "check a parent without expanding it"
   (an explicit requirement) fight the browser's own click handling.
   Plain button + aria-expanded sidesteps that entirely while keeping
   the same rotate-on-open chevron treatment as every other disclosure
   on this card. */
.category-expand-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: var(--space-5);
  height: var(--space-5);
  padding: 0;
  border: none;
  border-radius: var(--radius-tag);
  background: transparent;
  color: var(--color-ink-secondary);
  cursor: pointer;
}

.category-expand-btn:hover {
  background: var(--color-surface-sunken);
  color: var(--color-ink);
}

.category-expand-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: -2px;
}

/* Real Lucide chevron-right glyph (icons/icons.js), injected directly
   by category-tree.js's own node-building code — CSS-icon audit
   finding #5 (Reference/css-icon-audit.md), replacing the ‘›’ text
   character this used to be. Same width/height + inner svg pattern
   every other icon on this site uses (.controls-map-icon,
   .results-command-icon); color still inherits from
   .category-expand-btn's own color property above, same as before. */
.category-expand-chevron {
  display: inline-flex;
  width: var(--ui-h4-size);
  height: var(--ui-h4-size);
  transition: transform 0.15s ease;
}

.category-expand-chevron svg {
  display: block;
  width: 100%;
  height: 100%;
}

.category-expand-btn[aria-expanded="true"] .category-expand-chevron {
  transform: rotate(90deg);
}

.category-children {
  padding-left: var(--space-6);
}

/* ---- Date section — exact/range toggle (Section 27) ----
   First real content in this section (empty shell since Section 20).
   Native <input type="date"> throughout rather than a custom-built
   calendar widget — see the Reference doc's Decision Log for the full
   reasoning; in short, the browser already owns the hard parts of a
   date picker (keyboard entry, locale-aware formatting, mobile-native
   pickers, screen-reader semantics), and this project's own no-new-
   dependency convention cuts the same way here as it did for every
   prior vendoring decision: own more code only where the browser
   genuinely can't do the job (MapLibre for a real globe — Section 13),
   not for a solved primitive. What's stylable (border, background,
   padding, font, focus ring) already comes from .filter-text-input,
   the same shared base every input on this card uses (Section 25) —
   only what's genuinely specific to <input type="date"> lives below.

   Mode toggle is a plain two-button aria-pressed group, not a full ARIA
   radiogroup with roving-tabindex arrow-key navigation — the same
   scope .category-expand-btn already kept to (aria-expanded, no fuller
   ARIA widget pattern built out) rather than a heavier convention
   introduced for one control. */
.date-mode-toggle {
  display: flex;
  gap: var(--space-1);
  padding: var(--space-1);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  background: var(--color-surface-sunken);
}

.date-mode-btn {
  flex: 1;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-tag);
  background: transparent;
  color: var(--color-ink-secondary);
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  font-weight: var(--w-semibold);
  cursor: pointer;
}

.date-mode-btn:hover {
  color: var(--color-ink);
}

/* Selected segment reuses the same ink-fill treatment
   .card-filter__btn--primary and a checked .checkbox-tristate already
   use for "this is the selected/primary thing" — not a new color
   relationship invented for this control. */
.date-mode-btn.is-active {
  background: var(--color-ink);
  color: var(--color-surface);
}

.date-mode-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

.date-mode-panel {
  margin-top: var(--space-3);
}

/* Stacked, not side-by-side — tried side-by-side first (two flex:1
   columns) and rejected it live: at this card's ~232px content width,
   splitting it in half left each native date input too narrow to show
   its own "mm/dd/yyyy" placeholder without the calendar-icon affordance
   overlapping the truncated text. Full-width, one above the other,
   reads cleanly instead. */
.date-range-panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Author display:flex above otherwise beats the UA [hidden]{display:none}
   rule at equal specificity, since author styles always win over UA ones
   regardless of source order — without this, the range panel stayed
   visibly rendered even while its `hidden` attribute was set. */
.date-range-panel[hidden] {
  display: none;
}

.date-range-field {
  width: 100%;
}

.date-range-label {
  display: block;
  margin-bottom: var(--space-1);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  color: var(--color-muted);
}

/* color-scheme tells the browser which palette to render its own
   native chrome in — the calendar-picker glyph and, in Chromium, the
   popup calendar itself — so neither ever renders light-on-light or
   dark-on-dark against this card. Same :root[data-theme]/
   prefers-color-scheme dual-block pattern tokens.css already uses at
   the page level (Section 14), scoped down to just this input since
   nothing else on the page needs it. */
.date-input {
  padding: var(--space-2) var(--space-3);
  color-scheme: light;
}

:root[data-theme="dark"] .date-input {
  color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .date-input {
    color-scheme: dark;
  }
}

/* ---- Data Provenance — tri-state agency picker ----
   Agency scope only, nationwide, no map coupling (a prior pass wired
   this section into the map's own region selection; that integration
   was deliberately unwound — see the Reference doc entry superseding
   its own Section 30). Markup is built entirely by data-provenance.js
   from a small literal group spec, not hand-authored per group — same
   division of labor this file already has with category-tree.js/
   search.js elsewhere on this card. No left/right padding on any rule
   below — .filter-section-body's own 8px inset (Section 25) already
   reaches every row; the only horizontal spacing added here is
   internal to a group's own content (search dropdown padding, pill
   gaps), not a second inset stacked on top of the inherited one. All/
   National agencies/State agencies/County agencies' own tri-state
   checkboxes reuse .checkbox-tristate/.category-row/.category-label
   directly (same classes, zero new declarations for the checkbox
   itself) — the literal "reuse the Category tree's tri-state
   mechanics" the original task asked for, not just a visual match. */
.dp-group + .dp-group {
  margin-top: var(--space-4);
}

/* No .dp-group-header rule needed — .category-row's own padding
   (var(--space-1) 0) is enough on its own; nothing group-specific to
   add beyond the shared row layout it already reuses. */

/* ---- Search field + results dropdown ---- */
.dp-search-field {
  position: relative;
  margin-top: var(--space-2);
}

.dp-search-input {
  padding: var(--space-2) var(--space-3);
}

/* Absolutely positioned within .dp-search-field, not the card — floats
   over whatever pill content sits below it rather than pushing layout
   down. Shadow reuses .card-filter__panel's own exact formula (Section
   20) rather than inventing a second floating-surface shadow value.
   Known, accepted limitation: this sits inside .card-filter__scroll,
   which clips overflow on both axes — a dropdown opened very close to
   the scroll container's own bottom edge can get visually clipped
   rather than flipping to open upward. Not solved here (real added
   complexity — measuring available space, flipping direction — the
   task's own spec never asked for collision handling); flagging it
   plainly rather than either silently shipping it unnoted or over-
   building an unrequested fix. max-height is a deliberate literal, same
   category as .card-filter's own 224px width (Section 20) — no rung on
   the spacing scale targets "how tall a scrollable results list gets
   before it scrolls itself," so 200px (roughly 6-7 result rows) is
   chosen directly rather than forced onto an ill-fitting token. */
.dp-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--space-1);
  max-height: 200px;
  overflow-y: auto;
  background: var(--color-surface);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  box-shadow: 0 var(--space-1) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
  z-index: 3;
}

.dp-search-result {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
}

.dp-search-result:hover {
  background: var(--color-surface-sunken);
}

.dp-search-result-label {
  flex: 1;
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  color: var(--color-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dp-search-empty,
.dp-search-more {
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  color: var(--color-muted);
}

/* ---- Pills ---- */
/* Capped to roughly 3 lines (confirmed live against this card's actual
   pill height, not calculated blind) before .is-expanded lifts the cap
   — the "View all" affordance's entire job is toggling that one class,
   same "one class does the whole job" mechanism the filter card's own
   collapse-to-chip control already established (Section 20). */
.dp-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
  max-height: 84px;
  overflow: hidden;
}

.dp-pills.is-expanded {
  max-height: none;
  overflow: visible;
}

.dp-pills:empty {
  margin-top: 0;
}

/* Ink-filled, --color-surface text — the same "this is the selected/
   primary thing" treatment .card-filter__btn--primary, a checked
   .checkbox-tristate, and .date-mode-btn.is-active all already use
   (Sections 20, 26, 27) — reused here for the exact same reason: a
   blanket "every agency in this group" selection is the comprehensive/
   primary state, the same role ink-fill already carries everywhere
   else on this card. Sizing matches Tag-confidence's own pill padding/
   radius/font (components/tag-confidence.css) for visual consistency
   between this card's two pill-shaped things, not a coincidence. */
.dp-blanket-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--color-ink);
  color: var(--color-surface);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  white-space: nowrap;
}

/* A real Tag-source instance (component/tag-source.css, untouched)
   wrapped in this card's own chip container — sunken background +
   hairline border + pill radius, the same "contained" treatment
   .card-filter__chip already uses, so a selected agency reads as a
   discrete, removable item rather than a plain inline label.
   max-width is a second deliberate literal (same "no spacing-scale
   rung fits" category as .dp-search-results above): the section
   body's own 8px-inset content width is 232px at the card's own
   248px floor (248px card − 2×8px, Section 25) — the card's width is
   fluid above that floor now (clamp(), see .card-filter), so 232px is
   only ever the narrowest this content width gets, not a fixed fact;
   168px leaves comfortable room for the shape + a meaningfully-
   truncated label + the remove button at that narrowest case while
   still letting a second, narrower pill share a row rather than
   forcing one pill per line — and stays exactly as comfortable, not
   cramped, at every wider width the fluid card can now reach. */
.dp-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  border: var(--border-width-hairline) solid var(--color-hairline);
  max-width: 168px;
}

/* Scoped to this card's own pills, not a change to the shared
   component (components/tag-source.css) itself — a long agency name
   (many real ones run 40-60+ characters) wrapped across two or three
   lines inside a single pill otherwise, which ate the "roughly 3
   lines" overflow budget on just one or two agencies. min-width:0
   overrides the flex-item default of auto, which is what was actually
   stopping the label from shrinking below its own content width in
   the first place — the ellipsis rule alone doesn't do anything
   without it. */
.dp-pill .tag-source {
  min-width: 0;
}

.dp-pill .tag-source-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Exception pill: same .dp-pill chip look, plus a leading minus glyph
   (the map's own vendored zoom-out icon, reused rather than re-
   vendored) as the one distinguishing signal — deliberately not
   stacking a second signal (e.g. a dashed border) on top of it, the
   same restraint Tag-confidence's own "missing" state uses (one
   distinguishing treatment, not several). The whole pill is a button
   — click anywhere on it to undo the exception — since there's no
   second action it needs beyond that. */
.dp-pill-exception {
  cursor: pointer;
  font: inherit;
  text-align: left;
}

.dp-pill-exception:hover {
  border-color: var(--color-ink-secondary);
}

.dp-pill-exception:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

.dp-pill-exception-icon {
  display: inline-flex;
  flex-shrink: 0;
  width: var(--space-3);
  height: var(--space-3);
  color: var(--color-ink-secondary);
}

.dp-pill-exception-icon svg {
  display: block;
  width: 100%;
  height: 100%;
}

.dp-view-all-btn {
  display: inline-block;
  margin-top: var(--space-1);
  padding: 0;
  border: none;
  background: none;
  color: var(--color-ink-secondary);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  text-decoration: underline;
  cursor: pointer;
}

.dp-view-all-btn:hover {
  color: var(--color-ink);
}

.dp-view-all-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

/* Author display:inline-block above otherwise beats the UA
   [hidden]{display:none} rule at equal specificity (the same bug
   Section 27's .date-range-panel hit and fixed) — without this the
   button stayed visibly rendered even while its `hidden` attribute
   was set and read true. */
.dp-view-all-btn[hidden] {
  display: none;
}

/* ---- Region — structural shell only (build stage B.1 of 5; see
   Reference doc). No blanket toggle next to the "Region" header,
   unlike Data Provenance's "All" — an empty Region section has an
   implicit meaning (no geographic restriction) rather than a
   togglable state, so there's nothing for a checkbox to represent
   here. No count caption either, for the same reason: a "N selected"
   caption presumes selection-state bookkeeping that doesn't exist yet
   at this stage. Hand-authored directly in search.html rather than
   JS-built — unlike Category tree/Data Provenance's data-driven
   markup, this section has no per-item repetition to guard against at
   this stage (one input, one empty container), the same "small, fixed,
   non-repetitive structure" call Section 27's Date section already
   made. A future stage's real logic will presumably warrant its own
   region.js file once there's anything to build data-driven, matching
   how data-provenance.js itself only appeared once Data Provenance had
   real logic to own — not before. */
.region-search-field {
  position: relative; /* the anchor B.1 already anticipated — B.5's own .region-search-results (below) is the dropdown it was put here for */
}

.region-search-input {
  padding: var(--space-2) var(--space-3);
}

/* ---- Search results dropdown (build stage B.5 of 5 — see Reference
   doc) ---- Close duplicate of .dp-search-field's own floating-results
   treatment (Section 29): same absolute-positioned, max-height-200px,
   card-shadow dropdown, same known/accepted clipping limitation at the
   bottom of .card-filter__scroll, same "duplicate a 2nd consumer, extract
   at a 3rd" call B.2/B.3 already made for .region-pill against .dp-pill.
   .region-search-result is a real <button>, not a <label> wrapping a
   checkbox the way .dp-search-result is — Data Provenance's own search
   results toggle a persistent per-tier selection state in place, but
   this section's search is a "type, pick one, done" add flow (the task's
   own explicit requirement: the input clears after each pick), so a
   plain click target is the correct control here, not a checkbox with
   its own independent checked state to keep in sync. Each result
   composes the real Tag-source component (matching every pill's own
   shape+label treatment) rather than DP's plain text row — DP's results
   never needed a tier distinguisher because each of its three search
   fields is already scoped to one tier; this section's search field is
   one shared input across all three tiers at once, so the shape icon is
   what makes "Washington" (state) and "Washington" (a same-named county,
   were one to exist) visually distinguishable in the dropdown itself,
   not just after being added. */
.region-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: var(--space-1);
  max-height: 200px;
  overflow-y: auto;
  background: var(--color-surface);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  box-shadow: 0 var(--space-1) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
  z-index: 3;
}

.region-search-result {
  display: flex;
  align-items: center;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: none;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.region-search-result:hover {
  background: var(--color-surface-sunken);
}

.region-search-result:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: -2px;
}

/* Same min-width:0 + ellipsis fix every other Tag-source consumer on
   this card already needed (.dp-pill, .region-pill) — a long name
   otherwise wraps inside the dropdown row instead of truncating. */
.region-search-result .tag-source {
  min-width: 0;
}

.region-search-result .tag-source-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.region-search-empty,
.region-search-more {
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  color: var(--color-muted);
}

/* Build stage B.3 of 5 (see Reference doc): #region-pills now holds up
   to three .region-tier-group blocks (Country / State-Province /
   County), one per tier that currently has at least one fully-selected
   jurisdiction — drop-and-merge means a tier with nothing qualifying
   gets no group element at all (region.js never appends one), so this
   container is genuinely empty, not just visually empty, whenever
   nothing on the map qualifies for a pill anywhere. :empty's
   margin-top:0 (B.1) therefore still does real work unmodified — no new
   CSS needed for the empty case despite the structural change from a
   single flat pill row to stacked per-tier groups. No max-height/
   overflow cap on the container itself yet — deliberately: overflow/
   truncation is its own explicitly separate, not-yet-built stage. */
.region-pills {
  margin-top: var(--space-2);
}

.region-pills:empty {
  margin-top: 0;
}

/* Same vertical rhythm Data Provenance's own .dp-group + .dp-group
   already uses between its three agency-tier groups (Section 29) —
   reused directly since it's a generic "space between stacked card-
   filter groups" rule, not anything Data-Provenance-specific. */
.region-tier-group + .region-tier-group {
  margin-top: var(--space-4);
}

/* Reuses .category-row's own flex/gap/padding for the header row layout
   (Section 25) — that part carries no checkbox-specific meaning, just
   "a row inside this card." .region-tier-label is its own class rather
   than reusing .category-label: .category-label is a real <label>
   styled to imply "click me to toggle the checkbox this is for"
   (cursor: pointer included), and there is no checkbox here — Region
   has no blanket toggle at any level (Section 32) — so borrowing that
   class would apply a pointer cursor with no control behind it. */
.region-tier-label {
  flex: 1;
  min-width: 0;
  font-family: var(--font-ui);
  font-size: var(--ui-body-sm-size);
  color: var(--color-ink);
}

/* Same flex-wrap pill row .dp-pills established — no max-height cap
   here, unlike .dp-pills: build stage B.4 (see Reference doc) collapses
   an overflowing tier to a single summary pill (region.js's own
   countRows(), a real row-measurement, decides this — not a CSS clip),
   so there's nothing here that would ever need visually capping in the
   first place. A large, uncollapsed (or expanded) tier still relies on
   .card-filter__scroll's own scroll for whatever doesn't fit on screen,
   same as always. */
.region-tier-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Build stage B.4 of 5 (see Reference doc): once a tier's own
   individual pills would occupy more than 3 rows, region.js swaps this
   in for the whole .region-tier-pills row rather than just visually
   capping it — an ink-filled summary pill ("10 States/Provinces"),
   same treatment as .dp-blanket-pill's own "(All)" pill (Section 29) —
   reused as the visual precedent, not the class itself, since this one
   needs its own interactive affordances (cursor, focus-visible) a plain
   <span> blanket pill never needed. Real <button>, not a styled <span>,
   since clicking it is the whole point (expand to inspect/edit) rather
   than a passive summary. */
.region-tier-summary-pill {
  display: inline-flex;
  align-items: center;
  margin-top: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border: none;
  border-radius: var(--radius-pill);
  background: var(--color-ink);
  color: var(--color-surface);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  white-space: nowrap;
  cursor: pointer;
}

.region-tier-summary-pill:hover {
  background: var(--color-ink-secondary);
}

.region-tier-summary-pill:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

/* Only shown once a summary pill has already been expanded back to
   individual pills — same link-style toggle .dp-view-all-btn already
   established for "Show fewer" (Section 29), reused visually rather
   than reintroducing a second toggle-button treatment on this card. No
   "View all" equivalent needed here: the summary pill itself is that
   affordance (it's a real click target, not a passive label), so there
   is only ever one direction this particular button needs to offer. */
.region-tier-collapse-btn {
  display: inline-block;
  margin-top: var(--space-1);
  padding: 0;
  border: none;
  background: none;
  color: var(--color-ink-secondary);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  text-decoration: underline;
  cursor: pointer;
}

.region-tier-collapse-btn:hover {
  color: var(--color-ink);
}

.region-tier-collapse-btn:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

/* ---- Region pills (build stage B.2 of 5, unchanged in shape by B.3 —
   see Reference doc) ---- A real Tag-source instance
   (components/tag-source.css, untouched) wrapped in the same
   "contained chip" treatment .dp-pill already established for Data
   Provenance's own pills — sunken background, hairline border, pill
   radius. Duplicated here rather than shared with .dp-pill directly:
   Data Provenance and Region are deliberately separate concerns as of
   the Section 31 split, and this project's own convention (see
   NAMING.md, and card-datapoint.css's own duplicated tag-source
   primitive before its real extraction) is to duplicate a small shared
   shape across two consumers and only extract a common primitive once
   a third one shows up, not preemptively. No --shape-color is set on
   these pills' Tag-source instances at all — unlike Data Provenance's
   agency pills, which use color to distinguish individual data sources,
   a Region pill's shape alone (square/diamond/circle) already carries
   its one meaningful distinction (country/state/county), so color is
   left at tag-source.css's own neutral --color-ink-secondary fallback
   rather than claiming a color these pills have no real use for. */
.region-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-pill);
  background: var(--color-surface-sunken);
  border: var(--border-width-hairline) solid var(--color-hairline);
  max-width: 168px;
}

/* Same min-width:0 + ellipsis fix Data Provenance's own pills needed
   (Section 29) — a long county/state name otherwise wraps across
   multiple lines inside a single pill instead of truncating. */
.region-pill .tag-source {
  min-width: 0;
}

.region-pill .tag-source-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.card-filter__footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  border-top: var(--border-width-hairline) solid var(--color-hairline);
  background: var(--color-surface);
  flex-shrink: 0;
}

/* Conditional shadow (Reference doc) — no box-shadow by default; the
   .has-hidden-content class (search.js's own initFooterShadow(),
   toggled from real scrollHeight/clientHeight/scrollTop reads, not a
   CSS-only heuristic) is the entire visibility mechanism. Weight and
   color are the exact same value .card-filter__panel's own outer shadow
   already uses (var(--space-1) offset, var(--space-2) blur, the
   identical color-mix(...) formula) — not a new token, per the task's
   own instruction — with only the y-offset's sign flipped, since this
   shadow's job is the physical opposite of the panel's: the panel's own
   shadow shows the whole card floating above the map below it; this one
   shows content still hidden above/behind the footer, so it casts
   upward, not downward.

   No extra clipping ancestor was added — checked live in both layout
   modes rather than assumed, per the task's own "confirm this works
   correctly" framing, not just the "likely requires" hedge taken as a
   given. Desktop floating mode already has one for free:
   .card-filter__panel's own overflow:hidden (unrelated to this pass —
   Section 20) clips the footer's shadow to the panel's own rounded box
   exactly like it already clips everything else inside the card, since
   the footer is still an ordinary flex child of that panel there.
   Stacked mode's fixed footer already spans exactly the same width as
   the stacked card itself (both 100% of the viewport in that mode), so
   there is no adjacent map/page background for the shadow's blur to
   spill onto sideways, and it casts upward into the card's own visible
   content, never toward the viewport's outer edge — confirmed live, no
   visible bleed past the card in either mode. */
.card-filter__footer.has-hidden-content {
  box-shadow: 0 calc(-1 * var(--space-1)) var(--space-2) color-mix(in srgb, var(--color-faint) 35%, transparent);
}

.card-filter__btn {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-card);
  font-family: var(--font-ui);
  font-size: var(--ui-button-size);
  font-weight: var(--w-semibold);
  cursor: pointer;
  text-align: center;
}

/* Ink-filled, --color-surface text — the same inversion the masthead's
   own --color-ink bar uses, and for the same reason: --color-ink is
   this system's brand anchor (--color-brand aliases directly to it,
   per Section 8), so a primary CTA reuses that anchor rather than a
   separate accent color. */
.card-filter__btn--primary {
  border: var(--border-width-hairline) solid var(--color-ink);
  background: var(--color-ink);
  color: var(--color-surface);
}

.card-filter__btn--primary:disabled {
  cursor: not-allowed;
  border-color: var(--color-hairline);
  background: var(--color-surface-sunken);
  color: var(--color-faint);
}

.card-filter__btn--secondary {
  border: var(--border-width-hairline) solid var(--color-hairline);
  background: var(--color-surface);
  color: var(--color-ink-secondary);
}

/* Not disabled — genuinely inert instead, same treatment the icon
   stack's Layers/Draw buttons already established: a real, focusable,
   fully-styled control with no handler wired up yet, rather than a
   disabled one. "Clear filters" isn't gated on any state existing the
   way "View results" is (clicking it is always a valid, if currently
   no-op, action), so disabling it would overstate what's actually
   missing here. */
.card-filter__btn--secondary:hover {
  border-color: var(--color-ink-secondary);
  color: var(--color-ink);
}

/* ===================================================================
   STACKED/MOBILE REFLOW — see Reference doc for the full build entry.
   Below this breakpoint the filter card stops floating over the map
   and becomes a normal full-width block underneath it; above it,
   every rule in this file above this block is the whole layout,
   unchanged.

   Breakpoint — 592px, derived, not an eyeballed round number: the
   floating card's full footprint at its own floor width is
   2 × (248px card floor + 24px var(--space-6) edge offset) = 592px —
   the viewport width at which that footprint would consume half the
   screen. This supersedes an unrelated ~640px figure that appeared in
   an earlier, informal estimate elsewhere in project history; 592px is
   the one actually derived from this card's own real, shipped
   floor-width constant (`.card-filter`'s `clamp(248px, ...)`), not a
   guess, and is the only value this stylesheet treats as authoritative. */
@media (max-width: 592px) {
  /* Every selector below is scoped under .search-shell — search.html's
     own top-level wrapper, present nowhere else — not bare class names.
     This file's shared component classes (.card-filter*, .controls-map,
     #masthead-mount) are also used outside search.html itself: components.html
     loads this stylesheet for exactly those classes (its own component-
     showcase demos, components-page.css's file header) with no
     .search-shell ancestor around them. Confirmed live: unscoped, this
     block's .card-filter__footer rule alone was enough to pin that demo's
     own footer to the real viewport bottom as a persistent bar on top of
     components.html's entire page at this width — the fixed-position
     mobile toolbar meant only for the real search page. Scoping every
     rule here the same way closes that off for the whole block at once,
     not just the one rule that happened to be visibly broken. */
  /* .search-shell itself becomes the scroll container — not html/body,
     which stay exactly as this file's own header already committed to
     ("html/body are pinned to the viewport height with no overflow"),
     unchanged even here. This is the same proven pattern
     .card-filter__scroll already uses for desktop mode's internal
     section-list scroll (a flex item, explicit height + overflow-y:auto
     on the flex CONTAINER, not a document-level scroll) — confirmed
     live to matter, not a stylistic preference: giving html/body
     `overflow:auto` instead, and relying on .map-frame's own overflowing
     (but unclipped) content to bubble all the way up through the flex
     tree to them, measured a real `documentElement.scrollHeight` stuck
     at exactly the viewport height regardless of how much content
     actually rendered below the fold — a known flexbox-overflow
     interaction, not a typo. Scoping overflow-y:auto to .search-shell
     directly (the actual flex container whose item is overflowing)
     fixed it: confirmed live afterward that its own scrollHeight
     correctly reflects the full map+card content, not just the
     viewport. */
  .search-shell {
    overflow-y: auto;
  }

  /* The masthead's own position:sticky (masthead.css, unconditional,
     every page) turned out to be silently inert the instant real
     scrolling was introduced above — confirmed live: at any nonzero
     scrollTop the masthead moved exactly with the content instead of
     pinning, on both the real masthead and an isolated synthetic
     reproduction. Root cause: a sticky element's "stuck" range is
     bounded by its OWN PARENT's box, and #masthead-mount (the
     masthead's direct, JS-populated parent) shrink-wraps to exactly
     the masthead's own height with no extra room — so the parent's own
     box has already fully scrolled past before the child could ever
     visibly stick. Confirmed live, both ways: a sticky element nested
     one level inside a snug wrapper reproduced the exact same failure
     in isolation; moving position:sticky to the WRAPPER itself instead
     (here) — whose own parent, .search-shell, has real height to
     spare — fixed it immediately, in both the isolated test and the
     real masthead. .masthead's own sticky rule is left completely
     unedited (this is a stacked-mode-only, additive fix, not a change
     to the shared component all four other pages also mount) — it
     simply becomes redundant once its own parent is correctly pinned,
     not a conflict. */
  .search-shell #masthead-mount {
    position: sticky;
    top: 0;
    z-index: 10;
  }

  /* Map stops being "fill all remaining space after the masthead" and
     becomes a fixed fraction of the viewport instead — flex:none
     locks its own box to exactly 66vh regardless of what renders
     below it; overflow:visible (not the desktop rule's implicit
     clipping) is what lets .card-filter, still its DOM child, render
     below that 66vh box instead of being cut off at its edge. The
     66vh box itself is unaffected by that overflowing content — an
     explicit height always wins over a child's own content size — so
     .controls-map (still position:absolute, unchanged, within this
     same element) keeps anchoring to var(--space-6) from the bottom-
     right of just the map's own 66vh region, not the map+card total,
     satisfying the task's own "same 24px offset, still reads
     correctly at the map's new, shorter height" requirement for free,
     with no separate override needed for .controls-map itself. */
  .search-shell .map-frame {
    flex: none;
    height: 66vh;
    overflow: visible;
  }

  /* The card stops floating (position:absolute over the map) and
     becomes a normal, full-width block in flow instead — width:100%,
     not var(--card-filter-width); the fluid clamp() sizing is a
     floating-mode concern only and doesn't apply once the card is a
     stacked block. top/left/right/bottom are left as whatever the
     desktop rule set them to — position:static makes all four
     properties inert regardless of their computed values, so nothing
     needs explicitly clearing. */
  .search-shell .card-filter {
    position: static;
    width: 100%;
  }

  /* height:100% (desktop) was always relative to .card-filter's own
     height, which came from top+bottom positioning — meaningless once
     .card-filter is position:static with an auto (content-driven)
     height. auto here just lets the panel take exactly the height its
     own content (topbar + section list + footer clearance) needs. */
  /* overflow: clip, not the desktop rule's hidden — same visual corner-
     clipping (rounded corners still cut off any square-edged child
     content, unchanged), but `hidden` also establishes a scroll
     container per spec (it still permits programmatic scrollTo, even
     with nothing to scroll here since height:auto never overflows),
     which turned out to silently break .card-filter__topbar's sticky
     rule below: confirmed live — with `hidden` still in effect, the
     topbar's nearest ancestor scroll container was this panel, not the
     real scrolling ancestor further up (.search-shell), so its sticky
     offset was computed against a scrollport whose own scrollTop never
     moves, and it just tracked the page linearly forever instead of
     ever catching and holding at top:45. `clip` keeps the same visual
     clip without registering as a scroll container, so the sticky
     child correctly looks past this box to .search-shell instead. */
  .search-shell .card-filter__panel {
    height: auto;
    overflow: clip;
  }

  /* Sticky filter card (Phase 5 batch 1): once .search-shell's own real
     scroll (confirmed live — map 66vh + full-height card panel exceeds
     the viewport, unlike desktop mode's zero-scroll layout) carries the
     card past the map, .card-filter__topbar is the one piece of the
     card's own persistent chrome that wasn't already pinned — the
     footer got position:fixed for exactly this reason below, but the
     title/collapse-button bar was still plain in-flow content, scrolling
     out of view along with the filter-section list under it. Same
     proven mechanism as #masthead-mount above: the sticky child's own
     immediate parent (.card-filter__panel, height:auto ≈ full content
     height, no shrink-wrap) has real spare room to stick within, unlike
     the shrink-wrapped-parent failure mode documented above. top is
     offset by the sticky masthead's own rendered height (measured live,
     45px at this breakpoint — masthead.css defines no fixed height of
     its own to reference directly) so the two stacked sticky elements
     sit flush against each other instead of overlapping; z-index matches
     the masthead's and footer's own (10), since .card-filter__topbar
     needs to render over the section list scrolling underneath it and
     already has an opaque background (--color-surface-sunken) to do so
     cleanly. */
  .search-shell .card-filter__topbar {
    position: sticky;
    top: 45px;
    z-index: 10;
  }

  /* Collapse-all is suppressed entirely in stacked mode, not just its
     trigger button hidden — the feature exists specifically to reveal
     more map underneath a floating card (Section 20), and that benefit
     doesn't exist once the card is a full-width block already sitting
     below a height-constrained map rather than on top of it. Both
     rules below match the exact selector specificity of the desktop
     rules they override (.card-filter.card-filter--collapsed .card-filter__chip,
     .card-filter.card-filter--collapsed .card-filter__panel — three classes each)
     and win purely on source order, the same specificity-matching
     discipline this file already used for its date-range/view-all
     [hidden] overrides — so even a stale .card-filter--collapsed class left over
     from a wide-then-narrowed resize can never leave the chip showing
     instead of the real panel, or the panel hidden, while stacked.
     Individual section chevrons (.filter-section-summary/[open]) are
     untouched by any of this — they're a separate mechanism (native
     <details>/<summary>) this block never modifies, so they keep
     working exactly as in desktop mode. */
  .search-shell .card-filter__collapse-btn {
    display: none;
  }

  .search-shell .card-filter.card-filter--collapsed .card-filter__chip {
    display: none;
  }

  .search-shell .card-filter.card-filter--collapsed .card-filter__panel {
    display: flex;
  }

  /* Sticky footer reanchors from "the card's own internal scroll
     boundary" (desktop mode's plain-flexbox mechanism, Section 20) to
     the actual viewport bottom — a real persistent mobile action bar,
     not a scroll-container-relative one: it stays on screen through
     any amount of page scroll, the same way the masthead's own
     position:sticky keeps it pinned at the top through the same
     scroll. z-index (10, matching the masthead's own) beats every
     other z-index on this page (5) so the bar is never rendered under
     the map or card content. */
  .search-shell .card-filter__footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 10;
  }

  /* padding-bottom reserves clearance so the section list's own last
     real content doesn't render underneath the now-fixed footer sitting
     on top of it — a deliberate literal (no spacing-scale rung was
     built to describe "one specific floating footer's own rendered
     height"), measured live against the footer's actual height rather
     than guessed, with headroom rather than an exact match so it stays
     correct if the footer's own content ever wraps to a second line at
     a narrow width. .card-filter__scroll's own flex:1/overflow-y:auto
     (desktop mode, unchanged) naturally stops constraining height the
     moment its flex container (.card-filter__panel) no longer has a
     definite height of its own (see the height:auto rule above) — so
     in stacked mode this becomes a plain block that grows to its full
     content height and scrolls with the page, not a second, separate
     internally-scrolling region; "independently scrollable" (the
     task's own phrase) describes its relationship to the now-fixed
     footer, not a second nested scrollbar. */
  .search-shell .card-filter__scroll {
    padding-bottom: 140px;
  }
}

/* ---- Search modal (modal-search.js) — full-screen, only ever shown
   below the 592px breakpoint (its own isNarrow() check gates every
   open() call; nothing here needs its own media-query guard on top of
   that, since the module is simply never opened above it). Re-scopes
   the original "narrow viewport = full-screen modal, not dropdown"
   principle onto the four search inputs that actually exist today —
   Region's one field and Data Provenance's three — rather than a
   now-removed component. Generic shell only: row/empty/more styling
   (.region-search-result, .dp-search-result, .region-search-empty,
   .dp-search-empty, etc.) is entirely reused unchanged from each
   consumer's own existing rules above — those classes already resolve
   to a sensible full-width row regardless of which container they're
   appended into (.region-search-result's own explicit width:100%,
   .dp-search-result's display:flex block-level default), so nothing
   about them needed to change for this wider container. */
.modal-search {
  position: fixed;
  inset: 0;
  z-index: 30;
  display: flex;
  flex-direction: column;
  background: var(--color-surface);
}

.modal-search[hidden] {
  display: none;
}

.modal-search-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: var(--border-width-hairline) solid var(--color-hairline);
  flex-shrink: 0;
}

.modal-search-input {
  flex: 1;
  min-width: 0;
  padding: var(--space-2) var(--space-3);
}

/* A visible, explicit "Done" — the confirm/dismiss control a full-
   screen modal needs in place of the inline dropdown's own implicit
   dismissal (click anywhere outside it). Selecting a result is its own
   confirmation for Region's "pick one, done" flow (the row's own click
   handler closes the modal directly); this button is what lets a user
   leave Data Provenance's modal (a persistent checkbox list that never
   auto-closes) once they're done checking however many boxes they
   want, or back out of either modal with nothing selected. */
.modal-search-close {
  flex-shrink: 0;
  padding: var(--space-2) var(--space-3);
  border: var(--border-width-hairline) solid var(--color-hairline);
  border-radius: var(--radius-card);
  background: var(--color-surface);
  color: var(--color-ink);
  font-family: var(--font-ui);
  font-size: var(--ui-caption-size);
  font-weight: var(--w-semibold);
  cursor: pointer;
}

.modal-search-close:hover {
  border-color: var(--color-ink-secondary);
}

.modal-search-close:focus-visible {
  outline: var(--border-width-active) solid var(--color-ink-secondary);
  outline-offset: 2px;
}

.modal-search-body {
  flex: 1 1 auto;
  overflow-y: auto;
}
