/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *


















 */
/* Feature icons - both in list and as the main icon */
.feature-icon {
  font-size: 24px;
  color: #333;
}

/* Feature category icon inside the label input */
.feature-category-icon {
  position: absolute;
  right: 10px;
  top: 40%;
  transform: translateY(-50%);
  cursor: pointer;
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
}
/* Ensure the material icon inherits the positioning */
.feature-category-icon .material-icons {
  font-size: 24px;
  color: #333;
}

/* Category list dropdown */
.category-list {
  position: absolute;
  top: calc(100% + 5px);
  right: 0;
  background: white;
  border: 1px solid #ccc;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  z-index: 10;
  padding: 10px;
  width: 200px;
}

/* List of category items */
.category-list ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.category-list li {
  display: flex;
  align-items: center;
  padding: 8px;
  cursor: pointer;
  border-radius: 6px;
}

.category-list li:hover {
  background-color: #f0f0f0;
}

/* Category icons in the list */
.category-list .material-icons {
  width: 24px;
  height: 24px;
  margin-right: 8px;
  display: flex;
  align-items: center;
}

/* Delete icon styling */
.delete-feature .material-icons {
  color: #dc3545;
  font-size: 20px;
}
/* Zone Memberships Container */
#zone-memberships {
    margin-top: 20px;
  }
  
  /* Base Panel Styling */
  .zone-membership-panel {
    background-color: var(--brand-neutral-50);
    border-radius: 8px;
    padding: 12px;
  }
  
  /* Headers */
  .zone-membership-panel h3 {
    font-size: 14px;
    font-weight: 500;
    color: var(--brand-neutral-700);
    margin: 0;
  }
  
  .zone-membership-panel h4 {
    font-size: 12px;
    font-weight: 500;
    color: var(--brand-neutral-500);
    margin: 12px 0 4px 0;
  }
  
  /* Member/Zone List */
  .membership-list {
    margin-top: 8px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  
  /* Individual Member/Zone Item */
  .membership-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background-color: white;
    padding: 8px;
    border-radius: 4px;
    border: 1px solid var(--brand-neutral-200);
  }
  
  .membership-item span {
    font-size: 14px;
    color: var(--brand-neutral-700);
  }
  
  /* Remove Button */
  .remove-membership-button {
    background: none;
    border: none;
    padding: 4px;
    cursor: pointer;
    color: #dc2626;
  }
  
  .remove-membership-button:hover {
    color: #b91c1c;
  }
  
  .remove-membership-button svg {
    width: 16px;
    height: 16px;
  }
  
  /* Suggested Section */
  .suggested-section {
    margin-top: 12px;
  }
  
  /* Add Membership Form */
  .add-membership-form {
    display: inline-block;
  }
  
  /* Add Button */
  .add-membership-button {
    display: flex;
    align-items: center;
    gap: 4px;
    background: none;
    border: none;
    padding: 4px;
    cursor: pointer;
    color: #2563eb;
    font-size: 14px;
  }
  
  .add-membership-button:hover {
    color: #1d4ed8;
  }
  
  .add-membership-button svg {
    width: 16px;
    height: 16px;
  }
  
  /* Error messages: the #zone-membership-error rule was removed with the id it
     styled — nothing rendered that element, so validation failures were never
     shown. The slot now lives in map_features/_breadcrumb, keyed per feature and
     styled with Tailwind alongside the rest of that partial. */

  /* Suggestions List */
  .suggestions-list {
    margin-top: 4px;
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
.google-maps-deep-link-btn {
    position: absolute;
    left: -23px;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    cursor: pointer;
  }
  
  .google-maps-deep-link-btn:hover .tooltip {
    visibility: visible;
    opacity: 1;
  }
  
  .tooltip {
    visibility: hidden;
    opacity: 0;
    background-color: #555;
    color: #fff;
    text-align: center;
    border-radius: 5px;
    padding: 5px;
    position: absolute;
    z-index: 19;
    bottom: 150%; /* Adjust to position the tooltip above the button */
    left: 320%;
    transform: translateX(-50%);
    white-space: nowrap;
    transition: opacity 0.3s;
  }
  
#layer-switcher {
    position: absolute; /* Place it on top of the map */
    bottom: 35px; /* Positioned above the Mapbox attribution */
    left: 300px;
    background-color: rgba(255, 255, 255, 0.9); /* Slightly more transparent */
    padding: 3px;
    border-radius: 8px; /* Increased border-radius for a smoother look */
    box-shadow: 0 1px 10px rgba(0, 0, 0, 0.2); /* Softer shadow for better contrast */
    z-index: 10;
    display: flex;
    flex-direction: column;
    gap: 2px; /* Ensure a small gap between buttons */
    border: 1px solid #ddd; /* Subtle border */
  }
  
  #layer-switcher button {
    background-color: #fff;
    border: 1px solid #ccc; /* Define button border */
    padding: 5px 8px; /* Balanced padding for symmetry */
    cursor: pointer;
    text-align: center;
    width: 100%;
    font-size: 16px;
    border-radius: 4px; /* Slight rounding on buttons */
    box-shadow: none;
    transition: background-color 0.3s, border-color 0.3s;
  }
  
  #layer-switcher button:hover {
    background-color: #f0f0f0; /* Slight hover effect */
    border-color: #007bff; /* Change border on hover */
  }
  
  #layer-switcher button + button {
    margin-top: 4px; /* Space between buttons for improved symmetry */
  }
  
/* Comment input area and icons */
.comment-form {
    display: flex;
    flex-direction: column;
  }
  
  .comment-text-area {
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 8px;
    outline: none;
    resize: none;
    font-size: 14px;
    background-color: #fff;
    min-height: 50px;
    margin-bottom: 10px;
  }
  
  
  .mention-suggestions {
    position: relative;
    margin-top: 5px;
  }
  
  .comment-icons {
    display: flex;
    justify-content: flex-end;
    align-items: center;
  }
  
  .upload-icon {
    cursor: pointer;
    margin-right: 10px;
  }
  
  .file-selected-name {
    font-size: 14px;
    color: #333;
    margin-right: 10px;
  }
  
  .remove-file-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    font-size: 18px;
    color: red;
  }
  
  .submit-comment-btn {
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    width: 20px;
  }
  
  .icon {
    width: 20px;
    height: 20px;
  }
  
  .remove-icon {
    color: red;
    cursor: pointer;
  }
  
  .file-upload-input {
    display: none; /* Hidden file input */
  }
  
  .mention {
    background-color: #e0f7fa;
    color: #007c91;
    font-weight: bold;
    padding: 2px 5px;
    border-radius: 5px;
    margin-right: 5px;
  }
#feature-search-bar {
    padding: 10px;
    margin-top: 15px;
    border: 1px solid #ddd;
    border-radius: 8px;
    outline: none;
    resize: none;
    font-size: 14px;
    background-color: #fff;
    min-height: 20px;
    min-width: 240px;
    margin-bottom: 10px;
}
/* ======================================================================== */
/*  Shared location filter bar — multi-select category / status / person     */
/*  popovers. Used by location_filter_controller.js + the                    */
/*  shared/filters/_filter_popover partial. Ported from the metrics          */
/*  dashboard's .md-filter* styles so every location view reads as one       */
/*  family.                                                                  */
/* ======================================================================== */
.lf-filter { position: relative; }

.lf-btn {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 500; white-space: nowrap; cursor: pointer;
  border: 1px solid var(--brand-neutral-200); border-radius: 8px; padding: 5px 10px;
  background: #fff; color: var(--brand-primary-dark);
  transition: background-color 80ms ease, border-color 80ms ease;
}
.lf-btn:hover { background: var(--brand-primary-surface); border-color: rgb(var(--brand-primary-rgb) / 0.4); }
.lf-btn.is-active { background: var(--brand-primary-surface); border-color: var(--brand-primary); }

.lf-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
  font-size: 10px; font-weight: 700; line-height: 1; background: var(--brand-primary); color: #fff;
}

.lf-pop {
  position: absolute; top: calc(100% + 4px); left: 0; z-index: 60;
  width: 250px; padding: 6px;
  background: #fff; border: 1px solid var(--brand-neutral-200); border-radius: 10px;
  box-shadow: 0 12px 28px rgb(var(--brand-primary-dark-rgb) / 0.16);
  display: flex; flex-direction: column; max-height: 360px;
}
/* Keep `.hidden` working — `display: flex` above otherwise beats Tailwind's
   `.hidden { display: none }`. */
.lf-pop.hidden { display: none; }

/* Embedded variant — a section INSIDE an already-open combined menu (see
   `embedded` on the filter_popover partial), not its own floating popover:
   no absolute positioning, no shadow/border of its own, shorter option list
   so 2-3 stacked sections stay a reasonable overall height. */
.lf-pop-embedded { position: static; width: auto; padding: 0; box-shadow: none; border: 0; background: none; max-height: none; }
/* Each embedded section is its own `[data-location-filter]` wrapper (the
   `.lf-pop-embedded` div is nested one level inside it), so the divider
   belongs between THOSE siblings, not between .lf-pop-embedded elements
   directly. */
[data-location-filter] + [data-location-filter] > .lf-pop-embedded {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--brand-neutral-100);
}
.lf-pop-embedded .lf-opts { max-height: 160px; }
.lf-section-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 2px 4px;
}
.lf-section-label { font-size: 11px; font-weight: 700; color: var(--brand-neutral-400); text-transform: uppercase; letter-spacing: 0.03em; }
.lf-section-head button {
  font-size: 11px; font-weight: 500; color: var(--brand-primary); background: none; border: 0; padding: 0; cursor: pointer;
}
.lf-section-head button:hover { text-decoration: underline; }

.lf-pop-head { display: flex; gap: 6px; padding: 2px 2px 6px; border-bottom: 1px solid var(--brand-neutral-100); }
.lf-pop-head button {
  flex: 1; font-size: 11px; font-weight: 500; color: var(--brand-primary);
  background: var(--brand-primary-surface); border: 0; border-radius: 6px; padding: 5px; cursor: pointer;
  transition: background-color 80ms ease;
}
.lf-pop-head button:hover { background: #DCF5E6; }

/* Running "N locations match" total. Sits under the head buttons rather than in
   the footer, which the option search now occupies. */
.lf-pop-total {
  padding: 6px 2px 2px; font-size: 11px; font-weight: 500; color: #6B7280;
  font-variant-numeric: tabular-nums;
}

.lf-opts { overflow-y: auto; max-height: 244px; padding: 4px 0; }
.lf-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 6px; font-size: 12px; color: var(--brand-primary-dark); cursor: pointer;
}
.lf-opt:hover { background: #F7FDF8; }
.lf-opt input { accent-color: var(--brand-primary); width: 14px; height: 14px; flex: none; }
.lf-opt-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
.lf-opt-count { flex: none; min-width: 18px; text-align: right; font-size: 10px; color: var(--brand-neutral-400); }
/* Grey-out (never hide) options that would yield zero locations given the
   other active filters — house rule, matches the sales-dashboard legend. */
.lf-opt.is-unavailable { opacity: .45; filter: grayscale(1); pointer-events: none; }
/* Ruled out by the popover's own search box. Unlike is-unavailable (a count of
   zero, greyed so the user can still see it exists) a search miss is hidden
   outright — that is what the user asked for by typing. The checkbox keeps its
   state while hidden; selection is always read from :checked. */
.lf-opt.is-filtered-out { display: none; }

.lf-no-matches { padding: 10px 8px; font-size: 12px; color: #9CA3AF; text-align: center; }

.lf-pop-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px 8px 2px; margin-top: 2px; border-top: 1px solid var(--brand-neutral-100);
  font-size: 11px; font-weight: 500; color: var(--brand-neutral-500);
}
/* Option search — filters the rows above. Shown once a list reaches
   ApplicationHelper::FILTER_POPOVER_SEARCH_LIMIT options.

   Qualified with `input` on purpose. @tailwindcss/forms emits
   `[type=date],…,[type=search],…{border-radius:0;font-size:1rem;padding:.5rem .75rem}`
   — specificity (0,1,0), exactly the same as a bare `.lf-search`. tailwind.css
   is linked after application.css, so on a tie IT wins and silently reverts
   precisely these three properties. `input.lf-search` is (0,1,1) and settles it.
   Everything else here is safe: `.lf-opt input` and `.lf-apply` already outrank
   the plain-element reset. */
input.lf-search {
  flex: 1; min-width: 0;
  font-size: 11px; color: var(--brand-primary-dark);
  background: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 999px;
  padding: 4px 10px;
  transition: border-color 80ms ease, background-color 80ms ease;
}
input.lf-search::placeholder { color: #9CA3AF; }
input.lf-search:focus {
  outline: none; background: #fff; border-color: var(--brand-primary);
  box-shadow: 0 0 0 2px rgb(var(--brand-primary-rgb) / 0.15);
}

/* "Apply" — only shown in form mode (full-page pages like Farms & Fields). */
.lf-apply {
  flex: none; font-size: 11px; font-weight: 600; color: #fff;
  background: var(--brand-primary); border: 0; border-radius: 6px; padding: 4px 12px; cursor: pointer;
  transition: background-color 80ms ease;
}
.lf-apply:hover { background: var(--brand-primary-dark); }

/* "Done" — emit mode's visible way out. Emit mode applies each tick live, so
   there is nothing to submit; this only dismisses. Kept text-weight rather than
   a filled pill so it doesn't compete with the head's Select all / Clear all.
   margin-left:auto holds it right when it is the footer's only child. */
.lf-done {
  flex: none; margin-left: auto;
  font-family: inherit; font-size: 11px; font-weight: 600;
  color: var(--brand-primary); background: transparent; border: 0;
  border-radius: 6px; padding: 4px 8px; cursor: pointer;
  transition: background-color 80ms ease;
}
.lf-done:hover { background: var(--brand-primary-surface); }
.lf-done:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: -2px; }
/* Mapbox's own zoom and geolocate controls, restyled to match the map's other
   chrome (the floating pills and panels in maps/show.html.erb).

   The CONTROLS are Mapbox's, deliberately — GeolocateControl carries the
   permission prompt, the denial path, the accuracy circle and the follow-me
   state, none of which is worth rebuilding. Only the appearance is ours.

   Scoped to #map so no other Mapbox instance in the app is restyled by this. */

/* Stack the two groups with a gap; Mapbox butts them together by default. */
#map .mapboxgl-ctrl-bottom-right {
  /* Stacked ABOVE the "Ask Propel" FAB (peek.css: fixed, right 20px, bottom
     20px, ~38px tall) and clear of the notification stack below it.
     
     The FAB is the thing that does NOT move: it sits in the same place on every
     page in the app, and shifting it only here would cost that. These controls
     are the map's own, so they are the ones that give way. 84px clears the FAB
     and a single notification card; the right edge lines both of them up in one
     gutter rather than leaving two ragged columns. */
  bottom: 84px;
  right: 10px;
}

#map .mapboxgl-ctrl-bottom-right .mapboxgl-ctrl-group {
  margin: 0 0 8px 0;
  border-radius: 9999px;
  background: var(--brand-neutral-50);
  border: 1px solid var(--brand-neutral-200);
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

#map .mapboxgl-ctrl-group button {
  width: 34px;
  height: 34px;
  background-color: transparent;
}

#map .mapboxgl-ctrl-group button + button {
  /* Mapbox draws this in its own grey; match the panel hairline instead. */
  border-top: 1px solid var(--brand-neutral-200);
}

#map .mapboxgl-ctrl-group button:hover {
  background-color: var(--brand-primary-surface);
}

#map .mapboxgl-ctrl-group button:focus,
#map .mapboxgl-ctrl-group button:focus:not(:focus-visible) {
  box-shadow: none; /* Mapbox's default focus ring is a hard blue square */
}

#map .mapboxgl-ctrl-group button:focus-visible {
  outline: 2px solid var(--brand-primary);
  outline-offset: -2px;
}

/* The glyphs are background SVGs with a baked-in near-black. Tinting them to
   the palette needs a filter rather than `color`; this lands close to
   --brand-neutral-700 and keeps Mapbox's own icons rather than replacing
   the sprite. */
#map .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon {
  filter: opacity(0.72);
}

#map .mapboxgl-ctrl-group button:hover .mapboxgl-ctrl-icon {
  filter: opacity(1);
}

/* Following the user: tint the button so the state is visible at a glance
   rather than only in the dot on the map. */
#map .mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active,
#map .mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background {
  background-color: var(--brand-primary-surface);
}

#map .mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon,
#map .mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon {
  filter: none;
}

/* NOT hidden — Mapbox's terms require the logo and attribution to stay visible.
   The attribution is moved to bottom-left instead (compact, so it is an ⓘ that
   expands), which is done in mapbox_map_controller#connect by turning the
   default one off and adding it on the other side. Nothing here may reintroduce
   a display:none on .mapboxgl-ctrl-attrib or .mapboxgl-ctrl-logo. */
/* The bottom-centred guide bar, and how it keeps out of the way.
 *
 * The guide (data-mapbox-map-target="drawGuide") is the pill that explains the
 * gestures for whichever mode is active — right-click undoes, Enter finishes,
 * Ctrl adds to a selection. It is decoration: pointer-events-none, z-30, and
 * anchored bottom-centre at 1.5rem.
 *
 * The visualization TIMELINE (#viz-timeline) is anchored to the same corner at
 * 0.75rem and is a real control — two drag handles for the start and end date.
 * Those handles sit 20px off the bottom and are 16px tall; the guide's pill
 * spans roughly 24px to 56px. So they overlap, and because the guide is z-30
 * against the timeline's z-20, the pill paints straight over both handles and
 * the histogram behind them.
 *
 * pointer-events-none means the clicks still reach the handles, which is why
 * this reads as a rendering bug rather than a dead control — you simply cannot
 * see what you are grabbing, or where the range currently sits. That is worse
 * than a blocked control, not better: the handle appears to be missing.
 *
 * Lifting the guide is the right half to move. The timeline is a scrubber and
 * belongs low, near the edge it measures along; the guide is a hint and does
 * not care where it sits. 6rem clears the timeline card (which is ~71px tall
 * over a 12px offset, so it ends around 83px) with room to spare.
 *
 * Keyed off `body` rather than the map wrapper on purpose: the two elements sit
 * at different depths of maps/show, so any closer common ancestor would be a
 * selector that quietly stops matching the first time that markup is nested
 * differently. `body` cannot stop being an ancestor of both.
 */
body:has(#viz-timeline:not(.hidden)) [data-mapbox-map-target="drawGuide"] {
  bottom: 6rem;
}

/* Worth keeping in mind if another bottom-anchored overlay is added: the map's
 * bottom edge is already crowded. Toasts sit at 5rem (content_for
 * :flash_position in maps/show), the zoom and locate controls at 84px on the
 * right, and the Ask Propel FAB below those. Centre-anchored additions need to
 * declare how they stack with this one. */
/* Label for the "Go to a place" search result.
 *
 * Not a bubble: Mapbox's popup chrome is stripped to nothing and the label sits
 * directly on the map beside the red marker, the way map labels normally read.
 * The white halo is what keeps red text legible over satellite imagery, which
 * is why it is a halo and not a background card.
 *
 * Applied via `new mapboxgl.Popup({ className: 'place-bubble', anchor: 'left' })`
 * in mapbox_map_controller#viewLocationFromGoogleMaps. anchor:'left' puts the
 * popup's left edge at the marker, so the label lands to its RIGHT.
 *
 * Scoped to .place-bubble — every other popup on the map keeps Mapbox defaults.
 */

/* The dot in the middle of the marker. Mapbox's default pin SVG puts a white
 * circle there; dark red reads as part of the pin instead of a hole punched
 * through it. Matched on the fill value rather than position, so if Mapbox
 * restructures its marker SVG this rule simply stops applying instead of
 * recolouring the wrong shape.
 *
 * Applied via `new mapboxgl.Marker({ className: 'place-marker' })` by BOTH the
 * place search and the long-press dropped pin — they mark the same kind of
 * thing (a spot you are considering, not one you have saved), so they should
 * not look like two different ideas. */
.place-marker svg circle[fill="#FFFFFF"],
.place-marker svg circle[fill="#ffffff"],
.place-marker svg circle[fill="white"] {
  fill: #8B1E2F; /* dark red */
}

.place-bubble .mapboxgl-popup-content {
  padding: 0;
  background: transparent;
  box-shadow: none;
  border-radius: 0;
  /* Nothing here clips (Mapbox sets no overflow on its popup elements) — this
     is belt-and-braces for a popup stripped of its chrome, where the content is
     bare text with an outline rather than a card. */
  overflow: visible;
}

/* No card, so no tip pointing at one, and no × on a bare label. */
.place-bubble .mapboxgl-popup-tip,
.place-bubble .mapboxgl-popup-close-button {
  display: none;
}

.place-bubble__inner {
  display: flex;
  flex-direction: column;
  /* flex-end, not center: the button tucks under the RIGHT end of the name.
     The column is only as wide as the name, so this reads as right-aligned
     against the label rather than floating in the middle of it. */
  align-items: flex-end;
  gap: 0.3rem;
  overflow: visible;
  /* 2px of headroom so the topmost stroke has somewhere to live even if an
     ancestor does clip. Costs nothing visually — the label has no background. */
  padding-top: 2px;
}

.place-bubble__name {
  /* inline-block so the box hugs the text and the padding applies. The padding
     is breathing room around the outline, not a fix for the sheared tops — see
     the text-shadow note below for what that actually was. */
  display: inline-block;
  padding: 4px 5px;
  font-size: 1rem;
  font-weight: 700;
  /* 1.25 was tight enough that a stroked cap-height letter reached the top of
     its line box. 1.4 gives the outline room, and matters more now that long
     names wrap onto a second line. */
  line-height: 1.4;
  color: var(--brand-danger); /* matches the marker, which reads the same token */

  /* Wraps instead of truncating: a place name is the whole point of the label,
     so cutting it off with an ellipsis loses the information. max-width caps how
     far it can run before wrapping; break-word handles a single long token. */
  max-width: 14rem;
  white-space: normal;
  overflow-wrap: break-word;

  /* A real 1px white border, not a halo — eight zero-blur shadows, one per
     direction, so the edge is hard rather than feathered.
     
     This was -webkit-text-stroke, which kept getting its TOP shaved off. That
     was never an overflow clip: Mapbox's stylesheet has no overflow:hidden on
     any popup element (only .mapboxgl-map, which clips at the map's edges), and
     this box already carries ~7px of room above the ascenders from the padding
     and half-leading below. The clip lands exactly on the ascender line because
     Chromium computes a text fragment's ink-overflow rect without adding the
     stroke width — so the paint is cut at the glyphs' own ink bounds no matter
     how much padding the element has, which is why adding room never helped.
     text-shadow IS accounted for in that rect, so it does not get cut. */
  text-shadow:
     1px  0    0 #ffffff, -1px  0    0 #ffffff,
     0    1px  0 #ffffff,  0   -1px  0 #ffffff,
     1px  1px  0 #ffffff, -1px  1px  0 #ffffff,
     1px -1px  0 #ffffff, -1px -1px  0 #ffffff;
}

/* Still a real control, so it keeps a solid fill and a shadow to lift it off
 * the imagery — a haloed text link would be too weak to read as a button. */
/* Colours read --brand-* directly. shared/_brand_tokens emits those on :root
   for every brand-driven layout, so they resolve wherever this popup is
   rendered — no scope to opt into, and they follow BRAND like the rest. */
.place-bubble__add {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.55rem;
  border: 0;
  border-radius: 9999px;
  background: var(--brand-primary);
  color: #ffffff;
  font-size: 0.6875rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: 0 1px 4px rgb(var(--brand-primary-dark-rgb) / 0.35);
  transition: background-color 0.15s ease;
}

.place-bubble__add:hover {
  background: var(--brand-primary-dark);
}

.place-bubble__add:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}
/* Collapse the map's right-hand details panel while nothing is selected.
 *
 * The panel used to sit there permanently showing "Select a Point or Polygon",
 * covering the top-right corner of the map for the entire session. It should
 * only appear once there is something to show.
 *
 * Done in CSS rather than JS because the panel's contents are driven by Turbo:
 * selecting a feature replaces the frame's HTML, so the marker class appearing
 * and disappearing is already the exact signal we need. No controller has to
 * know about the panel's visibility.
 *
 * The rule reads: hide the panel when it contains the "nothing selected"
 * placeholder AND the bulk-actions panel is not currently shown. That second
 * clause matters — during multi-select the details frame still holds the
 * placeholder, and without it the bulk-actions UI would be hidden too.
 *
 * :has() is Chrome 105+ / Safari 15.4+ / Firefox 121+. Where it is unsupported
 * the rule simply does not apply and the panel behaves as it always did, which
 * is the right way for this to fail.
 */
.js-details-panel:has(.details-placeholder):not(:has(#bulk-actions-panel:not(.hidden))) {
  display: none;
}

/* The floating tool lane, sized around the details panel.
 *
 * The pills are absolutely positioned with only a left offset, so their width
 * is shrink-to-fit against the map column — they used to wrap only once they
 * reached the map's edge, which is BEHIND the details panel. The result was a
 * stack of pills sitting under a panel they had already slid beneath, with the
 * panel itself unclickable.
 *
 * Bounding the lane fixes that, but a STATIC bound is wrong: with nothing
 * selected there is no panel on screen, and reserving its width made the pills
 * wrap for no reason at all.
 *
 * So: reserve by default, and reclaim the full width while the panel is
 * collapsed. Reserving is the default deliberately — where :has() is
 * unsupported neither reclaim applies and the lane stays bounded, so the pills
 * wrap early but nothing is ever unreachable. That is the right way to fail.
 */
.js-map-tool-lane { right: calc(1rem + 240px); }

@media (min-width: 640px)  { .js-map-tool-lane { right: calc(1rem + 280px); } }
@media (min-width: 768px)  { .js-map-tool-lane { right: calc(1rem + 300px); } }
@media (min-width: 1024px) { .js-map-tool-lane { right: calc(1rem + 330px); } }

/* Reclaim when the details panel is collapsed.
 *
 * It expresses the SAME condition as the display:none rule above, but it cannot
 * be spelled the same way: :has() may not be nested inside :has(), and hanging
 * this off body would have done exactly that. A descendant combinator inside
 * the :has() says the same thing legally. Written the nested way first, the
 * whole selector was invalid and silently dropped — the lane reserved the
 * panel's width forever, including when no panel was on screen.
 *
 * No media queries needed: this selector carries an id (inside :has/:not), so
 * it outspecifies every plain .js-map-tool-lane rule above at every width.
 * Specificity, not source order, is doing the work. */
body:has(.js-details-panel .details-placeholder):not(:has(#bulk-actions-panel:not(.hidden))) .js-map-tool-lane {
  right: 0.5rem;
}

/* Icon-only tool pills when the lane is tight.
 *
 * A CONTAINER query, not a media query, because the deciding width is the
 * lane's, not the window's: opening the filters panel takes 270px off the left
 * and the details panel takes up to 346px off the right, so the same window can
 * be roomy or cramped depending on what else is open. A viewport breakpoint
 * cannot see any of that and would drop the labels on a wide screen with
 * nothing else on it.
 *
 * Fallback where container queries are unsupported: no rule applies, the labels
 * stay, and the row wraps inside its bounded lane — which is the behaviour
 * without this block, and still never hidden behind the panel.
 */
.js-map-tool-lane { container-type: inline-size; }

/* The toolbar gives ground in three steps, cheapest loss first:
 *
 *   1. beside the search  — labels, one Add pill per level
 *   2. BELOW the search   — the Add pills merge into a single "Add"
 *   3. still too narrow   — labels drop, leaving icons
 *
 * Step 2 is tied to being below the search, NOT to a width of its own. Moving
 * down already means the strip beside the search ran out, and the row that
 * lands underneath reads better as four pills than five — so the two changes
 * belong to one moment. The move-below rules further down carry it.
 *
 * Step 3 is a container query, because by then the only question left is how
 * wide the lane itself is.
 *
 * Measured: ~483px for the full row, ~327px once the Add pills merge into a
 * labelled "Add". Level labels are team data ("Add Homestead"), so there is a
 * little slack in both.
 */
.js-add-compact { display: none; }

/* Step 3 — icons only. */
@container (max-width: 340px) {
  .js-map-tool-row .js-tool-label { display: none; }

  /* Direct children only: the shape menus hanging off the Add buttons contain
   * their own buttons, and squashing those would be nonsense. */
  .js-map-tool-row > button,
  .js-map-tool-row > div > button {
    padding-left: 0.5rem;
    padding-right: 0.5rem;
    gap: 0;
  }
}

/* The search pill must never run under the details panel.
 *
 * `right` cannot do it here: an absolutely positioned box cannot honour left,
 * width AND right at once — the width wins and the right offset is dropped. So
 * max-width is what actually bounds it. 100% is the map column; subtract the
 * left offset, whatever the rail panel is currently covering, and the space the
 * details panel occupies.
 */
.js-map-search { max-width: calc(100% - 1rem - var(--map-panel-inset, 0px) - 1rem - 240px); }

@media (min-width: 640px)  { .js-map-search { max-width: calc(100% - 1rem - var(--map-panel-inset, 0px) - 1rem - 280px); } }
@media (min-width: 768px)  { .js-map-search { max-width: calc(100% - 1rem - var(--map-panel-inset, 0px) - 1rem - 300px); } }
@media (min-width: 1024px) { .js-map-search { max-width: calc(100% - 1rem - var(--map-panel-inset, 0px) - 1rem - 330px); } }

/* With no details panel on screen there is nothing to avoid, so the field keeps
 * ONE width at every window size — the roomy 320px — instead of stepping down
 * to Tailwind's 260px below `sm`. That step exists to make room for things
 * beside it; with nothing beside it, it only made the box smaller for free.
 *
 * The max-width is a floor-guard, not a cap: it engages only where 320px would
 * not physically fit in the map column at all (phone widths, which have their
 * own view anyway). Above that it never bites.
 *
 * Same selector as the rules above it, for the same reason: one definition of
 * "the panel is here", not two that can drift. */
body:has(.js-details-panel .details-placeholder):not(:has(#bulk-actions-panel:not(.hidden))) .js-map-search {
  width: 320px;
  max-width: calc(100% - 1.5rem - var(--map-panel-inset, 0px));
}

/* Dropping BELOW the search bar is the FIRST thing that gives, before any
 * content is lost.
 *
 * Moving down wins back 336px — the toolbar starts at left-4 instead of
 * left-22rem — so it buys far more room than merging the Add pills or dropping
 * the labels ever could. Doing it first means the icons-only state is reached
 * only when the row is genuinely out of room across the WHOLE map width, not
 * merely out of room in the strip beside the search.
 *
 * A media query, not a container query: the lane's own width is set by the very
 * left offset these rules change, so querying it would feed its own answer back
 * in and oscillate. The viewport does not move when the toolbar does.
 *
 * The thresholds are "viewport at which the strip beside the search drops below
 * ~520px", which is the full row plus slack. Each thing already eating the
 * column moves that point:
 *
 *   rail 48 + left offset 352 = 400 always spoken for
 *   filters panel  +270
 *   details panel  +346 (its widest)
 *
 * so 920 bare, ~1190 with filters, ~1270 with details, ~1540 with both.
 *
 * `body` on the first rule is only there to outspecify Tailwind's sm:top-4 and
 * sm:left-[22rem] (both 0,1,0) without reaching for !important.
 */
@media (max-width: 920px) {
  body .js-map-tool-lane { top: 4.5rem; left: 1rem; }
  body .js-map-tool-row > .js-add-level { display: none; }
  body .js-add-compact { display: block; }
}

/* Filters panel open. map_rail_controller publishes the flag, because CSS
 * cannot read a custom property's VALUE in a media query. */
@media (max-width: 1190px) {
  [data-map-panel-open="true"] .js-map-tool-lane { top: 4.5rem; left: 1rem; }
  [data-map-panel-open="true"] .js-map-tool-row > .js-add-level { display: none; }
  [data-map-panel-open="true"] .js-add-compact { display: block; }
}

/* Details panel on screen — the inverse of the reclaim condition above: no
 * placeholder means a feature is selected, and a visible bulk panel counts too. */
@media (max-width: 1270px) {
  body:not(:has(.js-details-panel .details-placeholder)) .js-map-tool-lane,
  body:has(#bulk-actions-panel:not(.hidden)) .js-map-tool-lane {
    top: 4.5rem;
    left: 1rem;
  }
  body:not(:has(.js-details-panel .details-placeholder)) .js-map-tool-row > .js-add-level,
  body:has(#bulk-actions-panel:not(.hidden)) .js-map-tool-row > .js-add-level {
    display: none;
  }
  body:not(:has(.js-details-panel .details-placeholder)) .js-add-compact,
  body:has(#bulk-actions-panel:not(.hidden)) .js-add-compact {
    display: block;
  }
}

/* Both at once. */
@media (max-width: 1540px) {
  body:not(:has(.js-details-panel .details-placeholder)) [data-map-panel-open="true"] .js-map-tool-lane,
  body:has(#bulk-actions-panel:not(.hidden)) [data-map-panel-open="true"] .js-map-tool-lane {
    top: 4.5rem;
    left: 1rem;
  }
  body:not(:has(.js-details-panel .details-placeholder)) [data-map-panel-open="true"] .js-map-tool-row > .js-add-level,
  body:has(#bulk-actions-panel:not(.hidden)) [data-map-panel-open="true"] .js-map-tool-row > .js-add-level {
    display: none;
  }
  body:not(:has(.js-details-panel .details-placeholder)) [data-map-panel-open="true"] .js-add-compact,
  body:has(#bulk-actions-panel:not(.hidden)) [data-map-panel-open="true"] .js-add-compact {
    display: block;
  }
}

/* The search field drops its arrow when there is no room for it.
 *
 * The icon sits in 40px of right padding. It earns that space for as long as
 * any wording fits beside it — even a bare "Go" reads better with the arrow
 * than without. Only at the very floor, where the box is two icons wide and
 * there is no room for both, does it go.
 *
 * A container query on the field itself. It cannot loop: hiding the icon does
 * not change the field's width, which is set by the rules above.
 */
.js-map-search { container-type: inline-size; }

@container (max-width: 56px) {
  .js-search-icon { display: none; }
  /* The padding existed only to clear the icon. */
  .js-search-input { padding-right: 1rem; }
}


/* Chrome out of the way while a panel has the column.
 *
 * map_rail_controller decides this, because only a measurement can answer "is
 * there anywhere left to stand" — the filters panel, the details panel and the
 * window all move independently.
 *
 * Opacity rather than display, so it can be TIMED. Leaving it merely unshifted
 * was no good — the panel is 270px and the search 320px, so its right edge
 * poked out past the panel — but `display: none` made it pop back the instant
 * the panel began to close, which reads as the bar arriving rather than as
 * having been behind the panel all along.
 *
 * So: fade out quickly on the way in, and on the way out wait for the panel to
 * clear before fading up. The delay is most of the panel's own 300ms slide.
 * pointer-events stops the invisible copy catching clicks meant for the panel
 * sitting over it.
 */
body .js-map-search,
body .js-map-tool-lane {
  /* `body` raises specificity above Tailwind's transition-[margin-left] on these
   * same elements. At equal weight the winner comes down to stylesheet order,
   * and losing meant the opacity here was never transitioned at all — the bar
   * blinked out and THEN the slide ran, which reads as it moving late.
   *
   * margin-left is restated, not inherited: this shorthand replaces the whole
   * transition property, so leaving it out would make the shift jump.
   *
   * max-width / width / right ride the SAME 300ms as the movement. They are
   * computed from --map-panel-inset, and custom properties do not transition —
   * so the box used to snap to its new size the instant the panel toggled and
   * only then slide, which is the "expand, then move back" that reads wrong.
   * On one clock it grows as it travels. */
  transition: opacity 180ms ease-out 160ms,
              margin-left 300ms ease-out,
              max-width  300ms ease-out,
              width      300ms ease-out,
              right      300ms ease-out;
}

[data-map-chrome-hidden="true"] .js-map-search,
[data-map-chrome-hidden="true"] .js-map-tool-lane {
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease-out 0ms,
              margin-left 300ms ease-out,
              max-width  300ms ease-out,
              width      300ms ease-out,
              right      300ms ease-out;
}
/* ── Feature chip vocabulary (shared) ───────────────────────────────────────
   The dashboard family's chip language for a map feature — status, category,
   responsible, level, counts, last-visit — plus the editable (`.fchip`) variant
   used where those values can be reassigned inline (the maps/show quick-view
   panel today; the locations-list / metrics rows next).

   The read-only `.ll-*` chips are lifted verbatim out of the locations-list
   table's scoped <style> so the exact same pixels render everywhere. Keep this
   file the single source — do not re-declare these in a view <style> block.
   Table LAYOUT (.ll-table / .ll-row / .ll-sticky / .ll-peek / .ll-actions)
   stays with the locations table; it's context-specific, not chip vocabulary. */

/* ── Read-only display chips ─────────────────────────────────────────────── */
.ll-muted { color:#C4CBD4; font-size:13px; }

.ll-badge { flex:none; font-size:10px; font-weight:600; line-height:1; padding:3px 6px;
  border-radius:5px; background:#ECFDF3; color:#15803D; border:1px solid #D1FADF; }
.ll-badge.is-top { background:#EEF2FF; color:#4338CA; border-color:#E0E7FF; }

/* Counts and measurements (feature area, child counts) — tabular numerals so a
   column of these lines up. Declared here rather than leaning on `num-tabular`,
   which is only defined in the design_review layout and is a no-op in the app. */
.ll-count { flex:none; font-size:11px; color:var(--brand-neutral-500); background:var(--brand-neutral-100);
  border-radius:999px; padding:1px 8px; white-space:nowrap;
  font-variant-numeric:tabular-nums; }

.ll-pill { flex:none; font-size:10px; font-weight:500; padding:2px 6px; border-radius:5px; line-height:1.3; }
.ll-pill-warn { background:#FFFBEB; color:#B45309; border:1px solid #FDE68A; }
.ll-pill-dist { background:#EFF6FF; color:#1D4ED8; border:1px solid #DBEAFE; }

.ll-status { display:inline-flex; align-items:center; max-width:100%; font-size:11px;
  font-weight:600; padding:2px 9px; border-radius:999px; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.ll-status .ll-dot { width:6px; height:6px; border-radius:50%; margin-right:6px; flex:none; }

.ll-cat { display:inline-flex; align-items:center; gap:7px; min-width:0; max-width:100%; }
.ll-cat-ic { font-size:15px; line-height:1; flex:none; }
.ll-cat-name { font-size:13px; color:var(--brand-neutral-700); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

.ll-avatar { display:inline-flex; align-items:center; justify-content:center; width:26px;
  height:26px; border-radius:999px; background:var(--brand-primary); color:#fff; font-size:11px; font-weight:600; flex:none; }
.ll-avatar.is-creator { background:var(--brand-neutral-400); }

.ll-visit { display:inline-flex; align-items:center; gap:7px; font-size:12px; color:var(--brand-neutral-500); white-space:nowrap; }
.ll-visit .ll-dot { width:7px; height:7px; border-radius:50%; background:var(--brand-neutral-300); flex:none; }
.ll-visit.is-fresh { color:var(--brand-primary-dark); }
.ll-visit.is-fresh .ll-dot { background:var(--brand-primary); }

/* Kebab + menu — the standard secondary-actions affordance. */
.ll-kebab { padding:5px; border-radius:6px; color:var(--brand-neutral-400); background:none; border:0;
  cursor:pointer; display:inline-flex; transition:background-color 80ms ease, color 80ms ease; }
.ll-kebab svg { width:16px; height:16px; }
.ll-kebab:hover { color:var(--brand-primary-dark); background:var(--brand-primary-surface); }
.ll-menu-item { display:flex; align-items:center; gap:9px; width:100%; text-align:left;
  padding:7px 12px; font-size:13px; color:var(--brand-primary-dark); background:none; border:0; cursor:pointer; text-decoration:none; }
.ll-menu-item:hover { background:var(--brand-primary-surface); }
.ll-menu-item.is-danger { color:var(--brand-danger); }
.ll-menu-item.is-danger:hover { background:#FCE4E8; }
.ll-menu-ic { width:15px; height:15px; flex:none; color:var(--brand-primary); }
.ll-menu-item.is-danger .ll-menu-ic { color:var(--brand-danger); }

/* ── Editable chip (`.fchip`) ────────────────────────────────────────────────
   Reads like the display chips above but is a button that opens a reassign
   dropdown. The white surface + stone border + chevron are the "you can change
   this" affordance; the inner dot / icon / avatar mirror the read-only chips so
   the row still looks native to the dashboard family. */
.fchips { display:flex; flex-wrap:wrap; align-items:center; gap:8px; }
/* Fixed height (not padding) so every chip matches regardless of whether its
   content is an emoji icon, a colour dot, an avatar, or plain text — an emoji
   category icon is intrinsically taller than a status dot, which is exactly why
   a padding-sized chip row ends up uneven. */
.fchip { display:inline-flex; align-items:center; gap:6px; max-width:100%;
  height:32px; box-sizing:border-box; padding:0 10px; font-size:12px; font-weight:600;
  line-height:1; border-radius:999px; background:#fff; border:1px solid var(--brand-neutral-200);
  color:var(--brand-primary-dark); cursor:pointer; overflow:hidden;
  transition:background-color 80ms ease, border-color 80ms ease; }
.fchip:hover { background:var(--brand-primary-surface); border-color:rgb(var(--brand-primary-rgb) / .45); }
.fchip:focus-visible { outline:2px solid var(--brand-primary); outline-offset:1px; }
.fchip--muted { color:var(--brand-neutral-500); font-weight:500; }
.fchip-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fchip-chev { width:12px; height:12px; opacity:.5; flex:none; }
.fchip-ic { font-size:14px; line-height:1; flex:none; }
.fchip-dot { width:8px; height:8px; border-radius:50%; flex:none; }
.fchip-av { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px;
  border-radius:999px; background:var(--brand-primary); color:#fff; font-size:9px; font-weight:700; flex:none; }
.fchip-av.is-creator { background:var(--brand-neutral-400); }
/* ── Turbo Frame loading feedback ────────────────────────────────────────────
   Turbo sets the `busy` attribute on a <turbo-frame> while it's fetching. We use
   it to show an immediate spinner so a click never feels dead while the server
   renders — no JS, no per-link wiring. Covers the two frames that felt slow:
   the global `modal` frame (share/delete/merge/… modals) and the maps/show
   feature-details panel (loaded via turboFrame.src on feature click). */
@keyframes tf-spin { to { transform: rotate(360deg); } }

/* Global modal frame — a dimmed backdrop + centered spinner the instant a
   modal-targeting link is clicked, before the modal HTML arrives. position:fixed
   so it shows even though the empty frame has zero size. */
turbo-frame#modal[busy]::after {
  content: ""; position: fixed; inset: 0; z-index: 60;
  background: rgba(15, 42, 36, 0.25); backdrop-filter: blur(1px);
}
turbo-frame#modal[busy]::before {
  content: ""; position: fixed; top: 50%; left: 50%; z-index: 61;
  width: 34px; height: 34px; margin: -17px 0 0 -17px; border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, 0.5); border-top-color: #fff;
  animation: tf-spin 0.7s linear infinite;
}

/* maps/show feature-details panel — dim the panel + spinner near the top while
   the clicked feature's details load (old content stays until the swap). */
turbo-frame#feature-details-frame { display: block; position: relative; }
/* …but never out-specify Tailwind's `.hidden`. mapbox_map_controller toggles
   `hidden` on this frame to swap the panel between the feature details and the
   multi-select (bulk actions) panel — without this rule the element+id selector
   above (specificity 0,1,1) beats `.hidden` (0,1,0) and both panels render
   stacked in the same card. */
turbo-frame#feature-details-frame.hidden { display: none; }
turbo-frame#feature-details-frame[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#feature-details-frame[busy]::before {
  content: ""; position: absolute; top: 40px; left: 50%; z-index: 21;
  width: 26px; height: 26px; margin-left: -13px; border-radius: 50%;
  border: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Metrics-dashboard table — EVERY interaction (sort, filter, window, roll-up,
   group-by, column toggle, load-more) re-navigates this frame, and the server
   takes ~300–900ms on a large team. Without feedback the stale table just sits
   there and the click reads as dead, so:
     ::after  — veil the stale table so it visibly recedes (z-index clears the
                sticky headers at 32 and the column ⋮ popover at 40)
     ::before — a fixed indeterminate bar pinned to the top of the VIEWPORT.
                Deliberately not centered-in-frame like the panel spinner above:
                the table is far taller than the viewport and has its own inner
                scroller, so an absolutely-positioned spinner is usually parked
                off-screen. A fixed bar is visible wherever you're scrolled. */
@keyframes tf-bar {
  0%   { transform: translateX(-100%) scaleX(0.35); }
  50%  { transform: translateX(0%)    scaleX(0.55); }
  100% { transform: translateX(100%)  scaleX(0.35); }
}
turbo-frame#metrics_table { display: block; position: relative; }
turbo-frame#metrics_table[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 50;
  background: rgba(255, 255, 255, 0.55);
}
turbo-frame#metrics_table[busy]::before {
  content: ""; position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  height: 3px; background: var(--brand-primary); transform-origin: 0 50%;
  animation: tf-bar 1.1s ease-in-out infinite;
}

/* ── Frames driven by a Stimulus controller assigning frame.src ─────────────
   These all had the same problem the three frames above used to have: the
   controller swaps `frame.src`, Turbo leaves the OLD content in place while it
   fetches, and the click reads as dead — long enough that people click again.
   Measured 130–180ms each on a heavy team, more on a slow connection.

   Shape is chosen by frame geometry, per the house rule:
     tall + internally scrolling → veil + FIXED top bar (a centred spinner
                                   would be parked off-screen)
     panel                       → veil + spinner near its top
     short / empty               → centred spinner (with a min-height when the
                                   frame is empty at rest and has no box) */

/* Locations list (map_features#index). Every filter, sort, view-mode and page
   click re-navigates this frame (map_features_index_controller#navigate,
   location_filter_controller), and a bulk delete now does too — the frame
   re-fetches itself so the pager, the totals and the rows flowing up from the
   next page all come from the server.

   Unlike the other frames here, this one does NOT draw its spinner in CSS.
   The list already has a first-load spinner (the "Loading locations…" card the
   page shell paints while the lazy frame fetches), and a re-fetch should look
   like a first load — so map_features/_list_loading is rendered a second time
   inside the frame as a hidden overlay, and the rule below just reveals it.
   Same markup, same svg, no second spinner to keep in sync.

   Centred overlay rather than the veil + fixed top bar used by #metrics_table
   above: that bar exists because the metrics table is far taller than the
   viewport, so an absolutely-positioned overlay's contents end up parked
   off-screen. This frame is bounded to the viewport (its container is
   h-[calc(100dvh-3.25rem)], or max-h-[74vh] when embedded) and the table
   scrolls INSIDE it, so the frame's centre is always where the user is looking.

   NOTE: deliberately does NOT set `display` — same trap as #app-peek-panel
   below. map_features/index declares this frame with
   `class="flex flex-col flex-1 min-h-0"`, and it is the middle link in the
   chain that gives the table its own scroller:
     .h-dvh.overflow-hidden container → THIS FRAME → .ll-card → .ll-scroll
   An element+id selector (1,0,1) beats Tailwind's `.flex` (0,1,0), so a
   `display: block` here severed that chain: .ll-card's `flex:1; min-height:0`
   went inert, it fell back to height:auto, .ll-scroll never got a bounded
   height to scroll inside, and the full 25-row page overflowed the
   overflow-hidden container — clipped, with no scrollbar anywhere and the
   footer and pagination unreachable. `position: relative` is all the overlay
   needs and it doesn't disturb the flex layout.
   (turbo_frame_display_guard_test enforces the no-`display` part.) */
turbo-frame#feature-list-frame { position: relative; }
/* The overlay ships with Tailwind's `hidden` on it, so it is invisible even if
   this stylesheet never loads. This selector is (1,2,1) and beats `.hidden`
   (0,1,0), so it still shows while the frame is fetching. */
turbo-frame#feature-list-frame[busy] .ll-loading-overlay {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  position: absolute; inset: 0; z-index: 50;
  background: rgba(255, 255, 255, 0.75);
}

/* App-wide peek slide-over (peek_controller). Re-opening a peek keeps the
   PREVIOUS peek's content on screen while the new one loads, which reads as
   the panel having opened on the wrong record. Veil it so the stale content
   visibly recedes.

   NOTE: deliberately does NOT set `display` or `min-height` here. peek.css has
   `.peek-panel > turbo-frame { flex: 1; min-height: 0; display: flex;
   flex-direction: column }` at specificity 0,1,1 — an id selector (1,0,1)
   would beat it and collapse the panel's flex column, which is what makes the
   sticky top bar and the scrolling body work. `position: relative` is all the
   veil needs, and it doesn't disturb the flex layout. The frame already fills
   the panel via `flex: 1`, so it always has a box to draw into. */
turbo-frame#app-peek-panel { position: relative; }
turbo-frame#app-peek-panel[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.65);
}
turbo-frame#app-peek-panel[busy]::before {
  content: ""; position: absolute; top: 56px; left: 50%; z-index: 21;
  width: 26px; height: 26px; margin-left: -13px; border-radius: 50%;
  border: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Settings modal body (region_preview_controller and the settings nav). A
   panel, so veil + spinner near the top rather than a viewport-fixed bar. */
turbo-frame#settings_content { display: block; position: relative; }
turbo-frame#settings_content[busy] { min-height: 140px; }
turbo-frame#settings_content[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.65);
}
turbo-frame#settings_content[busy]::before {
  content: ""; position: absolute; top: 48px; left: 50%; z-index: 21;
  width: 26px; height: 26px; margin-left: -13px; border-radius: 50%;
  border: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Saved-views dropdown (saved_views_controller). Short frame inside a small
   menu — a centred spinner is the whole feedback; no veil needed at this size.
   The frame itself stays visible; its parent `.saved-views-menu` is what
   toggles open/closed. */
turbo-frame#saved-views-list { display: block; position: relative; }
turbo-frame#saved-views-list[busy] { min-height: 56px; }
turbo-frame#saved-views-list[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.7);
}
turbo-frame#saved-views-list[busy]::before {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 21;
  width: 20px; height: 20px; margin: -10px 0 0 -10px; border-radius: 50%;
  border: 2px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Inline bulk-organise panel (bulk_organise_controller), in the maps/show
   sidebar. This one is EMPTY at rest — `turbo_frame_tag "bulk-organise-frame"`
   with no body — so there's no box to position a spinner inside until content
   arrives. Give it a height only while busy. */
turbo-frame#bulk-organise-frame[busy] {
  display: block; position: relative; min-height: 64px;
}
turbo-frame#bulk-organise-frame[busy]::before {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 21;
  width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%;
  border: 2px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Link popover panel (bulk_link_controller), in the Locations-list bulk bar.
   Same shape as #bulk-organise-frame above: EMPTY at rest (the toolbar renders
   a bare frame; openLinkPop assigns src on open), so it needs a box only while
   busy. Covers both the first open and the server-side candidate-search
   refetches (?q=), which would otherwise read as dead typing. */
turbo-frame#bulk-link-frame[busy] {
  display: block; position: relative; min-height: 64px;
}
turbo-frame#bulk-link-frame[busy]::before {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 21;
  width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%;
  border: 2px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Same guard as #feature-details-frame above: the element+id selectors here are
   specificity 0,1,1 and would beat Tailwind's `.hidden` (0,1,0) if anything
   ever toggles these frames that way. Nothing does today — this keeps it that
   way by construction rather than by luck. */
turbo-frame#feature-list-frame.hidden,
turbo-frame#app-peek-panel.hidden,
turbo-frame#settings_content.hidden,
turbo-frame#saved-views-list.hidden,
turbo-frame#bulk-organise-frame.hidden,
turbo-frame#bulk-link-frame.hidden { display: none; }


/* ── Sales board (#sales-kanban) ────────────────────────────────────────────
   Same shape as #metrics_table above, for the same reason: the board is tall
   and scrolls internally in both axes, so an absolutely positioned spinner is
   parked off-screen. Veil over the stale content + a fixed top progress bar.

   The frame is reloaded by board_refresh_controller when you come back from a
   location's detail page, and by the toolbar refresh button — both of which
   otherwise look like nothing happened until the new columns pop in. */
turbo-frame#sales-kanban { display: block; position: relative; }
turbo-frame#sales-kanban[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 50;
  background: rgba(247, 250, 248, 0.55);
}
turbo-frame#sales-kanban[busy]::before {
  content: ""; position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  height: 3px; background: var(--brand-primary); transform-origin: 0 50%;
  animation: tf-bar 1.1s ease-in-out infinite;
}

/* Toolbar "Search everything" dropdown (header_search_controller): each
   keystroke re-points this frame's src. Veil the previous results + a small
   spinner so a slow search doesn't read as "no matches". Empty on the first
   fetch, so the busy frame reserves some height for the spinner. */
turbo-frame#header_search_results { display: block; position: relative; }
turbo-frame#header_search_results[busy] { min-height: 64px; }
turbo-frame#header_search_results[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#header_search_results[busy]::before {
  content: ""; position: absolute; top: 20px; left: 50%; z-index: 21;
  width: 22px; height: 22px; margin-left: -11px; border-radius: 50%;
  border: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Visit Dashboard calendar card: the view switch, arrows and Today re-point
   this frame, and a ticked-off task redraws it. Veil the stale card so the
   click reads as taken; the frame fills its fixed-height card, so the veil
   covers it exactly. */
turbo-frame#visits_agenda_calendar { position: relative; }
turbo-frame#visits_agenda_calendar[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 30; border-radius: 14px;
  background: rgba(255, 255, 255, 0.55);
}
turbo-frame#visits_agenda_calendar[busy]::before {
  content: ""; position: absolute; top: 50%; left: 50%; z-index: 31;
  width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%;
  border: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}
/* Active-visit home widget (app/views/pages/home/widgets/_active_card.html.erb).
   Each .av-card is an inline-size container, so a card lays itself out from its
   OWN width: a wide (full-width widget) card puts the "Captured so far" panel to
   the RIGHT of the details in a hero row; a narrow (half-width widget) card stacks
   the panel BELOW the details and renders the legend as a wrapping horizontal row.
   All values are --pm-* tokens (see pm_tokens.css). No raw hex, no raw px, no
   left-accent bar, no gradient. */

.av-frame { container-type: inline-size; }

/* ── Card shell ─────────────────────────────────────────────────────────── */
.av-card {
  background: var(--pm-surface);
  border: 1px solid color-mix(in srgb, var(--pm-primary) 22%, transparent);
  border-radius: var(--pm-radius-xl);
  box-shadow: var(--pm-shadow-card);
  padding: var(--pm-space-m);
  transition: box-shadow var(--pm-duration-fast) var(--pm-ease-standard);
}
.av-card:hover { box-shadow: var(--pm-shadow-m); }
/* Stale card — whole-card warm border tint (no left bar). */
.av-card.stale { border-color: color-mix(in srgb, var(--pm-warning) 32%, transparent); }

/* Compact (default): details, then panel stacked below. */
.av-body    { display: flex; flex-direction: column; gap: var(--pm-space-m); }
.av-details { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }

/* ── Status row ─────────────────────────────────────────────────────────── */
.av-status-row { display: flex; align-items: center; gap: var(--pm-space-s); margin-bottom: var(--pm-space-s); }
.av-eyebrow {
  display: inline-flex; align-items: center; gap: var(--pm-space-s);
  font: var(--pm-label-s); text-transform: uppercase; letter-spacing: .06em;
  color: var(--pm-fg-2); white-space: nowrap;
}
.av-dot {
  width: var(--pm-space-s); height: var(--pm-space-s); border-radius: var(--pm-radius-pill);
  background: var(--pm-success); flex-shrink: 0;
  animation: av-pulse 1.8s ease-in-out infinite;
}
@keyframes av-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
.av-pill {
  margin-left: auto; display: inline-flex; align-items: center; gap: var(--pm-space-xs);
  padding: var(--pm-space-xxs) var(--pm-space-s);
  font: var(--pm-pill); text-transform: uppercase; letter-spacing: .05em;
  background: var(--pm-surface-warning); color: var(--pm-warning-dark);
  border-radius: var(--pm-radius-pill); white-space: nowrap;
}
.av-pill .fas { font-size: 10px; }

/* ── Title ──────────────────────────────────────────────────────────────── */
.av-title {
  font: var(--pm-headline-s); font-size: 1.0625rem; /* 17px compact; 18px in hero */
  color: var(--pm-fg-1); margin: 0; min-width: 0;
  /* Clamp to 2 lines so a long feature name/purpose truncates cleanly instead of
     growing the card unbounded. */
  overflow: hidden; text-overflow: ellipsis;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.av-title .av-dash { color: var(--pm-fg-2); font-weight: 600; }
/* Purpose recedes so the feature name dominates the title. */
.av-title .av-purpose { font-weight: 600; color: var(--pm-fg-2); }

/* ── Meta line ──────────────────────────────────────────────────────────── */
.av-meta {
  font: var(--pm-body-s); color: var(--pm-fg-2);
  margin: var(--pm-space-xs) 0 0;
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--pm-space-xxs) var(--pm-space-s);
}
.av-meta .av-pin { color: var(--pm-accent-critical); font-size: 12px; flex-shrink: 0; }
.av-meta .av-sep { color: var(--pm-fg-3); }
.av-meta .av-place { overflow: hidden; text-overflow: ellipsis; max-width: 12rem; white-space: nowrap; }
.av-meta .av-time { color: var(--pm-fg-1); font-weight: 600; } /* the one anchor */

/* ── Stale nudge — inline warm text, no colored surface ─────────────────── */
.av-nudge {
  display: flex; align-items: flex-start; gap: var(--pm-space-xs);
  margin-top: var(--pm-space-ms);
  font: var(--pm-body-s); line-height: 1.4; color: var(--pm-warning-dark);
}
.av-nudge .av-nudge-icon { font-size: 12px; line-height: 1.4; flex-shrink: 0; }

/* ── Actions — CTAs anchored to the bottom of the details column ─────────── */
.av-actions {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--pm-space-s);
  margin-top: auto; padding-top: var(--pm-space-m);
}
.av-btn {
  min-height: 3rem; display: inline-flex; align-items: center; justify-content: center;
  gap: var(--pm-space-s); font-size: 14px; font-weight: 600;
  border-radius: var(--pm-radius-pill); border: 1px solid transparent; cursor: pointer;
  transition: box-shadow var(--pm-duration-fast) var(--pm-ease-standard),
              background var(--pm-duration-fast) var(--pm-ease-standard),
              transform var(--pm-duration-fast) var(--pm-ease-standard);
}
.av-btn:focus-visible { outline: none; box-shadow: var(--pm-focus-ring); }
.av-btn .fas, .av-btn .far { font-size: 12px; }

/* Compact (default): the two primary pills share the row and fill the column
   side-by-side instead of stacking left-aligned. Reset to natural width in hero. */
.av-continue    { flex: 1 1 0; }
.av-finish-form { flex: 1 1 0; }
.av-finish      { width: 100%; }

/* Continue — primary GO, the only fill weight. */
.av-continue {
  padding: 0 var(--pm-space-l);
  background: var(--pm-primary); color: var(--pm-on-inverse); box-shadow: var(--pm-shadow-s);
}
.av-continue:hover { box-shadow: var(--pm-shadow-m); }
.av-continue:focus-visible { box-shadow: var(--pm-shadow-s), var(--pm-focus-ring); }
.av-arrow { transition: transform var(--pm-duration-fast) var(--pm-ease-standard); }
.av-continue:hover .av-arrow { transform: translateX(2px); }

/* Finish — END, outlined coral (lighter than Continue). */
.av-finish {
  padding: 0 var(--pm-space-l);
  background: transparent; color: var(--pm-accent-critical);
  border-color: var(--pm-accent-critical);
}
.av-finish:hover { background: color-mix(in srgb, var(--pm-accent-critical) 8%, transparent); }
/* Compact (default): flex sizes the two primary pills, so trim their inner
   padding (overrides the space-l shorthand above) to keep labels on one line.
   Hero restores the roomy padding. */
.av-continue, .av-finish { padding-left: var(--pm-space-s); padding-right: var(--pm-space-s); }

/* button_to wraps each action in a <form>; the FORM is the flex item, so the
   auto-margin that shoves Discard to the far edge must live on the form. */
.av-finish-form { margin: 0; }
.av-discard-form { margin: 0 0 0 auto; }

/* Discard — destructive, tertiary/quiet, pushed to the far edge. */
.av-discard {
  min-height: auto; padding: 0 var(--pm-space-xs);
  background: transparent; color: var(--pm-fg-3); font-weight: 500;
  box-shadow: none;
}
.av-discard:hover { color: var(--pm-accent-critical); }

/* ── "Captured so far" panel ────────────────────────────────────────────── */
.av-panel {
  background: var(--pm-surface-variant); border-radius: var(--pm-radius-l);
  padding: var(--pm-space-m); display: flex; flex-direction: column;
}
.av-panel-h {
  font: var(--pm-eyebrow); text-transform: uppercase; letter-spacing: .06em;
  color: var(--pm-fg-3); margin: 0 0 var(--pm-space-s);
}
.av-panel-empty { font: var(--pm-body-s); color: var(--pm-fg-3); margin: 0; }
/* Zero-observation stand-in — short muted inline line, NO grey sub-surface/rail.
   Sits at the top beside the details in hero (align-self), below in compact. */
.av-empty-note {
  margin: 0; font: var(--pm-body-s); color: var(--pm-fg-3);
  align-self: flex-start; flex: 0 0 auto;
}

/* Legend — compact default: wrapping horizontal row of "● Label N" units. */
.av-legend {
  display: flex; flex-direction: row; flex-wrap: wrap;
  gap: var(--pm-space-s) var(--pm-space-m);
}
.av-leg { display: flex; align-items: center; gap: var(--pm-space-xs); font: var(--pm-body-s); color: var(--pm-fg-2); }
.av-leg-dot { width: 7px; height: 7px; border-radius: var(--pm-radius-pill); flex-shrink: 0; }
.av-leg-label { white-space: nowrap; }
.av-leg-count { margin-left: var(--pm-space-xxs); font-weight: 700; color: var(--pm-fg-1); }

/* Total — sum of buckets, honest. */
.av-total {
  display: flex; align-items: center;
  margin-top: var(--pm-space-s); padding-top: var(--pm-space-s);
  border-top: 1px solid var(--pm-border);
  font: var(--pm-eyebrow);
}
.av-total-label { text-transform: uppercase; letter-spacing: .05em; color: var(--pm-fg-3); }
.av-total-count { margin-left: auto; font-size: 12px; color: var(--pm-fg-1); }

@media (prefers-reduced-motion: reduce) {
  .av-dot { animation: none; opacity: 1; }
  .av-arrow { transition: none; }
  .av-continue:hover .av-arrow { transform: none; }
}

/* ── Wide (hero) — panel BESIDE the details, roomier padding + lift ──────── */
@container (min-width: 34rem) {
  .av-card       { padding: var(--pm-space-l); box-shadow: var(--pm-shadow-m); }
  .av-card:hover { box-shadow: var(--pm-shadow-l); }

  .av-body    { flex-direction: row; align-items: flex-start; gap: var(--pm-space-l); }
  /* Panel is only as tall as its content — card height is driven by the details
     column, so a stale/low-observation card shows no dead grey rail below the
     legend. Pinned to the top beside the details. */
  .av-panel   { flex: 0 0 13.5rem; align-self: flex-start; }
  .av-title   { font-size: 1.125rem; }

  /* Hero has room — primary pills return to natural width + roomy padding. */
  .av-continue    { flex: 0 0 auto; }
  .av-finish-form { flex: 0 0 auto; }
  .av-finish      { width: auto; }
  .av-continue, .av-finish { padding-left: var(--pm-space-l); padding-right: var(--pm-space-l); }

  /* Legend returns to a vertical "summary table" with right-aligned counts. */
  .av-legend      { flex-direction: column; gap: var(--pm-space-ms); }
  .av-leg         { gap: var(--pm-space-s); }
  .av-leg-count   { margin-left: auto; }
}
/* Audio Briefings home widget (app/views/pages/home/widgets/_briefings.html.erb).
   Forward-looking: a player hero that plays the soonest upcoming visit's briefing,
   then one row per upcoming visit. The .bw-frame is an inline-size container, so
   the widget lays itself out from its OWN width — a wide (full-width) widget shows
   the "Next up" kicker, the row meta line, and the full transcript label; a narrow
   (half-width) widget drops those for a denser single-line read.

   All values are --pm-* tokens (see pm_tokens.css) except small icon/element
   sizing in px, matching home_active_widget.css. No raw hex, no left-accent bar,
   no gradient. */

.bw-frame { container-type: inline-size; }

/* ── Player hero ─────────────────────────────────────────────────────────── */
.bw-player { display: flex; align-items: center; gap: var(--pm-space-ms); }

.bw-play {
  flex-shrink: 0; width: 44px; height: 44px; border: none; cursor: pointer;
  border-radius: var(--pm-radius-pill);
  background: var(--pm-primary); color: var(--pm-fg-on-primary);
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: var(--pm-shadow-m);
  transition: transform var(--pm-duration-fast) var(--pm-ease-standard);
}
.bw-play:hover { transform: scale(1.04); }
.bw-play:focus-visible { outline: none; box-shadow: var(--pm-focus-ring), var(--pm-shadow-m); }
.bw-play .fas { font-size: 16px; }

.bw-play-meta { min-width: 0; flex: 1; }
.bw-kicker {
  display: none; /* full only */
  font: var(--pm-label-s); font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--pm-primary-light);
  margin: 0 0 var(--pm-space-xxs);
}
.bw-headline {
  font: var(--pm-title-m); color: var(--pm-fg-1); margin: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bw-subline {
  font: var(--pm-body-s); color: var(--pm-fg-2); margin: 2px 0 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Quiet hero — upcoming visits exist but none have audio yet. */
.bw-hero-quiet {
  display: flex; align-items: center; gap: var(--pm-space-s);
  font: var(--pm-body-m); color: var(--pm-fg-3);
}
.bw-hero-quiet .fas { color: var(--pm-primary-light); font-size: 14px; }

/* ── Scrubber ────────────────────────────────────────────────────────────── */
.bw-scrub {
  display: flex; align-items: center; gap: var(--pm-space-s);
  margin-top: var(--pm-space-ms);
}
.bw-t {
  font-family: var(--pm-font-mono); font-size: 10px; line-height: 1;
  color: var(--pm-fg-3); width: 30px; flex-shrink: 0;
}
.bw-t--end { text-align: right; }
.bw-track {
  position: relative; flex: 1; height: 5px; cursor: pointer;
  border-radius: var(--pm-radius-pill); background: var(--pm-surface-variant);
}
.bw-track:focus-visible { outline: none; box-shadow: var(--pm-focus-ring); }
.bw-fill {
  position: absolute; inset: 0 auto 0 0; width: 0%;
  border-radius: var(--pm-radius-pill); background: var(--pm-primary-light);
}
.bw-thumb {
  position: absolute; top: 50%; left: 0%; width: 12px; height: 12px;
  transform: translate(-50%, -50%); border-radius: var(--pm-radius-pill);
  background: var(--pm-surface); border: 2px solid var(--pm-primary-light);
  box-shadow: var(--pm-shadow-xs);
}

/* ── Transport ───────────────────────────────────────────────────────────── */
.bw-transport {
  display: flex; align-items: center; gap: var(--pm-space-s);
  margin-top: var(--pm-space-s);
}
.bw-tbtn {
  display: inline-flex; align-items: center; gap: var(--pm-space-xs);
  cursor: pointer; height: 30px; padding: 0 var(--pm-space-ms);
  border: 1px solid var(--pm-border); background: var(--pm-surface);
  color: var(--pm-fg-2); border-radius: var(--pm-radius-pill);
  font: var(--pm-title-s); font-size: 11px;
  transition: background var(--pm-duration-fast) var(--pm-ease-standard),
              color var(--pm-duration-fast) var(--pm-ease-standard);
}
.bw-tbtn:hover { background: var(--pm-surface-secondary); color: var(--pm-fg-1); }
.bw-tbtn:focus-visible { outline: none; box-shadow: var(--pm-focus-ring); }
.bw-tbtn .fas { font-size: 12px; }
.bw-sec {
  font-family: var(--pm-font-mono); font-size: 9px; color: var(--pm-fg-3);
  font-variant-numeric: tabular-nums;
}
.bw-speed {
  min-width: 46px; justify-content: center;
  font-variant-numeric: tabular-nums;
}
.bw-spacer { flex: 1; }
.bw-transcript {
  display: inline-flex; align-items: center; gap: var(--pm-space-xs);
  cursor: pointer; border: none; background: transparent;
  color: var(--pm-primary-light); font: var(--pm-title-s); font-size: 11px;
  padding: var(--pm-space-xs);
}
.bw-transcript:hover { text-decoration: underline; }
.bw-transcript:focus-visible { outline: none; box-shadow: var(--pm-focus-ring); border-radius: var(--pm-radius-s); }
.bw-transcript .fas { font-size: 13px; }
.bw-transcript-label { display: inline; } /* icon-only in compact */

.bw-audio { margin-top: var(--pm-space-s); }
.bw-audio audio { width: 100%; }

/* Transcript drawer — plain surface, no left bar. */
.bw-transcript-panel {
  margin-top: var(--pm-space-s); padding: var(--pm-space-ms);
  background: var(--pm-surface-secondary); border: 1px solid var(--pm-border-card);
  border-radius: var(--pm-radius-m);
  font: var(--pm-body-s); color: var(--pm-fg-2);
  max-height: 11rem; overflow-y: auto; white-space: pre-line;
}

/* ── Divider ─────────────────────────────────────────────────────────────── */
.bw-hr { height: 1px; border: 0; background: var(--pm-border-card); margin: var(--pm-space-ms) 0; }

/* ── List ────────────────────────────────────────────────────────────────── */
.bw-list-label {
  font: var(--pm-eyebrow); letter-spacing: .08em; text-transform: uppercase;
  color: var(--pm-fg-3); margin: 0 0 var(--pm-space-s);
}
.bw-rows { display: flex; flex-direction: column; gap: var(--pm-space-s); }

.bw-row {
  display: flex; align-items: center; gap: var(--pm-space-ms);
  padding: var(--pm-space-s) var(--pm-space-ms);
  border-radius: var(--pm-radius-m);
  border: 1px solid var(--pm-border-card); background: var(--pm-surface);
  text-align: left; width: 100%; cursor: pointer;
  transition: background var(--pm-duration-fast) var(--pm-ease-standard),
              border-color var(--pm-duration-fast) var(--pm-ease-standard);
}
.bw-row:hover { background: var(--pm-surface-secondary); }
.bw-row:focus-visible { outline: none; box-shadow: var(--pm-focus-ring); }

/* Current / loaded-in-player — 6% fill + 35% border + lift. No left bar. */
.bw-row.bw-current {
  background: color-mix(in srgb, var(--pm-primary) 6%, transparent);
  border-color: color-mix(in srgb, var(--pm-primary) 35%, transparent);
  box-shadow: var(--pm-shadow-card);
}

/* Preparing — recede, don't cage. */
.bw-row.bw-preparing { cursor: default; opacity: .85; }
.bw-row.bw-preparing:hover { background: var(--pm-surface); }

.bw-glyph {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: var(--pm-radius-m);
  display: inline-flex; align-items: center; justify-content: center;
}
.bw-glyph .fas { font-size: 14px; }

.bw-body { min-width: 0; flex: 1; }
.bw-name {
  display: block;
  font: var(--pm-body-m); font-weight: 600; color: var(--pm-fg-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bw-meta {
  display: block;
  font: var(--pm-body-s); color: var(--pm-fg-2); margin-top: 1px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.bw-sep { color: var(--pm-fg-3); margin: 0 var(--pm-space-xs); }
.bw-prepared { color: var(--pm-primary-light); font-weight: 600; font-variant-numeric: tabular-nums; }

.bw-right {
  flex-shrink: 0; display: flex; flex-direction: column;
  align-items: flex-end; gap: var(--pm-space-xxs);
}
.bw-day {
  display: block; /* shown on non-current rows in both full and compact */
  font-family: var(--pm-font-mono); font-size: 11px; line-height: 1;
  color: var(--pm-fg-2); font-variant-numeric: tabular-nums;
}
.bw-row.bw-current .bw-day { display: none; } /* equalizer takes its place */
.bw-status {
  font: var(--pm-label-s); font-weight: 700; font-size: 8px; letter-spacing: .06em;
  text-transform: uppercase; padding: var(--pm-space-xxs) var(--pm-space-xs);
  border-radius: var(--pm-radius-pill);
  background: var(--pm-surface-warning); color: var(--pm-warning-dark);
}
.bw-status-full { display: inline; }
.bw-status-compact { display: none; }

/* Equalizer — current-row cue. */
.bw-eq { display: none; align-items: flex-end; gap: 2px; height: 12px; }
.bw-row.bw-current .bw-eq { display: inline-flex; }
.bw-eq i {
  width: 2px; background: var(--pm-primary-light); border-radius: 1px;
  transform-origin: bottom; /* static bars at rest; animated only while playing */
}
.bw-eq i:nth-child(1) { height: 5px; animation-delay: 0ms; }
.bw-eq i:nth-child(2) { height: 11px; animation-delay: 150ms; }
.bw-eq i:nth-child(3) { height: 7px; animation-delay: 300ms; }
.bw-row.bw-current.is-playing .bw-eq i { animation: bw-eq 900ms ease-in-out infinite; }
@keyframes bw-eq { 0%, 100% { transform: scaleY(.5); } 50% { transform: scaleY(1); } }

@media (prefers-reduced-motion: reduce) {
  .bw-play { transition: none; }
  .bw-play:hover { transform: none; }
  .bw-row.bw-current.is-playing .bw-eq i { animation: none; }
  .bw-eq i { transform: scaleY(1); }
}

/* ── Wide (full-width widget) — kicker, row meta line, transcript label ────── */
@container (min-width: 26rem) {
  .bw-play { width: 52px; height: 52px; }
  .bw-play .fas { font-size: 20px; }
  .bw-kicker { display: block; }
  .bw-headline { font: var(--pm-title-l); }
  .bw-scrub { margin-top: var(--pm-space-m); }
  .bw-transport { margin-top: var(--pm-space-ms); }
  .bw-hr { margin: var(--pm-space-m) 0; }
}

/* ── Narrow (half-width widget) — denser rows, day rail, icon-only transcript ─ */
@container (max-width: 25.999rem) {
  .bw-row { padding: var(--pm-space-s); }
  .bw-glyph { width: 26px; height: 26px; }
  .bw-glyph .fas { font-size: 13px; }
  .bw-name { font: var(--pm-title-s); }
  .bw-meta-full { display: none; }
  .bw-transcript-label { display: none; }
  .bw-status-full { display: none; }
  .bw-status-compact { display: inline; }
}
/* Sidebar popovers (notifications) vs. overlaying panels.
 *
 * The sidebar <aside> is positioned WITH a z-index (z-40, md:z-10), so it opens
 * its own stacking context. Everything inside it — including the notifications
 * dropdown's own z-10 — is painted at the aside's level, no matter how high the
 * child's z-index goes. The team settings screen renders in the turbo modal
 * (.fixed inset-0 … z-50), which therefore covers the dropdown on desktop.
 *
 * So the fix has to lift the ASIDE, and only while a popover is open — leaving
 * it raised would put the sidebar permanently over every modal.
 *
 * Specificity matters here: a plain `z-[60]` utility toggled on the element
 * loses to the `md:z-10` utility (same 0,1,0 specificity, later in the sheet).
 * This attribute selector is 0,1,1, so it wins inside the media query too.
 */
aside[data-popover-open] {
  z-index: 60;
}
