/* ============================================
   Mobile bottom sheet (#298, Phase 5)
   Shell + detent chrome. Contents are mounted by later steps.

   THE MAP DOES NOT DIM. There is deliberately no scrim rule in this file and
   no use of the overlay dim token: that token stays with modals and the
   drawer. Sheet and map are one instrument, not a modal over a backdrop.

   Geometry note: the element is always `--sheet-full-h` tall and anchored to
   the bottom; a detent is a translateY offset, so only `transform` animates
   (no layout, no repaint of the map beneath). Both custom properties are set
   in px by bottom-sheet.js from a measured viewport — the values here are
   only the pre-JS fallback (fully collapsed, ~92vh tall).
   ============================================ */

.bottom-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-panel);
  display: flex;
  flex-direction: column;
  height: var(--sheet-full-h, 92vh);
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  border-radius: var(--radius-cards) var(--radius-cards) 0 0;
  box-shadow: var(--color-fab-primary-shadow);
  transform: translate3d(0, var(--sheet-offset, 100%), 0);
  /* The sheet owns vertical gestures on its own box; the map keeps everything
     outside it. `contain` stops a fling at the top of the list from becoming a
     document rubber-band on iOS. */
  touch-action: none;
  overscroll-behavior: contain;
  will-change: transform;
}

/* Only transition on a settle, never mid-drag: a transition during the drag
   would lag the finger. House easing (#ease-in-out) per the project rule. */
.bottom-sheet.is-animating {
  transition: transform var(--duration-slow) var(--ease-in-out);
}

.bottom-sheet.is-dragging {
  transition: none;
}

/* Out of the way while the map owns the screen (#488, widened in #581). The
   session controls are pinned to the bottom, and at the peek detent the sheet's
   search input sits over them and wins the hit-test — Finish/Done became
   untappable on a phone, so a route could be drawn but never completed (#488),
   and the pin-verification bar was covered outright, so a place added from a
   photo could never be confirmed (#581). `syncMapBusyClass()` toggles
   `body.map-busy` for the life of a session that takes the screen — click-to-add
   (#584), route/area draw-or-edit, or POI drag/placement — and the sheet slides
   fully out,
   returning to whatever detent it was on when the last one ends
   (`--sheet-offset` is untouched, so removing the class restores it).
   Transitioned here rather than via `.is-animating` because this move isn't a
   detent settle — nothing is being dragged. House easing per the project rule.

   Scoped to the phone breakpoint on purpose: the sheet is the mobile surface,
   and desktop already neutralises its transform, so an unscoped rule would be
   relying on cascade order to stay a no-op there. */
@media (max-width: 767px) {
  body.map-busy .bottom-sheet {
    transform: translate3d(0, 100%, 0);
    transition: transform var(--duration-slow) var(--ease-in-out);
  }
}

/* Grabber: a 4px bar inside a 32px-tall row.
   Was 44px to meet the touch floor on its own. Measured against the reference
   (Instagram's comment sheet: 15px above the bar, 2px bar, 16px below = 33px
   from sheet edge to first content), 44px + the peek row's own box spent an
   eighth of the peek detent on empty space. 32px here + spacing-4 on the row
   below lands at 36px, and the sheet does NOT lose hit area doing it: the drag
   is bound to the whole sheet element and canDragFrom() (bottom-sheet.js)
   accepts any pointerdown inside the content while it is scrolled to top, so
   the grabbable band runs well past this row. */
.bottom-sheet-handle {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  cursor: grab;
  touch-action: none;
  border-radius: var(--radius-cards) var(--radius-cards) 0 0;
}

.bottom-sheet.is-dragging .bottom-sheet-handle {
  cursor: grabbing;
}

.bottom-sheet-grabber {
  width: 36px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-text-faint);
  transition: background var(--duration-fast) var(--ease-in-out);
}

/* Field-guide (dark) theme: --color-text-faint is a near-black there and would
   disappear on the sheet's surface, so step the grabber up one rung. */
[data-theme="dark"] .bottom-sheet-grabber {
  background: var(--color-text-muted);
}

/* Split rather than combined: on touch, `:hover` latches after a tap and would
   leave the grabber darkened for the rest of the session — and this one latches
   on every drag, since a drag begins with a press on the handle. */
@media (hover: hover) {
  .bottom-sheet-handle:hover .bottom-sheet-grabber {
    background: var(--color-text-secondary);
  }
}

.bottom-sheet-handle:focus-visible .bottom-sheet-grabber {
  background: var(--color-text-secondary);
}

/* Mount target for steps 2-4. Scrolls only where there is room to scroll —
   at peek the sheet is a fixed 120px row, so scrolling it would just fight
   the drag gesture. */
.bottom-sheet-content {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  overscroll-behavior: contain;
  /* iOS home-indicator inset. The sheet's own detent math already reserves
     this height at peek (see bottom-sheet-detents.js); this keeps the content
     itself off the indicator at every detent. Read docs/gotchas.md (#52/#86)
     before touching any full-bleed viewport height in this app — the sheet is
     bottom-anchored and height-driven, so it is not subject to that quirk. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* When the settings footer is showing it is the sheet's bottom-most box and
   carries the inset itself (see .sheet-settings-row), so the scroller must give
   its copy up — otherwise the list ends on a band of dead space stacked above
   the footer. `:has()` follows panel.css's existing use of it. */
.bottom-sheet:has(.sheet-settings-row:not([hidden])) .bottom-sheet-content {
  padding-bottom: 0;
}

.bottom-sheet-content.is-scrollable {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}

/* Desktop keeps the right-side detail panel and the pinned library list; the
   sheet is a mobile-only surface (brief: "Untouched: the desktop right-side
   detail panel"). */
@media (min-width: 768px) {
  .bottom-sheet {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bottom-sheet.is-animating {
    transition-duration: 0.01ms;
  }
}

/* ============================================
   Mobile home content (#298 step 2)
   Wired by mobile-home.js — the peek row (search + Filters·N), the relocated
   settings row, and the reparented library list (#list-view).
   ============================================ */

/* Peek row: the ONLY thing meant to show at the peek detent.
   The library content after it is HIDDEN there (rule below), not clipped.
   Clipping was the original design and it cannot work: the sheet is a
   full-height element translated DOWN, so at peek its bottom — and therefore
   contentEl's overflow edge and its safe-area padding — hangs far below the
   screen. Nothing is clipped at the peek fold; the next element simply keeps
   rendering into it. That leaked the list's type-tab strip into peek on any
   device with a home indicator, which is why it survived the browser preview
   and Safari-in-simulator and only showed up in the installed PWA. */
.bottom-sheet[data-detent="peek"][data-content="library"] .list-view {
  display: none;
}
/* Sticky, not scrolling: the search field is how you get at a library of
   hundreds, and it left the screen entirely the moment the list scrolled —
   leaving a cut-off card butting against the grabber with no way back to
   search but scrolling all the way up. Same treatment .sheet-detail-topbar
   already uses for the back affordance, so both of the sheet's contents keep
   one reachable top band.

   padding-top is not decoration: the shared focus ring (base.css) is a 3px
   box-shadow drawn OUTSIDE the input's box, and with a zero top inset the
   input's edge sat exactly on contentEl's edge — overflow clipped the ring's
   top away, so a focused search field read as an open-topped rectangle. */
.sheet-peek-row {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--spacing-8);
  padding: var(--spacing-4) var(--spacing-16) var(--spacing-12);
  background: var(--color-surface);
}

.sheet-peek-row .list-search {
  flex: 1;
  min-width: 0;
  margin-bottom: 0; /* list.css's stacking margin doesn't apply in this row layout */
}

/* iOS zooms the viewport on focusing any input under 16px. The shared
   .list-search input rule (list.css) is 14px (--text-body) for its desktop
   placement, which is never touch-focused there — scoped bump, desktop
   untouched. Also lifts the input to the 44px touch floor for this placement. */
.sheet-peek-row .list-search input {
  font-size: 16px;
  min-height: 44px;
}

.sheet-peek-row .filters-shell-mount {
  flex-shrink: 0;
  margin-top: 0; /* ditto — this mount's list-header stacking margin doesn't apply here */
}

/* filters-shell.css anchors the popover `left: 0` on its mount, which is right
   for the DESKTOP placement (the mount sits at the left of the list header).
   In the sheet the same mount is the LAST item of the peek row, hard against
   the right gutter — so a `min(320px, 90vw)` panel opened ~200pt past the right
   edge of the screen and the photo-type chips in its right column were
   unreachable. Anchor it to the mount's right edge here instead, and cap the
   width to the viewport minus both gutters so it can never overflow again on a
   narrower phone. Found on an installed PWA at 402pt. */
.sheet-peek-row .filters-shell-mount .filters-shell-panel {
  left: auto;
  right: 0;
  width: min(320px, calc(100vw - var(--spacing-16) * 2));
}

/* Settings FOOTER (brief: "relocate settings to a row in the sheet's full
   state") — mobile-home.js toggles [hidden] on the Full/not-Full transition,
   via the sheet's own onDetentChange, so it's reachable only there.
   It is a sibling of contentEl, not a child: pinned below the scroller rather
   than sitting in the library's flow between the search field and the type
   tabs. `flex: 0 0 auto` keeps it at its own height while contentEl takes the
   rest, and the hairline moved to the TOP edge because it now separates the
   footer from the list above it rather than from the tabs below.
   The safe-area inset rides this element's own padding when it is the sheet's
   bottom-most box, so the tap target clears the home indicator. */
.sheet-settings-row {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 44px;
  padding: var(--spacing-10) var(--spacing-16);
  padding-bottom: calc(var(--spacing-10) + env(safe-area-inset-bottom, 0px));
  border: none;
  border-top: 1px solid var(--color-border);
  background: none;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-primary);
  text-align: left;
  cursor: pointer;
  transition: background var(--duration-base) var(--ease-in-out);
}

/* Hover-capability guarded (same treatment as panel.css's gallery arrows): on a
   touch screen `:hover` LATCHES after a tap and only releases when something
   else is tapped, so this full-width fill stayed lit long after the tap that
   opened Settings. Verified on an installed PWA. `:focus-visible` deliberately
   stays outside the guard — keyboard users need it on every device. */
@media (hover: hover) {
  .sheet-settings-row:hover {
    background: var(--color-surface-alt);
  }
}

/* The reparented library list is now sheet content, not a fixed full-viewport
   overlay — neutralize list.css's overlay geometry. Two classes beat list.css's
   single-class base rules regardless of <link> order, so this needs no
   !important. */
.bottom-sheet-content .list-view {
  position: static;
  width: 100%;
  height: auto;
  border: none;
  box-shadow: none;
  transform: none;
  transition: none;
  z-index: auto;
}

/* list.css's mobile notch-clearing padding-top doesn't apply once nested in
   the sheet — the sheet is bottom-anchored, nowhere near the status bar. */
/* The full padding, not just the top: list.css's mobile rule insets the header
   by spacing-12, but the sheet's spine is spacing-16 — the peek row, the back
   row, the detail body and the list rows' own card edge all sit there. Leaving
   the sides to list.css staggered the header 4px inside the cards below it. */
/* No top padding: the sticky peek row directly above already ends on a
   spacing-12 foot, and the header's own spacing-12 head stacked with it into a
   ~26pt trough under the search field — the doubled-gap the settings row used
   to sit in. One gap, owned by the peek row. */
.bottom-sheet-content .list-header {
  padding: 0 var(--spacing-16) var(--spacing-12);
}

/* The Select pill floats `position: absolute` over the header, and on desktop
   the header's 52px top padding is what reserves the band it floats in. The
   sheet cuts that padding to spacing-12 (above), so the pill had nothing to
   float in and landed on top of the type-toggle strip. Re-reserving a 52px
   band would spend the sheet's scarcest axis on empty space, so the pill joins
   `.list-controls`' flex flow instead, sitting after the strip. The strip must
   be allowed to shrink for that: it defaults to `flex-shrink: 0` and already
   scrolls its seven tabs (#421), so yielding width costs it nothing. */
.bottom-sheet-content .list-select-toggle {
  position: static;
  flex-shrink: 0;
}

.bottom-sheet-content .list-type-toggle {
  flex-shrink: 1;
  min-width: 0;
}

/* ============================================
   Sheet detail — the sheet's SECOND content (#298 step 3)
   Selecting a place REPLACES the library content inside this one sheet; it
   does not open a second sheet and it does not overlay. The library nodes
   above are simply [hidden] while this pane shows, so the sheet's box, its
   radius, its grabber, its detents and its (absent) scrim are unchanged — the
   only thing that swaps is what fills contentEl.

   entity-panel.js renders into `.sheet-detail-body` unchanged; that element
   also carries `.panel-body`, so every existing panel style (hero, gallery,
   notes, hours, visit log, admin actions) applies verbatim.
   ============================================ */

.sheet-detail {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Back to the library — the single visible affordance for the dismissal
   model's first rung (detail → list), which Escape and a swipe-down to peek
   also take. Sticky so it stays reachable however far the detail is scrolled. */
/* The detail's top bar: back affordance left, type badge right, one line. It
   owns the sticky band (same 56px box as .sheet-peek-row — 44px of row over
   spacing-12), so the sheet's top chrome keeps ONE height across both
   contents. The peek row is hidden while detail shows (mobile-home.js
   showDetail), so a shorter bar here would shrink the band and shunt every
   line of the detail up by the difference, making the swap read as a
   different surface rather than the same one changing contents.
   60px = the peek row's box after its spacing-4 top inset (4 + 44 + 12). */
.sheet-detail-topbar {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--spacing-8);
  min-height: 60px;
  padding: var(--spacing-4) var(--spacing-16) var(--spacing-12);
  background: var(--color-surface);
}

.sheet-detail-back {
  display: flex;
  align-items: center;
  gap: var(--spacing-6);
  min-height: 44px; /* touch floor, inside the bar's own box */
  padding: 0;
  border: none;
  background: none;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--font-weight-medium);
  color: var(--color-text-secondary);
  cursor: pointer;
  transition: color var(--duration-fast) var(--ease-in-out);
}

/* Guarded for the same latching reason as the other two — see .sheet-settings-row. */
@media (hover: hover) {
  .sheet-detail-back:hover {
    color: var(--color-text-primary);
  }
}

.sheet-detail-back:focus-visible {
  color: var(--color-text-primary);
}

.sheet-detail-back .ui-icon {
  width: 18px;
  height: 18px;
}

/* Holds the type badge lifted out of .poi-header-row. Never squeezes the badge
   to make room for a long place name — the name is not in this bar. */
.sheet-detail-badge-slot {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* The top bar already supplies the top gap, so the body starts flush. */
.sheet-detail-body {
  padding-top: 0;
}

/* ============================================
   Cover-photo hero mode — `[data-hero="photo"]`, set by mobile-home.js when the
   open place renders a `.panel-hero`.

   The photo owns the top of the sheet: it bleeds up BEHIND the grabber and the
   back row rather than starting below them, and that chrome inverts to
   light-on-dark. Every rule here is scoped under `.bottom-sheet`, so the same
   `.panel-hero` on the desktop panel is untouched.

   Contrast is guaranteed, not measured: `.panel-hero-top-scrim` (panel.css) is
   already a black-to-transparent gradient over the photo's top, which is
   exactly the band this chrome now sits in. Sampling the image's luminance to
   pick a grabber colour was rejected — see .impeccable/surfaces/mobile-home.md.

   `--sheet-hero-chrome` (below) is the ONE place this band's height is
   authored — every rule that needs it reads the variable, and mobile-home.js
   reads the same variable via getComputedStyle rather than hard-coding a
   second copy. A change to the grabber height or the topbar's box only ever
   needs this one number updated: get that wrong and the exact bug this mode
   was built to fix (a layout jump the instant the sheet crosses the
   scrolled-past-the-photo threshold) comes back, silently.
   ============================================ */

.bottom-sheet[data-hero="photo"] {
  /* handle (32px) + the topbar's own 60px box (spacing-4 head + 44px row +
     spacing-12 foot) = 92px. Bare number, not `92px`, so `calc()` below can
     multiply it — and so JS can `parseFloat()` it without stripping a unit. */
  --sheet-hero-chrome: 92;
}

/* The handle leaves the flex column so the content can start at the sheet's
   own top edge; it is painted over the photo instead. */
.bottom-sheet[data-hero="photo"] .bottom-sheet-handle {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
}

.bottom-sheet[data-hero="photo"]:not([data-hero-scrolled]) .bottom-sheet-grabber,
[data-theme="dark"] .bottom-sheet[data-hero="photo"]:not([data-hero-scrolled]) .bottom-sheet-grabber {
  background: var(--color-on-media-dim);
}

/* Sticky is kept — the back affordance must stay reachable at any scroll depth
   — and transparent, so the photo shows through. `padding-top` clears the
   grabber, which now shares this band. */
.bottom-sheet[data-hero="photo"] .sheet-detail-topbar {
  min-height: calc(var(--sheet-hero-chrome) * 1px);
  padding-top: 32px;
  background: none;
  transition: background var(--duration-base) var(--ease-in-out);
}

.bottom-sheet[data-hero="photo"]:not([data-hero-scrolled]) .sheet-detail-back {
  color: var(--color-on-media);
}

/* Scrolled past the photo there is nothing dark left to sit on, so the chrome
   takes its own surface back. Only paint changes here — NOTHING in this state
   may touch layout. An earlier cut gave the bar back its flow height at this
   threshold, and the content jumped by the chrome band the instant the state
   flipped. */
.bottom-sheet[data-hero="photo"][data-hero-scrolled] .sheet-detail-topbar {
  background: var(--color-surface);
}

/* The photo rises behind the chrome by pulling ITSELF up, not by the bar giving
   up its box: the negative margin is a constant, so the scroll height is
   identical in both states and crossing the threshold moves nothing. The height
   gains back exactly what the margin takes, so the amount of PHOTO on show
   below the bar is still 248px, and the top scrim's padding moves down by the
   same chrome band — keeping the neighborhood line and the Open-now badge clear
   of the back row instead of under it. */
.bottom-sheet[data-hero="photo"] .panel-hero {
  margin-top: calc(var(--sheet-hero-chrome) * -1px);
  height: calc(248px + var(--sheet-hero-chrome) * 1px);
}

/* The scrim has to be re-authored for this box, not just re-padded. panel.css
   fades 0.55 → transparent across the scrim's own height; that height was ~44px
   on the desktop panel, and the padding above pushes it past 140px here. The
   same two stops spread over three times the distance leave barely 0.11 of
   black under the neighborhood line — which is invisible over a bright sky, the
   case that actually decides this. Measured against a near-white overcast frame
   (WCAG AA on the composited pixels): the shipped stops gave the neighborhood
   line ~1.2:1. These give grabber 5.3:1, "Library" 7.1:1, neighborhood 5.5:1.
   Holding near-full strength through the chrome band and easing off after it
   is what lets this mode skip sampling the image — the guarantee comes from
   the scrim, so it only holds while the scrim is authored for THIS box.

   `min-height` is load-bearing, not slack: the last stop must land INSIDE the
   box or the gradient is truncated mid-fade and the scrim ends on a visible
   horizontal seam across the photo. The box has to outlast the stops. The
   150px stop is fixed (past the variable chrome band, into the photo proper)
   — only the chrome-band stop tracks `--sheet-hero-chrome`. */
.bottom-sheet[data-hero="photo"] .panel-hero-top-scrim {
  padding-top: calc(var(--sheet-hero-chrome) * 1px + var(--spacing-12));
  min-height: 230px;
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.65) 0%,
    rgba(0, 0, 0, 0.6) calc(var(--sheet-hero-chrome) * 1px),
    rgba(0, 0, 0, 0.42) 150px,
    transparent 100%
  );
}

/* #593 — while any modal is open the sheet is taken out of paint and hit
   testing. `#app` is position:fixed and so forms a stacking context, which
   confines every modal's `--z-modal` inside it; this body-level sheet carries
   `--z-panel` in the ROOT stacking context and therefore paints above an open
   modal no matter how high that modal's z-index goes. See modal-layer.js for
   the measurement and the rejected alternatives. Only visibility changes — the
   detent transform is untouched, so the sheet returns to where it was. */
:root[data-modal-open] .bottom-sheet {
  visibility: hidden;
  pointer-events: none;
}
