/*
 * 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); }
/* Press state (Apple HIG Buttons: "Always include a press state for a custom
   button"): the design system's pressed state layer, --pm-op-pressed (12%) of
   the control's own text colour over its fill, so a tap answers on a touch
   screen, where the hover fills never show. Same layer as .press-states in
   application.tailwind.css, and the same token, with its 12% fallback for
   the layouts (devise, offline) that load this without pm_tokens.css. */
.lf-btn:active, .lf-opt:active, .lf-pop-head button:active,
.lf-section-head button:active, .lf-apply:active, .lf-done:active {
  background-image: linear-gradient(color-mix(in srgb, currentColor calc(var(--pm-op-pressed, 0.12) * 100%), transparent) 0 0);
}

/* Counts are 11px, the smallest text iOS uses (Apple HIG Typography): every
   page with this bar is used on phones too. */
.lf-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
  font-size: 11px; 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. The merge
   modal's Filter panel sections use it too (map_features/merge/_merge_form). */
.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;
}
/* neutral-500 for the same reason as .lf-opt-count below: neutral-400 is
   2.5:1 on white in PropelMapper, under even the 3:1 bold text may have. */
.lf-section-label { font-size: 11px; font-weight: 700; color: var(--brand-neutral-500); 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;
}
/* The hover layer over the button's own primary-surface fill (the metrics
   table's filtered-header hover): --pm-op-hover of the brand's primary. */
.lf-pop-head button:hover { background: color-mix(in srgb, var(--brand-primary) calc(var(--pm-op-hover, 0.04) * 100%), var(--brand-primary-surface)); }

/* 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: var(--brand-neutral-500);
  font-variant-numeric: tabular-nums;
}

.lf-opts { overflow-y: auto; max-height: 244px; padding: 4px 0; }
/* A heading inside one section's list, before each group of rows (the Area
   section: a country's provinces, then each boundary layer), as the map's
   Area menu heads its groups. Quieter than the section label above it. */
.lf-opt-group {
  padding: 8px 8px 2px; font-size: 11px; font-weight: 600; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--brand-neutral-500);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.lf-opt-group:first-child { padding-top: 2px; }
.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;
}
/* The brand's primary at the V2 hover strength (--pm-op-hover, 4%), mixed
   into white: the metrics table's row hover, so Contour gets Harbour where
   PropelMapper gets green. */
.lf-opt:hover { background: color-mix(in srgb, var(--brand-primary) calc(var(--pm-op-hover, 0.04) * 100%), #fff); }
.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; }
/* neutral-500, not 400: grey text this small needs the darker step to stay
   legible (about 4.8:1 on white against 2.5:1), as on the sales cards' meta. */
.lf-opt-count { flex: none; min-width: 18px; text-align: right; font-size: 11px; color: var(--brand-neutral-500); }
/* 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: var(--brand-neutral-500); 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: var(--brand-neutral-50); border: 1px solid var(--brand-neutral-200); border-radius: 999px;
  padding: 4px 10px;
  transition: border-color 80ms ease, background-color 80ms ease;
}
/* neutral-500, the forms plugin's placeholder grey on the page's own search
   box: #9CA3AF was 2.5:1, and a placeholder is words too. */
input.lf-search::placeholder { color: var(--brand-neutral-500); }
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 icon when there is no room for it.
 *
 * The icon sits in 36px of left padding. It earns that space for as long as
 * any wording fits beside it; even a bare "Search…" reads better with the
 * icon 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-left: 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;
}

/* ── Map sidebar: the selected location row(s) ─────────────────────────────
 * One class, `is-feature-selected`, set by mapbox_map_controller for the single
 * selection and for multi-select alike (SIDEBAR_ROW_SELECTED). House style, as
 * the location filter's active button (.lf-btn.is-active): a full subtle tint
 * and a brand border on the row's card, never a coloured left accent bar
 * (CLAUDE.md, "No coloured left-accent bars"). `:first-child` is the row's own
 * card; on a group row the children sit in a later sibling and stay neutral. */
#feature-list li.is-feature-selected > div:first-child {
  background-color: var(--brand-primary-surface);
  border-color: var(--brand-primary);
}
/* ── 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 is the empty marker (a dash) and nothing else, the same faint
   neutral-300 as the Fields column's empty dash. Words and values that are
   merely secondary (a date, "No visits") take .ll-secondary: neutral-500 is
   4.8:1 on white in PropelMapper and about 6:1 in Contour, where the faint
   tone was 1.6:1. 11px is the floor for any text in this vocabulary. */
.ll-muted { color:var(--brand-neutral-300); font-size:13px; }
.ll-secondary { color:var(--brand-neutral-500); font-size:13px; }

/* The level tag ("Field", "Farm"). Under the top level it is the brand's own
   tint, the primary ramp Tailwind's green-50/700 is bridged to, so Contour
   reads Harbour where PropelMapper reads green (it was a mint literal). The
   top level keeps indigo in every brand: a categorical colour that tells it
   from the levels under it. */
.ll-badge { flex:none; font-size:11px; font-weight:600; line-height:1; padding:3px 6px;
  border-radius:5px; background:var(--brand-ramp-50); color:var(--brand-ramp-700); border:1px solid var(--brand-ramp-100); }
.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:11px; 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); }
/* The chip menus' current pick, as the option picker marks its selected row. */
.ll-menu-item.is-current { background:var(--brand-primary-surface); font-weight:600; }
.ll-menu-item.is-danger { color:var(--brand-danger); }
.ll-menu-item.is-danger:hover { background:var(--brand-danger-surface); }
.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; }
/* An unset category / status: every new location starts here, so the chip
   reads as the next thing to do rather than as a disabled control. A dashed
   border is the app's "add something here" mark (the sales board's field
   groups), in the brand colour; the plus says what clicking does. The text is
   primary-dark, not primary: PropelMapper's green is too light for 12px text. */
.fchip--unset { border-style:dashed; border-color:var(--brand-primary); color:var(--brand-primary-dark); }
.fchip--unset:hover { border-style:solid; border-color:var(--brand-primary); }
.fchip-plus { width:13px; height:13px; flex:none; color:var(--brand-primary); }
.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:11px; font-weight:700; flex:none; }
.fchip-av.is-creator { background:var(--brand-neutral-400); }

/* ── Location access popover (map_feature_accesses/_popover + _access_panel) ──
   One shell for every single-location surface. Brand tokens only, so it reads
   the same inside the pm-styled feature page, the maps sidebar and the peek. */
.acc-popover { width: 320px; max-width: 88vw; max-height: 72vh; overflow-y: auto; background: #fff;
  border: 1px solid var(--brand-neutral-200); border-radius: 12px; z-index: 50;
  box-shadow: 0 12px 32px rgb(var(--brand-primary-dark-rgb) / 0.16); }
.acc-count, .acc-num { font-variant-numeric: tabular-nums; }
.acc-count { opacity: .6; }
.acc-panel { padding: 14px; display: flex; flex-direction: column; gap: 14px; }
.acc-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--brand-neutral-500); }
.acc-sub { font-size: 13px; color: var(--brand-primary-dark); margin-top: 2px; }
.acc-label { display: block; font-size: 11px; font-weight: 600; color: var(--brand-neutral-500); margin-bottom: 6px; }
.acc-error { margin: 0; font-size: 12px; padding: 8px 10px; border-radius: 8px; background: var(--brand-danger-surface); color: var(--brand-danger); }
.acc-panel .acc-search { width: 100%; font-size: 13px; padding: 7px 10px; border-radius: 8px; border: 1px solid var(--brand-neutral-200); background: var(--brand-neutral-50); color: var(--brand-primary-dark); }
.acc-panel .acc-search:focus { outline: 2px solid var(--brand-primary); outline-offset: 0; border-color: transparent; background: #fff; }
.acc-candidates { margin-top: 6px; max-height: 168px; overflow-y: auto; }
.acc-candidate { margin: 0; }
.acc-candidate.hidden { display: none; }
.acc-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.acc-row { display: flex; align-items: center; gap: 9px; width: 100%; min-height: 40px; padding: 5px 6px; border-radius: 8px; border: 0; background: transparent; text-align: left; font-size: 13px; color: var(--brand-primary-dark); }
.acc-row--pick { cursor: pointer; }
.acc-row--pick:hover, .acc-row--pick:focus-visible { background: var(--brand-primary-surface); outline: none; }
.acc-row--pick:hover .acc-plus { color: var(--brand-primary); }
.acc-row--quiet { color: var(--brand-neutral-600); }
.acc-avatar { flex: none; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; color: #fff; background: var(--brand-primary-dark); }
.acc-avatar--quiet { background: var(--brand-neutral-100); color: var(--brand-neutral-600); }
.acc-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acc-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.acc-who .acc-name { flex: none; }
.acc-hint { margin: 6px 0 0; font-size: 11px; color: var(--brand-neutral-400); }
.acc-reasons { display: flex; flex-wrap: wrap; gap: 4px; }
.acc-pill { flex: none; font-size: 11px; font-weight: 600; line-height: 1; padding: 4px 6px; border-radius: 999px; background: var(--brand-neutral-100); color: var(--brand-neutral-600); }
.acc-pill--collaborator { background: var(--brand-primary-surface); color: var(--brand-primary-dark); }
.acc-plus { flex: none; width: 24px; text-align: center; font-size: 17px; line-height: 1; color: var(--brand-neutral-400); }
.acc-remove, .acc-locked { flex: none; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; border: 0; background: transparent; }
.acc-remove { color: var(--brand-neutral-400); cursor: pointer; }
.acc-remove:hover, .acc-remove:focus-visible { color: var(--brand-danger); background: var(--brand-danger-surface); outline: none; }
.acc-locked { color: var(--brand-neutral-300); cursor: help; }
.acc-remove svg, .acc-locked svg { width: 15px; height: 15px; }
.acc-empty { margin: 0; font-size: 12px; color: var(--brand-neutral-500); }
.acc-admins { border-top: 1px solid var(--brand-neutral-100); padding-top: 10px; }
.acc-admins summary { cursor: pointer; font-size: 12px; color: var(--brand-neutral-500); list-style: none; }
.acc-admins summary::-webkit-details-marker { display: none; }
.acc-admins summary::after { content: " ▸"; }
.acc-admins[open] summary::after { content: " ▾"; }
.acc-admins .acc-list { margin-top: 6px; }

/* ── "+ New category / status" shortcut (option_create_controller) ───────────
   The inline create row shared by the assignment-chip menus, the Add Location
   modal and the import wizard. Lives here so the three hosts render the same
   pixels. Swatch colours come from MapFeatureStatus::COLOUR_PALETTE (data). */
.oc-row { display:flex; flex-direction:column; gap:8px; padding:8px 10px; }
.ll-menu-item.oc-trigger { color:var(--brand-primary); font-weight:500; border-top:1px solid var(--brand-neutral-100); }
/* A chip menu never runs off the screen: it scrolls inside instead (the
   dropdown ignores scrolls within its menu). While the create row is open the
   options list gives up height to it, so name, icon or colour and Create fit
   without scrolling on an ordinary laptop screen. */
.oc-menu { max-height:calc(100vh - 16px); overflow-x:hidden; overflow-y:auto; }
.oc-menu:has(.oc-row:not(.hidden)) .oc-options,
.op-panel:has(.oc-row:not(.hidden)) .op-list { max-height:7.5rem; }
.oc-swatches { display:flex; flex-wrap:wrap; gap:6px; }
.oc-swatch { width:22px; height:22px; border-radius:999px; border:2px solid transparent; padding:0;
  cursor:pointer; transition:transform .12s ease; }
.oc-swatch:hover { transform:scale(1.1); }
.oc-swatch[aria-pressed="true"] { border-color:var(--brand-neutral-800); box-shadow:0 0 0 2px #fff, 0 0 0 4px var(--brand-neutral-400); }
.oc-swatch:focus-visible { outline:none; box-shadow:0 0 0 2px #fff, 0 0 0 4px var(--brand-primary); }

/* Icon chooser in the "+ New category" row. Same grouped list as Team
   Settings (shared/_icon_picker), icons only. The icons arrive in a lazy
   frame (feature_categories#icons), which is display:inline until told
   otherwise like any unknown element. */
.oc-icon-chooser { border:1px solid var(--brand-neutral-300); border-radius:6px; background:#fff; }
.oc-icon-summary { display:flex; align-items:center; gap:8px; padding:6px 8px; font-size:12px;
  color:var(--brand-neutral-600); cursor:pointer; list-style:none; border-radius:6px; }
.oc-icon-summary::-webkit-details-marker { display:none; }
.oc-icon-summary:hover { background:var(--brand-neutral-50); }
.oc-icon-summary:focus-visible { outline:2px solid var(--brand-primary); outline-offset:-2px; }
.oc-icon-current { font-size:16px; line-height:1; }
.oc-icon-frame { display:block; max-height:12rem; overflow-y:auto; border-top:1px solid var(--brand-neutral-200); padding:0 6px 6px; }
.oc-icon-loading { padding:8px 4px; font-size:12px; color:var(--brand-neutral-500); }
.oc-icon-group { padding:8px 4px 4px; font-size:11px; font-weight:600; letter-spacing:.05em;
  text-transform:uppercase; color:var(--brand-neutral-500); }
.oc-icons { display:flex; flex-wrap:wrap; gap:2px; }
.oc-icon { display:flex; align-items:center; justify-content:center; width:30px; height:30px; padding:0;
  border:1px solid transparent; border-radius:6px; background:none; font-size:17px; line-height:1; cursor:pointer; }
.oc-icon:hover { background:var(--brand-neutral-100); }
.oc-icon[aria-pressed="true"] { border-color:var(--brand-primary); background:var(--brand-primary-surface); }
.oc-icon:focus-visible { outline:2px solid var(--brand-primary); outline-offset:0; }

/* Category / status picker (option_picker_controller): a <select> that can
   show each category's emoji and each status's colour, in the chip
   vocabulary above. The list opens inline under the field. */
.op-toggle { display:flex; align-items:center; gap:8px; width:100%; min-height:42px; padding:8px 12px;
  text-align:left; background:#fff; color:var(--brand-primary-dark); border:1px solid var(--brand-neutral-300);
  border-radius:8px; cursor:pointer; }
.op-toggle.text-sm { min-height:38px; }
.op-toggle:focus-visible, .op-toggle[aria-expanded="true"] { outline:none; border-color:var(--brand-primary);
  box-shadow:0 0 0 1px var(--brand-primary); }
.op-value { display:flex; align-items:center; gap:8px; min-width:0; flex:1; }
.op-value .fchip-ic { font-size:16px; }
.op-chev { width:16px; height:16px; flex:none; color:var(--brand-neutral-500); }
.op-none { color:var(--brand-neutral-500); }
.op-panel { margin-top:4px; background:#fff; border:1px solid var(--brand-neutral-200); border-radius:8px; overflow:hidden; }
.op-list { max-height:13rem; overflow-y:auto; padding:4px 0; }
.op-list .fchip-ic { font-size:16px; }
.op-list [role="option"][aria-selected="true"] { background:var(--brand-primary-surface); font-weight:600; }
.op-list [role="option"]:focus-visible { outline:2px solid var(--brand-primary); outline-offset:-2px; }
/* ── 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); }
}
/* A column flex item as well as a flex container: the dashboard's table
   section hands the frame whatever height is left under the toolbar, and the
   frame passes it on to the card and its scroller (see metrics_dashboards/show). */
turbo-frame#metrics_table { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; 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;
}
/* The location dashboard's Metrics card is a small window, not a tall page:
   the veil is feedback enough. Its window-wide bar doubled up with Turbo's
   own page bar on every visit (two bars under the browser chrome). */
#metrics_panel turbo-frame#metrics_table[busy]::before { content: none; }

/* ── 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.5rem)], 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/_list_frame 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;
  /* Opaque, unlike the other frames' veils: the panel swaps between unrelated
     peeks, so a translucent veil showed the PREVIOUS peek's content ghosting
     under the new one. peek_controller used to clear the body to avoid that,
     which is what produced a second spinner. */
  background: #fff;
}
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;
}

/* Map sidebar location list (maps#feature_list), four frames deep:
     #feature-list         — the whole list; refreshFeatureList re-points it
                             (feature added elsewhere, sort, filters). A panel:
                             veil the stale list + spinner near its top.
                             NOTE: no `display` — the view declares it
                             `flex-1 flex flex-col min-h-0` and is the middle of
                             the sidebar's scroll chain (same trap as
                             #feature-list-frame above).
     #feature-list-rows    — the rows; the name filter re-points it on each
                             (debounced) keystroke. Veil + spinner near the top
                             (the filter scrolls the list to the top first).
     feature-list-page-N   — lazy "next page" sentinels and
     feature-children-<id> — a group's lazy children: both show a static
                             "Loading…" line while empty; [busy] adds a small
                             spinner beside it. Ids are per page / per group, so
                             these match on the id prefix. */
turbo-frame#feature-list { position: relative; }
turbo-frame#feature-list[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#feature-list[busy]::before {
  content: ""; position: absolute; top: 72px; 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;
}
turbo-frame#feature-list-rows { display: block; position: relative; }
turbo-frame#feature-list-rows[busy] { min-height: 64px; }
turbo-frame#feature-list-rows[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#feature-list-rows[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;
}
turbo-frame[id^="feature-list-page-"],
turbo-frame[id^="feature-children-"] { display: block; position: relative; }
turbo-frame[id^="feature-list-page-"][busy] .feature-list-loading::before,
turbo-frame[id^="feature-children-"][busy] .feature-list-loading::before {
  content: ""; display: inline-block; vertical-align: -2px; margin-right: 6px;
  width: 11px; height: 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;
}

/* Visit Dashboard active locations "+N" list: a search re-points this frame.
   The frame is itself the scroller, so a veil on it would scroll away with
   the rows; veil the popover around it instead (it doesn't scroll, and is
   fixed-positioned, so it contains the veil). */
[data-dropdown-target="menu"]:has(> turbo-frame#visits_active_locations[busy])::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
[data-dropdown-target="menu"]:has(> turbo-frame#visits_active_locations[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: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Visit Dashboard field notes card: each list's ‹ › swaps just its own frame.
   Veil the stale page + a spinner near its top (the list scrolls inside a
   fixed-height card). The frames are flex columns already. */
turbo-frame#completed_field_notes,
turbo-frame#in_progress_field_notes { position: relative; }
turbo-frame#completed_field_notes[busy]::after,
turbo-frame#in_progress_field_notes[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#completed_field_notes[busy]::before,
turbo-frame#in_progress_field_notes[busy]::before {
  content: ""; position: absolute; top: 24px; 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;
}

/* The period dropdown's calendar and the briefing picker's results: short
   panels whose content is replaced in place (month arrows, a search). */
turbo-frame#visits_period_calendar,
turbo-frame#briefing_picker_results { display: block; position: relative; }
turbo-frame#visits_period_calendar[busy]::after,
turbo-frame#briefing_picker_results[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#visits_period_calendar[busy]::before,
turbo-frame#briefing_picker_results[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: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Reduce motion: the Visit Dashboard's spinners stand still. The veil and the
   ring still say the card is loading; only the endless spin goes (the brand
   loader does the same). */
@media (prefers-reduced-motion: reduce) {
  turbo-frame#visits_agenda_calendar[busy]::before,
  [data-dropdown-target="menu"]:has(> turbo-frame#visits_active_locations[busy])::before,
  turbo-frame#completed_field_notes[busy]::before,
  turbo-frame#in_progress_field_notes[busy]::before,
  turbo-frame#visits_period_calendar[busy]::before,
  turbo-frame#briefing_picker_results[busy]::before { animation: none; }
}

/* Notifications "View all" inbox (notifications/_inbox_list). Filter changes,
   paging and the bulk actions all re-navigate this frame. A panel inside the
   modal, so veil + spinner near the top. Layout (flex column) comes from the
   frame's Tailwind classes; only the positioning context lives here. */
turbo-frame#notifications_inbox_list { position: relative; }
turbo-frame#notifications_inbox_list[busy] { min-height: 140px; }
turbo-frame#notifications_inbox_list[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.65);
}
turbo-frame#notifications_inbox_list[busy]::before {
  content: ""; position: absolute; top: 72px; 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;
}

/* A notification opened from the inbox, stacking over it
   (notifications/inbox.html.erb). The frame is empty until the detail
   arrives, so the same shape as #modal above: dim + centred spinner, fixed. */
turbo-frame#notification_detail[busy]::after {
  content: ""; position: fixed; inset: 0; z-index: 60;
  background: rgba(15, 42, 36, 0.25);
}
turbo-frame#notification_detail[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 panel, Observations tab (map_feature_observations#index). Every
   period / visit / kind change re-navigates this frame and the old list stays
   put until the swap, so veil it with a spinner near the top (panel shape).
   "Load more" pages land in nested observations_page_N frames: those only fade
   the link, since the list above them is still current. */
turbo-frame#map_feature_observations { display: block; position: relative; }
turbo-frame#map_feature_observations[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#map_feature_observations[busy]::before {
  content: ""; position: absolute; top: 40px; 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;
}
turbo-frame[id^="observations_page_"][busy] { opacity: 0.5; pointer-events: none; }

/* Location dashboard, Field visits and Activity: "Show N earlier"
   (map_features/v2/_earlier_frame) loads the next page into its own frame,
   like observations_page_N above. Until then the frame holds only that
   button, so the button fades and says "Loading…" instead of its count. */
.earlier-rows .earlier-busy { display: none; }
turbo-frame.earlier-rows[busy] > .earlier-more { opacity: 0.5; pointer-events: none; }
turbo-frame.earlier-rows[busy] > .earlier-more .earlier-idle { display: none; }
turbo-frame.earlier-rows[busy] > .earlier-more .earlier-busy { display: inline; }

/* Location dashboard, the optional windows (map_features/v2/_section_card):
   Docs, Briefings, Report terms and the rest load their body into a lazy
   section_card_<kind> frame. Try again on a window that didn't load reloads
   it while the "didn't load" text stays put, so without this the click read
   as dead. Panel shape: veil + spinner near the top. The box and positioning
   context apply only while busy, like #bulk-organise-frame above, so nothing
   a section positions inside a loaded window moves. */
turbo-frame[id^="section_card_"][busy] {
  display: block; position: relative; min-height: 64px;
}
turbo-frame[id^="section_card_"][busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame[id^="section_card_"][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;
}

/* maps/show panel, the lazy Tasks / Contacts / Assets / Activity tabs
   (map_features#panel_tab). Each frame carries its own "Loading…" line for the
   first fetch; this veil and spinner cover a request on top of content that is
   already there, the same shape as the Visits frame above. */
turbo-frame#map_feature_panel_tasks,
turbo-frame#map_feature_panel_contacts,
turbo-frame#map_feature_panel_entities,
turbo-frame#map_feature_panel_activity { display: block; position: relative; }
turbo-frame#map_feature_panel_tasks[busy]::after,
turbo-frame#map_feature_panel_contacts[busy]::after,
turbo-frame#map_feature_panel_entities[busy]::after,
turbo-frame#map_feature_panel_activity[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#map_feature_panel_tasks[busy]::before,
turbo-frame#map_feature_panel_contacts[busy]::before,
turbo-frame#map_feature_panel_entities[busy]::before,
turbo-frame#map_feature_panel_activity[busy]::before {
  content: ""; position: absolute; top: 40px; 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;
}

/* "Check a location" resolver on the superadmin template page: short frame,
   driven by a debounced search — veil the stale list while the next result
   loads so a quick second keystroke doesn't read as ignored. */
turbo-frame#template-resolver { display: block; position: relative; }
turbo-frame#template-resolver[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 5;
  background: rgba(255, 255, 255, 0.55);
}

/* Form template page, Entries (visit_form_templates/_entries). The pager
   re-navigates this frame, and the old page of up to 50 rows stays put until
   the swap. The pager sits at the foot of a tall table, so a spinner near the
   top would be off screen: veil the stale rows + a fixed top bar, the same
   shape as #metrics_table and #sales-kanban. With Reduce Motion on the bar
   stands still, full width, and still says the page is coming. */
turbo-frame#visit_form_template_entries { display: block; position: relative; }
turbo-frame#visit_form_template_entries[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#visit_form_template_entries[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;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame#visit_form_template_entries[busy]::before { animation: none; }
}

/* Trip logbook, /logbook/trips — each trip row is its own frame (dom_id(trip),
   "trip_<uuid>") that swaps to its edit form and back. Short panel shape: veil
   the row and spin at its right edge, where the Edit / Save button was. */
turbo-frame[id^="trip_"] { display: block; position: relative; }
turbo-frame[id^="trip_"][busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 5;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame[id^="trip_"][busy]::before {
  content: ""; position: absolute; top: 18px; right: 24px; z-index: 6;
  width: 20px; height: 20px; border-radius: 50%;
  border: 2.5px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}

/* Location dashboard, the frames whose body loads the first time it is
   shown: Map & assets → Assets (`location_assets_list`), Contacts & email
   (`people_body`) and the Tasks card's History (`task_history`). Each frame
   holds a static "Loading…" line until then; [busy] adds a small spinner
   beside it, like the map sidebar's feature-list-page-N frames above, so a
   slow fetch reads as working. Keyed on the loading line rather than a
   veil: each frame is fetched once, and once the body is in there is
   nothing stale to cover. */
turbo-frame#location_assets_list[busy] .location-frame-loading::before,
turbo-frame#people_body[busy] .location-frame-loading::before,
turbo-frame#task_history[busy] .location-frame-loading::before {
  content: ""; display: inline-block; vertical-align: -2px; margin-right: 6px;
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame#location_assets_list[busy] .location-frame-loading::before,
  turbo-frame#people_body[busy] .location-frame-loading::before,
  turbo-frame#task_history[busy] .location-frame-loading::before { animation: none; }
}

/* Client Hub. The Locations card loads the client's own Locations list into
   `feature_index_embed` as the page opens and holds a "Loading locations…"
   line until it lands; [busy] adds the same small spinner as the location
   dashboard's cards above. Filters inside the embed run in its
   #feature-list-frame, which has its own overlay. */
turbo-frame#feature_index_embed[busy] .location-frame-loading::before {
  content: ""; display: inline-block; vertical-align: -2px; margin-right: 6px;
  width: 11px; height: 11px; border-radius: 50%;
  border: 2px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame#feature_index_embed[busy] .location-frame-loading::before { animation: none; }
}

/* Client Hub activity rail: "Show older activity" re-renders the whole
   `client_activity_frame` at a wider window. The link sits at the foot of a
   long rail, so a veil or a spinner at the top would be off screen; the link
   itself fades and says "Loading…", like .earlier-rows on the location
   dashboard. */
.activity-older .activity-older-busy { display: none; }
turbo-frame#client_activity_frame[busy] .activity-older { opacity: 0.5; pointer-events: none; }
turbo-frame#client_activity_frame[busy] .activity-older .activity-older-idle { display: none; }
turbo-frame#client_activity_frame[busy] .activity-older .activity-older-busy { display: inline; }

/* Papertrail. The location picker (documents/_location_picker, on the upload
   modal and the review desk) re-points its results frame on every keystroke
   and loads it lazily the first time the box is opened; without this the old
   rows sit there and a slow search reads as "no change". Veil + a spinner
   near the top, like the toolbar search above: the dropdown scrolls inside a
   max-height panel, so a centred spinner can sit below the fold. The frame id
   carries a per-picker suffix, hence the prefix match.

   The Date range dropdown's calendar is the twin of the Visit Dashboard's
   period calendar: its month arrows replace just this frame. Short panel,
   so veil + centred spinner. The All Files list (`papertrail_files`) has no
   rule on purpose: every Papertrail control is a full Turbo Drive visit (see
   documents/papertrail/_nav), so nothing navigates that frame and [busy]
   never fires on it. */
turbo-frame[id^="location_picker_results_"] { display: block; position: relative; }
turbo-frame#papertrail_daterange_calendar { display: block; position: relative; }
turbo-frame[id^="location_picker_results_"][busy] { min-height: 64px; }
turbo-frame[id^="location_picker_results_"][busy]::after,
turbo-frame#papertrail_daterange_calendar[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame[id^="location_picker_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;
}
turbo-frame#papertrail_daterange_calendar[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: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame[id^="location_picker_results_"][busy]::before,
  turbo-frame#papertrail_daterange_calendar[busy]::before { animation: none; }
}

/* User Settings. My Signature (preferences/_signature) is one frame, and Save
   replaces it with the saved card; until then the pad sits there unchanged.
   Veil + centred spinner, so a slow save reads as working and can't be sent
   twice.

   My Terms. The add box (commonly_used_terms/_form) is keyed on the form, not
   on its `term_form_<owner>` frame: CommonlyUsedTermsController#create
   replaces that frame with a bare form, so from the second add on there is no
   frame to go [busy]. Turbo sets aria-busy on the submitting form itself every
   time. A removed chip is its own frame (dom_id(term)), so it fades while the
   delete runs. The list frame `terms_list_<owner>` gets no rule: nothing
   navigates it, it is only a stream target. The add form and the chips are
   shared with the location peek's terms board, the field-note report panel
   and the mobile preferences page, so they fade there too. */
turbo-frame#user_signature { position: relative; }
turbo-frame#user_signature[busy]::after {
  content: ""; position: absolute; inset: 0; z-index: 20;
  background: rgba(255, 255, 255, 0.6);
}
turbo-frame#user_signature[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: 3px solid var(--brand-neutral-200); border-top-color: var(--brand-primary);
  animation: tf-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  turbo-frame#user_signature[busy]::before { animation: none; }
}
form.terms-add-form[aria-busy="true"],
turbo-frame[id^="commonly_used_term_"][busy] { opacity: 0.5; pointer-events: none; }
/* The standard brand loader (shared/_brand_loader): bars rise left to right
   and leave in the same order, so the loop never snaps back to empty — each
   bar is already faded out when it resets. The stagger is the per-bar
   animation-delay in the partial. Same motion language as Contour's own
   animated mark (brand/contour/contour-mark-load.svg), at loader speed. */
.brand-loader rect {
  fill: var(--brand-primary);
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: brand-loader-bar 1.8s infinite both;
}

@keyframes brand-loader-bar {
  0%      { transform: scaleY(0); opacity: 1; animation-timing-function: cubic-bezier(0.2, 0, 0, 1); }
  28%     { transform: scaleY(1); opacity: 1; }
  68%     { transform: scaleY(1); opacity: 1; animation-timing-function: ease-in-out; }
  84%, to { transform: scaleY(1); opacity: 0; }
}

/* Motion off: the bars, standing. */
@media (prefers-reduced-motion: reduce) {
  .brand-loader rect { animation: none; }
}
/* Audio Briefings player (app/views/teams/dashboard/_briefings_player.html.erb),
   the Visit Dashboard's card: a player hero that plays the soonest upcoming
   visit's briefing, then one row per visit. The .bw-frame is an inline-size
   container, so the player lays itself out from its OWN width: wide, it shows
   the "Next up" kicker, the row meta line, and the full transcript label; narrow,
   it drops those for a denser single-line read. (The bw- prefix is from the home
   board's "briefings widget", where this started.)

   All values are --pm-* tokens (see pm_tokens.css) except small icon/element
   sizing in px. 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; font-size: 11px; 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: 11px; line-height: 1;
  color: var(--pm-fg-3); width: 34px; 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: 11px; 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: 11px; 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; }
}

/* ── Split (the Visit Dashboard's full-width card) — list beside the player ──
   _briefings_player with split: true wraps the player and the list in
   .bw-split. Wide enough, they sit side by side and the divider between them
   is the gap; narrower, they stack. */
@container (min-width: 48rem) {
  .bw-split {
    display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--pm-space-xl); align-items: start;
  }
  .bw-split .bw-hr { display: none; }
}
/* 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;
}
/* app/assets/stylesheets/card_dock.css
   Location dashboard (map_features/show) — the user decides which cards get
   the room. Driven entirely by the data attributes card_dock_controller.js
   (and, on first paint, map_features/show.html.erb) set; see the controller
   header for the state model.

   TWO MODES, split by media query so they can never both apply:

   LARGE  (min-width 1024px AND min-height 560px) — the five-card grid; focus
          1–3 cards to show them side by side, the rest fold into a dock row.
   SMALL  (narrower than 1024px OR shorter than 560px) — five cards don't fit,
          and a stacked page means scrolling past four to reach one. The cards
          become big icon tiles under the strip; tapping one opens it below a
          row of small icon boxes, filling the rest of the window, with the
          app's own navigation left in place. "All cards" goes back. SMALL mode is gated on [data-card-dock-ready], which only the
          controller sets: if the JS never runs, the page falls back to the
          plain stacked cards instead of tiles that can't be tapped.

   The 560px height floor is deliberate: a 1366×768 laptop has ~650px of
   viewport and should keep the grid; a landscape phone should not.

   Selectors are two+ attributes/classes deep so they out-rank the single-
   class Tailwind utilities on the same elements by specificity rather than by
   stylesheet load order.

   ⚠️ Every rule that HIDES grid content must be scoped to
   [data-card-dock-expanded] (something is focused) or [data-card-dock-ready]
   (the controller is running). An unscoped hiding rule applies in the default
   state and blanks the page — that shipped once.
   test/lib/card_dock_css_guard_test.rb enforces this. */

.card-dock-toggle,
.card-dock-min,
.card-dock-grip,
.card-dock-resize,
.card-dock-rowresize,
.card-dock-open,
.card-dock-unmax,
.card-dock-bar { display: none; }

/* Shared look for the focus button and dock items, whichever mode shows them.
   The focus button is pinned to the card's top-right corner (it is rendered
   in the card's wrapper, not its header) and sits level with the header's
   title row. */
[data-card-dock-target~="card"] { position: relative; container: dock-card / inline-size; }
/* An optional card (Contacts & email) is only ever on the page when focused:
   it isn't in the default grid, and is added from the dock. */
[data-card-dock-target~="card"][data-card-dock-optional]:not([data-card-dock-state="focus"]) { display: none; }

/* A card too narrow for labelled header buttons (a thin column in a custom
   layout) shows them icon-only — same buttons, same actions, the label is
   still in each button's title/aria text where one exists. Keyed on the
   CARD's width via the container query, not the window's. */
@container dock-card (max-width: 320px) {
  .card-dock-head > :last-child:not(:first-child) { flex-wrap: wrap; justify-content: flex-end; min-width: 0; }
  /* Not tabs (Map | Assets, Contacts | Email): a tab IS its label. */
  .card-dock-head > :last-child:not(:first-child) :is(a, button):not([role="tab"]) { font-size: 0; gap: 0; padding-inline: 7px; }
  /* Icon-only buttons drop their dropdown ▾ (Metrics' Download). */
  .card-dock-head .dl-chev { display: none; }
}
.card-dock-toggle {
  position: absolute; top: 13px; right: 10px; z-index: 2;
  align-items: center; justify-content: center; flex: none;
  width: 26px; height: 26px; border-radius: 8px;
  color: var(--brand-neutral-400); background: transparent;
  transition: background-color 120ms, color 120ms;
}
.card-dock-min {
  position: absolute; top: 13px; right: 38px; z-index: 2;
  align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 8px;
  color: var(--brand-neutral-400); background: transparent;
}
.card-dock-toggle:hover,
.card-dock-min:hover { background: var(--brand-neutral-100); color: var(--brand-primary-dark); }
.card-dock-min:focus-visible,
.card-dock-toggle:focus-visible,
.card-dock-item:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 1px; }

.card-dock-item {
  display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px;
  border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; flex: none;
  color: var(--brand-neutral-600); background: #fff;
  border: 1px solid var(--brand-neutral-200);
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.card-dock-item:hover { color: var(--brand-primary-dark); border-color: var(--brand-neutral-300); }
.card-dock-item[aria-checked="true"] {
  color: #fff; background: var(--brand-primary-dark); border-color: var(--brand-primary-dark);
}
.card-dock-restore {
  display: inline-flex; align-items: center; gap: 6px; flex: none;
  margin-left: auto; font-size: 12px; font-weight: 600; color: var(--brand-neutral-500);
  padding: 0 8px; height: 32px; white-space: nowrap;
}
.card-dock-restore:hover { color: var(--brand-primary-dark); text-decoration: underline; }

/* ══════════════════════════════ LARGE ══════════════════════════════ */
@media (min-width: 1024px) and (min-height: 560px) {
  /* Only inside a card-dock page — a button with no controller above it
     would do nothing. */
  [data-controller~="card-dock"] .card-dock-toggle { display: inline-flex; }
  [data-card-dock-ready] .card-dock-grid .card-dock-min { display: inline-flex; }
  [data-card-dock-expanded][data-card-dock-count="1"] .card-dock-grid .card-dock-min { display: none; }

  /* Card headers: keep the corner clear for the focus button, and when the
     card is too narrow for title + actions on one line, drop the actions to a
     second line instead of pushing the card wider than its column. */
  .card-dock-grid .card-dock-head { padding-right: 44px; flex-wrap: wrap; row-gap: 8px; }
  /* The title group stays on one line and, in a very narrow column, trims
     its count rather than pushing past the card's edge. */
  .card-dock-grid .card-dock-head > :first-child { white-space: nowrap; min-width: 0; max-width: 100%; overflow: hidden; }
  .card-dock-grid .card-dock-head > :last-child:not(:first-child) { margin-left: auto; }

  /* A grid track sized `auto` grows to its content's min-content width, so a
     crowded header used to widen its card past the column and over the
     neighbouring card. Every track here is minmax(0, …) and every card may
     shrink below its content. */
  .card-dock-grid > [data-card-dock-target~="column"] { grid-template-columns: minmax(0, 1fr); min-width: 0; }
  .card-dock-grid [data-card-dock-target~="card"] { min-width: 0; }

  /* The dock only when something is actually put away. With every card on
     the page (data-card-dock-full) there is nothing to dock,
     and the page should look like the normal view, just in the user's
     arrangement: no icon row, no presets. */
  [data-card-dock-expanded]:not([data-card-dock-full]) > .card-dock-bar {
    display: flex; align-items: center; gap: 6px; flex: none; flex-wrap: wrap;
    margin-bottom: 12px;
  }

  /* The layout itself: columns and rows from card_dock_placement /
     card_dock_controller#_placement, written as CSS variables so they apply
     only here — a small window ignores them. */
  /* Vertical spacing comes from an 8px margin on every card, not the grid's
     row gap: a card spanning several row tracks would otherwise swallow the
     gaps between them and come out taller than a card spanning one, so
     resizing rows in one column nudged the cards in the next. */
  [data-card-dock-expanded] .card-dock-grid {
    grid-template-columns: var(--cd-cols, minmax(0, 1fr));
    grid-template-rows: var(--cd-rows, minmax(0, 1fr));
    row-gap: 0;
  }

  /* The layout columns step out of the way so their cards become grid items
     directly — that is what lets any card sit in any column. Field visits is
     its own column AND its own card, so it keeps its box. */
  [data-card-dock-expanded] .card-dock-grid > [data-card-dock-target~="column"]:not([data-card-dock-target~="card"]) {
    display: contents;
  }

  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="docked"] { display: none; }

  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"] {
    grid-column: var(--cd-col, auto);
    grid-row: var(--cd-row, auto);
    max-height: none; /* tasks carries a 40vh cap in the default grid */
    min-height: 0;
    margin-block: 8px;
  }

  /* ── Too small to use → compact box; "Open" maximizes ───────────────────
     Inside a column layout a card's height comes from the grid track, never
     its content, so it can take SIZE containment and be queried on its own
     height. (Not in the default grid: its tasks row is content-sized, and
     size containment would collapse it to nothing.) */
  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"] { container: dock-card / size; }

  /* Maximize (the ⤢ corner button, or "Open" on a compact card): one card
     fills the grid, from the default grid or any custom layout. The layout
     columns step aside, every other card hides, and the dock row goes too —
     restore (⤡) or Esc brings it all back. */
  [data-card-dock-ready][data-card-dock-maximized] .card-dock-grid {
    /* One column per widget in full view (card_dock_controller#_applyMax). */
    grid-template-columns: repeat(var(--cd-max-cols, 1), minmax(0, 1fr));
    grid-template-rows: minmax(0, 1fr);
  }
  [data-card-dock-ready][data-card-dock-maximized] .card-dock-grid > [data-card-dock-target~="column"]:not([data-card-dock-target~="card"]) { display: contents; }
  [data-card-dock-ready][data-card-dock-maximized] .card-dock-grid [data-card-dock-target~="card"]:not([data-card-dock-max]) { display: none; }
  [data-card-dock-ready][data-card-dock-maximized] .card-dock-grid [data-card-dock-max] {
    grid-column: auto; grid-row: 1 / -1; max-height: none; min-height: 0;
  }
  /* A put-away widget opened full-size (card_dock_controller#openFull —
     the strip's Briefing button) shows despite its docked state. */
  [data-card-dock-ready][data-card-dock-maximized] .card-dock-grid [data-card-dock-max][data-card-dock-state="docked"] { display: block; }
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-toggle,
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-min,
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-grip,
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-resize,
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-rowresize { display: none; }
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-unmax {
    display: inline-flex; align-items: center; justify-content: center;
    position: absolute; top: 13px; right: 10px; z-index: 3;
    width: 26px; height: 26px; border-radius: 8px;
    color: var(--brand-primary-dark); background: var(--brand-neutral-100);
  }

  /* ── Drag to rearrange (card_dock_controller#dragStart…drop) ─────────── */
  /* Grips on every focused card, and on every card of the default grid once
     the controller is running (dragging there converts it to a layout). */
  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"] .card-dock-grip,
  [data-card-dock-ready]:not([data-card-dock-expanded]) .card-dock-grid .card-dock-grip {
    display: inline-flex; align-items: center; justify-content: center;
    position: absolute; top: 13px; right: 66px; z-index: 2;
    width: 22px; height: 26px; border-radius: 7px; cursor: grab;
    color: var(--brand-neutral-300);
  }
  .card-dock-grid .card-dock-grip:hover {
    color: var(--brand-primary-dark); background: var(--brand-neutral-100);
  }
  /* Room for grip + minimize + maximize once the controller is running. */
  [data-card-dock-ready] .card-dock-grid .card-dock-head { padding-right: 96px; }
  [data-card-dock-expanded] .card-dock-item { cursor: grab; }

  /* Drop preview — a tinted band on the side the card will land. */
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop]::after {
    content: ""; position: absolute; z-index: 5; pointer-events: none;
    border-radius: 12px; background: rgb(var(--brand-primary-rgb) / 0.16);
    border: 2px dashed var(--brand-primary);
  }
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop="swap"]::after  { inset: 0; }
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop="left"]::after  { top: 0; bottom: 0; left: 0; width: 40%; }
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop="right"]::after { top: 0; bottom: 0; right: 0; width: 40%; }
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop="above"]::after { left: 0; right: 0; top: 0; height: 45%; }
  [data-card-dock-dragging] .card-dock-grid [data-card-dock-drop="below"]::after { left: 0; right: 0; bottom: 0; height: 45%; }
  [data-card-dock-dragging] > .card-dock-bar {
    outline: 2px dashed var(--brand-neutral-300); outline-offset: 4px; border-radius: 12px;
  }

  /* Column divider: the left edge of any card not in the first column. It
     sits in the grid gap, so it doesn't cover the card's content. */
  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"]:not([data-card-dock-col="0"]) .card-dock-resize {
    display: block; position: absolute; z-index: 3;
    /* -8px top and bottom reaches through the card's margin, so the
       dividers of stacked cards meet into one bar down the whole column. */
    top: -8px; bottom: -8px; left: -12px; width: 10px;
    cursor: col-resize;
  }
  /* One bar per column: hovering any card's divider, or dragging it, lights
     every divider in that column (the controller marks the dragged column). */
  .card-dock-grid:has([data-card-dock-col="1"] .card-dock-resize:hover) [data-card-dock-col="1"] .card-dock-resize,
  .card-dock-grid:has([data-card-dock-col="2"] .card-dock-resize:hover) [data-card-dock-col="2"] .card-dock-resize,
  .card-dock-grid:has([data-card-dock-col="3"] .card-dock-resize:hover) [data-card-dock-col="3"] .card-dock-resize,
  .card-dock-grid:has([data-card-dock-col="4"] .card-dock-resize:hover) [data-card-dock-col="4"] .card-dock-resize,
  [data-card-dock-resizing-col="1"] .card-dock-grid [data-card-dock-col="1"] .card-dock-resize,
  [data-card-dock-resizing-col="2"] .card-dock-grid [data-card-dock-col="2"] .card-dock-resize,
  [data-card-dock-resizing-col="3"] .card-dock-grid [data-card-dock-col="3"] .card-dock-resize,
  [data-card-dock-resizing-col="4"] .card-dock-grid [data-card-dock-col="4"] .card-dock-resize {
    background: rgb(var(--brand-primary-rgb) / 0.35);
  }
  [data-card-dock-resizing] { cursor: col-resize; user-select: none; }

  /* Row divider: the top edge of any card stacked under another in its
     column. Sits in the grid's row gap, like the column divider. */
  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"]:not([data-card-dock-row="0"]) .card-dock-rowresize {
    display: block; position: absolute; z-index: 3;
    left: 0; right: 0; top: -12px; height: 10px;
    cursor: row-resize; border-radius: 5px;
  }
  [data-card-dock-expanded] .card-dock-grid .card-dock-rowresize:hover,
  [data-card-dock-resizing="row"] .card-dock-grid .card-dock-rowresize {
    background: rgb(var(--brand-primary-rgb) / 0.35);
  }
  [data-card-dock-resizing="row"] { cursor: row-resize; }

  /* The dock's presets (2+ cards; "1 + 2 columns" only with 4+) and the "+"
     on cards that can still be added. */
  .card-dock-arrange-group { display: none; }
  [data-card-dock-expanded]:not([data-card-dock-count="1"]) .card-dock-arrange-group {
    display: inline-flex; align-items: center; gap: 2px; padding: 2px; margin-left: auto;
    border-radius: 9px; background: var(--brand-neutral-100);
  }
  [data-card-dock-count="2"] .card-dock-arrange[data-card-dock-preset-param="main2"],
  [data-card-dock-count="3"] .card-dock-arrange[data-card-dock-preset-param="main2"] { display: none; }
  .card-dock-arrange {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 26px; border-radius: 7px; color: var(--brand-neutral-500);
  }
  .card-dock-arrange:hover { color: var(--brand-primary-dark); background: #fff; box-shadow: var(--brand-shadow-button); }
  /* With the presets showing they take the auto margin and "All cards" sits
     right after them; with one card they are hidden and "All cards" keeps
     its own auto margin. */
  [data-card-dock-expanded]:not([data-card-dock-count="1"]) .card-dock-restore { margin-left: 8px; }
  [data-card-dock-expanded] .card-dock-item[aria-checked="false"]::after {
    content: "+"; font-weight: 700; color: var(--brand-primary); margin-left: 2px;
  }
}

@media (min-width: 1024px) and (min-height: 560px) {
  /* After the dock rules on purpose: same specificity, later wins. */
  [data-card-dock-ready][data-card-dock-maximized] > .card-dock-bar { display: none; }
}

/* Field visits rows (field_notes/_field_note_row). Photos sit on the right
   of the text; how many depends on the CARD's width, so the words are never
   what gets squeezed out:
     < 360px   no photos           · 360-639px   the first photo, 44px
     >= 640px  up to 4, 64px, beside the title (fixed 300px text block);
               the two-line summary (.fn-rich) on its own line below
   Sizes are set here rather than with Tailwind classes: Tailwind loads after
   this file, so its display/size utilities on the same elements would win. */
/* Tasks header: in a narrow card "6 open · 2 late" plus the + button didn't
   fit beside the corner controls, so the word "open" goes first —
   "TASKS 6 · 2 late". */
@container dock-card (max-width: 299.98px) {
  .tasks-open-word { display: none; }
}

/* Tasks: a hairline between rows in a group (scheduled_reminder_v2 rows
   are plain rows now, not bordered boxes). */
.task-list > * + * { border-top: 1px solid var(--brand-neutral-200); }

/* A hairline between visits in the same month (a month header already
   separates months, so the first row under one gets no line), so each row
   reads as its own item. */
.fn-item + .fn-item > .fn-row { border-top: 1px solid var(--brand-neutral-200); }
/* The same line on the first row of a "Show N earlier" page (v2/_earlier_frame):
   it sits inside the frame, so it is no sibling of the row above it. A page
   that opens with a month header gets no line, as above. */
.earlier-rows > .fn-item:first-child > .fn-row { border-top: 1px solid var(--brand-neutral-200); }
.fn-rich { display: none; }
.fn-photos { display: flex; gap: 6px; flex: none; }
/* A <button>: the radius is for its focus ring, which follows the photo's corners. */
.fn-photo { position: relative; display: block; width: 44px; height: 44px; flex: none; border-radius: 8px; }
.fn-photo img { width: 100%; height: 100%; border-radius: 8px; object-fit: cover; background: var(--brand-neutral-100); }
.fn-photo-more { display: none; }
.fn-photo-count {
  position: absolute; inset: 0; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-size: 13px; font-weight: 600; color: #fff; background: rgb(0 0 0 / 0.45);
}
@container dock-card (max-width: 359.98px) {
  .fn-photos { display: none; }
}
@container dock-card (min-width: 640px) {
  .fn-rich { display: block; }
  .fn-photo { width: 64px; height: 64px; }
  .fn-photo-more { display: block; }
  .fn-row { padding-block: 12px; }
  /* Title block fixed-width, so every row's photos start at the same x,
     right beside the title; the date keeps the far right. Two-class
     selectors so Tailwind's flex-1 on the same element can't win. */
  .fn-top > .fn-text { flex: 0 0 300px; }
  .fn-top > .fn-actions { margin-left: auto; }
}

/* Icon view — only reachable where cards have SIZE containment (a column
   layout on a large window, above). A card a layout makes too small to use
   (under 125px tall — just above where a drag STOPS a card, ~108px, so
   shrinking goes window → icon view at the minimum → snap shut on further
   push (Richard, 2026-10-09) — or under 180px wide: a four-column layout's
   narrowest column is ~200px and must stay a window) shows just its icon and name, like the
   dock and the small-window tiles — no "Open …" text. Clicking it maximizes
   the card. Top right keeps only the move grip and minimize; the ⤢ is
   redundant with clicking the tile. */
@container dock-card (max-height: 124.98px) or (max-width: 179.98px) {
  .pm-card { visibility: hidden; }
  [data-card-dock-expanded] [data-card-dock-target~="card"] .card-dock-toggle { display: none; }
  .card-dock-grip, .card-dock-min { z-index: 4; }
  .card-dock-open {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
    position: absolute; inset: 0; z-index: 2;
    border-radius: 14px; font-size: 13px; font-weight: 600; text-align: center;
    color: var(--brand-primary-dark); background: #fff;
    border: 1px solid var(--brand-neutral-200); box-shadow: var(--brand-shadow-card);
  }
  .card-dock-open:hover { background: var(--brand-neutral-50); }
}
/* Very short: icon beside the name instead of above it. */
@container dock-card (max-height: 109.98px) {
  .card-dock-open { flex-direction: row; gap: 10px; }
  .card-dock-open svg { width: 20px; height: 20px; }
}

/* ══════════════════════════════ SMALL ══════════════════════════════ */
@media (max-width: 1023.98px), (max-height: 559.98px) {
  /* No window controls here — the open card already fills the window, and
     "All cards" is the way back. */
  [data-card-dock-ready] .card-dock-grid .card-dock-head { flex-wrap: wrap; row-gap: 8px; }
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :last-child:not(:first-child) { margin-left: auto; }
  .card-dock-arrange-group { display: none; }

  /* ── Tiles: nothing open ─────────────────────────────────────────────── */
  [data-card-dock-ready]:not([data-card-dock-expanded]) > .card-dock-grid { display: none; }

  [data-card-dock-ready]:not([data-card-dock-expanded]) > .card-dock-bar {
    display: grid; gap: 12px;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  }
  [data-card-dock-ready]:not([data-card-dock-expanded]) .card-dock-item {
    flex-direction: column; justify-content: center; gap: 10px;
    height: 120px; padding: 12px; border-radius: 16px;
    font-size: 14px; white-space: normal; text-align: center;
    color: var(--brand-primary-dark); box-shadow: var(--brand-shadow-card);
  }
  [data-card-dock-ready]:not([data-card-dock-expanded]) .card-dock-item svg { width: 32px; height: 32px; }
  [data-card-dock-ready]:not([data-card-dock-expanded]) .card-dock-restore { display: none; }

  /* ── One card open, inside the normal page ────────────────────────────
     The page keeps its frame — app sidebar, top bar, identity strip — and
     the card fills what is left of the window below the strip and a compact
     row of icon boxes. (It used to go position:fixed over everything, which
     hid the app's own navigation.) --card-dock-offset is the distance from
     the top of the document to this container, measured by the controller,
     so `100dvh - offset` is exactly the window below the app chrome. On a
     phone whose strip is tall the grid keeps a 300px floor and the page
     scrolls a little instead of squeezing the card to nothing. */
  [data-card-dock-ready][data-card-dock-expanded] {
    display: flex; flex-direction: column; min-height: 0;
    height: calc(100dvh - var(--card-dock-offset, 0px));
  }
  [data-card-dock-ready][data-card-dock-expanded] > #section-identity { flex: none; }
  /* Device rows (device features only) would push the card off-screen. */
  [data-card-dock-ready][data-card-dock-expanded] > :not(.card-dock-bar):not(.card-dock-grid):not(#section-identity) { display: none; }

  /* The dock as a row of small icon boxes — the same boxes as the tiles,
     just smaller, so the page keeps one look whether a card is open or not. */
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar {
    display: flex; align-items: stretch; gap: 8px; flex: none;
    margin-bottom: 12px; overflow-x: auto; padding-bottom: 8px;
    /* A visible slider under the icons (Richard, 2026-10-09): in a narrow
       window the row runs off the right edge, and a hidden scrollbar gave no
       hint there was more. Thin, brand-tinted, always shown. */
    scrollbar-width: thin; scrollbar-color: rgb(var(--brand-primary-rgb) / 0.45) var(--brand-neutral-100);
  }
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar::-webkit-scrollbar { height: 6px; }
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar::-webkit-scrollbar-track { background: var(--brand-neutral-100); border-radius: 999px; }
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar::-webkit-scrollbar-thumb { background: rgb(var(--brand-primary-rgb) / 0.45); border-radius: 999px; }
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar::-webkit-scrollbar-thumb:hover { background: var(--brand-primary); }
  /* Boxes keep their size and scroll, rather than squeezing. */
  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-bar > *,
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-items > * { flex-shrink: 0; }
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-item,
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-restore {
    flex-direction: column; justify-content: center; gap: 4px;
    height: 58px; min-width: 76px; padding: 6px 10px; border-radius: 12px;
    /* 11px, the HIG's text floor. Leading 1.1 keeps a two-line label as
       tall as it was at 10.5px and 1.15, so the box needn't grow. */
    font-size: 11px; line-height: 1.1; white-space: normal; text-align: center;
  }
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-item svg,
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-restore svg { width: 18px; height: 18px; }
  /* "All cards" reads as the back button here, so it leads, boxed like the rest. */
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-restore {
    order: -1; margin-left: 0; text-decoration: none;
    background: var(--brand-neutral-100); color: var(--brand-primary-dark);
  }

  [data-card-dock-ready][data-card-dock-expanded] > .card-dock-grid {
    display: grid; flex: 1 1 0%; min-height: 300px; gap: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
  }
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-grid > [data-card-dock-target~="column"]:not([data-card-dock-target~="card"]) {
    display: contents;
  }
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-grid [data-card-dock-state="docked"] { display: none; }
  /* One card at a time here — the layout's lead card (first column, top).
     Only reachable with several focused when a large window is narrowed. */
  [data-card-dock-ready][data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"]:not([data-card-dock-lead]) { display: none; }

  [data-card-dock-ready][data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"] {
    grid-column: 1; grid-row: 1; min-height: 0; max-height: none;
  }

  /* The card partials are built to FILL a grid cell from `lg` up, and fall
     back to capped, page-scrolling boxes below it. Below lg the open card
     needs the lg behaviour, so the handful of sizing utilities the partials
     use are re-applied inside it. Layout only: nothing here shows or hides. */
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:flex     { display: flex; }
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:flex-col { flex-direction: column; }
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:flex-1   { flex: 1 1 0%; }
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:min-h-0  { min-height: 0; }
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:h-full   { height: 100%; }
  [data-card-dock-ready][data-card-dock-expanded] [data-card-dock-state="focus"] .lg\:max-h-none { max-height: none; }
}

/* ── Map & assets card: the asset popup (entity_map_controller#showPopup) ──
   A small card instead of Mapbox's default box: the asset type's emoji in a
   tile, the identifier, then the type. No tip and no ×: it sits just above
   the pin, or above an area's top edge so the shape stays visible. Two-class
   selectors so they win over Mapbox's own stylesheet whatever the load order. */
.asset-popup .mapboxgl-popup-content {
  padding: 8px 12px 8px 8px;
  border-radius: 12px;
  border: 1px solid var(--brand-neutral-200);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
  font-family: inherit;
}
.asset-popup .mapboxgl-popup-tip { display: none; }
.asset-pop { display: flex; align-items: center; gap: 10px; }
.asset-pop-icon {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--brand-primary-surface);
  font-size: 19px; line-height: 1;
}
.asset-pop-text { min-width: 0; }
.asset-pop-name { font-size: 13px; font-weight: 700; color: var(--pm-deep); line-height: 1.25; overflow-wrap: anywhere; }
.asset-pop-type { margin-top: 1px; font-size: 11px; color: var(--brand-neutral-500); line-height: 1.3; }

/* ── Activity card rows (map_features/sections/_timeline_content_v2) ──
   A small icon tile per type and a hairline between consecutive rows (not
   after a month header — the header's own rule is the divider there). */
.act-icon {
  flex: none; display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; margin-top: 1px; border-radius: 8px;
  background: var(--brand-neutral-100); color: var(--brand-neutral-600);
}
.act-row + .act-row { border-top: 1px solid var(--brand-neutral-200); }

/* ── Snap to close (card_dock_controller#_armSnap) ──
   Dragging a row divider well past a card's minimum arms it: the card fades
   behind a dashed outline and says what releasing will do. Releasing puts
   it away (same as its – button); dragging back disarms. */
[data-card-dock-snap] > * { opacity: 0.12; transition: opacity 120ms ease; }
[data-card-dock-snap]::before {
  content: ""; position: absolute; inset: 8px 0; z-index: 30;
  border: 2px dashed var(--brand-neutral-400); border-radius: 14px;
  pointer-events: none;
}
[data-card-dock-snap]::after {
  content: "Release to minimise";
  position: absolute; z-index: 31; top: 50%; left: 50%; transform: translate(-50%, -50%);
  padding: 5px 12px; border-radius: 999px; white-space: nowrap;
  background: #ffffff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
  font-size: 12px; font-weight: 600; color: var(--brand-neutral-700);
  pointer-events: none;
}

/* ── Metrics card: a metric's readings table (metrics/_readings_table) ──
   Sized to show 10 rows (28px header + 10 × 28px rows, measured, + 2px
   border); more scroll. Drag the bottom-right corner to make it taller or
   shorter. */
.metric-table-box { height: calc(28px + 10 * 28px + 2px); min-height: 7rem; max-height: 80vh; resize: vertical; }
/* A metric's chart box: drag the bottom-right corner to resize it (Chart.js
   is responsive, so the chart follows). */
.metric-chart-box { position: relative; height: 170px; min-height: 110px; max-height: 80vh; resize: vertical; overflow: hidden; }

/* ══════════════ The dock as two menus (HIG review, 2026-10-09) ══════════════
   + Add window (the hidden windows) and Layout ▾ (presets + Reset), instead of
   a chip per window. Large windows only; on small windows the wrappers
   dissolve and the items are the tiles, as before. */
.card-dock-add, .card-dock-layout { position: relative; }
.card-dock-menu-btn {
  display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px;
  border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap;
  color: var(--brand-neutral-600); background: #fff; border: 1px solid var(--brand-neutral-200);
  transition: background-color 120ms, color 120ms, border-color 120ms;
}
.card-dock-menu-btn:hover { color: var(--brand-primary-dark); border-color: var(--brand-neutral-300); }
.card-dock-menu-btn:active { background: var(--brand-neutral-100); }
.card-dock-menu-btn:focus-visible { outline: 2px solid var(--brand-primary); outline-offset: 1px; }

@media (min-width: 1024px) and (min-height: 560px) {
  /* Always shown on the dashboard now (it is two small buttons). */
  [data-card-dock-expanded] > .card-dock-bar {
    display: flex; align-items: center; gap: 8px; flex: none; flex-wrap: wrap; margin-bottom: 12px;
  }
  .card-dock-items, .card-dock-layout-menu {
    position: absolute; top: calc(100% + 4px); left: 0; z-index: 60; min-width: 230px;
    display: flex; flex-direction: column; gap: 1px; padding: 4px;
    background: #fff; border: 1px solid var(--brand-neutral-200); border-radius: 12px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.14);
  }
  /* Menu rows: the windows NOT on the page (an open window has its own –). */
  .card-dock-items .card-dock-item {
    width: 100%; justify-content: flex-start; height: 36px; border: 0; border-radius: 8px; background: transparent;
    color: var(--brand-neutral-700);
  }
  .card-dock-items .card-dock-item:hover { background: var(--brand-neutral-50); }
  .card-dock-items .card-dock-item[aria-checked="true"] { display: none; }
  [data-card-dock-expanded] .card-dock-items .card-dock-item[aria-checked="false"]::after { margin-left: auto; }
  /* Nothing hidden → nothing to add. */
  .card-dock-add:not(:has(.card-dock-item[aria-checked="false"])) { display: none; }
  /* Layout menu rows: preset glyph + its name; Reset under a rule. */
  .card-dock-layout-menu .card-dock-arrange {
    width: 100%; height: 36px; justify-content: flex-start; gap: 10px; padding: 0 10px;
    border-radius: 8px; font-size: 12px; font-weight: 600; color: var(--brand-neutral-700); background: transparent; box-shadow: none;
  }
  .card-dock-layout-menu .card-dock-arrange:hover { background: var(--brand-neutral-50); box-shadow: none; }
  .card-dock-layout-rule { height: 1px; margin: 4px 6px; background: var(--brand-neutral-100); }
  /* The tiles' "All windows" is a small-window control. */
  .card-dock-bar > .card-dock-restore { display: none; }
}

@media (max-width: 1023.98px), (max-height: 559.98px) {
  .card-dock-add, .card-dock-items { display: contents !important; }
  .card-dock-menu-btn, .card-dock-layout { display: none !important; }
}

/* ══════════════ 44px hit areas (HIG review, 2026-10-09) ══════════════
   Apple's minimum target is 44×44; on an iPad in the field these controls
   were 22–28px. The visuals stay; an invisible ::before makes the target
   44px tall (and a little wider) around each one. Window controls are
   absolutely positioned already, so they are left as they are; the rest
   get position: relative for the layer to anchor to. */
.hit-44, .fn-actions > a, .card-dock-head [role="tab"],
.card-dock-head [data-action="click->dropdown#toggle"], .card-dock-menu-btn { position: relative; }
.hit-44::before, .fn-actions > a::before, .card-dock-head [role="tab"]::before,
.card-dock-head [data-action="click->dropdown#toggle"]::before, .card-dock-menu-btn::before,
.card-dock-min::before, .card-dock-toggle::before, .card-dock-grip::before {
  content: ""; position: absolute; left: -3px; right: -3px; top: 50%; height: 44px; transform: translateY(-50%);
}

/* ══════════════ No hover-only actions on touch (HIG review) ══════════════
   Delete on a reading or comment, a task's action rail, etc. appear on
   hover — which never happens on a touchscreen, so on an iPad they were
   unreachable. Without hover, show them. */
@media (hover: none) {
  /* Controls only — not decorative hover hints (the task checkbox's tick
     fades in on hover too; forcing it on would make open tasks look done). */
  [data-card-dock-ready] button[class*="group-hover"][class*="opacity-0"],
  [data-card-dock-ready] a[class*="group-hover"][class*="opacity-0"],
  [data-card-dock-ready] [class*="group-hover:pointer-events-auto"] { opacity: 1 !important; pointer-events: auto !important; }
  /* A task's action rail floats over the title's end on hover; shown all
     the time it would cover the text, so on touch it joins the row's flex
     line instead, beside the title at the row's right end (the title
     column narrows to make room). */
  [data-card-dock-ready] [aria-label="Task actions"] {
    position: static; width: max-content; margin: -8px 6px 6px auto; background: transparent !important;
  }
}

/* Under a finger (HIG Buttons: a 44x44 pt hit region) the rail's 24px
   buttons grow to 28px, the HIG's smallest control, and each takes a 44px
   box (touch-hit). 16px apart, so no box lands on the button beside it: the
   Locations rows' peek and view (map_features/_locations_table). 44px-wide
   buttons instead took 40 to 60px from the task's title on a phone. */
@media (pointer: coarse) {
  [data-card-dock-ready] [aria-label="Task actions"] { gap: 16px; }
  [data-card-dock-ready] [aria-label="Task actions"] :is(a, button) { width: 28px; height: 28px; }
}

/* ── Asset view band (v2/_asset_banner, inside the identity strip) ──
   Brand-coloured so "you are looking at one asset" can't be missed. The
   name wraps (anywhere, for long identifiers) — never truncates. */
.asset-band { background: var(--brand-primary); color: #fff; }
/* A real basis, so on a phone "Back to …" drops to its own line instead of
   squeezing the name into a one-word column. break-word (not anywhere) keeps
   whole words together and only splits a word longer than the line. */
.asset-band-body { flex: 1 1 14rem; min-width: 0; }
.asset-band-tag {
  display: inline-flex; align-items: center; height: 18px; padding: 0 7px; border-radius: 999px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap;
  color: var(--brand-primary-dark); background: #fff;
}
.asset-band-name { font-size: 15px; font-weight: 700; line-height: 1.3; color: #fff; overflow-wrap: break-word; }
.asset-band-meta { margin-top: 1px; font-size: 12px; color: rgb(255 255 255 / 0.85); overflow-wrap: break-word; }
.asset-band-back { color: #fff; background: rgb(255 255 255 / 0.14); border: 1px solid rgb(255 255 255 / 0.45); }
.asset-band-back:hover { background: rgb(255 255 255 / 0.24); }
.asset-band-back:active { background: rgb(255 255 255 / 0.32); }

/* ── Large windows: Add widget + Layout INSIDE the strip's row 1 ──────────
   A whole row for two buttons cost ~48px of grid (Richard, 2026-10-09), so
   they sit in the strip's empty space, dark so they're easy to find. The
   bar stays a sibling of the strip (outside its turbo frame); card_dock_
   controller#_placeDock sizes the strip's [data-card-dock-slot] to the bar
   and positions the bar over it. Hidden until placed, so it never flashes
   at the top-left. Small windows keep the icon row (rules above). */
@media (min-width: 1024px) and (min-height: 560px) {
  [data-controller~="card-dock"][data-card-dock-ready] { position: relative; }
  [data-controller~="card-dock"][data-card-dock-ready] > .card-dock-bar {
    position: absolute; z-index: 20; margin: 0;
    display: flex; flex-direction: row; align-items: center; gap: 8px; flex-wrap: nowrap;
  }
  [data-controller~="card-dock"][data-card-dock-ready] > .card-dock-bar:not([data-placed]) { visibility: hidden; }
  [data-controller~="card-dock"][data-card-dock-ready] > .card-dock-bar .card-dock-items,
  [data-controller~="card-dock"][data-card-dock-ready] > .card-dock-bar .card-dock-layout-menu { left: auto; right: 0; }
  .card-dock-menu-btn {
    color: #fff; background: var(--brand-primary-dark); border-color: var(--brand-primary-dark);
  }
  .card-dock-menu-btn:hover { color: #fff; background: var(--brand-primary-active); border-color: var(--brand-primary-active); }
  .card-dock-menu-btn:active { color: #fff; background: var(--brand-accent); }
  .card-dock-menu-btn[aria-expanded="true"] { background: var(--brand-primary-active); }
}
.strip-dock-slot { display: none; }
@media (min-width: 1024px) and (min-height: 560px) {
  .strip-dock-slot { display: block; height: 32px; }
}

/* Narrow widgets (≤320px): the header stays ONE line — title (truncating)
   on the left, icon-only actions on the right. It used to wrap, dropping a
   lone "+" onto its own centred line under the title (Richard, 2026-10-09). */
@container dock-card (max-width: 320px) {
  [data-card-dock-ready] .card-dock-grid .card-dock-head { flex-wrap: nowrap; }
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :first-child { flex: 1 1 0; }
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :last-child:not(:first-child) { flex-wrap: nowrap; flex: none; margin-left: 8px; }
  /* Title and count both shorten with "…" (a flex item won't shrink below
     its text without min-width: 0 — the title was clipped mid-letter). */
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :first-child > * {
    flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :first-child > h2 { min-width: 4ch; }
}

/* Metrics rows open (metrics/_metrics_table_v2): a chevron that turns, and
   the open row tinted so it reads as selected — with its chart below it. */
.metric-item .metric-chev { color: var(--brand-neutral-400); transition: transform 150ms ease, color 150ms ease; }
.metric-item > summary:hover .metric-chev { color: var(--brand-primary); }
.metric-item[open] .metric-chev { transform: rotate(90deg); color: var(--brand-primary); }
.metric-item[open] > summary { background: var(--brand-primary-surface); }
@media (prefers-reduced-motion: reduce) { .metric-item .metric-chev { transition: none; } }
/* Very narrow: a count cut to "2…" reads as broken — drop it, keep the title. */
@container dock-card (max-width: 259.98px) {
  [data-card-dock-ready] .card-dock-grid .card-dock-head > :first-child > :not(h2) { display: none; }
}

/* The strip's Briefing button (v2/_header_strip): a quiet outline, YELLOW
   while a new briefing of yours is ready — the thing to read before a visit
   (Richard, 2026-10-09). Dark text on the highlight: ≥4.5:1. */
.strip-briefing { background: #fff; border: 1px solid var(--pm-stone-200); color: var(--pm-forest); }
.strip-briefing[data-new-briefing] {
  background: var(--brand-highlight); border-color: rgb(var(--brand-highlight-text-rgb) / 0.45); color: var(--brand-accent);
}
.strip-briefing[data-new-briefing]:hover { background: rgb(var(--brand-highlight-rgb) / 0.8); }

/* Email filters (sections/_communications + email_filter_controller): only
   in a full-view widget. Outside it the bar is hidden AND filtered rows show,
   so a filter left set never silently hides mail (Richard, 2026-10-09). */
.email-filters { display: none; }
[data-card-dock-max] .email-filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px;
}
/* Two-class selectors: the app's global input styles (loaded after this
   file) would otherwise give the search box a black border and 16px text. */
.email-filters .email-filter-input, .email-filters .email-filter-select {
  height: 32px; border-radius: 8px; border: 1px solid var(--brand-neutral-200); background-color: #fff;
  font-size: 12.5px; line-height: 30px; color: var(--brand-neutral-700); padding: 0 10px; box-shadow: none;
}
.email-filter-input { flex: 1 1 220px; min-width: 0; }
.email-filter-select { flex: 0 1 auto; padding-right: 28px; }
.email-filters .email-filter-input:focus, .email-filters .email-filter-select:focus { outline: 2px solid rgb(var(--brand-primary-rgb) / 0.4); outline-offset: 0; border-color: var(--brand-primary); }
.email-filter-count { margin-left: auto; font-size: 11.5px; color: var(--brand-neutral-500); }
[data-card-dock-max] .email-row.is-filtered-out { display: none; }
.email-filter-empty { display: none; font-size: 12.5px; color: var(--brand-neutral-500); padding: 8px 2px; }
[data-card-dock-max] .email-filter-empty:not(.hidden) { display: block; }

/* The farm part of a field's name in the strip links to the farm. */
.strip-farm-link { color: inherit; text-decoration: none; border-radius: 4px; }
.strip-farm-link:hover { color: var(--brand-primary); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }

/* The strip's "?" (reopens the page guide): an outline circle with a bold
   question mark, in the About / Briefing pills' register — the faint grey
   glyph it replaced was easy to miss. */
.strip-guide-btn { flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 999px; background: #fff; border: 1.5px solid var(--pm-stone-200);
  color: var(--pm-forest); font-size: 14px; font-weight: 800; line-height: 1; transition: background-color 120ms, border-color 120ms; }
.strip-guide-btn:hover { background: var(--brand-primary-surface); border-color: rgb(var(--brand-primary-rgb) / 0.45); }
.strip-guide-btn:focus-visible { outline: 2px solid var(--pm-forest); outline-offset: 2px; }

/* ── A widget's own "?" (v2/_card_expand_button → its tour) ──────────────
   Large windows: in the corner cluster, between the grip and –, so the grip
   moves out one place and the header reserves room for four. Full view:
   beside the restore button. Small windows: the only corner control. Hidden
   in the icon view (nothing to tour) and in a very narrow widget. */
.card-dock-help { display: none; }
[data-card-dock-ready] .card-dock-help {
  display: inline-flex; align-items: center; justify-content: center;
  position: absolute; top: 15px; right: 68px; z-index: 2;
  width: 22px; height: 22px; border-radius: 999px; background: #fff;
  border: 1.5px solid var(--pm-stone-200); color: var(--pm-forest); font-size: 12px; font-weight: 800; line-height: 1;
}
[data-card-dock-ready] .card-dock-help:hover { background: var(--brand-primary-surface); border-color: rgb(var(--brand-primary-rgb) / 0.45); }
[data-card-dock-ready] .card-dock-help:focus-visible { outline: 2px solid var(--pm-forest); outline-offset: 2px; }
.card-dock-help::before { content: ""; position: absolute; inset: -11px -3px; }
/* Large windows: a tight four-up cluster (⤢ · – · ? · grip, right to
   left, 24px steps) so the header reserves 104px — 8 more than the three
   it had — and the Map & assets / Tasks headers stay on one line. */
@media (min-width: 1024px) and (min-height: 560px) {
  [data-controller~="card-dock"] .card-dock-toggle { right: 8px; width: 24px; }
  [data-card-dock-ready] .card-dock-grid .card-dock-min { right: 32px; width: 24px; }
  [data-card-dock-ready] .card-dock-help { right: 58px; width: 20px; height: 20px; top: 16px; font-size: 11px; }
  [data-card-dock-expanded] .card-dock-grid [data-card-dock-state="focus"] .card-dock-grip,
  [data-card-dock-ready]:not([data-card-dock-expanded]) .card-dock-grid .card-dock-grip { right: 80px; width: 18px; }
  [data-card-dock-ready] .card-dock-grid .card-dock-head { padding-right: 104px; }
  [data-card-dock-maximized] [data-card-dock-max] .card-dock-help { right: 42px; }
}
@media (max-width: 1023.98px), (max-height: 559.98px) {
  [data-card-dock-ready] .card-dock-help { right: 12px; }
  [data-card-dock-ready] .card-dock-grid .card-dock-head { padding-right: 44px; }
}
@container dock-card (max-height: 124.98px) or (max-width: 179.98px) {
  .card-dock-help { display: none !important; }
}
@container dock-card (max-width: 259.98px) {
  .card-dock-help { display: none !important; }
}

/* ── Visit page widgets (field_notes/show_v2) ─────────────────────────────
   Each widget hosts an existing partial that brings its own white card;
   inside a widget that's a box in a box, so the inner chrome goes flat.
   No tour "?" on these widgets: the grip takes its place in the cluster. */
.visit-widget-body .shadow-propel-card, .visit-widget-body .pm-elev-1 { box-shadow: none !important; }
.visit-widget-body > .bg-white, .visit-widget-body > .space-y-4 > .bg-white,
.visit-widget-body > .space-y-4 > div > .bg-white { border-radius: 0 !important; }
/* Forms widget (v2/_forms_panel): flat. The wizard's own card and its
   completed list are for the legacy show page; here v2/_completed_forms lists
   the same forms grouped by place, and the widget is the card. The "done"
   section closes up while there's nothing in it. */
.visit-widget--forms [id^="visit_form_wizard_"] > .bg-white { border: 0 !important; border-radius: 0 !important; }
.visit-widget--forms [data-visit-form-wizard-target="completedFormsSection"] { display: none; }
.visit-widget--forms .visit-forms-done:not(:has(details, .visit-forms-none)) { display: none; }
/* "Start a form" names the list, so it goes once a form is under way. */
.visit-widget--forms section:has([data-visit-form-wizard-target="templateSelectWrapper"].hidden) > h3 { display: none; }
.visit-widget--map .visit-widget-body > div { flex: 1 1 auto; min-height: 0; border-radius: 0; }
/* Checklists widget (_checklist_selector, shared with the legacy show page):
   the widget head already says CHECKLISTS, so the partial's heading goes. */
.visit-widget--checklists .visit-widget-body > .bg-white > h3 { display: none; }
@media (min-width: 1024px) and (min-height: 560px) {
  [data-card-dock-target~="card"]:not(:has(.card-dock-help)) .card-dock-grip { right: 58px !important; }
  .visit-widget .card-dock-head { padding-right: 84px; }
}

/* ── Visit page: the Reminders widget ────────────────────────────────────
   Striking while something is still to do (Richard, 2026-10-15): amber
   with an open reminder, red with an overdue one — the head tinted, a
   coloured edge round the widget, a dot by the title. Driven by the rows'
   data-reminder-state, so it follows the live broadcast. The section's own
   header and card chrome are dropped inside the widget (the widget is the
   card). Done-only or empty: a calm widget with a one-line note. */
.visit-widget--reminders .reminders-empty { display: none; padding: 16px 20px; font-size: 13px; color: var(--pm-stone-500); }
.visit-widget--reminders:not(:has([data-reminder-state])) .reminders-empty { display: block; }
.visit-widget--reminders [id^="field_note_reminders_"] > div { margin: 0 !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; }
.visit-widget--reminders [id^="field_note_reminders_"] > div > div:first-child { display: none; }
.visit-widget--reminders:has([data-reminder-state="open"]) {
  border-color: rgb(var(--brand-highlight-text-rgb) / 0.45);
  box-shadow: 0 0 0 1px rgb(var(--brand-highlight-text-rgb) / 0.25), 0 2px 8px rgb(var(--brand-highlight-text-rgb) / 0.12);
}
.visit-widget--reminders:has([data-reminder-state="open"]) .card-dock-head { background: var(--brand-highlight-surface); }
.visit-widget--reminders:has([data-reminder-state="open"]) .card-dock-head h2 { color: color-mix(in srgb, var(--brand-highlight-text) 82%, black) !important; }
.visit-widget--reminders:has([data-reminder-state="overdue"]) {
  border-color: rgb(var(--brand-danger-rgb) / 0.45);
  box-shadow: 0 0 0 1px rgb(var(--brand-danger-rgb) / 0.25), 0 2px 8px rgb(var(--brand-danger-rgb) / 0.12);
}
.visit-widget--reminders:has([data-reminder-state="overdue"]) .card-dock-head { background: var(--brand-danger-surface); }
.visit-widget--reminders:has([data-reminder-state="overdue"]) .card-dock-head h2 { color: var(--brand-danger-dark) !important; }
.visit-widget--reminders:has([data-reminder-state="open"], [data-reminder-state="overdue"]) .card-dock-head h2::before {
  content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 999px;
  vertical-align: 1px; background: currentColor;
}
.visit-widget--reminders [data-reminder-state="overdue"] { background: rgb(var(--brand-danger-rgb) / 0.04); }
