/* style.css's own html/body overflow:hidden is now scoped to <html class="feed"> (index.html
   only) precisely so grid.html doesn't inherit it — this used to be a real bug: overriding just
   body wasn't enough on its own, since body's overflow only becomes the page's actual scrolling
   behavior when html's own overflow is the default `visible` (CSS overflow-propagation rule).
   This explicit height/overflow is now redundant with that upstream fix (html here was never
   `hidden` to begin with), but kept as a belt-and-suspenders local guarantee — inexpensive,
   and it's the thing that was actually verified working in production. */
html { overflow-y: auto; overflow-x: hidden; height: auto; }
/* grid.js compensates the scroll itself when it mounts/unmounts a page above the viewport. Chrome's
   scroll anchoring corrected the same removal too, so pruning past MAX_PAGES jumped back a whole
   page and scrolling looped over the same tiles instead of going on. */
html, body { overflow-anchor: none; }
/* `overflow: auto` here used to make body a scroll container of its own, nested inside the
   html one above — a second, sideways-scrollable surface that a horizontal fav/hide swipe could
   grab instead of the gesture ever reaching JS. The page has exactly one scroller (html), so
   body leaves overflow alone; touch-action:pan-y is what keeps a horizontal touch out of the
   scroller in the first place. */
.grid-body {
  touch-action: pan-y; height: auto;
}
/* Nothing on this page is draggable content — see cellFor() in grid.js. */
.cell img, .cell video, a.cell {
  -webkit-user-drag: none; user-drag: none;
  /* Suppresses iOS's own long-press menu (save image / open link preview) on a cell. Without
     this, a finger that lingers a beat too long mid-tap can trigger the system's gesture
     instead of — or as well as — this page's own tap handling. */
  -webkit-touch-callout: none;
}
/* Stacked view is where the horizontal fav/hide swipe lives, so a horizontal touch there must
   belong to the gesture outright, never to any scrolling. */
body.stacked .cell { touch-action: pan-y; }
#gridBar {
  position: sticky; top: 0; z-index: 5;
  display: flex; flex-direction: column; gap: 5px;
  padding: 6px 8px; background: rgba(0,0,0,0.85);
  font-size: 13px; border-bottom: 1px solid #333;
}
.gridBarRow { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* The filters are the controls here worth permanent space — they change what the whole page
   is, and the alternative is going back to the feed to set them. Chips shrink to share the
   row rather than holding a fixed width and pushing the row past the screen edge. */
#gridBar .seg { flex: 1 1 auto; flex-wrap: nowrap; min-width: 0; gap: 4px; }
#gridBar .seg > * {
  flex: 1 1 0; min-width: 0; min-height: 36px; font-size: 12px; padding: 0 4px;
  overflow: hidden; text-overflow: ellipsis;
}
#gridBar #backBtn { flex: none; min-width: 34px; min-height: 36px; padding: 4px 8px; }
#gridBar #zoomOut, #gridBar #zoomIn { flex: none; }
#gridBar #emptyTrashBtn { flex: none; min-height: 36px; }
/* Hidden below the count's own breakpoint: at phone widths the filter and the zoom buttons are
   what the bar is for, and an item count is not worth pushing them off the edge. */
#gridCount { color: #888; margin-left: auto; white-space: nowrap; }
@media (max-width: 460px) { #gridCount { display: none; } }
#siteDescription {
  color: #999; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0; flex: 0 1 auto;
}
#siteDescription[hidden] { display: none; }
/* Same breakpoint gridCount already gives up at — neither is essential enough to cost the
   scope tabs or zoom controls room on a phone screen. */
@media (max-width: 460px) { #siteDescription { display: none; } }
#gridBar .btn { min-width: 38px; padding: 4px 8px; min-height: 34px; font-size: 16px; line-height: 1; }
#gridBar .btn:disabled { opacity: 0.3; }
/* A floor for the tab groups, so the icon buttons give way before the tabs turn into slivers
   (seen live on xfarm, where the trash tab and the map buttons are all present). */
#gridBar #gridScopeSeg { min-width: 120px; }
#gridBar #gridTypeSeg { min-width: 136px; }
/* Phone width: icon buttons get a fixed width instead of sizing to their glyph. iOS draws
   ⌕ ☑ ⤨ ◷ ⧉ from fallback fonts noticeably wider than desktop Chrome does, so glyph-sized
   buttons came out wide enough there to push the row's last button onto a line of its own.
   They may shrink a little further before the row overflows; the glyph is centred and a
   clipped sliver of its side bearing is invisible. Text buttons keep their natural width. */
@media (max-width: 460px) {
  .gridBarRow { gap: 4px; }
  #gridBar .btn { min-width: 32px; padding: 4px; }
  /* "☆ save" drops its word here: the star alone is the same button the favourites view
     already shows saved searches under, and the word is what pushed row two off the edge. */
  #gridBar .btnLabel { display: none; }
  #gridBar .gridBarRow > .btn:not(#emptyTrashBtn):not(#searchGoBtn) {
    flex: 0 1 32px; width: 32px; min-width: 27px; overflow: hidden;
  }
  #gridBar #backBtn { flex: 0 1 28px; width: 28px; min-width: 24px; padding: 4px; }
}
#gridWrap { padding: 8px; padding-bottom: 60px; }
#gridEl {
  display: grid;
  /* --cols is driven by the pinch/zoom controls in grid.js */
  grid-template-columns: repeat(var(--cols, 3), 1fr);
  gap: 4px;
}
.cell {
  position: relative; aspect-ratio: 1 / 1; background: #111;
  overflow: hidden; border-radius: 4px;
  /* skip layout/paint work for offscreen cells so a full page of
     thumbnails stays cheap even at 200/page */
  content-visibility: auto;
  contain-intrinsic-size: 200px 200px;
}
.cell img, .cell video { width: 100%; height: 100%; object-fit: cover; display: block; background: #1a1a1a; will-change: transform; }

/* Single-column "stacked" view: full-size media at its own aspect ratio, not square-cropped —
   the point of zooming all the way in is to actually see the whole image. */
body.stacked #gridEl { gap: 10px; }
body.stacked .cell {
  aspect-ratio: auto;
  contain-intrinsic-size: auto 600px;
  border-radius: 6px;
  /* Reserve height before the media arrives. Without it an unloaded tile is 0px tall, so the
     page height lurches as each one loads and the scroll position drifts under the reader.
     Matches the loaded media's own max-height (below) rather than guessing something smaller —
     50vh undershot almost every real image, so the tile visibly grew from half-screen to full
     the moment it loaded instead of just being there already. */
  min-height: 90vh;
}
/* Once the real dimensions are known the reservation has done its job and has to go, or a
   portrait-cropped phone photo keeps a full 90vh box while its media only fills part of it —
   the tile reads as padded with dead space and the gap between items looks arbitrary. From
   here the tile is exactly as tall as what's in it. */
body.stacked .cell.sized { min-height: 0; }
body.stacked .cell img, body.stacked .cell video {
  height: auto; max-height: 90vh; object-fit: contain;
}
.cell.vid::after {
  content: '▶'; position: absolute; right: 4px; bottom: 4px;
  font-size: 13px; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,0.9);
}
/* Brief acknowledgement for a fav-swipe in stacked view — same ★ the feed bursts on favouriting,
   just anchored to the tile itself rather than the centre of the screen. */
.cell.fav-flash::before {
  content: '★'; position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: 15vh; color: #fff; background: rgba(0,0,0,0.25);
  animation: cellfavburst 400ms ease-out;
  pointer-events: none;
}
@keyframes cellfavburst {
  0% { opacity: 0; transform: scale(0.6); }
  35% { opacity: 1; transform: scale(1.1); }
  100% { opacity: 0; transform: scale(1.25); }
}
/* A restore that the server refused (usually: something else now owns that path). Reuses the
   fav-flash animation, in red and with a ✕, so a failed double-tap is visibly not a no-op. */
.cell.untrash-failed::before {
  content: '✕'; position: absolute; inset: 0; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  font-size: 15vh; color: #ff6b6b; background: rgba(0,0,0,0.35);
  animation: cellfavburst 400ms ease-out;
  pointer-events: none;
}
/* Trash tiles have no href (they navigate nowhere) — keep the pointer cursor anyway, since a
   double-tap on them does do something. */
body.trash .cell { cursor: pointer; }
#gridHint { color: #888; font-size: 13px; padding: 20px 4px; text-align: center; }
#gridHint[hidden] { display: none; }
/* Watched by the infinite scroller — sits below the grid so it comes into view slightly
   before the user reaches the bottom, giving the next page time to load. */
#gridSentinel { height: 1px; }
/* Same idea, above the grid — triggers restoring pruned pages when scrolling back up. */
#gridTopSentinel { height: 1px; }

/* ---- select mode. Drag-to-paint across tiles is what makes cleaning at this scale possible:
   tapping 500 things one at a time is not a real path, sweeping a finger over them is. */
body.selecting #gridEl {
  /* The drag has to belong to the selection, not to the page scroll, or a sweep just scrolls.
     Reaching the top/bottom edge auto-scrolls instead (see grid.js), so nothing is unreachable. */
  touch-action: none;
}
body.selecting .cell { cursor: pointer; }
/* Unselected tiles recede so the selected set reads as the figure, not the ground. */
body.selecting .cell:not(.picked) { opacity: 0.55; }
.cell.picked { outline: 3px solid #6ea8fe; outline-offset: -3px; }
.cell.picked::after {
  content: '✓';
  position: absolute; top: 4px; left: 4px; z-index: 2;
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: #6ea8fe; color: #06121f;
  font-size: 13px; font-weight: 700;
}
/* The video marker also uses ::after and sits bottom-right; a picked video would otherwise
   lose one of the two. Selection wins the corner it wants, the play glyph keeps its own. */
.cell.vid.picked::after { content: '✓'; }

#selectBar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
  background: rgba(0,0,0,0.92); border-top: 1px solid #333;
  font-size: 13px;
}
#selectBar[hidden] { display: none; }
#selectCount { margin-right: auto; font-variant-numeric: tabular-nums; color: #ddd; }
#selDelete { border-color: #5a2020; color: #ff8a8a; }
#selDelete:active { background: rgba(255,80,80,0.18); }
#gridBar #selectBtn.active { background: #6ea8fe; color: #06121f; border-color: #6ea8fe; }
#gridBar #searchBtn.active { background: #eee; color: #000; border-color: #eee; }
#gridBar #driftBtn.active { background: #eee; color: #000; border-color: #eee; }
#searchRow[hidden] { display: none; }

/* Search sits on its own row: it's a text field, so it needs real width, and squeezing it in
   beside the filter chips would have left both too small to use on a phone. */
#gridSearch {
  flex: 1 1 auto; min-width: 0; min-height: 36px;
  padding: 4px 10px; font-size: 14px;
  background: #000; color: #eee;
  border: 1px solid #444; border-radius: 8px;
}
#gridSearch::placeholder { color: #666; }
#gridSearch:focus { outline: none; border-color: #6ea8fe; }
#searchClear { flex: none; min-height: 36px; }
#searchClear[hidden] { display: none; }

/* ---- the map view ----------------------------------------------------------
   The canvas is fixed and full-bleed, sitting under the sticky bar rather than inside the
   scrolling grid: the map has its own pan and zoom, so the page must not scroll behind it.
   body.mapping is what turns the page from a scroller into a single canvas surface. */
#mapCanvas {
  position: fixed; inset: 0; width: 100%; height: 100%;
  display: block; background: #000; z-index: 1;
  /* Every finger on this element belongs to the map's own pan/pinch, never to the page. */
  touch-action: none;
}
/* grid.js toggles the `hidden` attribute directly (enterMap/exitMap), so this has to win over
   the bare rule above — matching the `#id[hidden]{display:none}` pattern already used
   everywhere else in this codebase (#soundBtn, #authOverlay, #vidScrub, #searchClear...).
   Without it, the plain `display:block` above (same specificity as the browser's own
   attribute-only `[hidden]` rule, but an id beats a bare attribute selector) left an opaque
   full-screen canvas sitting over the grid at all times, hidden or not — this is exactly the
   bug that made the grid look empty. */
#mapCanvas[hidden] { display: none; }
body.mapping { overflow: hidden; height: 100%; touch-action: none; }
body.mapping #gridWrap { display: none; }
/* The bar stays on top of the canvas — it is how you get back out of the map. */
body.mapping #gridBar { position: fixed; top: 0; left: 0; right: 0; z-index: 5; }
/* A short status line the map speaks through: how many items, what a search found, why a
   search could not run. Same place the grid's own hint sits, so the page has one voice. */
#mapStatus {
  position: fixed; left: 50%; transform: translateX(-50%);
  bottom: calc(env(safe-area-inset-bottom, 0px) + 12px); z-index: 6;
  padding: 7px 14px; border-radius: 16px;
  background: rgba(0,0,0,0.85); border: 1px solid #333; color: #bbb;
  font-size: 12px; white-space: nowrap; pointer-events: none;
  transition: opacity 0.25s;
}
#mapStatus.fade { opacity: 0; }
#gridBar #searchModeSeg { flex: 0 0 auto; }
#gridBar #searchModeSeg > * { min-width: 56px; }

/* ---- saved searches/similarity sets, shown in the favourites view ------------------------- */
#savedSection { padding: 10px 8px 4px; }
#savedSection[hidden] { display: none; }
#savedSection.collapsed { padding-bottom: 0; }
#savedToggle {
  display: flex; align-items: center; gap: 6px; width: 100%;
  min-height: 32px; padding: 0 2px 6px; margin: 0;
  background: none; border: none; color: #888; cursor: pointer;
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  text-align: left;
}
#savedCount { color: #666; }
#savedChevron { margin-left: auto; font-size: 13px; transition: transform 0.15s; }
#savedSection.collapsed #savedChevron { transform: rotate(-90deg); }
#savedRow[hidden] { display: none; }
#savedRow {
  display: flex; flex-wrap: wrap; gap: 8px;
}
.savedCard {
  position: relative;
  width: 92px; display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 6px; border-radius: 10px; background: #141414; border: 1px solid #2a2a2a;
  cursor: pointer; text-decoration: none; color: inherit;
}
.savedCard:active { background: #1c1c1c; }
.savedCard img {
  width: 72px; height: 72px; object-fit: cover; border-radius: 8px; background: #222;
}
.savedCard .savedName {
  font-size: 11px; text-align: center; color: #ddd; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word;
}
.savedCard .savedKind {
  font-size: 9px; color: #888; text-transform: uppercase; letter-spacing: 0.05em;
}
.savedCard .savedDelete {
  position: absolute; top: -6px; right: -6px;
  width: 20px; height: 20px; border-radius: 10px;
  border: 1px solid #444; background: #222; color: #eee;
  font-size: 11px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
#gridBar #saveCollectionBtn { flex: none; }

/* ---- desktop side panel: what's currently visible on the map --------------------------- */
/* Desktop only: a phone has no screen space to spare for this, and no mouse-hover reason to
   want a list alongside the thing it's a list OF. min-width matches the boot-time desktop
   check in app.js, so the same visit that redirected '/' to the map also gets this. */
@media (max-width: 899px), (hover: none), (pointer: coarse) {
  #mapSidePanel { display: none !important; }
}
#mapSidePanel {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 4;
  width: 220px; overflow-y: auto; padding: 12px 10px;
  background: rgba(10,10,12,0.92); border-left: 1px solid #262626;
  display: flex; flex-direction: column; gap: 6px;
}
#mapSidePanel[hidden] { display: none; }
/* The canvas and bar both ignore this panel's width — simplest fix is giving the canvas the
   same right margin the panel occupies, only while the panel is actually shown. */
body.mapping:has(#mapSidePanel:not([hidden])) #mapCanvas { right: 220px; width: auto; }
.sidePanelTitle {
  font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.06em;
  padding: 2px 2px 6px; position: sticky; top: 0; background: inherit;
}
.sidePanelItem {
  display: flex; align-items: center; gap: 8px; padding: 4px; border-radius: 8px;
  cursor: pointer; text-decoration: none; color: inherit;
}
.sidePanelItem:hover { background: rgba(255,255,255,0.08); }
.sidePanelItem img {
  width: 40px; height: 40px; object-fit: cover; border-radius: 6px;
  background: #1a1a1a; flex: none;
}

/* ---- floating "back to favorites" link, shown only over a search/similar result grid -------- */
#favsFab {
  position: fixed; z-index: 6;
  right: 14px; bottom: 14px;
  width: 46px; height: 46px; border-radius: 23px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255,255,255,0.25); background: rgba(20,20,24,0.85);
  color: #fff; font-size: 19px; text-decoration: none;
  box-shadow: 0 2px 10px rgba(0,0,0,0.4);
}
#favsFab[hidden] { display: none; }
#favsFab:active { background: rgba(255,255,255,0.18); }

/* ---- save-collection dialog: a real one, not window.prompt() (see grid.html's own note) ---- */
#saveOverlay {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(0,0,0,0.75);
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: calc(env(safe-area-inset-top, 0px) + 60px);
  touch-action: auto;
}
#saveOverlay[hidden] { display: none; }
#saveBox {
  width: 320px; max-width: 88vw; padding: 16px;
  border: 1px solid #444; border-radius: 12px; background: #111;
}
#saveBoxKind {
  font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.06em;
  margin-bottom: 8px;
}
#saveNameInput {
  width: 100%; padding: 10px 12px; border-radius: 8px;
  border: 1px solid #444; background: #1a1a1a; color: #eee; font-size: 16px;
}
#saveNameInput:focus { outline: none; border-color: #888; }
#saveStatus { margin-top: 8px; font-size: 12px; color: #ff8080; }
#saveStatus[hidden] { display: none; }
#saveBoxRow { display: flex; gap: 8px; margin-top: 12px; }
#saveBoxRow button {
  flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #444;
  background: #1a1a1a; color: #eee; font-size: 14px; cursor: pointer;
}
#saveConfirmBtn { background: #eee; color: #000; border-color: #eee; font-weight: 600; }
#saveConfirmBtn:disabled { opacity: 0.5; }

/* ---- "you're in a filtered domain" badge, grid's own version of the wall's #scopeBadge ---- */
#gridScopeBadgeRow[hidden] { display: none; }
#gridScopeBadge {
  display: block; width: 100%; text-align: center;
  padding: 6px 8px; border-radius: 8px;
  background: rgba(255,255,255,0.08); color: #ddd; text-decoration: none;
  font-size: 12px;
}
#gridScopeBadge:active { background: rgba(255,255,255,0.16); }

/* ---- tag browser ---------------------------------------------------------------------------
   A sheet from the right edge, over everything but the page's own dialogs. One long scroll of
   every group expanded: flicking down a list is faster on a phone than tapping into menus. */
#tagsOverlay {
  position: fixed; inset: 0; z-index: 30;
  background: rgba(0,0,0,0.55);
}
#tagsOverlay[hidden] { display: none; }
#tagsPanel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(360px, 88vw);
  display: flex; flex-direction: column;
  background: #0b0b0b; border-left: 1px solid #262626;
  padding-top: env(safe-area-inset-top);
}
#tagsHead { flex: none; display: flex; gap: 6px; padding: 8px 8px 6px; }
#tagsFilter {
  flex: 1 1 auto; min-width: 0; min-height: 38px;
  padding: 4px 10px; font-size: 16px; /* 16px keeps iOS from zooming the page on focus */
  background: #000; color: #eee; border: 1px solid #444; border-radius: 8px;
}
#tagsFilter::placeholder { color: #666; }
#tagsFilter:focus { outline: none; border-color: #6ea8fe; }
#tagsClose { flex: none; min-width: 38px; min-height: 38px; font-size: 16px; }
/* Group index: one horizontal strip, swiped sideways, tap to jump down the list. */
#tagsJump {
  flex: none; display: flex; gap: 6px; overflow-x: auto; padding: 0 8px 8px;
  scrollbar-width: none; overscroll-behavior-x: contain;
  border-bottom: 1px solid #1f1f1f;
}
#tagsJump::-webkit-scrollbar { display: none; }
#tagsJump[hidden] { display: none; }
#tagsJump > button {
  flex: none; padding: 6px 10px; border-radius: 14px;
  background: #1a1a1a; color: #aaa; border: 1px solid #2a2a2a;
  font-size: 12px; white-space: nowrap; cursor: pointer;
}
#tagsJump > button:active { background: #2a2a2a; }
#tagsBody {
  flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain; /* the page underneath must not scroll along */
  padding: 4px 8px calc(24px + env(safe-area-inset-bottom));
}
.tagGroup[hidden] { display: none; }
.tagGroup h4 {
  position: sticky; top: 0; z-index: 1; margin: 0;
  padding: 10px 2px 6px; background: #0b0b0b;
  font-size: 11px; font-weight: 600; color: #888;
  text-transform: uppercase; letter-spacing: 0.06em;
}
.tagChips { display: flex; flex-wrap: wrap; gap: 5px; padding-bottom: 6px; }
.tagChip {
  min-height: 30px; padding: 4px 10px; border-radius: 15px;
  background: rgba(255,255,255,0.06); color: #ddd; border: 1px solid #333;
  font-size: 13px; line-height: 1.2; cursor: pointer; text-align: left;
}
.tagChip[hidden] { display: none; }
.tagChip:active { background: rgba(255,255,255,0.16); }
.tagChip.on { background: #eee; color: #000; border-color: #eee; font-weight: 700; }
@media (hover: hover) { .tagChip:hover { border-color: #666; } }
