/**
 * Collection browser (CB-5a) — the shared chrome every collection surface
 * consumes: grid/rows list, poster cards, chip row + overflow drawer, empty
 * states, the floating selection bar, and the staggered entrance.
 *
 * Cascade position is the design: linked immediately AFTER builder.css and
 * BEFORE every feature sheet (see index.html), so this sheet CONSUMES
 * builder.css primitives (.pb-modal, .pb-checkbox-input, .pb-skeleton-*) and
 * LOSES equal-specificity ties to donor sheets that come later — which is what
 * lets each donor keep its exact look during migration without a single
 * `!important`. Do not move the <link>; design-tokens COLLECTION-5 pins it.
 *
 * All geometry reads --pb-coll-* tokens minted in tokens.css (bare :root) —
 * COLLECTION-1/2/3 pin the family. Colors read the theme tokens, so .pb-dark
 * flips this sheet for free; the ONE deliberate exception is the selection
 * bar, which is dark in BOTH themes (--pb-coll-selbar-surface is a literal on
 * purpose).
 *
 * `content-visibility: auto` is deliberately NOT used here. It stays on
 * .pb-template-card only: paint containment would clip the project card's
 * .is-selected ring, which draws at a positive outline-offset OUTSIDE the
 * poster box.
 */

/* ── Shell ──────────────────────────────────────────────────────────────── */

.pb-coll-shell {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

.pb-coll-toolbar {
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  padding-bottom: var(--pb-space-4);
}

/* The right-hand slot (a sort order, a source). `margin-left: auto` rather than
   `justify-content: space-between` on the row: the meta span is empty at rest,
   and space-between would then park the control mid-row wherever the empty span
   happened to sit. */
.pb-coll-toolbar-trailing {
  flex: 0 0 auto;
  margin-left: auto;
  min-width: 0;
}

/* A search field does not get more useful past ~440px — it gets harder to scan
   back to the start of what you typed — and capping it is what frees the right
   of the row for a second control.

   Scoped with `:has()` to a toolbar that ACTUALLY has one. A blanket cap would
   change all twelve collection surfaces, most of which have nothing to put on
   the right, and would leave their meta span floating mid-row for no reason.

   Stated as the BASE and undone at the phone band, not stated inside a
   `min-width: 641px` — MOBILE-P1 requires every in-band breakpoint to be the
   one canonical 640px, and 641 is how a second phone breakpoint gets born. */
.pb-coll-toolbar:has(> .pb-coll-toolbar-trailing) > .pb-coll-search-wrap {
  max-width: 440px;
}

/* Phone: the control wraps under the field rather than squeezing it to nothing,
   and the field takes the row back. `flex-wrap` scoped to a toolbar that has a
   trailing control — an unwrapped row is the contract everywhere else. */
@media (max-width: 640px) {
  .pb-coll-toolbar:has(> .pb-coll-toolbar-trailing) {
    flex-wrap: wrap;
  }
  .pb-coll-toolbar:has(> .pb-coll-toolbar-trailing) > .pb-coll-search-wrap {
    max-width: none;
  }
  .pb-coll-toolbar > .pb-coll-toolbar-trailing {
    margin-left: 0;
  }
}

/* ── The search field (MK-1b) ────────────────────────────────────────────
   A label, so the glyph is part of the target: clicking the magnifier focuses
   the field, which a wrapping <div> cannot do without a listener.

   The BOX moved from the input to the wrap. That is the whole trick — a
   leading icon inside a bordered input means the input's own border has to
   start left of its text, and every surface that tried it ended up faking the
   inset with padding. Here the wrap owns the border, the radius and the focus
   ring; the input is transparent and borderless inside it. */
.pb-coll-search-wrap {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  /* space-4 rather than space-5: the input inside contributes 8px of its own
     horizontal padding from the global input rule (see below), so 8 + 8 lands
     the text where 12 would have been intended to. */
  padding: 0 var(--pb-space-4);
  background: var(--pb-surface-2);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  transition:
    border-color var(--pb-transition),
    box-shadow var(--pb-transition);
}

.pb-coll-search-wrap:focus-within {
  border-color: var(--pb-accent);
  box-shadow: 0 0 0 3px var(--pb-accent-tint);
}

/* The glyph is an affordance, not decoration, so it takes the accent with the
   ring — one state change, not two things that happen to move together. */
.pb-coll-search-icon {
  flex: none;
  color: var(--pb-text-faint);
  transition: color var(--pb-transition);
}

.pb-coll-search-wrap:focus-within .pb-coll-search-icon {
  color: var(--pb-accent);
}

.pb-coll-search {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: var(--pb-font-md);
  color: var(--pb-text);
  background: var(--pb-surface-2);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  padding: var(--pb-space-3) var(--pb-space-5);
}

/* Inside the wrap the input surrenders its own box — two nested borders and
   two radii is the look this rule exists to prevent.

   PADDING IS DELIBERATELY NOT SET HERE, and the reason is worth knowing before
   anyone adds it back: `builder.css` styles every text input through
   `input:not([type='color']):not([type='checkbox']):not([type='range'])
   :not([type='file'])`, and four `:not()` arguments make that (0,4,1) — it
   outranks any sane class selector, so the input keeps its global 6px/8px
   whatever this rule says. (The same is true of `.pb-coll-search`'s own padding
   above, which has therefore never rendered; it is identical to the global
   value by coincidence, not by effect.) The wrap's own padding is sized with
   that 8px accounted for, rather than starting a specificity arms race with a
   rule a hundred other inputs depend on. */
.pb-coll-search-wrap .pb-coll-search {
  background: none;
  border: 0;
  border-radius: 0;
}

.pb-coll-search:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: 1px;
}

/* The wrap already paints focus for the whole control; a second ring on the
   input inside it is two rings for one focus — and it is TWO declarations that
   have to be reset, not one. `builder.css` gives every `input:focus-visible` an
   outline AND a 4px `--pb-accent-glow` box-shadow, so clearing only the outline
   leaves the glow drawing a hard rectangle inside the pill. That is invisible
   to the geometry gate — a box-shadow moves no box — and was caught by looking
   at the harness PNG. */
.pb-coll-search-wrap .pb-coll-search:focus-visible {
  outline: none;
  box-shadow: none;
}

.pb-coll-meta {
  color: var(--pb-text-dim);
  font-size: var(--pb-font-sm);
  white-space: nowrap;
}

/* ── Chip row + overflow drawer ─────────────────────────────────────────── */

.pb-coll-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pb-space-2);
  padding-bottom: var(--pb-space-4);
}

.pb-coll-chips-sticky {
  flex: 0 0 auto;
  z-index: var(--pb-z-base);
}

/* ── Phone: one scrolling row, with the trailing control pinned ──
   Wrapping is right on a desktop, where the row has the width to spend. At
   390px the template selector's filters wrapped to THREE rows and pushed
   "Built by" onto a fourth of its own — 131px of chips plus a full-width
   select before a single template card is visible.

   So the row stops wrapping and scrolls instead, and the trailing control
   (`renderChipRow`'s owned end slot — the "Built by" select) sticks to the
   right edge so the thing that narrows the whole list does not scroll out of
   reach behind the categories it narrows.

   `background: inherit` rather than a token: the row's own surface already
   paints opaque, and inheriting tracks it through light/dark and through
   whichever surface embeds the row, instead of pinning a second copy of the
   colour that could drift from the first. */
@media (max-width: 640px) {
  .pb-coll-chips {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    /* A fling that runs out of chips must not hand its momentum to the sheet
       behind, or to the browser's back gesture — same reasoning as the command
       bar's page-tab strip. */
    overscroll-behavior-x: contain;
  }

  .pb-coll-chips::-webkit-scrollbar {
    display: none;
  }

  /* Chips size to content in a scroller; without this they compress to fit and
     the row never overflows, which is the failure that looks like success. */
  .pb-coll-chips > * {
    flex: 0 0 auto;
  }

  /* Descendant selector, not the bare class: the trailing control brings its
     own `position: relative` and `margin-left: auto` (palette.css
     `.pb-tpl-v2-filter`), which are the same 0,1,0 as this class and win on
     file order. Qualifying by the row it lives in is both true and enough. */
  .pb-coll-chips .pb-coll-chips-trailing {
    position: sticky;
    inset-inline-end: 0;
    /* Clears the chips passing underneath, and re-establishes the gap the
       scroller would otherwise let a chip close up against. */
    background: inherit;
    padding-inline-start: var(--pb-space-2);
    /* `inset-inline-end: 0` sticks to the CONTENT edge, so a chip scrolling
       through the row's own inline padding stays visible past this control —
       a sliver of "…3" showing to the right of it. Reach into that padding and
       repay it as padding here, so the inherited background covers the strip.
       A surface whose row has no inline padding needs no correction. */
    margin-inline-end: calc(-1 * var(--pb-modal-body-pad));
    padding-inline-end: var(--pb-modal-body-pad);
    /* `margin-left: auto` (palette.css) pushes it to the end of a WRAPPING row;
       in a scroller it would add infinite free space and strand it far off to
       the right, past everything. */
    margin-inline-start: 0;
    /* Unconstrained it grew to 348 of 390px and covered every chip — a pinned
       control that hides what it filters. Half the row is the most it may take;
       the value truncates inside that. */
    max-width: 50%;
    min-width: 0;
  }

  /* The caption is redundant on a phone: the value beside it already reads
     "All developers (371)". Dropped from the layout, NOT from the accessible
     name — the trigger's `aria-labelledby` still points at this element, and a
     `display: none` target still contributes to the computed name. */
  .pb-coll-chips .pb-coll-chips-trailing .pb-tpl-v2-filter-label {
    display: none;
  }

  .pb-coll-chips .pb-coll-chips-trailing .pb-tpl-v2-filter-trigger {
    min-width: 0;
  }

  .pb-coll-chips .pb-coll-chips-trailing .pb-tpl-v2-filter-value {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.pb-coll-chip-sentinel {
  display: block;
  height: 1px;
  margin-bottom: -1px;
}

.pb-coll-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--pb-space-2);
  font: inherit;
  font-size: var(--pb-font-sm);
  color: var(--pb-text);
  background: var(--pb-surface-2);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  padding: var(--pb-space-1) var(--pb-space-4);
  cursor: pointer;
  /* SS-7: WCAG 2.2 Target Size (Minimum) is 24x24 and is NOT pointer-scoped —
     a 21px chip is under it with a mouse too. Measured at 21px tall before
     this line, on every chip-bearing collection surface; surfaced by the
     variant picker's phone gate, but never a defect this initiative
     introduced. `min-height` rather than a padding bump, so the pill's ink
     keeps its rhythm and only the box grows. */
  min-height: 24px;
}

.pb-coll-chip:hover {
  background: var(--pb-bg-hover);
}

/* The active chip is a filled control, so its ink is the on-accent token, not
   the body text colour. --pb-accent-subtle + --pb-text-bright measured 3.5:1
   and the count inside it (--pb-text-dim) 1.9:1 — the number was effectively
   invisible on the fill. The pairing --pb-accent / --pb-on-accent is white on
   blue in the light register and dark on lifted blue under .pb-dark, so both
   registers stay legible without a second colour written here. */
.pb-coll-chip[aria-pressed='true'] {
  background: var(--pb-accent);
  border-color: var(--pb-accent);
  color: var(--pb-on-accent);
}

/* The count must not fall back to --pb-text-dim on the fill — inherit the
   chip's ink and lose a little weight instead. */
.pb-coll-chip[aria-pressed='true'] .pb-coll-chip-count {
  color: inherit;
  opacity: 0.85;
}

.pb-coll-chip[aria-pressed='true']:hover {
  background: var(--pb-accent-hover);
  border-color: var(--pb-accent-hover);
}

.pb-coll-chip:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: 1px;
}

.pb-coll-chip-count {
  color: var(--pb-text-dim);
  font-size: var(--pb-font-xs);
}

.pb-coll-more {
  display: inline-flex;
  align-items: center;
  gap: var(--pb-space-2);
  color: var(--pb-text-dim);
  border: 1px dashed var(--pb-border);
  border-radius: var(--pb-radius-pill);
  padding: var(--pb-space-1) var(--pb-space-4);
  cursor: pointer;
}

.pb-coll-more:hover {
  color: var(--pb-text-bright);
  border-color: var(--pb-accent);
  background: var(--pb-bg-hover);
}

/* Open reads exactly like an active chip, and for the same measured reason.
   This rule used to pair `--pb-text-bright` with `--pb-accent-subtle` — which
   is the 3.5:1 combination the active-chip comment forty lines above says was
   measured and rejected, with its count at 1.9:1. The chip was fixed and this
   control kept the defect, because they are two rules and only one was read.
   `--pb-accent` / `--pb-on-accent` is white on blue in the light register and
   dark on lifted blue under .pb-dark, so both stay legible. */
.pb-coll-more[aria-expanded='true'] {
  color: var(--pb-on-accent);
  border-color: var(--pb-accent);
  border-style: solid;
  background: var(--pb-accent);
}

.pb-coll-more[aria-expanded='true']:hover {
  background: var(--pb-accent-hover);
  border-color: var(--pb-accent-hover);
  color: var(--pb-on-accent);
}

/* Same as the chip's: the count inherits the filled ink rather than falling
   back to --pb-text-dim on top of the accent. */
.pb-coll-more[aria-expanded='true'] .pb-coll-more-count {
  color: inherit;
  opacity: 0.85;
}

.pb-coll-more-count {
  font-size: var(--pb-font-xs);
  font-variant-numeric: tabular-nums;
}

.pb-coll-drawer[hidden] {
  display: none;
}

.pb-coll-drawer {
  flex: 0 0 auto;
  columns: var(--pb-coll-track-min);
  column-gap: var(--pb-space-6);
  max-height: min(46dvh, 420px);
  overflow-y: auto;
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-island);
  background: var(--pb-surface-2);
  padding: var(--pb-space-5);
  margin-bottom: var(--pb-space-4);
}

.pb-coll-drawer-group {
  break-inside: avoid;
  padding-bottom: var(--pb-space-4);
}

.pb-coll-drawer-letter {
  color: var(--pb-text-dim);
  font-size: var(--pb-font-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding-bottom: var(--pb-space-2);
}

.pb-coll-drawer-items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pb-space-2);
}

/* ── Scroller + the two list layouts ────────────────────────────────────── */

.pb-coll-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

.pb-coll-scroll-empty {
  display: flex;
  flex-direction: column;
}

.pb-coll-scroll-empty .pb-coll-empty {
  flex: 1 1 auto;
}

/* The selection bar's strip. `pb-coll-scroll-reserve` is stamped ONCE at
   build time on surfaces that have selection, and the padding then never
   toggles — reserving only while .is-selecting would grow the body when the
   first checkbox is ticked and shove every card upward at the exact moment
   the user is aiming at the next one; collection-surface-tests pins with
   css-tree that this rule is not gated on .is-selecting. A selection-less
   surface (Fleet) carries no reserve at all — there is no bar to reserve
   for. */
.pb-coll-scroll-reserve {
  padding-bottom: var(--pb-coll-selbar-reserve);
}

.pb-coll-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--pb-coll-track-min), 1fr));
  column-gap: var(--pb-coll-gap-col);
  row-gap: var(--pb-coll-gap-row);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* `density: 'dense'` — an icon grid. Same rule shape, a different token, so
   the geometry still lives in exactly one place per density. */
.pb-coll-grid-dense {
  grid-template-columns: repeat(auto-fill, minmax(var(--pb-coll-track-min-dense), 1fr));
  column-gap: var(--pb-space-4);
  row-gap: var(--pb-space-4);
}

.pb-coll-grid-wide {
  grid-template-columns: repeat(auto-fill, minmax(var(--pb-coll-track-min-wide), 1fr));
}

/* `density: 'strip'` (SS-6) — ONE full-width column. The only density with no
   track token, deliberately: the others answer "how small may a card get
   before it wraps", and this one answers "the card is the row".

   It exists because for a strip poster, legibility is arithmetic on card
   WIDTH, not on the aspect ratio. A nav bar occupies a fixed ~11% of its
   square capture, so its rendered height is 0.11 x the card width whatever
   ratio the frame uses: 215px cards put the bar at 24px, 333px at 37px, one
   column at ~900px puts it at ~101px. The donor popover this replaced was
   single-column for exactly this reason (`grid-template-columns: 1fr`), and
   dropping to a multi-column grid is what made SS-5's nav cards illegible.
   Affordable at 452 records only BECAUSE this phase adds the facet chips and
   the search that cut the list. */
.pb-coll-grid-strip {
  grid-template-columns: 1fr;
}

/* `density: 'media'` — a grid of IMAGES. Same rule shape as the two above; the
   row gap opens up because the caption sits on the surface under the frame
   rather than inside a card, so the gap is what separates one tile's facts
   line from the next tile's picture. */
.pb-coll-grid-media {
  grid-template-columns: repeat(auto-fill, minmax(var(--pb-coll-track-min-media), 1fr));
  column-gap: var(--pb-space-6);
  row-gap: var(--pb-space-6h);
}

.pb-coll-rows {
  display: flex;
  flex-direction: column;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ── Items ──────────────────────────────────────────────────────────────── */

.pb-coll-item {
  position: relative;
  animation: pb-coll-item-in 260ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  animation-delay: calc(var(--pb-coll-idx, 0) * var(--pb-coll-stagger-step));
}

/* The disarm is the non-obvious half of the stagger: `[hidden]` is
   display:none, and an element returning from display:none RESTARTS its CSS
   animation — so a still-armed grid replays the whole entrance on every
   keystroke. armSettle() stamps the container after the entrance has played. */
[data-settled='true'] .pb-coll-item {
  animation: none;
}

.pb-coll-item[hidden] {
  display: none;
}

.pb-coll-card {
  display: flex;
  flex-direction: column;
  gap: var(--pb-space-2);
}

.pb-coll-row {
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  padding: var(--pb-space-3) var(--pb-space-2);
  border-radius: var(--pb-radius-md);
}

/* ── Tiles (PP-0) ──────────────────────────────────────────────────────────
   Leading/trailing non-record items: the container is inert; the primary
   <button> is the tile. Same track, same poster box, same title line as a
   record — one shape for everything a user can click to insert or create. */

.pb-coll-tile {
  cursor: pointer;
}

.pb-coll-tile-primary {
  display: flex;
  flex-direction: column;
  gap: var(--pb-space-2);
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  text-align: left;
  color: inherit;
  font: inherit;
  cursor: pointer;
  border-radius: var(--pb-coll-poster-radius);
}

.pb-coll-row .pb-coll-tile-primary {
  flex-direction: row;
  align-items: center;
  gap: var(--pb-space-4);
}

.pb-coll-tile-primary:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: var(--pb-space-1);
}

.pb-coll-tile .pb-coll-poster {
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: none;
  transition:
    border-color 160ms ease,
    background 160ms ease,
    color 160ms ease;
}

/* The dashed "make one" affordance — the same idiom as .pb-coll-more. */
.pb-coll-tile-create .pb-coll-poster {
  border: 1px dashed var(--pb-border);
  background: transparent;
  color: var(--pb-text-dim);
}

.pb-coll-tile-create .pb-coll-tile-primary:hover .pb-coll-poster,
.pb-coll-tile-create .pb-coll-tile-primary:focus-visible .pb-coll-poster {
  border-color: var(--pb-accent);
  background: var(--pb-accent-subtle);
  color: var(--pb-text-bright);
}

.pb-coll-tile-clipboard .pb-coll-poster {
  border: 1px solid var(--pb-accent);
}

.pb-coll-tile-glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--pb-icon-btn);
  height: var(--pb-icon-btn);
  font-size: var(--pb-font-xl);
  line-height: 1;
}

.pb-coll-tile-glyph svg {
  width: 100%;
  height: 100%;
}

.pb-coll-tile-tag {
  position: absolute;
  top: var(--pb-space-2);
  left: var(--pb-space-2);
  padding: 2px var(--pb-space-2);
  border-radius: var(--pb-radius-sm);
  background: var(--pb-accent);
  color: #fff;
  font-size: var(--pb-font-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* A dismiss must be reachable on a touch screen, where hover never comes. */
@media (pointer: coarse) {
  .pb-coll-tile .pb-coll-actions {
    opacity: 1;
    pointer-events: auto;
  }
}

/* MK-7 — the phone track.
   `minmax(220px, 1fr)` resolves to ONE column in a 388px-wide bottom sheet, so
   every collection grid showed one record per screenful on a phone: the
   picker, My Projects, the template selector, all of them. The dense and wide
   variants are deliberately NOT re-aimed — a 100px icon grid already fits
   three across, and a 300px section preview is wide because a page band is not
   legible narrower, which is as true on a phone as on a desktop. */
@media (max-width: 640px) {
  .pb-coll-grid {
    grid-template-columns: repeat(auto-fill, minmax(var(--pb-coll-track-min-phone), 1fr));
  }
}

/* ── Disclosure row (PP-0) ─────────────────────────────────────────────────
   "N more from other templates — Show": the end-of-list reveal for records a
   surface keeps folded. A sibling of the list inside the scroller, so it
   scrolls with content and needs no grid-column span. */

/* The row sits at the very end of the scroller, so its bottom padding is the
   only thing between the button and the edge of the box. `--pb-space-2` (4px)
   left it welded to the bottom; the trailing inset now matches the leading one
   so the control reads as sitting in its own band rather than falling out of
   the list. */
.pb-coll-disclose {
  display: flex;
  justify-content: center;
  padding: var(--pb-space-6) 0 var(--pb-space-8);
}

.pb-coll-disclose[hidden] {
  display: none;
}

/* Records that arrived through a reveal replay the entrance even though the
   grid has settled: [data-settled] disarms the stagger for the whole list, and
   these are the exception it exists to make room for. */
[data-settled='true'] .pb-coll-item.pb-coll-item-new {
  animation: pb-coll-item-in 260ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
  animation-delay: calc(var(--pb-coll-idx, 0) * var(--pb-coll-stagger-step));
}

/* Opt-in hover lift — no lift on the base (the donors disagree on the number;
   palette keeps its own -3px until CB-13 converges the divergence visibly). */
.pb-coll-card-lift:hover .pb-coll-poster,
.pb-coll-card-lift:hover .pb-coll-poster-img {
  transform: translateY(var(--pb-coll-lift));
  box-shadow: var(--pb-coll-poster-shadow-hover);
}

/* ── The two poster forms ───────────────────────────────────────────────────
   BOX form (.pb-coll-poster): a <div> that carries the aspect-ratio with an
   absolutely positioned <img> inside — the project-card mechanics, and the
   layout-stability doctrine (the BOX reserves the space, not the image).
   IMG form (.pb-coll-poster-img): the <img> IS the poster — the template-card
   mechanics. Same tokens, different mechanics; do NOT collapse them. */

.pb-coll-poster {
  position: relative;
  aspect-ratio: var(--pb-coll-poster-ratio);
  border-radius: var(--pb-coll-poster-radius);
  box-shadow: var(--pb-coll-poster-shadow);
  overflow: hidden;
  transition:
    transform 160ms ease,
    box-shadow 160ms ease;
}

/* MK-2a: `:not(.pb-checker)` rather than a plain declaration. builder.css
   loads FIRST, so an unconditional `background` shorthand here would win and
   erase the shared checkerboard — the same cascade trap `.pb-decorative-thumb`
   was sitting in before MK-1c. Guarding the surface fill instead of raising
   the checker's specificity keeps the gradient quartet in exactly one place. */
.pb-coll-poster:not(.pb-checker) {
  background: var(--pb-surface-2);
}

.pb-coll-poster > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ── `poster.fit: 'contain'` (MK-2a) ────────────────────────────────────────
   A thumbnail's fit is a property of the asset, not of the grid. A photograph
   survives cropping; a wordmark does not.

   `width/height: auto` is the half of this that `object-fit: contain` alone
   does NOT give you: object-fit still stretches the element to the box and
   then letterboxes the picture inside it, so a 228px mark in a 220px track is
   upscaled either way. Intrinsic sizing clamped by max-* means the asset is
   shown at natural size or smaller, never larger.

   The max-* values subtract the inset explicitly. A percentage on an
   absolutely positioned box resolves against the containing block — the
   poster's whole padding box, not the region left by `inset` — so a bare
   `max-width: 100%` lets an oversized mark overrun the pad symmetrically and
   get clipped at the poster edge. `margin: auto` against four non-auto insets
   is what centres it. */
.pb-coll-poster-contain > img {
  inset: var(--pb-coll-poster-pad);
  margin: auto;
  width: auto;
  height: auto;
  max-width: calc(100% - 2 * var(--pb-coll-poster-pad));
  max-height: calc(100% - 2 * var(--pb-coll-poster-pad));
  object-fit: contain;
}

.pb-coll-poster-img {
  display: block;
  width: 100%;
  aspect-ratio: var(--pb-coll-poster-ratio);
  object-fit: cover;
  border-radius: var(--pb-coll-poster-radius);
  box-shadow: var(--pb-coll-poster-shadow);
  background: var(--pb-surface-2);
  transition:
    transform 160ms ease,
    box-shadow 160ms ease;
}

.pb-coll-poster-wide {
  aspect-ratio: var(--pb-coll-poster-ratio-wide);
}

/* SS-6: `poster.aspect: 'strip'` — for records whose art is a SHORT section in
   a square capture (a nav bar in the top ~10% of 1200x1200). The token's
   definition carries the measurement.

   `object-position: top center` is half the mechanism and not a nicety: with
   `cover` the default centre anchor would crop a nav bar straight out of the
   frame and show blank page. It is scoped to the strip box rather than added
   to `.pb-coll-poster > img`, because a photograph in a `wide` poster is
   better centred. */
.pb-coll-poster-strip {
  aspect-ratio: var(--pb-coll-poster-ratio-strip);
}

.pb-coll-poster-footer {
  aspect-ratio: var(--pb-coll-poster-ratio-footer);
}

.pb-coll-poster-short {
  aspect-ratio: var(--pb-coll-poster-ratio-short);
}

.pb-coll-poster-strip > img,
.pb-coll-poster-footer > img,
.pb-coll-poster-short > img {
  object-position: top center;
}

.pb-coll-poster > .pb-coll-poster-unavailable {
  position: absolute;
  inset: 0;
}

.pb-coll-poster-unavailable {
  display: flex;
  width: 100%;
  height: 100%;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--pb-space-2);
  padding: var(--pb-space-2);
  color: var(--pb-text-dim);
  font-size: var(--pb-font-sm);
  text-align: center;
}

.pb-coll-poster-unavailable .pb-illo {
  width: var(--pb-space-12);
  max-height: 50%;
}

.pb-coll-poster-unavailable .pb-illo > svg {
  display: block;
  width: 100%;
  height: 100%;
}

.pb-coll-monogram {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--pb-font-2xl);
  font-weight: 600;
  color: var(--pb-text-dim);
}

/* Poster-IS-the-button (primaryAction): the ring paints INSIDE the clipped
   poster — the negative offset is the point. */
.pb-coll-primary {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: var(--pb-coll-poster-radius);
}

.pb-coll-primary:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: var(--pb-coll-ring-offset-inset);
}

/* ── Text + marks ───────────────────────────────────────────────────────── */

.pb-coll-title {
  font-size: var(--pb-font-md);
  font-weight: 600;
  color: var(--pb-text-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-coll-meta-line {
  font-size: var(--pb-font-sm);
  color: var(--pb-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-coll-mark {
  background: var(--pb-accent-subtle);
  color: inherit;
  border-radius: 2px;
}

.pb-coll-badge {
  display: inline-flex;
  align-items: center;
  font-size: var(--pb-font-xs);
  color: var(--pb-text-dim);
  background: var(--pb-surface-2);
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-pill);
  padding: 0 var(--pb-space-2);
}

/* The detail stage's chrome when there is no dialog header to put it on —
   `mountCollectionList` used bare, which today is the render harness. The
   controls still have to exist and still have to work: a detail stage with no
   way out is not an acceptable degradation. */
.pb-coll-detail-bar {
  display: flex;
  align-items: center;
  gap: var(--pb-space-3);
  padding-bottom: var(--pb-space-4);
}

/* ── Selection + focus ──────────────────────────────────────────────────── */

.pb-coll-item:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: var(--pb-space-1);
}

.pb-coll-item.is-selected .pb-coll-poster,
.pb-coll-item.is-selected .pb-coll-poster-img {
  outline: var(--pb-coll-ring-w-selected) solid var(--pb-accent);
  outline-offset: var(--pb-coll-ring-offset-selected);
}

.pb-coll-row.is-selected {
  background: var(--pb-accent-subtle);
}

.pb-coll-check {
  position: absolute;
  top: var(--pb-space-2);
  left: var(--pb-space-2);
  z-index: var(--pb-z-base);
}

.pb-coll-row .pb-coll-check {
  position: static;
}

/* ── Hover-revealed overlay actions ─────────────────────────────────────────
   Revealed by hover AND :focus-within — the keyboard idiom the donors use;
   never display:none, so the reveal cannot restart entrance animations. */

.pb-coll-actions {
  position: absolute;
  top: var(--pb-space-2);
  right: var(--pb-space-2);
  display: flex;
  gap: var(--pb-space-2);
  min-height: var(--pb-coll-action-size);
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
  z-index: var(--pb-z-base);
}

.pb-coll-item:hover .pb-coll-actions,
.pb-coll-item:focus-within .pb-coll-actions {
  opacity: 1;
  pointer-events: auto;
}

.pb-coll-trail {
  display: flex;
  align-items: center;
  gap: var(--pb-space-2);
  margin-left: auto;
}

/* ── Empty / error states ───────────────────────────────────────────────── */

/* `[hidden]` MUST beat the display:flex below — the UA's [hidden] rule ties
   with .pb-coll-empty on specificity and loses to author CSS, so without this
   the "hidden" empty state renders as an 80px padded ghost under every list.
   The render harness caught it as a modal 80px too tall; jsdom never could.
   Same bug class as the folded-row rule project-history-diff-tests pins. */
.pb-coll-empty[hidden] {
  display: none;
}

.pb-coll-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--pb-space-2);
  padding: var(--pb-space-12) var(--pb-space-6);
  color: var(--pb-text-dim);
  text-align: center;
}

.pb-coll-empty-title {
  font-size: var(--pb-font-lg);
  font-weight: 600;
  color: var(--pb-text);
}

/* Illustration slot (empty-state initiative) — the scene painted above the
   title by paintEmptyState when a surface opts in. Sizing lives on .pb-illo
   (builder.css); this is only the breathing room against the title. */
.pb-coll-empty-illo {
  margin-bottom: var(--pb-space-2);
}

/* ── The two-view stage ─────────────────────────────────────────────────────
   Geometry is ALSO applied inline by createTwoViewStage — these rules are for
   readability; the behaviour must not depend on this sheet having loaded. */

.pb-coll-stage {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

.pb-coll-view {
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
}

/* ── The floating selection bar ─────────────────────────────────────────────
   An out-of-flow pill anchored to the MODAL CARD (offsetParent = .pb-modal),
   never sticky in the scroller: a sticky in-flow bar grows the body when the
   first checkbox is ticked and shoves every card upward — the click target
   moves out from under the pointer at the exact moment the user is aiming at
   the next one. The scroller reserves its strip unconditionally (above). */

.pb-coll-selbar {
  position: absolute;
  left: 50%;
  bottom: var(--pb-space-6);
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  background: var(--pb-coll-selbar-surface);
  color: var(--pb-coll-selbar-fg);
  border-radius: var(--pb-radius-pill);
  box-shadow: var(--pb-coll-selbar-shadow);
  padding: var(--pb-space-3) var(--pb-space-5);
  z-index: var(--pb-z-base);
  animation: pb-coll-selbar-in 180ms ease both;
}

.pb-coll-selbar[hidden] {
  display: none;
}

.pb-coll-selbar-count {
  font-size: var(--pb-font-sm);
  white-space: nowrap;
}

/* ── The slot queue (DS-1) ─────────────────────────────────────────────────
   Promoted out of media-kit.css, where these rules styled the image picker's
   own `.pb-image-picker-queue|-slots|-slot|-queue-*`. media-kit.css's own
   header says a rule there that re-styles something the kit owns is a private
   fork by construction — so this move is what that comment predicted, and the
   image picker is consumer 1 rather than the owner.

   THE BAR is the kit's modal header wearing `.pb-coll-bar`: the queue at the
   start, the search as the elastic middle, the close at the end. */

.pb-coll-bar {
  gap: var(--pb-space-5);
  padding: var(--pb-space-5) var(--pb-space-5) var(--pb-space-5) var(--pb-space-6);
  /* The header defaults to space-between for title-left/close-right. Here the
     search is the elastic middle, so the row packs from the start and the
     search takes the slack. */
  justify-content: flex-start;
}

/* `0 1 auto`, and the 0 is load-bearing. With `1 1 auto` the queue absorbs the
   row's free space, so on a phone — where the search has already taken its own
   line — it filled line one and pushed the CLOSE BUTTON down to a line of its
   own. It may shrink (the meta ellipsizes; `min-width: 0` is what lets it) and
   it may never grow. */
.pb-coll-queue {
  display: flex;
  align-items: center;
  gap: var(--pb-space-4);
  min-width: 0;
  flex: 0 1 auto;
}

.pb-coll-slots {
  display: flex;
  align-items: center;
  gap: var(--pb-space-3);
}

.pb-coll-slots[hidden] {
  display: none;
}

/* A slot chip is a PICTURE of the thing you are about to change — the size of
   a thumbnail, not of a button, because what it has to answer is "which one is
   that". Its accessible name carries the label and the filled state; the check
   badge below is decoration on top of that, never instead of it. */
.pb-coll-slot {
  position: relative;
  flex: none;
  width: 46px;
  height: 32px;
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  border: 1px solid var(--pb-border);
  border-radius: var(--pb-radius-md);
  background: var(--pb-surface-2);
  transition:
    transform var(--pb-transition),
    border-color var(--pb-transition),
    box-shadow var(--pb-transition);
}

.pb-coll-slot > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pb-coll-slot:hover {
  transform: translateY(-1px);
  border-color: var(--pb-border-strong);
}

.pb-coll-slot.is-current {
  border-color: var(--pb-accent);
  box-shadow: 0 0 0 2px var(--pb-accent-glow);
}

.pb-coll-slot:focus-visible {
  outline: var(--pb-coll-ring-w) solid var(--pb-accent);
  outline-offset: 1px;
}

/* Filled — a check, drawn in CSS so it cannot be mistaken for content. The
   ring in the card's own colour is what lifts it off a busy thumbnail. */
.pb-coll-slot.is-done::after {
  content: '';
  position: absolute;
  right: 2px;
  bottom: 2px;
  width: 12px;
  height: 12px;
  border-radius: var(--pb-radius-pill);
  background: var(--pb-accent);
  box-shadow: 0 0 0 1.5px var(--pb-modal-surface);
}

.pb-coll-slot.is-done::before {
  content: '';
  position: absolute;
  right: 5px;
  bottom: 6px;
  z-index: var(--pb-z-base);
  width: 6px;
  height: 3px;
  border: 1.5px solid var(--pb-on-accent);
  border-top: 0;
  border-right: 0;
  transform: rotate(-45deg);
}

.pb-coll-queue-meta {
  min-width: 0;
}

.pb-coll-queue-title {
  margin: 0;
  font-size: var(--pb-font-md);
  font-weight: 600;
  line-height: 1.25;
  color: var(--pb-text-bright);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-coll-queue-sub {
  margin: 1px 0 0;
  font-size: var(--pb-font-sm);
  line-height: 1.25;
  color: var(--pb-text-dim);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pb-coll-queue-sub:empty {
  display: none;
}

/* The queue's phone form. It sits AFTER the desktop rules above and not in the
   earlier grid media block, and that position is the whole rule: both are
   `.pb-coll-bar` at the same specificity, so the LATER one wins — and the
   first draft of this move put the phone form earlier in the file, where the
   desktop `gap: --pb-space-5` overrode the phone `--pb-space-4` and made the
   bar 4px taller on every phone. The geometry harness caught it as a 16-delta
   shift down the whole sheet; nothing in jsdom could have. */
@media (max-width: 640px) {
  /* DS-1, promoted with the queue. GRID, not a wrapping flex row, and the
     reason is a flexbox rule rather than a preference: flex WRAPS BEFORE IT
     SHRINKS. With the search on a line of its own, the queue and the 28px close
     button fit the first line only if the queue happens to be narrow enough —
     and at four slot chips it is not, so the close wrapped to a third line by
     itself. Named areas say what the layout IS; `minmax(0, 1fr)` is what lets
     the queue column give way. */
  .pb-coll-bar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      'queue close'
      'search search';
    align-items: center;
    gap: var(--pb-space-4) var(--pb-space-5);
  }

  .pb-coll-bar .pb-coll-queue {
    grid-area: queue;
  }

  .pb-coll-bar .pb-coll-search-wrap {
    grid-area: search;
  }

  .pb-coll-bar .pb-modal-close {
    grid-area: close;
    /* The header's -8px optical nudge is a flex-row alignment trick; in a grid
       cell it just pulls the button off its own track. */
    margin: 0;
  }

  /* The chips scroll rather than squeezing the label off the screen. A phone
     that shows two of four slots and scrolls to the rest is still a queue; one
     that shows four chips and no words is not. */
  .pb-coll-slots {
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .pb-coll-slots::-webkit-scrollbar {
    display: none;
  }
}

@keyframes pb-coll-item-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes pb-coll-selbar-in {
  from {
    opacity: 0;
    transform: translateX(-50%) translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateX(-50%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .pb-coll-item,
  [data-settled='true'] .pb-coll-item.pb-coll-item-new {
    animation: none;
  }

  /* Drop ONLY the animation. The centering transform must survive — dropping
     `transform` here would leave the bar half a width off (COLLECTION-4 pins
     exactly this). */
  .pb-coll-selbar {
    animation: none;
  }
}

@media (forced-colors: active) {
  .pb-coll-selbar {
    background: Canvas;
    color: CanvasText;
    border: 1px solid CanvasText;
  }

  .pb-coll-item.is-selected .pb-coll-poster,
  .pb-coll-item.is-selected .pb-coll-poster-img {
    outline-color: Highlight;
  }

  /* A dashed/accent poster border is the tile's only visual identity; it must
     survive forced colors or the tile is an unlabeled box. */
  .pb-coll-tile-create .pb-coll-poster,
  .pb-coll-tile-clipboard .pb-coll-poster {
    border-color: CanvasText;
  }

  .pb-coll-tile-tag {
    background: Highlight;
    color: HighlightText;
  }
}

/* ── The popover form (2026-09-05) ──────────────────────────────────────────
   `openCollectionPopover`: the engine inside the shared `.pb-popover-menu`
   primitive instead of a dialog. The primitive owns position, the open/close
   transition and the phone sheet (property-panel.css); this block turns its
   menu padding into a card with a header and a flex body, and dresses it in
   the MODAL's surface tokens so it reads as the same card as every kit picker,
   floating — the treatment `prototypes/variant-picker-demo.html` argued for.
   `.pb-popover-menu` alone is 0,1,0 and its sheet rule 0,2,0, so every
   override here carries the primitive's class and lands at 0,2,0 / 0,3,0. */

.pb-popover-menu.pb-coll-popover {
  padding: 0;
  gap: 0;
  /* The JS `width` option sets min-width; reposition() clamps max-width to the
     visual frame. This is the resting width between the two. */
  width: 520px;
  background: var(--pb-modal-surface);
  border-color: var(--pb-modal-border);
  box-shadow: var(--pb-shadow-popover);
  font-size: inherit;
  /* The kit's own scroller scrolls, never the card. */
  overflow: hidden;
}

/* The sheet rule makes the MENU the scroller; here the inner scroller is, and
   a scrolling flex column would let the header slide away. */
.pb-popover-menu.pb-coll-popover.pb-popover-menu-sheet {
  overflow: hidden;
}

.pb-coll-popover-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--pb-space-4);
  padding: var(--pb-space-5) var(--pb-space-5) var(--pb-space-4);
  border-bottom: 1px solid var(--pb-modal-border);
  flex: none;
}

.pb-coll-popover-heading {
  display: flex;
  flex-direction: column;
  gap: var(--pb-space-1);
  min-width: 0;
}

.pb-coll-popover-title {
  margin: 0;
  font-size: var(--pb-font-lg);
  font-weight: 600;
  line-height: 1.25;
  color: var(--pb-text-bright);
}

/* The close button's `margin: -8px` (builder.css) is a header-alignment nudge
   sized for the modal's larger header pad; in this tighter header it would
   climb above the title. */
.pb-coll-popover-header .pb-modal-close {
  margin: -4px -4px 0 0;
}

/* Mirrors what `.pb-modal-body` is for the dialog form, as a flex column so
   the kit shell's `flex: 1; min-height: 0` chain reaches the scroller. The
   card's max-height is set inline by the primitive (the visual frame); this
   cap keeps a short list from becoming a tall empty card. */
.pb-coll-popover-body {
  position: relative;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  max-height: 640px;
  padding: var(--pb-space-4) var(--pb-space-5) var(--pb-space-5);
}

/* A sheet has the whole width; let the list take the height it is given. */
.pb-popover-menu-sheet .pb-coll-popover-body {
  max-height: none;
}
