/* =============================================================================
   71 — LIBRARY GRID PAGE  (v15.553)
   Full-screen poster grid of the whole library, three per row, ranked by
   rating as ONE merged list — a 5-star album sits beside a 5-star film rather
   than four hundred movies below it. Opened from the `titles` stat.
   Sized off --shelfd-px like the rest of the app so it holds from 360px up.
   ========================================================================== */

.shelfd-lib-grid-overlay {
  position: fixed;
  inset: 0;
  /* Above the bottom nav (1300) and below the media profile overlay (3100).
     Sitting above 3100 — as this did at 4200 — hid the full-page media profile
     BEHIND this page, so tapping a poster on a friend's shelf looked like it
     did nothing. Every other destination is higher still (album 7320, game
     profile 7600, episode page 12000), so one value clears them all and the
     grid stays mounted underneath as the back target. */
  z-index: 2600;
  background: #0d0f12;
  transform: translate3d(100%, 0, 0);
  transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: transform;
}
.shelfd-lib-grid-overlay.is-open { transform: translate3d(0, 0, 0); }

.shelfd-lib-grid-page {
  display: flex;
  flex-direction: column;
  height: 100%;
  /* Containing block for the category-slide ghost, which is placed at
     coordinates measured against this element. `z-index: auto` keeps it out of
     the stacking order, so the rating popup (6) still sits above everything the
     grid draws. */
  position: relative;
  color: #fff;
  font-family: 'Sohne', 'DM Sans', system-ui, -apple-system, sans-serif;
}

/* ── header ───────────────────────────────────────────────────────────────── */
.shelfd-lib-grid-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  /* The notch/dynamic-island inset, same as every other full-screen surface. */
  padding: calc(env(safe-area-inset-top, 0px) + 10px) 12px 10px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  background: rgba(13, 15, 18, 0.96);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
}
/* Bare glyphs, no chrome. The 24px is the ICON; the button itself keeps a
   larger invisible hit area so a bare chevron is still comfortably tappable —
   an icon sized to the finger is not the same as a target sized to it. */
.shelfd-lib-grid-iconbtn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(40 * var(--shelfd-px, 1px));
  height: calc(40 * var(--shelfd-px, 1px));
  margin: calc(-8 * var(--shelfd-px, 1px)) calc(-8 * var(--shelfd-px, 1px));
  padding: 0;
  border: 0;
  background: none;
  color: #fff;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.shelfd-lib-grid-iconbtn svg {
  width: 24px;
  height: 24px;
  display: block;
}
.shelfd-lib-grid-iconbtn:active { opacity: 0.55; }

.shelfd-lib-grid-titlebar {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.15;
}
/* Occupies exactly one icon button's worth of space on the left so the two
   control clusters weigh the same and the title lands in the true centre of the
   header. Inert and invisible — `visibility: hidden` rather than `display: none`
   because the box still has to take up room, which is its entire job. */
.shelfd-lib-grid-headspacer {
  visibility: hidden;
  pointer-events: none;
}
.shelfd-lib-grid-pagetitle {
  font-size: calc(15.5 * var(--shelfd-px, 1px));
  font-weight: 600;
  letter-spacing: 0.01em;
}
.shelfd-lib-grid-total {
  font-size: calc(11.5 * var(--shelfd-px, 1px));
  color: rgba(255, 255, 255, 0.45);
  margin-top: 2px;
}
/* ── search ───────────────────────────────────────────────────────────────
   Expands leftward across the whole header, hiding the back chevron, the title
   and the count while it is open. It is laid over the header rather than
   pushing it aside so nothing reflows — opening and closing costs one
   transform, not a relayout of the bar. */
.shelfd-lib-grid-header { position: relative; }

.shelfd-lib-search {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 6px);
  right: 10px;
  bottom: 6px;
  /* Collapsed: a circle behind the search button. Opening scales it out to the
     full width of the bar. */
  left: auto;
  width: calc(40 * var(--shelfd-px, 1px));
  display: flex;
  align-items: center;
  gap: calc(8 * var(--shelfd-px, 1px));
  padding: 0;
  border-radius: 999px;
  background: #14181d;
  border: 1px solid rgba(255, 255, 255, 0.10);
  opacity: 0;
  pointer-events: none;
  overflow: hidden;
  transition: width 300ms cubic-bezier(0.22, 1, 0.36, 1),
              opacity 160ms ease,
              padding 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
.shelfd-lib-grid-overlay.is-searching .shelfd-lib-search {
  width: calc(100% - 20px);
  padding: 0 calc(6 * var(--shelfd-px, 1px)) 0 calc(12 * var(--shelfd-px, 1px));
  opacity: 1;
  pointer-events: auto;
}
/* The header's own controls step aside while the field is open. */
.shelfd-lib-grid-overlay.is-searching .shelfd-lib-grid-back,
.shelfd-lib-grid-overlay.is-searching .shelfd-lib-grid-titlebar,
.shelfd-lib-grid-overlay.is-searching .shelfd-lib-grid-searchbtn,
.shelfd-lib-grid-overlay.is-searching .shelfd-lib-grid-sortbtn {
  opacity: 0;
  pointer-events: none;
  transition: opacity 140ms ease;
}

.shelfd-lib-search-icon {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  color: rgba(255, 255, 255, 0.42);
}
/* The global stylesheet gives every bare `input` a border, a background and a
   purple focus glow (05-messages-early-polish.css). Inside this pill that reads
   as a second box drawn within the composer, so every one of those properties is
   neutralised here — in every state, focus included. */
.shelfd-lib-search .shelfd-lib-search-input,
.shelfd-lib-search .shelfd-lib-search-input:focus,
.shelfd-lib-search .shelfd-lib-search-input:focus-visible,
.shelfd-lib-search .shelfd-lib-search-input:active {
  flex: 1 1 auto;
  min-width: 0;
  appearance: none; -webkit-appearance: none;
  border: 0;
  border-radius: 0;
  outline: none;
  box-shadow: none;
  background: none;
  background-color: transparent;
  color: #fff;
  font-family: inherit;
  font-size: calc(15 * var(--shelfd-px, 1px));
  font-weight: 500;
  padding: 0;
  margin: 0;
  min-height: 0;
  height: auto;
  line-height: normal;
  -webkit-tap-highlight-color: transparent;
}
/* The pill itself stays a flat, quiet surface — no ring, no tint, whether or not
   the caret is inside it. */
.shelfd-lib-search:focus-within {
  background: #14181d;
  border-color: rgba(255, 255, 255, 0.10);
  box-shadow: none;
  outline: none;
}
.shelfd-lib-search-input::placeholder { color: rgba(255, 255, 255, 0.32); }
/* Safari draws its own clear button on type=search; ours is the X below. */
.shelfd-lib-search-input::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }

.shelfd-lib-search-close {
  flex: 0 0 auto;
  appearance: none; -webkit-appearance: none;
  width: calc(30 * var(--shelfd-px, 1px));
  height: calc(30 * var(--shelfd-px, 1px));
  display: flex; align-items: center; justify-content: center;
  border: 0; padding: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.07);
  color: rgba(255, 255, 255, 0.7);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.shelfd-lib-search-close svg { width: 15px; height: 15px; }

/* Filtered out by the query — hidden, never removed, so the tile keeps its
   already-decoded poster and typing costs nothing. */
.shelfd-lib-grid-tile.is-filtered-out,
.shelfd-lib-grid-divider.is-filtered-out { display: none; }

@media (prefers-reduced-motion: reduce) {
  .shelfd-lib-search { transition: opacity 120ms ease; }
}

/* ── filter / sort modal (centred) ────────────────────────────────────────── */
.shelfd-lib-modal {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.shelfd-lib-modal-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.58);
  opacity: 0;
  transition: opacity 200ms ease;
}
.shelfd-lib-modal.is-open .shelfd-lib-modal-scrim { opacity: 1; }

.shelfd-lib-modal-panel {
  position: relative;
  width: 100%;
  max-width: calc(340 * var(--shelfd-px, 1px));
  /* Never taller than the viewport: the genre list can be long, and the body
     below scrolls rather than pushing the footer off-screen. */
  max-height: min(78vh, calc(560 * var(--shelfd-px, 1px)));
  display: flex;
  flex-direction: column;
  background: #14181d;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: calc(16 * var(--shelfd-px, 1px));
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 200ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.shelfd-lib-modal.is-open .shelfd-lib-modal-panel { opacity: 1; transform: scale(1); }

.shelfd-lib-modal-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(14 * var(--shelfd-px, 1px)) calc(14 * var(--shelfd-px, 1px)) calc(10 * var(--shelfd-px, 1px));
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.shelfd-lib-modal-title {
  font-size: calc(15 * var(--shelfd-px, 1px));
  font-weight: 600;
  color: #fff;
}
.shelfd-lib-modal-x {
  appearance: none; -webkit-appearance: none;
  width: calc(28 * var(--shelfd-px, 1px));
  height: calc(28 * var(--shelfd-px, 1px));
  display: flex; align-items: center; justify-content: center;
  border: 0; background: none; padding: 0;
  color: rgba(255, 255, 255, 0.55);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.shelfd-lib-modal-x svg { width: 18px; height: 18px; }

.shelfd-lib-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: calc(10 * var(--shelfd-px, 1px)) calc(14 * var(--shelfd-px, 1px));
}
.shelfd-lib-modal-label {
  font-size: calc(11 * var(--shelfd-px, 1px));
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.38);
  margin: calc(6 * var(--shelfd-px, 1px)) 0 calc(4 * var(--shelfd-px, 1px));
}
.shelfd-lib-modal-label:first-child { margin-top: 0; }

.shelfd-lib-modal-options { display: flex; flex-direction: column; }
.shelfd-lib-modal-option {
  appearance: none; -webkit-appearance: none;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  width: 100%;
  padding: calc(11 * var(--shelfd-px, 1px)) 0;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  background: none;
  color: rgba(255, 255, 255, 0.8);
  font-family: inherit;
  font-size: calc(14 * var(--shelfd-px, 1px));
  font-weight: 500;
  text-align: left;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.shelfd-lib-modal-option:last-child { border-bottom: 0; }
.shelfd-lib-modal-option.is-active { color: #fff; font-weight: 600; }
.shelfd-lib-modal-check {
  width: calc(17 * var(--shelfd-px, 1px));
  height: calc(17 * var(--shelfd-px, 1px));
  flex: 0 0 auto;
  color: #E6C766;
  opacity: 0;
}
.shelfd-lib-modal-option.is-active .shelfd-lib-modal-check { opacity: 1; }

.shelfd-lib-modal-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(9 * var(--shelfd-px, 1px)) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.shelfd-lib-modal-field:last-of-type { border-bottom: 0; }
.shelfd-lib-modal-fieldname {
  font-size: calc(14 * var(--shelfd-px, 1px));
  font-weight: 500;
  color: rgba(255, 255, 255, 0.8);
}
.shelfd-lib-modal-select,
.shelfd-lib-modal-input {
  appearance: none; -webkit-appearance: none;
  flex: 0 1 auto;
  max-width: calc(180 * var(--shelfd-px, 1px));
  padding: calc(7 * var(--shelfd-px, 1px)) calc(10 * var(--shelfd-px, 1px));
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: calc(8 * var(--shelfd-px, 1px));
  background: rgba(255, 255, 255, 0.05);
  color: #fff;
  font-family: inherit;
  font-size: calc(13.5 * var(--shelfd-px, 1px));
  text-align: right;
}
.shelfd-lib-modal-input { width: calc(96 * var(--shelfd-px, 1px)); }

/* ── rating filter ────────────────────────────────────────────────────────
   Eleven chips do not fit on one line beside a label, so this row breaks out
   of the label-left / control-right shape the other fields use and stacks. */
.shelfd-lib-modal-field-stacked {
  display: block;
}
.shelfd-lib-modal-stars {
  display: flex;
  flex-wrap: wrap;
  gap: calc(6 * var(--shelfd-px, 1px));
  margin-top: calc(9 * var(--shelfd-px, 1px));
}
.shelfd-lib-modal-star {
  appearance: none; -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  gap: calc(3 * var(--shelfd-px, 1px));
  padding: calc(6 * var(--shelfd-px, 1px)) calc(9 * var(--shelfd-px, 1px));
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.05);
  color: rgba(255, 255, 255, 0.72);
  font-family: inherit;
  font-size: calc(13 * var(--shelfd-px, 1px));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
.shelfd-lib-modal-star-glyph {
  color: #E6C766;
  font-size: calc(12 * var(--shelfd-px, 1px));
  /* The glyph sits a shade high against tabular digits at this size. */
  transform: translateY(-0.5px);
}
/* Selected chips carry the same champagne gold as the band markers, so the
   filter and the thing it filters read as one system. */
.shelfd-lib-modal-star.is-active {
  background: rgba(230, 199, 102, 0.16);
  border-color: rgba(230, 199, 102, 0.55);
  color: #F0DFA8;
}
.shelfd-lib-modal-star.is-active .shelfd-lib-modal-star-glyph { color: #E6C766; }
/* "Not rated" has no star, so it needs the extra room to not read as cramped
   next to the numeric chips. */
.shelfd-lib-modal-star.is-unrated { padding-left: calc(11 * var(--shelfd-px, 1px)); padding-right: calc(11 * var(--shelfd-px, 1px)); }
.shelfd-lib-modal-select:focus,
.shelfd-lib-modal-input:focus { outline: none; border-color: rgba(230, 199, 102, 0.5); }
.shelfd-lib-modal-input::placeholder { color: rgba(255, 255, 255, 0.3); }

.shelfd-lib-modal-foot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(11 * var(--shelfd-px, 1px)) calc(14 * var(--shelfd-px, 1px)) calc(13 * var(--shelfd-px, 1px));
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.shelfd-lib-modal-clear,
.shelfd-lib-modal-done {
  appearance: none; -webkit-appearance: none;
  border: 0;
  border-radius: calc(9 * var(--shelfd-px, 1px));
  font-family: inherit;
  font-size: calc(13.5 * var(--shelfd-px, 1px));
  font-weight: 600;
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  padding: calc(9 * var(--shelfd-px, 1px)) calc(14 * var(--shelfd-px, 1px));
}
.shelfd-lib-modal-clear { background: none; color: rgba(255, 255, 255, 0.5); padding-left: 0; }
.shelfd-lib-modal-done { background: #fff; color: #0d0f12; min-width: calc(84 * var(--shelfd-px, 1px)); }
.shelfd-lib-modal-clear:active, .shelfd-lib-modal-done:active { opacity: 0.7; }

/* ── category toggles ─────────────────────────────────────────────────────── */
.shelfd-lib-grid-filters {
  flex: 0 0 auto;
  display: flex;
  gap: calc(7 * var(--shelfd-px, 1px));
  padding: calc(9 * var(--shelfd-px, 1px)) 12px;
  overflow-x: auto;
  overflow-y: hidden;
  /* The row is meant to run off the edge on a narrow phone; the scrollbar
     would just eat vertical space in a 34px strip. */
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  background: #0d0f12;
}
.shelfd-lib-grid-filters::-webkit-scrollbar { display: none; }

.shelfd-lib-grid-chip {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: calc(5 * var(--shelfd-px, 1px));
  padding: calc(6 * var(--shelfd-px, 1px)) calc(11 * var(--shelfd-px, 1px));
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.045);
  color: rgba(255, 255, 255, 0.72);
  font-family: inherit;
  font-size: calc(12.5 * var(--shelfd-px, 1px));
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.shelfd-lib-grid-chip:active { transform: scale(0.97); }
.shelfd-lib-grid-chip.is-active {
  background: #fff;
  border-color: #fff;
  color: #0d0f12;
  font-weight: 600;
}
.shelfd-lib-grid-chip-count {
  font-size: calc(11 * var(--shelfd-px, 1px));
  font-weight: 500;
  color: rgba(255, 255, 255, 0.42);
}
.shelfd-lib-grid-chip.is-active .shelfd-lib-grid-chip-count { color: rgba(13, 15, 18, 0.55); }

/* Category selection REBUILDS the grid rather than hiding blocks of it: the
   view is one merged list ordered by rating, so "Movies" is a different list
   than a filtered view of the same DOM, not the same list with parts hidden. */

/* ── scroller ─────────────────────────────────────────────────────────────── */
.shelfd-lib-grid-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 14px 12px calc(env(safe-area-inset-bottom, 0px) + 28px);
}

/* ── category slide ───────────────────────────────────────────────────────
   While the two grids cross, the scroller clips instead of scrolling: it hides
   the incoming grid for as long as it is still off to the side, and stops a
   stray touch scrolling content that is mid-transform. */
.shelfd-lib-grid-scroll.is-sliding {
  overflow: hidden;
  /* A flick that lands during the slide must not be interpreted at all — an
     interrupted scroll on a transformed grid reads as a glitch. */
  touch-action: none;
}

/* The outgoing snapshot. Only the rows that were actually on screen live in
   here, absolutely placed at the coordinates they already occupied, so it is a
   handful of nodes rather than a second copy of the library. */
.shelfd-lib-slide-ghost {
  position: absolute;
  z-index: 4;
  pointer-events: none;
  overflow: hidden;
  will-change: transform;
  /* Nothing inside can affect layout outside, and nothing outside needs to be
     consulted to paint it. */
  contain: layout paint;
}
/* The originals are lazily rendered as they scroll into view; a clone that
   inherited that would be free to skip painting exactly while it is the thing
   being watched. */
.shelfd-lib-slide-ghost .shelfd-lib-grid-tile {
  content-visibility: visible;
  contain-intrinsic-size: auto;
}

/* ── the grid ─────────────────────────────────────────────────────────────── */
.shelfd-lib-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: calc(10 * var(--shelfd-px, 1px)) calc(9 * var(--shelfd-px, 1px));
}

.shelfd-lib-grid-tile {
  appearance: none;
  -webkit-appearance: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Cheap virtualisation: off-screen tiles skip layout and paint entirely, so a
     library of several thousand posters scrolls like a short one. The intrinsic
     size keeps the scrollbar honest while they are skipped. */
  content-visibility: auto;
  contain-intrinsic-size: auto calc(190 * var(--shelfd-px, 1px));
}
.shelfd-lib-grid-tile:active { transform: scale(0.97); }

.shelfd-lib-grid-art {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  border-radius: calc(9 * var(--shelfd-px, 1px));
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
  /* The hairline rides as an INSET shadow rather than a border or outline: it
     follows the corner radius exactly, costs no layout box, and so cannot
     nudge the grid columns the way a 0.9px border would. */
  box-shadow:
    inset 0 0 0 0.9px rgba(255, 255, 255, 0.12),
    0 2px 10px rgba(0, 0, 0, 0.28);
}

/* Movies, TV and anime are true posters and take the app's canonical poster
   radius (3.9px, v12.797 — the same value used by the person filmography,
   More Like This and notification posters). Games and music keep the softer
   corner: box art and album covers are not poster-shaped and read better with
   it. */
/* Keyed off the TILE, not a section wrapper: the grid is one merged list now,
   so a movie and an album can sit side by side in the same rating band and each
   still has to carry its own shape. */
.shelfd-lib-grid-tile[data-lib-section="movies"] .shelfd-lib-grid-art,
.shelfd-lib-grid-tile[data-lib-section="shows"] .shelfd-lib-grid-art,
.shelfd-lib-grid-tile[data-lib-section="anime"] .shelfd-lib-grid-art {
  border-radius: 3.9px;
}

/* Album art is square, not poster-shaped. Forcing it into 2:3 either crops the
   sleeve or pillarboxes it — both wrong for a format whose artwork IS a square.
   Sitting inside a mixed row it aligns to the top, so the shorter tile hangs
   from the same line as the posters beside it rather than floating mid-row. */
.shelfd-lib-grid-tile[data-lib-section="music"] {
  align-self: start;
  contain-intrinsic-size: auto calc(150 * var(--shelfd-px, 1px));
}
.shelfd-lib-grid-tile[data-lib-section="music"] .shelfd-lib-grid-art {
  aspect-ratio: 1 / 1;
  border-radius: 3.6px;
}

/* Band marker: the score on the LEFT, then the rule running out to the right.
   It labels the band BELOW it, so scrolling past "★4" means everything from
   there down is a 4. Spanning every column is what makes it a band separator
   rather than a line under one tile. */
.shelfd-lib-grid-divider {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: calc(8 * var(--shelfd-px, 1px));
  margin: calc(6 * var(--shelfd-px, 1px)) 0 calc(2 * var(--shelfd-px, 1px));
}
.shelfd-lib-grid-divider:first-child { margin-top: 0; }

.shelfd-lib-grid-band {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: calc(3 * var(--shelfd-px, 1px));
  font-size: calc(12 * var(--shelfd-px, 1px));
  font-weight: 600;
  line-height: 1;
  color: rgba(255, 255, 255, 0.72);
  letter-spacing: 0.01em;
}
.shelfd-lib-grid-band-star {
  color: #E6C766;
  font-size: calc(11 * var(--shelfd-px, 1px));
  line-height: 1;
}
.shelfd-lib-grid-band-rule {
  flex: 1 1 auto;
  height: 1px;
  background: linear-gradient(
    to right,
    rgba(255, 255, 255, 0.11),
    rgba(255, 255, 255, 0.05) 60%,
    rgba(255, 255, 255, 0.012)
  );
}
.shelfd-lib-grid-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Posters legitimately go missing (a title added before art resolved, a dead
   CDN url). A flat placeholder reads as "no art", where a broken-image glyph
   reads as "the app is broken". */
.shelfd-lib-grid-art.is-empty {
  background:
    linear-gradient(145deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
}
.shelfd-lib-grid-art.is-empty::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.10), transparent 62%);
}

/* The user's own score, out of 5, with the app's canonical champagne-gold star
   (#E6C766, v10.523 / v14.623 — the same glyph colour every other rating
   surface uses). */
.shelfd-lib-grid-rating {
  position: absolute;
  right: calc(5 * var(--shelfd-px, 1px));
  bottom: calc(5 * var(--shelfd-px, 1px));
  display: inline-flex;
  align-items: center;
  gap: calc(2.5 * var(--shelfd-px, 1px));
  padding: 0 calc(5 * var(--shelfd-px, 1px));
  border-radius: calc(5 * var(--shelfd-px, 1px));
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  font-size: calc(10.5 * var(--shelfd-px, 1px));
  font-weight: 600;
  line-height: calc(16 * var(--shelfd-px, 1px));
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.shelfd-lib-grid-star {
  color: #E6C766;
  font-size: calc(9.5 * var(--shelfd-px, 1px));
  line-height: 1;
}

.shelfd-lib-grid-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: calc(11.5 * var(--shelfd-px, 1px));
  font-weight: 500;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.82);
}

/* ── long-press rating popup ──────────────────────────────────────────────── */
.shelfd-lib-ratepop { position: absolute; inset: 0; z-index: 6; }

/* The dim + blur applies to EVERYTHING behind this layer. The pressed poster
   and the rating card render above it, so they stay crisp without needing to
   be excluded by hand. */
.shelfd-lib-ratepop-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  opacity: 0;
  transition: opacity 180ms ease;
  /* NO backdrop-filter here — see .is-settled below. */
}
.shelfd-lib-ratepop.is-open .shelfd-lib-ratepop-scrim { opacity: 1; }

/* THE BLUR IS SWITCHED ON ONLY AFTER THE POSTER HAS LANDED.

   `backdrop-filter` forces WebKit to snapshot and re-blur everything behind
   this layer once per frame, and "everything behind" is a 700-tile grid several
   screens tall. Running that concurrently with the entrance meant the travel
   was sharing a frame budget with a full-viewport blur of a huge document —
   which is exactly why a 300ms flight rendered as a handful of steps. Dimming
   is a flat fill and stays on the compositor; the blur waits for stillness. */
.shelfd-lib-ratepop.is-settled .shelfd-lib-ratepop-scrim {
  backdrop-filter: blur(1.2px);
  -webkit-backdrop-filter: blur(1.2px);
}

/* A clone pinned over the original's exact rect — the real tile stays in the
   grid so nothing reflows behind the scrim. */
/* Starts life exactly over the tile it was cloned from — the source tile is
   hidden at the same moment, so this reads as the poster itself lifting out of
   the grid rather than a copy appearing beside it. No opacity fade: the swap
   has to be invisible for the illusion to hold. Position and scale are driven
   by the Web Animations entrance, then committed to real geometry. */
.shelfd-lib-ratepop-poster {
  position: fixed;
  z-index: 1;
  pointer-events: none;
  /* Promoted for its whole life, not just mid-flight. A layer created on the
     first animated frame costs that frame a rasterisation; declaring it up
     front means the entrance starts on an already-composited layer. */
  will-change: transform;
  transform-origin: 50% 50%;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
/* The shadow is a `filter`, which re-rasterises the element every time its
   scale changes — a per-frame repaint on the one thing that has to stay on the
   compositor. It is applied once the poster is parked, and dropped again for
   the flight out. */
.shelfd-lib-ratepop.is-settled .shelfd-lib-ratepop-poster {
  filter: drop-shadow(0 14px 34px rgba(0, 0, 0, 0.55));
}
.shelfd-lib-ratepop-poster .shelfd-lib-grid-art { width: 100%; height: 100%; }

/* Mid-flight: the scrim and the rating card clear out so only the poster is
   moving, and the CSS transition is dropped — the Web Animations flight owns
   `transform` from here, and leaving a transition on it would fight the
   keyframes and stutter the travel. */
.shelfd-lib-ratepop.is-flying { pointer-events: none; }
/* The .is-flying / .is-closing CARD rules are NOT here — they live after the
   `.is-open` rule further down. All three selectors weigh exactly the same
   (three classes), so the cascade falls through to source order, and `.is-open`
   used to sit last and win: the card stayed fully opaque for the entire flight
   and then vanished when the popup was removed. See the block below
   `.shelfd-lib-ratepop.is-open .shelfd-lib-ratepop-card`. */
.shelfd-lib-ratepop.is-flying .shelfd-lib-ratepop-scrim {
  opacity: 0;
  transition: opacity 260ms ease 60ms;
  /* Killed before the poster sets off: a fading blur is the most expensive
     thing on screen, and the flight is the longest move on this page. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.shelfd-lib-ratepop.is-flying .shelfd-lib-ratepop-poster {
  transition: none;
  will-change: transform;
  filter: none;
  /* Above the sticky category headings it passes on the way. */
  z-index: 3;
}

/* Dismissed without rating — the entrance in reverse. The scrim and card clear
   while the poster travels home, and both the blur and the drop-shadow are gone
   before the first frame so the return is a pure compositor transform, exactly
   like the way in. Declared after .is-settled so it wins on equal specificity. */
.shelfd-lib-ratepop.is-closing { pointer-events: none; }
.shelfd-lib-ratepop.is-closing .shelfd-lib-ratepop-scrim {
  opacity: 0;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: opacity 200ms ease;
}
/* The .is-closing CARD rule also lives below `.is-open` — same source-order
   tie as .is-flying. */
.shelfd-lib-ratepop.is-closing .shelfd-lib-ratepop-poster {
  transition: none;
  will-change: transform;
  filter: none;
  z-index: 3;
}

/* Sits directly UNDER the centred poster — `top` is written inline from the
   poster's computed rect, so the pair always reads as one unit and the card can
   never land on top of the title being rated. */
.shelfd-lib-ratepop-card {
  position: absolute;
  left: 50%;
  z-index: 2;
  width: min(88%, calc(340 * var(--shelfd-px, 1px)));
  padding: calc(14 * var(--shelfd-px, 1px)) calc(16 * var(--shelfd-px, 1px)) calc(13 * var(--shelfd-px, 1px));
  background: #14181d;
  border: 1px solid rgba(255, 255, 255, 0.09);
  border-radius: calc(16 * var(--shelfd-px, 1px));
  box-shadow: 0 20px 48px rgba(0, 0, 0, 0.55);
  text-align: center;
  opacity: 0;
  transform: translate(-50%, 10px);
  /* Same curve as the poster's entrance, so the pair arrives as one movement
     rather than the card snapping into place while the poster is still on its
     way. */
  transition: opacity 220ms ease 80ms, transform 300ms cubic-bezier(0.40, 0.05, 0.20, 1) 80ms;
}
.shelfd-lib-ratepop.is-open .shelfd-lib-ratepop-card { opacity: 1; transform: translate(-50%, 0); }

/* ── card exit states — MUST stay below `.is-open` ─────────────────────────
   `.is-open`, `.is-flying` and `.is-closing` are all three-class selectors, so
   none of them out-specifies the others and the last one written wins. These
   two used to be declared ABOVE `.is-open`, which meant that during a re-rank
   flight — where `is-flying` is added but `is-open` is deliberately NOT removed,
   because the popup is mid-animation rather than being dismissed — the card
   held `opacity: 1` for the whole 680-1050ms flight and then blinked out when
   the popup was removed. That is the "modal sits there while the poster has
   already moved, then disappears".

   Order of events now: the rating lands, the card clears, THEN the poster
   travels. The card is given 160ms and the poster's flight is delayed past it,
   so the panel is gone before anything moves rather than racing it. */
.shelfd-lib-ratepop.is-flying .shelfd-lib-ratepop-card {
  opacity: 0;
  transform: translate(-50%, 6px);
  /* No entry delay on the way out, and quicker than the 220ms it takes to
     arrive — leaving should feel decisive, not like a rewind. */
  transition: opacity 160ms ease, transform 200ms ease;
}
.shelfd-lib-ratepop.is-closing .shelfd-lib-ratepop-card {
  opacity: 0;
  transform: translate(-50%, 8px);
  transition: opacity 160ms ease, transform 220ms cubic-bezier(0.40, 0.05, 0.20, 1);
}

.shelfd-lib-ratepop-title {
  font-size: calc(14 * var(--shelfd-px, 1px));
  font-weight: 600;
  color: #fff;
  margin-bottom: calc(12 * var(--shelfd-px, 1px));
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.shelfd-lib-ratepop-hint {
  margin-top: calc(10 * var(--shelfd-px, 1px));
  font-size: calc(11 * var(--shelfd-px, 1px));
  color: rgba(255, 255, 255, 0.38);
}

/* ── v15.620 CIRCULAR ACTIONS ON THE RATING POPUP ──────────────────────────
   Was two stacked full-width rows. Two controls of identical weight sitting one
   above the other read as a list to work through; side by side as discs with a
   caption under each they read as a choice, which is what they are. Same shape
   as the title-card sheet uses (.mylist-card-menu-circles), so the two
   long-press surfaces in the app now speak the same language.

   The pair is CENTRED rather than stretched: with only two actions, a grid
   would push one to each edge of the card and leave a gap in the middle wide
   enough to read as a missing third option. */
.shelfd-lib-ratepop-actions {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: calc(30 * var(--shelfd-px, 1px));
  margin-top: calc(12 * var(--shelfd-px, 1px));
  padding-top: calc(13 * var(--shelfd-px, 1px));
  border-top: 1px solid rgba(255, 255, 255, 0.07);
}
.shelfd-lib-ratepop-action {
  appearance: none; -webkit-appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(7 * var(--shelfd-px, 1px));
  min-width: calc(62 * var(--shelfd-px, 1px));
  padding: 0;
  border: 0;
  background: none;
  color: #f5f5f7;
  font-family: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Only the disc reacts to a press — scaling the caption with it makes the text
   shimmer. Transform only, so this never touches layout. */
.shelfd-lib-ratepop-action-ico {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: calc(52 * var(--shelfd-px, 1px));
  height: calc(52 * var(--shelfd-px, 1px));
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.055);
  border: 1px solid rgba(255, 255, 255, 0.09);
  color: #c4b5fd;
  transition: background 0.18s ease, transform 0.16s cubic-bezier(0.22, 1, 0.36, 1);
}
.shelfd-lib-ratepop-action-ico svg {
  width: calc(22 * var(--shelfd-px, 1px));
  height: calc(22 * var(--shelfd-px, 1px));
}
.shelfd-lib-ratepop-action:active .shelfd-lib-ratepop-action-ico {
  transform: scale(0.92);
  background: rgba(255, 255, 255, 0.09);
}
.shelfd-lib-ratepop-action-label {
  font-size: calc(11.5 * var(--shelfd-px, 1px));
  font-weight: 500;
  line-height: 1.15;
  color: rgba(235, 232, 244, 0.78);
}
/* The running count, on the disc itself. This tap commits silently and leaves
   the popup open, so this badge IS the confirmation — without it there would be
   nothing on screen to show the tap did anything. Border-matched to the card so
   it reads as sitting on top of the disc rather than inside it. */
.shelfd-lib-ratepop-action-count {
  position: absolute;
  top: calc(-3 * var(--shelfd-px, 1px));
  right: calc(-3 * var(--shelfd-px, 1px));
  min-width: calc(19 * var(--shelfd-px, 1px));
  height: calc(19 * var(--shelfd-px, 1px));
  padding: 0 calc(4 * var(--shelfd-px, 1px));
  border-radius: 999px;
  border: 2px solid #17161d;
  background: #7c5cf0;
  color: #fff;
  font-size: calc(10.5 * var(--shelfd-px, 1px));
  font-weight: 700;
  line-height: calc(15 * var(--shelfd-px, 1px));
  text-align: center;
  box-sizing: border-box;
}
.shelfd-lib-ratepop-action-count[hidden] { display: none; }
/* One pop per pass. Restarted in JS by removing the class and reflowing, so a
   burst of taps shows a burst of pops rather than one long one. */
.shelfd-lib-ratepop-action.is-bumped .shelfd-lib-ratepop-action-count {
  animation: shelfd-lib-count-pop 320ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes shelfd-lib-count-pop {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.42); }
  100% { transform: scale(1); }
}
.shelfd-lib-ratepop-action--danger .shelfd-lib-ratepop-action-ico {
  color: #ff5a6e;
  border-color: rgba(255, 90, 110, 0.28);
  background: rgba(255, 90, 110, 0.08);
}
.shelfd-lib-ratepop-action--danger .shelfd-lib-ratepop-action-label {
  color: rgba(255, 138, 152, 0.9);
}
@media (prefers-reduced-motion: reduce) {
  .shelfd-lib-ratepop-action-ico { transition: none; }
  .shelfd-lib-ratepop-action:active .shelfd-lib-ratepop-action-ico { transform: none; }
  .shelfd-lib-ratepop-action.is-bumped .shelfd-lib-ratepop-action-count { animation: none; }
}
/* Once the confirm is showing, the stars and the actions step out of the way —
   there is exactly one decision left to make. */
.shelfd-lib-ratepop.is-confirming .shelfd-lib-ratepop-actions,
.shelfd-lib-ratepop.is-confirming .shelfd-lib-ratepop-hint,
.shelfd-lib-ratepop.is-confirming .shelfd-lib-stars { display: none; }

.shelfd-lib-ratepop-confirm { margin-top: calc(4 * var(--shelfd-px, 1px)); }
.shelfd-lib-ratepop-confirm-title {
  font-size: calc(14 * var(--shelfd-px, 1px));
  font-weight: 700;
  color: #fff;
}
.shelfd-lib-ratepop-confirm-sub {
  margin-top: calc(5 * var(--shelfd-px, 1px));
  font-size: calc(11.5 * var(--shelfd-px, 1px));
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.5);
}
.shelfd-lib-ratepop-confirm-row {
  display: flex;
  gap: calc(8 * var(--shelfd-px, 1px));
  margin-top: calc(12 * var(--shelfd-px, 1px));
}
.shelfd-lib-ratepop-cancel,
.shelfd-lib-ratepop-danger {
  appearance: none; -webkit-appearance: none;
  flex: 1 1 0;
  padding: calc(10 * var(--shelfd-px, 1px)) 0;
  border-radius: calc(10 * var(--shelfd-px, 1px));
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
  font-family: inherit;
  font-size: calc(13.5 * var(--shelfd-px, 1px));
  font-weight: 600;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.shelfd-lib-ratepop-danger {
  border-color: rgba(239, 68, 68, 0.5);
  background: rgba(239, 68, 68, 0.16);
  color: #FF8A8A;
}
/* Mid-delete the popup is on its way out; nothing in it should still be
   pressable. */
.shelfd-lib-ratepop.is-deleting { pointer-events: none; }

/* ── stars ────────────────────────────────────────────────────────────────
   Two identical rows of five glyphs, the top one clipped by width to the
   score. A half star is a real 50% clip of the rendered glyph, so there is no
   seam at any size — unlike composing halves from two separately-sized
   buttons, which is what showed a join at 30px. */
.shelfd-lib-stars {
  --lib-star-size: 45px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(6 * var(--shelfd-px, 1px));
}
.shelfd-lib-stars-track {
  position: relative;
  display: inline-block;
  line-height: 1;
  /* The scrub is a horizontal drag, which the browser would otherwise claim as
     a scroll gesture and never hand to the pointer handlers. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  transition: transform 160ms cubic-bezier(0.34, 1.4, 0.64, 1);
}
.shelfd-lib-stars-track.is-bumped { animation: shelfd-lib-star-bump 300ms ease-out; }
@keyframes shelfd-lib-star-bump {
  0% { transform: scale(1); }
  38% { transform: scale(1.09); }
  100% { transform: scale(1); }
}
.shelfd-lib-stars-base,
.shelfd-lib-stars-fill {
  font-size: var(--lib-star-size);
  line-height: 1;
  /* Locked so both rows advance identically — any per-glyph spacing difference
     between the two layers would show as a ghost edge on the clip. Tight at
     45px so five stars plus the value still fit a 360px screen. */
  letter-spacing: calc(var(--lib-star-size) * 0.03);
  white-space: nowrap;
  font-family: system-ui, -apple-system, 'Segoe UI Symbol', sans-serif;
}
.shelfd-lib-stars-base { color: rgba(255, 255, 255, 0.20); }
.shelfd-lib-stars-fill {
  position: absolute;
  inset: 0;
  overflow: hidden;
  color: #E6C766;
  transition: width 200ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Ten half-step targets across the row. Pointer input is handled on the TRACK
   so a drag reads continuously; these stay for keyboard and screen-reader
   access, and must not intercept the pointer or they would break the scrub the
   moment a finger crossed a boundary. */
.shelfd-lib-stars-hits {
  position: absolute;
  inset: 0;
  display: flex;
  pointer-events: none;
}
.shelfd-lib-star-hit {
  appearance: none; -webkit-appearance: none;
  flex: 1 1 0;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  /* Taller than the glyphs so the row is comfortable to hit with a thumb. */
  margin: calc(-10 * var(--shelfd-px, 1px)) 0;
}
.shelfd-lib-star-hit:focus-visible {
  pointer-events: auto;
  outline: 2px solid #E6C766;
  outline-offset: 2px;
  border-radius: 3px;
}
.shelfd-lib-stars-value {
  font-size: calc(15 * var(--shelfd-px, 1px));
  font-weight: 600;
  color: rgba(255, 255, 255, 0.86);
  min-width: calc(26 * var(--shelfd-px, 1px));
  text-align: left;
}

/* A hold is a rating gesture here, so the OS text-selection and callout menus
   must not compete with it. */
.shelfd-lib-grid-tile {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

@media (prefers-reduced-motion: reduce) {
  .shelfd-lib-ratepop-poster,
  .shelfd-lib-ratepop-card { transition: opacity 120ms ease; }
  .shelfd-lib-ratepop.is-open .shelfd-lib-ratepop-poster { transform: none; }
}

/* ── empty state ──────────────────────────────────────────────────────────── */
.shelfd-lib-grid-empty {
  padding: 18vh 24px 0;
  text-align: center;
  color: rgba(255, 255, 255, 0.6);
}
.shelfd-lib-grid-empty p { margin: 0 0 6px; font-size: calc(14 * var(--shelfd-px, 1px)); }
.shelfd-lib-grid-empty-sub {
  font-size: calc(12 * var(--shelfd-px, 1px)) !important;
  color: rgba(255, 255, 255, 0.4);
}

/* Four across once there is room for it — on a tablet or landscape phone a
   three-wide grid leaves the posters comically large. */
@media (min-width: 620px) {
  .shelfd-lib-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  .shelfd-lib-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .shelfd-lib-grid-scroll { padding-left: 20px; padding-right: 20px; }
}

@media (prefers-reduced-motion: reduce) {
  .shelfd-lib-grid-overlay { transition: none; }
  .shelfd-lib-grid-tile:active,
  .shelfd-lib-grid-back:active { transform: none; }
}
