/* Overlays: team, collection, expansion, lightbox + responsive */

/* TEAM PANEL */
#team-fab {
  position: fixed; bottom: 35px; right: 35px;
  width: 58px; height: 58px; border-radius: 50%;
  background: var(--green); color: white;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 20px rgba(0,0,0,0.25);
  transition: transform var(--transition), box-shadow var(--transition);
  cursor: pointer; border: none; z-index: 205; position: fixed;
}
#team-fab img { width: 30px; height: 30px; object-fit: contain; filter: brightness(0) invert(1); }
#team-count-badge {
  position: absolute; top: -3px; right: -3px;
  background: var(--color-red); color: var(--color-white);
  font-family: var(--font-sans); font-variant-numeric: tabular-nums; font-size: 11px; font-weight: 600;
  width: 22px; height: 22px; border-radius: 50%;
  display: none; align-items: center; justify-content: center;
  border: 2px solid white;
}

/* TEAM SIDE PANEL */
#team-side-panel {
  position: fixed; bottom: 121px; right: 35px;
  width: 420px;
  z-index: 205;
  transform: translateX(calc(100% + 40px));
  transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
  pointer-events: none;
}
#team-side-panel.open {
  transform: translateX(0);
  pointer-events: all;
}
/* Detail opened FROM the team panel sits above it (team z-205) so the hero shows on
   top; closing reveals the team panel again. (Team opened over a detail stays on top.) */
#detail-overlay.above-team { z-index: 215; }
/* One tier further up for a detail opened from the PLAYS overlay (z-215, see its own
   comment further down). 215 alone would only TIE, and on a tie DOM order decides —
   #detail-overlay sits before #plays-overlay in index.html, so the tie loses and the
   character modal renders behind the stats list it was opened from. Declared after
   .above-team (same specificity) so the higher tier wins when both classes are on. */
#detail-overlay.above-plays { z-index: 217; }
#team-panel-inner {
  background: var(--surface-card); border-radius: var(--card-radius);
  width: 100%; padding: 30px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.25); position: relative;
  border: 1px solid var(--color-border);
}
/* The green handle + scrim are mobile-only (see the ≤600px block). On desktop the
   body wrapper is layout-invisible so the panel renders exactly as before. */
#team-sheet-handle, #team-scrim { display: none; }
#team-sheet-body { display: contents; }
#team-title {
  font-family: 'Amboy', sans-serif; font-size: 18px;
  color: var(--color-text-primary); text-align: center; letter-spacing: var(--tracking-wide); margin-bottom: 20px;
}
#team-heroes-row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2-5); margin-bottom: 8px;
  position: relative;
}
.team-slot {
  aspect-ratio: 1/1; border-radius: 16px; overflow: hidden;
  background: var(--off-grey); border: 2px dashed var(--light-grey);
  display: flex; align-items: center; justify-content: center; position: relative;
  /* Forces its own compositing layer so the rounded clip rasterizes cleanly on
     first paint — without this, a stray hairline can linger at the clip edge
     until something else (e.g. the :hover border-color swap below) repaints it. */
  transform: translateZ(0);
  /* Mobile: suppress the native long-press callout / image-save menu / selection
     so the hold-to-remove gesture isn't hijacked by the OS (mirrors .char-card). */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.team-slot.filled { border: 2px solid var(--light-grey); cursor: pointer; }
/* Filled slots are clickable (open the hero) — show a pointer cursor and highlight
   the border on hover to signal the affordance. */
/* position:absolute + inset:0 (rather than a flexed 100%/100% child) pins all four
   edges to the container's own box edges, so a fractional-pixel slot width (4-col
   grid on odd viewport widths) can't round the image a hair short of the container
   and leave a sliver of --off-grey showing at the bottom before a repaint hides it. */
.team-slot-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; }
.team-slot-empty { color: var(--light-grey); font-size: 20px; font-weight: 300; }
/* Specialty chip — the team-view counterpart to the hero-card pill. An opaque
   black lozenge (matching the card name banner) holds a color-tinted dot, so
   contrast is identical over any portrait. Always rendered; revealed only under
   body.specialty-view, mirroring .char-card-specialty. */
.team-slot-specialty {
  position: absolute; left: 5px; bottom: 5px;
  display: none; align-items: center;
  height: 23px; padding: 0 7px;
  background: var(--color-black); border-radius: 999px;
  z-index: 3; pointer-events: none;
}
body.specialty-view .team-slot-specialty { display: flex; }
.team-slot-specialty span { width: 13px; height: 13px; border-radius: 50%; }
.remove-hero {
  position: absolute; top: 4px; right: 4px;
  background: rgba(0,0,0,0.55); color: white;
  width: 26px; height: 26px; border-radius: 50%;
  font-size: 0;
  opacity: 0; transition: opacity var(--transition); cursor: pointer;
}
/* Geometric ✕ so it's perfectly centered in the circle (the text glyph sat high). */
.remove-hero::before, .remove-hero::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 11px; height: 2px; background: currentColor; border-radius: 1px;
}
.remove-hero::before { transform: translate(-50%, -50%) rotate(45deg); }
.remove-hero::after { transform: translate(-50%, -50%) rotate(-45deg); }
/* Hold-to-remove ring, mirroring the hero-card gesture. The ring is hidden until a
   hold starts (.long-press-active on the slot), then drains as the removal timer
   runs. The × stays the mouse affordance (hover-revealed above); touch uses the
   hold instead — the × is hidden on coarse pointers below. */
.team-slot .char-card-progress-ring { opacity: 0; transition: opacity 0.1s ease; }
.team-slot.long-press-active .char-card-progress-ring { opacity: 1; }
.team-slot.long-press-active { transform: scale(0.93); transition: transform 80ms ease; }
.team-slot.long-press-active .char-card-ring-fill { animation: ring-drain 650ms linear forwards; }
@media (pointer: coarse) {
  .remove-hero { display: none; }
}
/* Long-press-armed slot (see js/long-press-arm.js): the pill is the sole action, so
   hide the × and specialty chip rather than z-index-juggling them above the scrim
   like the grid card's badge/fav-heart — a filled slot has no second control worth
   keeping visible once armed. */
.team-slot.card-armed .remove-hero,
.team-slot.card-armed .team-slot-specialty { display: none; }
#team-empty-msg {
  display: none;
  position: absolute; inset: 0;
  align-items: center; justify-content: center;
  font-family: 'Amboy', sans-serif; font-size: 16px;
  color: var(--dark-grey); letter-spacing: var(--tracking-wide);
  pointer-events: none;
  grid-column: 1 / -1;
}
#team-heroes-row.empty #team-empty-msg { display: flex; }
#team-heroes-row.empty .team-slot { opacity: 0.4; }

/* Empty slot's "+" — reuses the geometry .team-slot-empty already had (vestigial
   until now: every empty slot became an add-affordance in the matchup builder
   redesign, since there was previously no way to add a hero FROM the panel itself). */
.team-add-tile { cursor: pointer; }
.team-add-icon { color: var(--light-grey); font-size: 26px; font-weight: 300; line-height: 1; }
@media (hover: hover) {
  .team-add-tile:hover { border-color: var(--color-hero-tag); }
  .team-add-tile:hover .team-add-icon { color: var(--color-hero-tag); }
}

/* ===== MODE GROUP SELECTOR (Co-op / Team vs Team / Civil War) =====
   Same sunken-track + JS-measured sliding thumb idiom as the header nav pill
   (js/main.js's movePill/#nav-bar) — reused rather than the anti-hero toggle's
   CSS-only thumb, which is hardcoded to exactly two options. Shared between the
   team panel (#team-mode-selector, js/team.js) and Log Play (#pl-mode-selector,
   js/play-log-form.js) — same three-option control, same reuse-the-DOM-nodes
   rendering discipline (see either module's renderModeSelector: rebuilding the
   thumb element on every render, rather than repositioning the SAME one, is what
   caused it to always animate in from the left regardless of the active tab). */
#team-mode-selector, #pl-mode-selector {
  position: relative; display: flex; gap: 2px;
  background: var(--color-off-white); border-radius: 999px; padding: 4px;
  margin-bottom: 16px;
}
.mode-group-thumb {
  position: absolute; left: 0; top: 4px; height: calc(100% - 8px);
  background: var(--surface-raised); border-radius: 999px;
  box-shadow: var(--shadow-pill);
  transition: transform 0.25s ease, width 0.25s ease;
  z-index: 0; pointer-events: none;
}
.mode-group-pill {
  position: relative; z-index: 1; flex: 1;
  padding: 8px 2px; border-radius: 999px; border: none; background: none;
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: var(--tracking-tight);
  color: var(--mid-grey); text-align: center; white-space: nowrap;
  transition: color var(--transition);
}
.mode-group-pill.active { color: var(--color-text-primary); }

/* Civil War's second-row variant control (Clash of Heroes / Registration Clash) —
   a plain two-option .sf-pill-style row, no thumb needed at only two options and
   this secondary control doesn't need the same visual weight as the primary one. */
/* A labelled VARIANT field (see renderModeVariant in js/play-log-form.js), not a second
   row of pills — the two Civil War modes are structurally identical, so the pick is only
   a label on the record and shouldn't compete with the mode selector above it. The
   negative top margin absorbs #pl-mode-selector's own 16px bottom margin. */
#pl-mode-variant { display: block; margin: -6px 0 0; }
#pl-mode-variant[hidden] { display: none; }

/* ===== VILLAIN SLOT (co-op + Team vs Team — the Civil War pair has no villain
   faction and hides this row entirely, see js/team.js's renderVillainSlot) ===== */
#team-villain-slot {
  display: flex; align-items: center; gap: 12px;
  background: #F1CCCD; border-radius: 12px; padding: 10px 14px;
  margin-bottom: 14px;
}
:root[data-theme="dark"] #team-villain-slot { background: rgba(226,70,81,0.16); }
.team-villain-tile {
  /* Matches the co-op hero slot's own rendered size (a 4-col grid at typical panel
     widths ~76px) — villain also appears in Team vs Team, where hero slots run
     closer to ~98px (a 3-col grid), so this can't be pixel-identical in both modes
     at once without JS-measuring the live grid; co-op is the more common context. */
  flex-shrink: 0; width: 76px; height: 76px; border-radius: 12px; overflow: hidden;
  background: var(--color-panel-dark);
  border: 2px solid var(--color-red); display: flex; align-items: center; justify-content: center;
  position: relative; cursor: pointer;
}
.team-villain-tile.team-add-tile { background: rgba(255,255,255,0.5); border-style: dashed; }
.team-villain-tile.team-add-tile .team-add-icon { color: var(--color-red); }
@media (hover: hover) {
  /* Neutral white, not a darker red or the hero-blue accent — stands out clearly
     against the tile's red border without borrowing another faction's color. */
  .team-villain-tile:hover { border-color: var(--color-white); }
  .team-villain-tile.team-add-tile:hover { background: rgba(255,255,255,0.75); }
}
.team-villain-img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
.team-villain-text { flex: 1; min-width: 0; }
.team-villain-label {
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: var(--tracking-wide);
  color: var(--color-red-dark); margin-bottom: 2px;
}
.team-villain-name {
  font-family: var(--font-sans); font-size: 13px; font-weight: 600; color: var(--color-text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#team-villain-clear {
  flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; border: none;
  background: rgba(0,0,0,0.08); color: var(--color-red-dark);
  font-size: 0; cursor: pointer; position: relative;
}
/* Geometric × so it's perfectly centered (the &times; glyph sat off-center) —
   mirrors .remove-hero's identical fix elsewhere in this file. */
#team-villain-clear::before, #team-villain-clear::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: 11px; height: 2px; background: currentColor; border-radius: 1px;
}
#team-villain-clear::before { transform: translate(-50%, -50%) rotate(45deg); }
#team-villain-clear::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* ===== VERSUS BANDS (Team vs Team / Civil War) =====
   #team-heroes-row switches from the co-op 4-column grid to a vertical stack of
   two color-coded bands + a VS badge — same container (see index.html's comment on
   why: main.js's hold-to-remove touch gesture is bound to #team-heroes-row itself,
   once, at module load). */
#team-heroes-row.versus { display: flex; flex-direction: column; gap: 0; margin-bottom: 14px; }
.team-band {
  border-radius: 12px; padding: 12px;
  background: color-mix(in srgb, var(--band-color) 7%, var(--surface-card));
  border: 1.5px solid color-mix(in srgb, var(--band-color) 35%, transparent);
}
.team-band-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.team-band-bar { width: 3px; height: 13px; border-radius: 2px; background: var(--band-color); flex-shrink: 0; }
.team-band-name {
  font-family: 'Amboy', sans-serif; font-size: 11px; letter-spacing: var(--tracking-wide);
  color: var(--color-text-primary);
}
.team-band-count { margin-left: auto; font-family: var(--font-sans); font-size: 11px; color: var(--mid-grey); }
.team-band-chart-toggle {
  margin-left: auto; flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%;
  border: 1.5px solid var(--band-color); background: none; color: var(--band-color);
  display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
}
.team-band-chart-caret { font-size: 12px; line-height: 1; transition: transform 0.15s ease; }
.team-band-chart-toggle[aria-expanded="true"] .team-band-chart-caret { transform: rotate(90deg); }
.team-band-slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.team-band .team-slot.filled { border-color: var(--band-color); }
.team-band .team-add-tile:not(:hover) { border-color: color-mix(in srgb, var(--band-color) 45%, transparent); }
.team-band-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 9px; }
.team-band-tag {
  font-family: var(--font-sans); font-size: 10px; font-weight: 600; letter-spacing: 0.03em;
  color: var(--band-color); border: 1px solid var(--band-color); border-radius: 999px;
  padding: 4px 9px; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 3px;
}
.team-band-tag.selectable { cursor: pointer; }
.team-band-tag.selected { background: var(--band-color); color: var(--color-white); }
.team-band-tag-more { color: var(--mid-grey); border-color: var(--light-grey); }
/* Small view-icon, shown only on the currently-selected pill (co-op .team-tag or a
   band's .team-band-tag) — a secondary tap target every unselected pill doesn't
   need to carry. Sized up slightly inside the larger co-op .team-tag. */
.team-pill-view {
  display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; margin-left: 1px; padding: 0; flex-shrink: 0;
  border: none; border-radius: 50%; background: rgba(255,255,255,0.35);
  color: inherit; font-size: 10px; line-height: 1; cursor: pointer;
}
.team-tag .team-pill-view { width: 16px; height: 16px; font-size: 12px; }
.team-band-summary {
  display: flex; flex-wrap: wrap; gap: 4px 9px; margin-top: 8px; padding-top: 8px;
}
.team-vs-badge {
  align-self: center; width: 32px; height: 32px; margin: -10px 0; z-index: 2;
  border-radius: 50%; background: var(--color-villain-dark); color: var(--color-white);
  display: flex; align-items: center; justify-content: center;
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: 0.02em;
  border: 3px solid var(--surface-card);
}

/* ===== PER-BAND CHART (expandable — replaces the old head-to-head bars, which
   compared both bands at once instead of showing one side's own split). Same
   donut js/team.js's paintDonutChart() paints for the co-op tab (#team-chart-crop/
   #team-chart-wrap below), just at a smaller `size` and without the %/# toggle —
   .team-chart-label/.team-chart-label img are shared/reused as-is, since their
   position is a % of whichever wrap contains them. ===== */
.team-band-chart-body {
  margin-top: 10px; padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--band-color) 25%, transparent);
}
.team-band-chart-body[hidden] { display: none; }
.team-band-chart-crop { overflow: hidden; }
.team-band-chart-wrap {
  position: relative; width: 340px; max-width: 100%;
  aspect-ratio: 340 / 310;
  margin: 0 auto; overflow: visible;
}
.team-band-chart-wrap svg { width: 100%; height: 100%; display: block; }

/* #team-chart-crop clips #team-chart-wrap's own 340x310 box down to just the
   content each render actually uses (see the trim comment in renderDonutChart) —
   overflow:hidden makes it a new block formatting context so wrap's negative
   top margin shifts content inside this box instead of collapsing through to
   #team-summary's margin-bottom above it. */
#team-chart-crop { overflow: hidden; margin: 20px 0; }
#team-chart-wrap {
  position: relative; width: 340px; max-width: 100%;
  aspect-ratio: 340 / 310;
  margin: 0 auto; overflow: visible;
}
#team-chart-wrap svg { width: 100%; height: 100%; display: block; }
.team-chart-label {
  position: absolute; transform: translate(-50%, -50%);
  background: var(--surface-raised); border-radius: 8px; padding: 5px 9px;
  border: 2px solid var(--light-grey);
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  display: flex; align-items: center; gap: 5px;
  font-family: var(--font-sans); font-variant-numeric: tabular-nums;
  font-size: 19px; font-weight: 600; color: var(--color-text-secondary);
  letter-spacing: var(--tracking-tight);
  white-space: nowrap; pointer-events: none;
}
.team-chart-label img { width: 32px; height: 32px; object-fit: contain; }

/* Donut label mode toggle — sits inside the donut's own center hole (innerR=60 of
   the 340x310 SVG, so a 28%-wide circle fits with room to spare), sized as a %
   of #team-chart-wrap so it scales with the chart at any width. Same sunken-track
   + raised-thumb idiom as the main nav pill (css/layout.css:220-233): depth signals
   selection instead of a dark fill, which pulled attention off the chart.
   TEMP: horizontal split with "Count"/"Percent" text, trying the full-word
   version per request — easy to revert to the %/# vertical split if it doesn't fit. */
.chart-mode-toggle {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 28%; aspect-ratio: 1 / 1;
  border-radius: 50%; background: var(--color-off-white);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.14);
  display: flex; flex-direction: column; padding: 4px; z-index: 2;
}
.chart-mode-thumb {
  position: absolute; top: 4px; left: 4px;
  width: calc(100% - 8px); height: calc(50% - 4px);
  border-radius: 999px 999px 0 0;
  background: var(--surface-raised); box-shadow: var(--shadow-pill);
  transition: top var(--transition), border-radius var(--transition);
  pointer-events: none;
}
.chart-mode-toggle[data-mode="pct"] .chart-mode-thumb {
  top: 50%; border-radius: 0 0 999px 999px;
}
.chart-mode-btn {
  position: relative; z-index: 1; flex: 1;
  display: flex; align-items: center; justify-content: center;
  border: none; background: transparent; cursor: pointer; padding: 0 2px;
  font-family: 'Amboy', sans-serif; font-size: 9px; line-height: 1.1;
  letter-spacing: 0.04em;
  text-align: center; white-space: nowrap;
  color: var(--color-ui-grey); transition: color var(--transition);
}
.chart-mode-toggle[data-mode="pct"] .chart-mode-btn[data-mode="pct"],
.chart-mode-toggle[data-mode="count"] .chart-mode-btn[data-mode="count"] {
  color: var(--color-text-primary);
}
/* --surface-raised is intentionally the DARKEST surface in dark mode (see the
   comment in tokens.css — it's tuned for the char-card art panel), so it can't
   supply a "raised = lighter than the track" thumb here. Override with a grey
   a couple steps up from --color-off-white's dark value (#2A2A2A) instead. */
:root[data-theme="dark"] .chart-mode-thumb { background: #454545; }

/* Strengths/weaknesses read supplementing the donut (team panel) / stat bars (hero
   detail modal). Strength + weakness sit on one line when they fit; wraps (weakness
   drops to its own line) when they don't. */
#team-summary, #hero-summary {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 4px 10px;
  margin: 0 0 14px; font-family: var(--font-sans);
}
.team-summary-strength, .team-summary-weakness {
  display: flex; align-items: center; gap: 4px;
  font-weight: 600;
}
.team-summary-strength { font-size: 14px; color: var(--color-text-primary); }
.team-summary-weakness { font-size: 13px; color: var(--color-orange); }
.team-summary-icon { width: 14px; height: 14px; flex-shrink: 0; }
.team-summary-action-icon { height: 26px; width: auto; flex-shrink: 0; vertical-align: middle; }

#team-pills {
  text-align: center; margin-top: 0; margin-bottom: 18px;
}
#team-pills-label {
  font-size: 13px; color: var(--mid-grey);
  margin-bottom: 8px; font-family: system-ui, sans-serif;
}
#team-pills-list {
  display: flex; flex-wrap: wrap; gap: 8px;
  justify-content: center;
}

/* CLEAR TEAM + Log a Play share one row — Log a Play gets more of the width since
   it's the primary action; Clear Team is the secondary, narrower outline pill. */
#team-bottom-actions { display: flex; align-items: stretch; gap: 10px; margin-top: 8px; }
#clear-team-btn {
  flex: 1; margin: 0;
  background: transparent; color: var(--red);
  font-family: 'Amboy', sans-serif; font-size: 13px;
  padding: 11px 10px; border-radius: 999px; letter-spacing: var(--tracking-tight);
  border: 2px solid var(--red); white-space: nowrap;
  transition: background var(--transition), color var(--transition); cursor: pointer;
}
#team-log-play-btn {
  flex: 2; margin-top: 0; width: auto;
  font-family: 'Amboy', sans-serif; font-weight: 900; letter-spacing: var(--tracking-tight);
}
/* No heroes yet: nothing to chart and nothing to clear, so hide both — the panel
   (auto height on desktop; sheet-body content height on mobile) then condenses to
   just the title + empty hero-slot row instead of reserving a blank 340x310 chart box. */
#team-side-panel.team-empty #team-chart-crop,
#team-side-panel.team-empty #team-summary,
#team-side-panel.team-empty #clear-team-btn {
  display: none;
}

/* Toast — brief auto-dismissing message (replaces blocking alert()s). */
#mu-toast {
  position: fixed; left: 50%; bottom: 40px;
  transform: translate(-50%, 20px);
  background: rgba(26,26,26,0.95); color: var(--color-white);
  font-family: var(--font-sans); font-size: 13px; font-weight: 500;
  padding: 10px 18px; border-radius: 20px;
  box-shadow: var(--shadow-lg);
  z-index: 400; opacity: 0; pointer-events: none;
  max-width: 80vw; text-align: center;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
#mu-toast.show { opacity: 1; transform: translate(-50%, 0); }

/* COLLECTION OVERLAY */
#collection-overlay { z-index: 210; } /* team panel (205) below; expansion (211) above */
#collection-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 960px; padding: 30px;
  box-shadow: var(--shadow-lg); position: relative; margin: auto;
}

/* SHARE MODAL */
#share-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 380px; padding: 30px;
  box-shadow: var(--shadow-lg); position: relative; margin: auto;
  display: flex; flex-direction: column; align-items: center; text-align: center;
}
#share-title { font-family: 'Amboy', sans-serif; font-size: 18px; color: var(--color-text-primary); }
#share-subtitle { font-size: 13px; color: var(--mid-grey); margin: 6px 0 20px; line-height: 1.4; }

/* PLAYS overlay + LOG PLAY overlay: same recipe as #share-panel (~6 lines), left-aligned
   rather than centered since both host a list/form rather than a single QR+link. Content
   (history list, stats, form fields) is filled in by later build steps. */
#plays-panel, #play-log-panel, #bgstats-reconcile-panel, #bgstats-export-prompt-panel,
#bgstats-export-settings-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 420px; padding: 30px;
  box-shadow: var(--shadow-lg); position: relative; margin: auto;
  max-height: calc(100dvh - 40px); overflow-y: auto; overscroll-behavior: contain;
}
/* LOG PLAY opts out of the shared internal-scroll recipe above and scrolls like the
   hero/villain detail modal instead: the panel is NOT its own scroll container, so
   .overlay-backdrop (which is already `overflow-y: auto`, see css/components.css) moves
   the WHOLE panel top-to-bottom rather than the form scrolling inside a fixed-height box.
   `clip` clips the rounded corners exactly like `hidden` but without establishing a scroll
   container — the same trick and the same reason as #detail-panel. Scoped to this panel
   only: #plays-panel hosts a long history list and the two bgstats panels are short, so
   all three keep the internal scroll. */
#play-log-panel {
  max-height: none;
  overflow: hidden;
  overflow: clip;
  overscroll-behavior: auto;
}
#plays-title, #play-log-title, #bgstats-reconcile-title, #bgstats-export-prompt-title,
#bgstats-export-settings-title {
  font-family: 'Amboy', sans-serif; font-size: 18px; color: var(--color-text-primary);
  margin-bottom: 16px;
}

/* BG STATS EXPORT SETTINGS — one labelled <select> per routable field
   (js/bgstats-export-settings.js generates the rows from EXPORTABLE_TYPES). */
.bgs-setting-row { margin-bottom: 14px; }
.bgs-setting-label {
  display: block; margin-bottom: 6px;
  font-family: var(--font-sans); font-size: 13px; font-weight: 600;
  color: var(--color-text-primary);
}
.bgs-setting-help { font-size: 12px; margin-top: 5px; line-height: 1.4; }
#bgstats-export-settings-actions { display: flex; gap: 10px; margin-top: 22px; }
#bgstats-export-settings-actions .offline-btn { flex: 2; }
#bgstats-export-settings-actions .pl-seat-remove-btn { flex: 1; }
#bgstats-export-settings-btn { margin-top: 10px; }
#plays-log-new-btn { margin-bottom: 18px; }

/* PLAYS select-mode bar — toggle button by default, select-all/cancel/export-count/
   delete-count once select mode is on (js/plays.js's selectBarHtml()). Empty when
   there's nothing to show (never true today, but keeps the :empty collapse
   consistent with #plays-stats). */
#plays-select-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px;
}
#plays-select-bar:empty { display: none; }
.plays-row-check { width: 18px; height: 18px; flex-shrink: 0; }

/* BG STATS EXPORT — single-play prompt (the multi-select bundle export has no overlay
   of its own, see index.html's comment). Functional-first (see the log-form note: get
   the data right, style it in a dedicated pass). */
#bgstats-export-prompt-body {
  font-family: var(--font-sans); font-size: 14px; color: var(--color-text-primary);
  margin-bottom: 20px;
}
#bgstats-export-prompt-actions {
  display: flex; gap: 10px; justify-content: flex-end;
}
#bgstats-export-prompt-send { text-decoration: none; text-align: center; }

/* PLAYS stats block — plain text stat line + the win/loss donut (js/plays.js's own
   small SVG helper; team.js's renderDonutChart is private to the team panel and reads
   state.team directly, so it isn't reused here — see plays.js's header comment). */
#plays-stats {
  display: flex; align-items: center; gap: 16px; margin-bottom: 20px;
  padding-bottom: 16px; border-bottom: 1.5px solid var(--hairline);
}
#plays-stats:empty { display: none; border-bottom: none; }
.plays-stats-text { font-family: var(--font-sans); font-size: 13px; color: var(--dark-grey); line-height: 1.6; }
.plays-stats-text strong { color: var(--color-text-primary); }

/* ===== PLAYS history rows =====
   Character TILES, not text. Deliberately built on the team sheet's own visual language
   (.team-slot / .pl-hero-row-avatar above): square portrait, rounded corners, a colored
   border carrying faction/team identity. A play row and the matchup builder are showing
   the same thing — who was at the table — so they read as one system rather than two.

   Width is the binding constraint: at a 375px phone the panel's content box is ~299px
   (see the STATS VIEW note below), leaving ~276px inside a row's own padding. Villain
   tile (36) + 4 hero columns (34 each) + the result/date column (~70) lands just inside
   that, which is why the hero tile count is capped at 4 in js/plays.js (HERO_TILE_CAP)
   rather than allowed to wrap. */
.plays-history-empty { font-family: var(--font-sans); font-size: 13px; color: var(--mid-grey); text-align: center; padding: 20px 0; }
/* Sticky inside #plays-panel, which is its own scroll container (overflow-y:auto, set
   with the shared panel rule above) — so this needs no extra positioning context. */
.plays-month-header {
  position: sticky; top: -30px; z-index: 2;
  background: var(--surface-card);
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: var(--tracking-wide);
  color: var(--mid-grey);
  padding: 8px 0 6px;
}
/* Two lines: the result/date strip, then the tiles. Stacking them rather than putting
   the result in a trailing column is what frees the full row width for tiles — a
   trailing column cost ~72px, which at phone width was the difference between fitting
   5 tiles and fitting all 7 of a full Team vs Team table. */
.plays-row {
  display: flex; flex-direction: column; gap: 4px;
  border: 1.5px solid var(--hairline); border-radius: 10px;
  padding: 8px 10px; margin-bottom: 8px; cursor: pointer;
}
.plays-row-body { display: flex; align-items: center; gap: 8px; }
/* An ignored play (excluded from every stat) or one with no recorded result. Dimmed so
   the list visibly explains why the aggregate counts are smaller than the row count —
   the badge in the meta column names which of the two it is. */
.plays-row.dim { opacity: 0.55; }
/* Strip above the tiles: date pinned left, result pinned right. */
.plays-row-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  min-height: 17px;
}
.plays-row-date { font-family: var(--font-sans); font-size: 10px; color: var(--mid-grey); }
.plays-row-result {
  font-family: var(--font-sans); font-size: 10px; font-weight: 600;
  padding: 2px 7px; border-radius: var(--radius-pill); white-space: nowrap;
  display: inline-block;
}
.plays-row-result.win { background: #DCF5E3; color: #1E7A3D; }
.plays-row-result.loss { background: #FBE0E1; color: #A8333B; }
.plays-row-result.muted { border: 1px solid var(--light-grey); color: var(--mid-grey); font-weight: 500; }

/* ---- tiles ---- */
.plays-tile-group { display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 0; }
/* Wraps by TEAM, never mid-team: each .plays-team-group is its own nowrap box, so the
   wrap points are only ever between sides. On a narrow row a versus play lands as the
   villain on the left with the two teams stacked beside it, which is the same reading
   the team sheet gives. The column gap between groups is wider than the gap between
   tiles inside one, so the sides stay legible as separate blocks. */
.plays-tile-row { display: flex; flex-wrap: wrap; gap: 6px 10px; }
.plays-team-group { display: flex; flex-wrap: nowrap; gap: 3px; }
/* Tile size scales with the space available, but NOT with a row's own contents — it
   depends only on viewport width, so every row in the list is identical and a 2-hero
   play never renders bigger tiles than a 4-hero one.
   The ceiling is what stops it: #plays-panel is max-width 420px, so past a ~436px
   viewport the row stops getting wider while vw keeps climbing. Both ends are measured
   against the BINDING case — a full Team vs Team table, villain + 6 heroes = 7 tiles —
   because the size is uniform and so has to be chosen for the widest row that must fit
   on one line: 30px is the most a 375px phone seats, 38px the most a 420px panel does.
   Below 375px the columns shrink (see .plays-tile-col) rather than the row overflowing.
   Column width carries the caption, so it's derived from the tile rather than fixed:
   hero columns need ~30px for a first name ("Weston" is the longest real one), while
   the villain column runs wider because its caption is a CHARACTER name and those are
   much longer ("Taskmaster", "Green Goblin"). */
#plays-history { --plays-tile: clamp(30px, 8vw, 38px); }
.plays-tile-col {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  width: calc(var(--plays-tile) + 4px);
}
.plays-tile-col.is-villain-col { width: calc(var(--plays-tile) + 20px); }
.plays-tile {
  position: relative; flex-shrink: 0; overflow: hidden;
  width: var(--plays-tile); height: var(--plays-tile); border-radius: 6px;
  background: var(--off-grey);
  border: 1.5px solid var(--tile-color, var(--light-grey));
  display: flex; align-items: center; justify-content: center;
  /* Same first-paint clip fix as .team-slot — without its own layer a hairline of the
     background can linger at the rounded edge until something forces a repaint. */
  transform: translateZ(0);
}
/* Villain leads the row and runs larger than the hero tiles on purpose: only ~29/132
   villains have a square portrait (js/naming.js), so the rest fall back to a cropped
   card-back, which needs the extra pixels to stay recognizable at all. */
.plays-tile.is-villain {
  width: calc(var(--plays-tile) + 8px); height: calc(var(--plays-tile) + 8px);
  border-radius: 7px; border-width: 2px; border-color: var(--color-red);
}
.plays-tile-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: top; }
/* Sits UNDER the image: setTileImage() hides the <img> once every candidate URL has
   failed, which would otherwise leave a blank box. */
/* Scaled off the tile too, so the fallback doesn't sit as a tiny mark in a large tile. */
.plays-tile-initials {
  font-family: var(--font-sans); font-size: calc(var(--plays-tile) * 0.34); font-weight: 700;
  color: var(--mid-grey); line-height: 1; user-select: none;
}
.plays-tile.is-villain .plays-tile-initials { font-size: calc(var(--plays-tile) * 0.4); }
.plays-tile-more {
  border-style: dashed; background: none; color: var(--mid-grey);
  font-family: var(--font-sans); font-size: 10px; font-weight: 600;
}
/* First names only (js/plays.js's firstName) — full names in a real log run to 18
   characters and can't fit under a 34px column. Clamped to two lines so an unusually
   long single token wraps instead of overflowing, without any row growing past that. */
/* min-height reserves BOTH lines whether or not this caption uses them, so one long
   name in a row can't make that row taller than its neighbours — a list of uniform
   rows scans far better than a ragged one, and the reserved line costs ~10px. */
.plays-tile-caption {
  font-family: var(--font-sans); font-size: 9px; line-height: 1.15; color: var(--mid-grey);
  text-align: center; max-width: 100%; min-height: 21px;
  overflow: hidden; overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
/* Explicit box: the glyph is drawn by .icon-x's absolutely-positioned bars, so with no
   text content there's nothing left to give this button intrinsic size. Still used by
   the Settings "Player names" hide button (js/plays.js's playerNamesSettingsHtml); the
   history rows themselves no longer carry a per-row delete. */
.plays-row-delete {
  background: none; border: none; color: var(--mid-grey); cursor: pointer;
  width: 26px; height: 26px; padding: 0; flex-shrink: 0;
}

/* History/Stats view switcher — reuses .sf-seg/.sf-pill (sort/filter panel)
   verbatim, same "don't invent a parallel pill system" call as the rest of this
   file's PLAYS/LOG PLAY sections. */
#plays-view-tabs { margin-bottom: 16px; }
#plays-view-tabs .sf-seg { display: flex; width: 100%; }
#plays-view-tabs .sf-pill { flex: 1; text-align: center; }
/* Stats view hides the History-only chrome that's otherwise static markup
   (#plays-log-new-btn) — the two JS-owned mounts collapse on their own via the
   :empty rules above. */
#plays-panel.stats-view #plays-log-new-btn { display: none; }
#plays-stats-view:empty { display: none; }

/* ===== STATS VIEW (js/plays-stats.js) =====
   Mobile-first: #play-log-panel doesn't apply here, but #plays-panel shares the same
   8px overlay-backdrop padding + panel padding, so the usable content box is ~299px
   at a 375px phone width — no 3-column rows anywhere below. Progress/breakdown rows
   are all the same two-line shape (label/value head, full-width bar under) so the
   tab reads as one system regardless of section. */
.pstat-overview {
  display: flex; align-items: center; gap: 14px;
  padding-bottom: 16px; margin-bottom: 4px; border-bottom: 1.5px solid var(--hairline);
}
.pstat-overview-text { font-family: var(--font-sans); font-size: 13px; color: var(--dark-grey); line-height: 1.6; }
.pstat-overview-text strong { color: var(--color-text-primary); font-variant-numeric: tabular-nums; }
.pstat-range { font-size: 12px; color: var(--mid-grey); margin-top: 2px; }

.pstat-section-spaced { margin-top: 20px; }

/* Progress rows (collection coverage) double as drill-in buttons — plain <button>
   reset to look like the rest of this view (left-aligned, no button chrome) rather
   than a visually distinct control, since the bar itself is the affordance. */
.pstat-progress-row {
  display: block; width: 100%; text-align: left; background: none; border: none;
  padding: 0; margin-bottom: 14px; cursor: pointer; font: inherit; color: inherit;
}
/* Title row for a section that carries the ME/ANYONE toggle (Heroes played, Most
   played heroes) — label left, toggle right. Kept OUTSIDE the drill-in
   .pstat-progress-row button below it: the toggle's own buttons can't nest inside
   another button, so the row that titles a drill-in target and the button that
   triggers the drill-in are two separate elements. */
.pstat-progress-title-row, .pstat-title-row {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  margin-bottom: 8px;
  font-family: var(--font-sans); font-size: 13px; color: var(--color-text-primary);
}
.pstat-title-row { margin-bottom: 10px; }
/* Shrink the reused .sf-seg/.sf-pill toggle to fit inline next to a row label —
   the sort/filter panel's own instances have room to breathe that this doesn't. */
.pstat-scope-toggle.sf-seg { flex-shrink: 0; }
.pstat-scope-toggle .sf-pill { padding: 3px 10px; font-size: 11px; }
.pstat-progress-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  font-family: var(--font-sans); font-size: 13px; color: var(--color-text-primary);
  margin-bottom: 5px;
}
.pstat-progress-head span:last-child { font-variant-numeric: tabular-nums; font-weight: 600; flex-shrink: 0; }
/* Value-only line for a row whose label already lives in its own title-row above
   (Heroes played) — right-aligned to match the label+value rows around it
   (Villains faced) even though it has no label of its own to space-between with. */
.pstat-progress-value {
  text-align: right; font-variant-numeric: tabular-nums; font-weight: 600;
  font-family: var(--font-sans); font-size: 13px; color: var(--color-text-primary);
  margin-bottom: 5px;
}
.pstat-bar { margin-bottom: 4px; }
.pstat-progress-sub { font-family: var(--font-sans); font-size: 12px; color: var(--mid-grey); }
.pstat-callout {
  font-family: var(--font-sans); font-size: 13px; color: var(--dark-grey);
  background: var(--color-off-white); border-radius: 8px; padding: 10px 12px; margin-bottom: 14px;
}
.pstat-callout strong { color: var(--color-text-primary); }

/* Per-box <details> tree — native disclosure, zero JS, one per season. */
.pstat-seasons { border-top: 1.5px solid var(--hairline); }
.pstat-season { border-bottom: 1px solid var(--hairline); }
.pstat-season summary {
  display: flex; justify-content: space-between; align-items: center;
  padding: 11px 0; cursor: pointer; list-style: none;
  font-family: var(--font-sans); font-size: 13px; font-weight: 600; color: var(--color-text-primary);
}
.pstat-season summary::-webkit-details-marker { display: none; }
.pstat-season summary::before {
  content: ''; display: inline-block; width: 0; height: 0;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  border-left: 5px solid var(--mid-grey); margin-right: 8px; transition: transform 0.15s ease;
}
.pstat-season[open] summary::before { transform: rotate(90deg); }
.pstat-season-boxes { padding: 0 0 12px 13px; }
.pstat-box-row {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  width: 100%; background: none; border: none; padding: 4px 0; margin: 0; cursor: pointer;
  font-family: var(--font-sans); font-size: 12px; color: var(--dark-grey); text-align: left; font: inherit;
}
.pstat-box-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pstat-box-count { font-variant-numeric: tabular-nums; flex-shrink: 0; color: var(--mid-grey); }
.pstat-box-bar { height: 5px; margin-bottom: 7px; }

/* Leaderboard / partner rows */
.pstat-list { display: flex; flex-direction: column; }
.pstat-char-row, .pstat-partner-row {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid var(--hairline);
  background: none; border-left: none; border-right: none; border-top: none;
  width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer;
}
.pstat-list > :last-child { border-bottom: none; }
.pstat-icon {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: var(--radius-sm);
  overflow: hidden; background: var(--off-grey);
}
.pstat-icon-img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.pstat-char-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-sans); font-size: 13px; color: var(--color-text-primary);
}
.pstat-char-count, .pstat-muted {
  flex: 0 0 auto; font-family: var(--font-sans); font-size: 12px; color: var(--mid-grey);
  font-variant-numeric: tabular-nums;
}
.pstat-no-result { flex: 0 0 auto; font-family: var(--font-sans); font-size: 11px; color: var(--mid-grey); }
.pstat-partner-row { cursor: default; }

/* Breakdown rows (player count, mode) — same two-line shape as the progress rows
   above, no icon (these aren't characters). */
.pstat-breakdown-row { margin-bottom: 12px; }
.pstat-breakdown-row .pstat-progress-head { color: var(--color-text-primary); }

/* BG STATS RECONCILE — functional-first (see the log-form note above: get the data
   right, style it in a dedicated pass). */
.bgs-summary {
  font-family: var(--font-sans); font-size: 14px; color: var(--color-text-primary);
  margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1.5px solid var(--hairline);
}
.bgs-section { margin-bottom: 18px; }
.bgs-section:last-child { margin-bottom: 0; }
.bgs-section-title {
  font-family: var(--font-sans); font-size: 12px; font-weight: 600;
  color: var(--mid-grey); letter-spacing: var(--tracking-wide); text-transform: uppercase;
  margin-bottom: 8px;
}
.bgs-format-row { font-family: var(--font-sans); font-size: 13px; margin-bottom: 4px; }
.bgs-format-row code, .bgs-drift-option code {
  background: var(--off-grey); border-radius: 4px; padding: 1px 5px; font-size: 12px;
}
.bgs-muted { color: var(--mid-grey); }
.bgs-drift-option {
  display: flex; align-items: center; gap: 8px; font-family: var(--font-sans);
  font-size: 13px; margin-bottom: 6px; cursor: pointer;
}
.bgs-list { font-family: var(--font-sans); font-size: 13px; margin: 0; padding-left: 18px; }
.bgs-list li { margin-bottom: 4px; }
/* One "did you mean?" row: the raw name, the confidence note, then the picker. */
.bgs-name-row {
  font-family: var(--font-sans); font-size: 13px;
  padding: 10px 0; border-top: 1px solid var(--hairline);
}
.bgs-name-row:first-of-type { border-top: none; padding-top: 0; }
.bgs-name-row code {
  background: var(--off-grey); border-radius: 4px; padding: 1px 5px; font-size: 12px;
}
.bgs-name-row .bgs-muted { font-size: 12px; }
.bgs-name-row .pl-select { margin-top: 6px; }
#bgstats-reconcile-actions { display: flex; gap: 10px; margin-top: 22px; }
#bgstats-reconcile-actions .offline-btn { flex: 1; }
#bgstats-reconcile-actions .pl-seat-remove-btn { flex: 1; }

/* LOG PLAY form — functional styling only (per the user's "get it working, style it
   later" call for this feature). Reuses .sf-pill/.sf-pills/.sf-sublabel (sort/filter
   panel) rather than inventing a parallel pill system. */
.pl-field { margin-bottom: 18px; }
.pl-field:last-child { margin-bottom: 0; }
.pl-row { display: flex; gap: 16px; }
.pl-row > div { flex: 1; }
.pl-select, .pl-input {
  width: 100%; font-family: var(--font-sans); font-size: 14px;
  padding: 8px 10px; border-radius: 8px; border: 1.5px solid var(--hairline);
  background: var(--surface-card); color: var(--color-text-primary);
}
textarea.pl-input { resize: vertical; font-family: var(--font-sans); }
#pl-footer { margin-top: 20px; }
/* Secondary-action row above the primary footer button — same shape/weighting as
   #bgstats-reconcile-actions and #bgstats-export-settings-actions (offline-btn wider
   than pl-seat-remove-btn), reused here rather than a parallel recipe. */
#pl-footer-actions { display: flex; gap: 10px; margin-bottom: 10px; }
#pl-footer-actions .pl-seat-remove-btn { flex: 1; }

/* ---- Locked "Play details" view (formMode 'view' in play-log-form.js) ----
   heroRowHtml/villainCardHtml already drop the interactive markup (add/remove/ME/
   picker) for most of the panel when locked; these rules cover what's left: disabled
   native fields shouldn't look "broken" (browser default is a washed-out grey), and
   the remaining static rows shouldn't carry a pointer cursor that promises a tap will
   do something. */
.pl-view-mode .pl-input:disabled,
.pl-view-mode .pl-select:disabled,
.pl-view-mode textarea.pl-input:disabled {
  opacity: 1; color: var(--color-text-primary); -webkit-text-fill-color: var(--color-text-primary);
  cursor: default;
}
.pl-view-mode .pl-hero-row-main,
.pl-view-mode .pl-villain-picker-row,
.pl-view-mode .pl-villain-switch { cursor: default; }
/* Mode selector + result panels stay full-color (still showing what was recorded) —
   only their interactivity is switched off, same as the wireOnce click guard they
   back up. */
.pl-view-mode #pl-mode-selector,
.pl-view-mode #pl-result-panels { pointer-events: none; }
.pl-checkbox-row {
  display: flex; align-items: center; gap: 8px; margin-top: 8px;
  font-family: var(--font-sans); font-size: 13px; color: var(--dark-grey); cursor: pointer;
}
.pl-checkbox-row[hidden] { display: none; }
.pl-required-star { color: var(--color-red); }
/* Generic outlined button — also used outside the Log Play form (bgstats-reconcile's
   Cancel, the export prompt's "Not now", plays.js's Select/Cancel toggle), so this
   base rule can't be scoped to Log Play or folded away when that form's own layout
   changes. */
.pl-seat-remove-btn {
  font-family: var(--font-sans); font-size: 12px; white-space: nowrap;
  padding: 6px 10px; border-radius: 8px; border: 1.5px solid var(--hairline);
  background: none; color: var(--dark-grey); cursor: pointer;
}
.pl-label {
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: var(--tracking-wide);
  color: var(--mid-grey); margin-bottom: 9px;
}

/* ===== LOG PLAY — row/card redesign =====
   Villain card (picker row + human-toggle switch + conditional name row) sits full
   width above either a plain PLAYERS list (co-op) or two team cards (versus modes),
   reusing the team panel's own .team-band/.team-vs-badge classes verbatim so the two
   surfaces can't visually drift apart. Every hero row is permanently "in edit mode" —
   no more tap-to-expand editor — so add/change/remove are always-visible controls. */
#pl-stage { margin-bottom: 0; }
.pl-section-label {
  font-family: 'Amboy', sans-serif; font-size: 10px; letter-spacing: var(--tracking-wide);
  margin-bottom: 10px;
}
.pl-add-icon { color: var(--light-grey); font-size: 22px; font-weight: 300; line-height: 1; }
.pl-required-pill {
  margin-left: auto; flex-shrink: 0; font-family: var(--font-sans); font-size: 9px; font-weight: 700;
  letter-spacing: 0.04em; color: var(--color-red); border: 1.5px solid var(--color-red);
  border-radius: 999px; padding: 3px 8px; white-space: nowrap;
}
.pl-row-img { width: 100%; height: 100%; object-fit: cover; object-position: top; }

/* ---- Villain band ----
   Built on .team-band (above) with --band-color: var(--color-red) — the villain is a
   third faction, not a bespoke tinted card, so it shares the color-mix background/border
   recipe and dark-mode behavior with the Gold/Blue/Red bands instead of needing its own
   hardcoded tint. This is what fixed the old .pl-villain-card's #F1CCCD-in-light /
   rgba(226,70,81,0.16)-in-dark split, and its section-label contrast failure — the label
   is .team-band-name now, which already sets --color-text-primary. */
/* Separates the villain from the PLAYERS list / team cards below it. .team-band carries
   no margin of its own (the team panel spaces its bands with a flex gap), so the villain
   band — which sits in normal flow inside #pl-stage — has to bring its own. */
.pl-villain-band { margin-bottom: 18px; }
.pl-villain-switch {
  margin-left: auto; flex-shrink: 0; display: flex; align-items: center; gap: 8px;
  font-family: var(--font-sans); font-size: 11px; color: var(--dark-grey); cursor: pointer;
}
/* Switch — same recipe as Settings' .settings-row input[type=checkbox] (css/layout.css),
   kept as its own class since this lives in Log Play, not Settings. */
.pl-villain-switch input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; margin: 0;
  position: relative; flex-shrink: 0; cursor: pointer;
  width: 44px; height: 26px; border-radius: 999px;
  background: var(--off-grey); transition: background var(--transition);
}
.pl-villain-switch input[type="checkbox"]::before {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--white); box-shadow: 0 1px 2px rgba(0,0,0,0.25);
  transition: transform var(--transition);
}
.pl-villain-switch input[type="checkbox"]:checked { background: var(--purple); }
.pl-villain-switch input[type="checkbox"]:checked::before { transform: translateX(18px); }
/* Touch target without layout cost: an invisible 44px-tall overlay on the 26px track.
   Giving the LABEL a min-height instead would push .team-band-header from ~26px to 44px
   and open a visible gap at the top of the band — the header has to stay the same height
   the team panel's own bands use. */
.pl-villain-switch input[type="checkbox"]::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}
/* Same recipe as .team-band .team-add-tile:not(:hover) / .team-slot.filled above — dashed
   + a soft tint of the band color while empty, solid + the full band color once filled.
   The remove × is a child of this row (not a sibling beside it), so it reads as belonging
   to the villain card rather than floating in the band's padding — matching how a hero
   row carries its own remove button. */
.pl-villain-picker-row {
  min-width: 0; display: flex; align-items: center; gap: 10px;
  padding: 8px; border-radius: 10px;
  border: 2px dashed color-mix(in srgb, var(--band-color) 45%, transparent);
  background: var(--surface-card); cursor: pointer;
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.pl-villain-picker-row.filled { border-style: solid; border-color: var(--band-color); }
.pl-villain-avatar {
  flex-shrink: 0; width: 44px; height: 44px; border-radius: 8px; overflow: hidden;
  background: var(--off-grey); display: flex; align-items: center; justify-content: center;
}
.pl-villain-picker-text {
  flex: 1; min-width: 0; font-family: var(--font-sans); font-size: 14px; font-weight: 600;
  color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pl-villain-name-row { display: flex; gap: 8px; margin-top: 10px; }
.pl-villain-name-row .pl-input { flex: 1; }

/* Player-name autocomplete dropdown (js/play-log-form.js) — a single shared instance
   positioned via inline top/left/width (set in JS from the focused input's own
   getBoundingClientRect, since position:fixed here is viewport-relative and doesn't
   need to know about the panel's internal scroll). z-index above every Log Play
   surface (200) and above the char-picker it can open (220), since a stray leftover
   frame from either should never be able to sit on top of it. */
#pl-name-suggest {
  display: none; position: fixed; z-index: 225;
  background: var(--surface-card); border: 1.5px solid var(--hairline); border-radius: 8px;
  box-shadow: var(--shadow-xl); overflow: hidden; max-height: 260px; overflow-y: auto;
}
.pl-name-suggest-row {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 9px 12px; cursor: pointer; font-family: var(--font-sans); font-size: 14px;
  border-bottom: 1px solid var(--hairline);
}
.pl-name-suggest-row:last-child { border-bottom: none; }
.pl-name-suggest-row.active, .pl-name-suggest-row:hover { background: var(--off-grey); }
.pl-name-suggest-name {
  color: var(--color-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pl-name-suggest-count {
  flex-shrink: 0; color: var(--mid-grey); font-size: 12px; font-variant-numeric: tabular-nums;
}

/* ---- Shared hero-row list (co-op PLAYERS section + versus team cards) ----
   Two lines, not one: a top line (avatar + full name + ME + remove) and a full-width
   name input below it. A single line can't fit a real hero name AND a usable input at
   once — splitting is what lets "Captain America (Sam Wilson)" render whole and gives
   the input room to show its placeholder/value instead of clipping both. */
.pl-players-section { margin-bottom: 18px; }
.pl-hero-rows { display: flex; flex-direction: column; gap: 8px; }
.pl-hero-row {
  display: flex; flex-direction: column; gap: 8px; padding: 8px;
  border-radius: 10px; background: var(--surface-card); border: 1.5px solid var(--hairline);
}
.pl-hero-row-top { display: flex; align-items: center; gap: 8px; }
.pl-hero-row-main {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px;
  background: none; border: none; padding: 0; cursor: pointer; text-align: left;
}
.pl-hero-row-avatar {
  flex-shrink: 0; width: 40px; height: 40px; border-radius: 8px; overflow: hidden; background: var(--off-grey);
}
/* No line clamp and no ellipsis: the whole point of the two-line row is that a hero name
   is never hidden. A 2-line clamp still cut "Captain America (Sam Wilson)" at 320px, where
   the name column is only ~91px — so it wraps as far as it needs to and the row grows.
   Only the longest names on the narrowest phones reach three lines. */
.pl-hero-row-name {
  min-width: 0; font-family: var(--font-sans); font-size: 13px; font-weight: 700; color: var(--color-hero-tag);
  line-height: 1.3; overflow-wrap: anywhere;
}
.team-band .pl-hero-row-name { color: var(--band-color); }
.pl-hero-row-input { width: 100%; }
/* Rebuilt on .sf-pill's own metrics (layout.css) rather than a parallel size/weight, so
   ME reads as the same secondary control everywhere else in the app uses .sf-pill. */
.pl-me-btn {
  position: relative; flex-shrink: 0;
  font-family: var(--font-sans); font-size: 13px; font-weight: 500; white-space: nowrap;
  height: 32px; padding: 0 16px; border-radius: var(--radius-pill);
  border: 1.5px solid var(--hairline);
  background: none; color: var(--dark-grey); cursor: pointer;
  transition: background var(--transition), border-color var(--transition), color var(--transition);
}
/* Wider than it is tall, so it reads as a pill. At a 44px box the 2-character label made
   it a circle. The touch target is restored by an invisible overlay rather than by growing
   the control, same trick as the villain switch above. */
.pl-me-btn::after {
  content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px;
}
.pl-me-btn.active { background: var(--color-purple); border-color: var(--color-purple); color: var(--color-white); }
.pl-hero-row-remove {
  flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%; border: none;
  background: none; color: var(--mid-grey); font-size: 16px; line-height: 1; cursor: pointer;
}
@media (hover: hover) {
  .pl-hero-row-remove:hover { color: var(--color-red); }
}
.pl-add-hero-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 10px; border-radius: 10px; border: 2px dashed var(--light-grey);
  background: none; font-family: var(--font-sans); font-size: 13px; color: var(--mid-grey); cursor: pointer;
}
@media (hover: hover) {
  .pl-add-hero-row:hover { border-color: var(--color-hero-tag); color: var(--color-hero-tag); }
}
.pl-hint { font-family: var(--font-sans); font-size: 11px; color: var(--mid-grey); margin-top: 8px; line-height: 1.4; }

/* ---- Team cards (versus modes) — .team-band/.team-vs-badge reused verbatim from the
   team panel (js/team.js), so the two surfaces can't drift apart. ---- */
.pl-teams-row { display: flex; flex-direction: column; gap: 0; margin-bottom: 18px; }
.pl-teams-row .team-band-header { margin-bottom: 10px; }

/* ---- Details card (player count/date + more-details) ---- */
.pl-details-card { border: 1.5px solid var(--hairline); border-radius: 12px; padding: 14px; margin-bottom: 18px; }
.pl-details-card .pl-field:last-child { margin-bottom: 0; }

/* ===== RESULT PANELS (replaces the old small .sf-pill outcome buttons) ===== */
#pl-result-panels { display: flex; gap: 10px; }
.pl-result-panel {
  flex: 1; padding: 14px 4px; border-radius: 11px; text-align: center;
  font-family: var(--font-sans); font-size: 13px; font-weight: 600; letter-spacing: 0.02em;
  border: 1.5px solid var(--hairline); background: none; color: var(--mid-grey); cursor: pointer;
  transition: background-color var(--transition), border-color var(--transition), color var(--transition);
}
.pl-result-panel.required-missing { border-color: var(--color-red); }
.pl-result-panel[data-outcome="heroes"].active { background: var(--green); border-color: var(--green); color: var(--color-white); }
.pl-result-panel[data-outcome="villain"].active { background: var(--color-red); border-color: var(--color-red); color: var(--color-white); }
/* team0/team1 get their inline --panel-color from JS (gold/blue or red/blue,
   matching whichever the active mode-group uses) rather than a fixed color here. */
.pl-result-panel[data-outcome="team0"].active, .pl-result-panel[data-outcome="team1"].active {
  background: var(--panel-color, var(--color-purple)); border-color: var(--panel-color, var(--color-purple)); color: var(--color-white);
}

@media (min-width: 700px) {
  #play-log-panel { max-width: 720px; }
  .pl-teams-row { flex-direction: row; align-items: stretch; }
  .pl-teams-row .team-band { flex: 1; min-width: 0; }
  .pl-teams-row .team-vs-badge { margin: 0 -10px; align-self: center; }
}

@keyframes pl-shake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-4px); }
  40%, 60% { transform: translateX(4px); }
}
.pl-shake { animation: pl-shake 0.4s; }
@media (prefers-reduced-motion: reduce) {
  .pl-shake { animation: none; }
}
#share-qr {
  width: 220px; height: 220px; background: #fff; padding: 10px; border-radius: 12px;
  border: 1px solid var(--off-grey); display: flex; align-items: center; justify-content: center;
}
#share-qr svg { width: 100%; height: 100%; display: block; }
#share-link-row { display: flex; width: 100%; gap: 8px; margin: 18px 0 12px; }
#share-link-input {
  flex: 1; min-width: 0; height: 40px; padding: 0 12px;
  border: 1px solid var(--off-grey); border-radius: 10px;
  font-family: var(--font-sans); font-size: 12px; color: var(--dark-grey); background: var(--color-off-white);
}
#share-copy-btn {
  background: var(--purple); color: #fff; border: none; cursor: pointer;
  font-family: 'Amboy', sans-serif; font-size: 12px; letter-spacing: var(--tracking-wide);
  padding: 0 18px; height: 40px; border-radius: 10px; transition: background var(--transition);
}
#share-download-btn {
  background: none; border: none; cursor: pointer; color: var(--mid-grey);
  font-family: var(--font-sans); font-size: 13px; text-decoration: underline;
}
#collection-header {
  display: flex; align-items: center; gap: 12px; margin-bottom: 4px;
}
#collection-back-btn {
  display: none; align-items: center; gap: 6px;
  font-family: 'Amboy', sans-serif; font-size: 12px;
  color: var(--purple); cursor: pointer; letter-spacing: var(--tracking-tight);
  padding: 4px 0;
}
#collection-title {
  font-family: 'Amboy', sans-serif; font-size: 20px; color: var(--color-text-primary);
}
#collection-subtitle { font-size: 13px; color: var(--mid-grey); margin-bottom: 16px; }

/* LEVEL 1 - Expansion grid with box covers */
#collection-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
  margin-bottom: 16px;
}
.collection-box {
  position: relative; border-radius: 10px; overflow: hidden;
  cursor: pointer; transition: transform var(--transition), box-shadow var(--transition), opacity var(--transition);
  background: var(--off-grey);
}
@media (hover: hover) {
  .collection-box:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
}
.collection-box:not(.owned):not(.partial) img,
.collection-box:not(.owned):not(.partial) .box-placeholder {
  filter: grayscale(100%);
  opacity: 0.4;
}
.collection-box img {
  width: 100%; aspect-ratio: 1/1; object-fit: cover; display: block;
  background: var(--off-grey);
}
.collection-box .box-placeholder {
  display: none; width: 100%; aspect-ratio: 1/1;
  align-items: center; justify-content: center;
  background: var(--off-grey);
  font-family: 'Amboy', sans-serif; font-size: 11px;
  color: var(--mid-grey); text-align: center; padding: 12px;
  line-height: 1.3;
}
.collection-box-name {
  font-family: 'Amboy', sans-serif; font-size: 11px;
  color: var(--dark-grey); text-align: center;
  padding: 6px 4px 0; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}
.collection-box-check {
  position: absolute; top: 6px; right: 6px;
  width: 38px; height: 38px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.8);
  background: rgba(0,0,0,0.4);
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; color: transparent;
  transition: all var(--transition);
}
.collection-box.owned .collection-box-check {
  background: white; border-color: white; color: black;
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.collection-box.partial .collection-box-check {
  background: white; border-color: white; color: black;
  font-size: 13px; line-height: 1.1; text-align: center;
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
}
.collection-box-drill {
  position: absolute; top: 6px; left: 6px;
  width: 22px; height: 22px; border-radius: 50%;
  background: rgba(0,0,0,0.5); color: white;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px;
}

/* COLLECTION BOX WRAP */
.collection-box-wrap {
  display: flex; flex-direction: column;
  filter: drop-shadow(0 2px 6px rgba(0,0,0,0.15));
}

/* SEASON HEADERS */
.collection-season-header {
  grid-column: 1 / -1;
  display: flex; align-items: center; justify-content: space-between;
  font-family: 'Amboy', sans-serif; font-size: 16px;
  color: var(--dark-grey); letter-spacing: var(--tracking-wide);
  padding: 8px 0 4px;
  border-bottom: 2px solid var(--off-grey);
  margin-top: 8px;
}
.collection-season-header:first-child { margin-top: 0; }
.season-toggle-btn {
  font-family: 'Amboy', sans-serif; font-size: 12px;
  color: var(--purple); letter-spacing: var(--tracking-tight);
  cursor: pointer; padding: 4px 12px;
  border: 1px solid var(--purple); border-radius: 12px;
  transition: all var(--transition); white-space: nowrap;
  background: none;
}

/* EXPANSION OVERLAY */
#expansion-overlay {
  z-index: 211; /* above collection overlay (210) */
}
#expansion-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 800px; padding: 30px;
  box-shadow: 0 25px 60px rgba(0,0,0,0.4);
  position: relative; margin: auto;
}

/* LEVEL 2 - Characters within expansion */
#expansion-detail { display: none; }
#expansion-char-grid {
  margin-bottom: 16px;
}
.expansion-section-title {
  font-family: 'Amboy', sans-serif; font-size: 16px;
  color: var(--dark-grey); letter-spacing: var(--tracking-wide);
  text-transform: uppercase; margin: 14px 0 8px;
  border-bottom: 2px solid var(--off-grey); padding-bottom: 6px;
}
.expansion-char-list {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
}
.expansion-char-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 14px; border-radius: 8px;
  border: 2px solid transparent;
  position: relative;
  overflow: hidden;
  cursor: pointer; transition: all var(--transition);
}
.expansion-char-item::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0;
  width: 4px; background: transparent; transition: all var(--transition);
}
.expansion-char-item.type-hero::before     { background: rgba(75,184,214,0.35); }
.expansion-char-item.type-antihero::before { background: rgba(91,63,166,0.35); }
.expansion-char-item.type-villain::before  { background: rgba(204,41,54,0.35); }
.expansion-char-item.type-mode::before     { background: rgba(217,164,65,0.35); }
.expansion-char-item.type-hero.owned     { background: rgba(75,184,214,0.06); }
.expansion-char-item.type-antihero.owned { background: rgba(91,63,166,0.06); }
.expansion-char-item.type-villain.owned  { background: rgba(204,41,54,0.06); }
.expansion-char-item.type-mode.owned     { background: rgba(217,164,65,0.08); }
.expansion-char-item.type-hero.owned::before     { background: var(--color-hero-tag); }
.expansion-char-item.type-antihero.owned::before { background: var(--color-purple); }
.expansion-char-item.type-villain.owned::before  { background: var(--color-red); }
.expansion-char-item.type-mode.owned::before     { background: #d9a441; }
.expansion-char-check {
  width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0;
  border: 2px solid var(--light-grey);
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; color: transparent; transition: all var(--transition);
}
.expansion-char-item.owned .expansion-char-check {
  color: white;
}
.expansion-char-item.type-hero.owned     .expansion-char-check { background: var(--color-hero-tag); border-color: var(--color-hero-tag); }
.expansion-char-item.type-antihero.owned .expansion-char-check { background: var(--color-purple); border-color: var(--color-purple); }
.expansion-char-item.type-villain.owned  .expansion-char-check { background: var(--color-red); border-color: var(--color-red); }
.expansion-char-item.type-mode.owned     .expansion-char-check { background: #d9a441; border-color: #d9a441; }
.expansion-char-name {
  font-family: 'Amboy', sans-serif; font-size: 14px; color: var(--color-text-primary); flex: 1;
}
.expansion-char-type {
  font-size: 9px; color: var(--mid-grey); text-transform: uppercase; letter-spacing: var(--tracking-tight);
}
.expansion-char-ks {
  width: 16px; height: 16px; object-fit: contain; flex-shrink: 0; margin-left: auto;
}
/* THREAT CARDS */
#threat-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
  margin-bottom: 8px;
}
#threat-grid .detail-card-thumb {
  aspect-ratio: 283/196 !important;
}

/* EQUIPMENT CARDS */
#equipment-section { margin-bottom: 20px; }
#equipment-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px;
}
.equip-card-wrap {
  position: relative; cursor: pointer;
  perspective: 800px;
  /* Drop-shadow lives here, NOT on .equip-card-inner: a filter on the
     preserve-3d element flattens its 3D context and defeats backface-visibility
     (the flip would show a mirrored front instead of the back face). */
  filter: drop-shadow(3px 4px 4px rgba(0,0,0,0.3));
  transition: filter var(--transition), transform var(--transition);
}
.equip-card-inner {
  position: relative; width: 100%; aspect-ratio: 196/124;
  transform-style: preserve-3d;
  transition: transform 0.5s ease;
  border-radius: 7px;
}
.equip-card-wrap.flipped .equip-card-inner {
  transform: rotateY(180deg);
}
@media (hover: hover) {
  .equip-card-wrap:hover {
    /* Lift instead of scale (scaling resampled the card art and looked fuzzy,
       most noticeably on the wide villain dashboard). */
    filter: drop-shadow(4px 12px 12px rgba(0,0,0,0.3));
    transform: translateY(-6px);
  }
}
.equip-card-front, .equip-card-back {
  position: absolute; inset: -1px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  border-radius: 7px; overflow: hidden;
  background: var(--off-grey);
}
.equip-card-back { transform: rotateY(180deg); }
.equip-card-front img, .equip-card-back img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.equip-flip-hint {
  position: absolute; bottom: 6px; right: 6px; z-index: 2;
  background: rgba(0,0,0,0.6); color: white;
  width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 17px; transition: background var(--transition);
  border: 2px solid rgba(255,255,255,0.3);
}

/* VILLAIN DASHBOARD (reuses the .equip-card-* flip classes) */
#villain-dashboard-section { margin-bottom: 20px; }
#villain-dashboard-grid { display: grid; grid-template-columns: 1fr; gap: 12px; max-width: 800px; }
#villain-dashboard-grid .equip-card-inner { aspect-ratio: 2324/1032; }
/* Phoenix Five dashboards are shorter (~2324x700) than the standard 2324x1032 —
   use their true ratio so they aren't cropped/stretched by object-fit:cover. */
#villain-dashboard-grid.dash-phoenix-five .equip-card-inner { aspect-ratio: 2324/700; }

/* HERO CARD - simple, no flip */
.hero-card-wrap {
  position: relative; cursor: pointer;
  border-radius: 7px; overflow: hidden;
  background: var(--off-grey); aspect-ratio: 196/283;
  transition: transform var(--transition);
}
@media (hover: hover) {
  .hero-card-wrap:hover { transform: translateY(-6px); }
}
.hero-card-wrap img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.sh-badge {
  position: absolute; top: 6px; left: 6px; z-index: 1; pointer-events: none;
  font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: var(--tracking-wide);
  color: var(--color-black); background: var(--color-white);
  padding: 3px 7px; border-radius: var(--radius-pill);
}

/* UNIQUE CARDS (per-character extra sections; reuses .detail-card-thumb + flip classes) */
/* Sized to match the standard grids: vertical = hero/villain cards (#detail-cards-grid),
   horizontal = threat cards (#threat-grid). Breakpoint column counts are mirrored in the
   media queries below. */
.unique-section { margin-bottom: 20px; }
.unique-grid { display: grid; }
.unique-vertical   .unique-grid { grid-template-columns: repeat(4, 1fr); gap: 20px; }
.unique-horizontal .unique-grid { grid-template-columns: repeat(3, 1fr); gap: 12px; }
.unique-dashboard  .unique-grid { grid-template-columns: 1fr; gap: 12px; max-width: 720px; }
/* shape overrides (thumbs default to portrait 196/283; equip flip defaults to 196/124) */
.unique-vertical   .equip-card-inner { aspect-ratio: 196/283; }
.unique-horizontal .detail-card-thumb,
.unique-horizontal .equip-card-inner { aspect-ratio: 283/196; }
.unique-dashboard  .detail-card-thumb,
.unique-dashboard  .equip-card-inner { aspect-ratio: 2324/1032; }

/* IMPERSONATING PICK SLOT (Hulkling) — sits in the unique grid beside Impersonating */
.sh-pick-slot { position: relative; aspect-ratio: 196/283; border-radius: 7px; }
.sh-pick-slot.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  border: 2px dashed var(--light-grey); background: var(--off-grey);
  color: var(--mid-grey); cursor: pointer; text-align: center;
  transition: border-color var(--transition), color var(--transition);
}
.sh-pick-plus { font-size: 34px; font-weight: 300; line-height: 1; }
.sh-pick-empty-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: var(--tracking-wide); padding: 0 6px;
}
.sh-pick-slot.filled {
  overflow: hidden; background: var(--off-grey); cursor: zoom-in;
  transition: transform var(--transition);
}
.sh-pick-slot.filled img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sh-pick-remove {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 26px; height: 26px; border: none; border-radius: 50%; padding: 0;
  background: rgba(26,26,26,0.85); color: var(--color-white);
  font-size: 18px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}

/* PLAYS / LOG PLAY overlays — both can be opened while the team panel (z-index 205)
   or its scrim (200) is already open (the team panel's own "Log a Play" button, or the
   header Plays button with the sheet up), and the base .overlay-backdrop z-index (200)
   sits AT OR BELOW those, not above. Without this, the team panel/scrim visually and
   interactively sits on top of these overlays — clicks that look like they're hitting
   the modal actually land on the team scrim underneath, which closes the team step and
   (since closing a step unwinds everything stacked above it) cascades to close this one
   too. Same reasoning as #sh-picker-overlay below, one section down. */
#plays-overlay, #play-log-overlay, #bgstats-reconcile-overlay,
#bgstats-export-prompt-overlay { z-index: 215; }

/* CHARACTER PICKER OVERLAY — always opened FROM another overlay (the team panel,
   or any of the 215-tier overlays above, e.g. Log Play's villain/hero tiles, or
   PLAYS → Log Play → picker three deep). At z-index 215 it tied with whichever
   215 overlay opened it, and on a tie the later DOM node (index.html places
   #play-log-overlay/#plays-overlay after #char-picker-overlay) painted its own
   backdrop blur/dim on top of the picker instead of the other way around — the
   picker looked washed out and unusable from Log Play. One tier above everything
   it can be opened from fixes that regardless of DOM order. */
#char-picker-overlay { z-index: 220; }

/* STARTING-HAND PICKER OVERLAY */
#sh-picker-overlay { z-index: 215; } /* above the detail modal (200) it opens from */
#sh-picker-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 900px; padding: 30px;
  box-shadow: 0 25px 60px rgba(0,0,0,0.4);
  position: relative; margin: auto;
}
#sh-picker-title {
  font-family: 'Amboy', sans-serif; font-size: 20px;
  color: var(--dark-grey); letter-spacing: var(--tracking-wide); text-transform: uppercase;
}
#sh-picker-subtitle { font-size: 13px; color: var(--mid-grey); margin: 6px 0 16px; }
/* Favorites sort to the front of this single grid (js/sh-pick.js buildGrid) and are
   identified by their heart badge (below) rather than a separate pinned section. */
#sh-picker-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.sh-pick-tile {
  position: relative; border-radius: 7px; overflow: hidden;
  background: var(--off-grey); aspect-ratio: 196/283; cursor: pointer;
  transition: transform var(--transition);
  /* Mobile: suppress native long-press callout / text-selection so the
     hold-to-favorite gesture isn't hijacked by the OS context menu — mirrors
     .char-card (components.css). */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* Hover lift only on true hover-capable pointers, matching .char-card — otherwise
   the lift "sticks" after a tap on touch devices. Touch gets a press-down scale
   instead, mirroring .char-card.long-press-active's tap feedback. */
@media (hover: hover) {
  .sh-pick-tile:hover { transform: translateY(-6px); }
  .sh-pick-tile:hover .sh-pick-fav-btn { opacity: 1; }
}
.sh-pick-tile:active { transform: scale(0.93); transition: transform 80ms ease; }
.sh-pick-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* === PICKER TILE FAVORITE BADGE — reuses the grid team-badge's ring geometry/keyframes
   (components.css) for the hover/mobile/long-press system, but an INVERTED (light) chip
   instead of the grid's dark translucent one: starting-hand card art often has a printed
   symbol in the same corner, and a light opaque disc reads clearly against it regardless
   of whether that art is bright or dark there, which a dark backdrop can't guarantee.
   Ring/heart use --favorite (cyan) rather than the team badge's green — this isn't adding
   to a team. === */
.sh-pick-fav-btn {
  position: absolute; top: 8px; left: 8px;
  width: 48px; height: 48px; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 0; cursor: pointer;
  /* Hidden by default; desktop hover reveals it (above); always visible once
     favorited — same rule as .char-card-fav-btn.favorited. */
  opacity: 0; transition: opacity var(--transition);
}
.sh-pick-fav-btn.favorited { opacity: 1; }
/* The opaque light disc — mirrors .char-card-badge-inner's geometry (scaled up ~1.5x
   from the grid team-badge's 24-in-32 ratio) so the ring below still frames it
   correctly. Deliberately a literal white, not a themed surface token: its job is
   contrast against arbitrary card art, not against the app's own light/dark theme. */
.sh-pick-fav-chip {
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--white); border: 2.5px solid rgba(0,0,0,0.15);
  display: flex; align-items: center; justify-content: center;
  position: relative; z-index: 1;
  transition: border-color var(--transition);
}
.sh-pick-fav-btn.favorited .sh-pick-fav-chip { border-color: var(--favorite); }
.sh-pick-fav-btn .fav-heart-icon {
  width: 21px; height: 21px;
  fill: none; stroke: var(--mid-grey); stroke-width: 2;
}
.sh-pick-fav-btn.favorited .fav-heart-icon { fill: var(--favorite); stroke: var(--favorite); }
/* Reveal + press feedback during a hold, mirroring .char-card.long-press-active. */
.sh-pick-tile.long-press-active .sh-pick-fav-btn { opacity: 1; }
.sh-pick-tile.long-press-active { transform: scale(0.93); transition: transform 80ms ease; }
/* Ring recolored to --favorite (cyan) rather than the team badge's green — this isn't
   adding to a team. Fill animates in on a hold; a hold on an already-favorited tile
   drains instead, signalling the favorite coming off (mirrors .char-card.in-team). */
.sh-pick-tile .char-card-ring-fill { stroke: var(--favorite); }
.sh-pick-tile.long-press-active .char-card-ring-fill { animation: ring-fill 650ms linear forwards; }
.sh-pick-tile.long-press-active:has(.sh-pick-fav-btn.favorited) .char-card-ring-fill {
  animation: ring-drain 650ms linear forwards;
}

/* Shown when the card-images manifest hasn't been generated (picker is gated off it). */
.sh-picker-note {
  grid-column: 1 / -1; text-align: center; padding: 32px 16px;
  color: var(--mid-grey); font-size: 14px; line-height: 1.6;
}
.sh-picker-note code {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
  background: var(--off-grey); color: var(--dark-grey);
  padding: 2px 6px; border-radius: 4px;
}
@media (max-width: 600px) {
  #sh-picker-panel { padding: 20px 16px; }
  #sh-picker-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  /* No separate mobile size here: the base 80px badge (2.5x the app's 44px touch-target
     convention) already clears it comfortably. */
}

/* CHARACTER PICKER — searchable square-portrait grid shared by the matchup builder
   and Log Play (js/char-picker.js). Same panel recipe as #sh-picker-panel; square
   tiles (portrait aspect 1/1) rather than the card aspect ratio, since these mirror
   the app icons/ headshots, not card faces. */
#char-picker-panel {
  background: var(--surface-card); border-radius: var(--overlay-radius);
  width: 100%; max-width: 640px; padding: 24px;
  box-shadow: var(--shadow-xl);
  position: relative; margin: auto;
  max-height: calc(100dvh - 40px); display: flex; flex-direction: column;
}
#char-picker-title {
  font-family: 'Amboy', sans-serif; font-size: 18px;
  color: var(--color-text-primary); margin-bottom: 14px;
}
#char-picker-search-wrap { margin-bottom: 14px; flex-shrink: 0; }
#char-picker-search {
  width: 100%; height: 44px; padding: 0 16px; border-radius: var(--radius-pill);
  border: none; background: var(--color-off-white);
  font-family: var(--font-sans); font-size: 16px; color: var(--color-text-primary);
}
#char-picker-search::placeholder { color: var(--mid-grey); }
/* The scroll region: owned grid + not-owned toggle + not-owned grid all move
   together, so the toggle/extra grid don't get clipped by a scroll boundary that
   used to end at #char-picker-grid alone. flex:1/min-height:0 is what lets this
   child's own overflow-y actually engage inside the panel's capped-height flex
   column (title/search-wrap stay fixed above it). */
#char-picker-scroll {
  flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding-bottom: 4px;
}
/* Grid layout lives on .char-picker-subgrid, not the #char-picker-grid/-extra-grid
   mounts themselves — a Stats drill-in (js/char-picker.js's "view" mode) can render
   TWO of these (unplayed, then played) separated by a plain block label, and neither
   would work as a full-width row if the mount itself were the grid container (a
   label div would just become a stray implicit grid item). */
.char-picker-subgrid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
}
.char-picker-subgrid + .char-picker-group-label { margin-top: 18px; }
#char-picker-extra-grid { padding-top: 12px; }
.char-picker-empty {
  text-align: center; padding: 32px 16px;
  color: var(--mid-grey); font-size: 14px;
}
/* Section label between the unplayed/played (or not-beaten/beaten) grids — plain
   text, not the Amboy .pl-section-label treatment, since this sits inside a modal
   that's otherwise unstyled/functional (per the log-form note elsewhere in this
   file: get it working first). */
.char-picker-group-label {
  font-family: var(--font-sans); font-size: 12px; font-weight: 600;
  color: var(--mid-grey); text-transform: uppercase; letter-spacing: 0.04em;
  margin-bottom: 10px;
}
/* Faded = already played/beaten (the Pokédex-style "collected" convention — full
   color reads as done, grey as outstanding). Portrait + name both dim; the tile
   stays fully clickable (still opens detail), it just recedes visually behind the
   unplayed/not-beaten section above it. */
.char-picker-tile.is-done .char-picker-tile-img-wrap { opacity: 0.4; }
.char-picker-tile.is-done .char-picker-tile-name { color: var(--mid-grey); }
/* min-width:0 overrides the grid item's default min-width:auto — without it, a
   tile's content (the portrait image) can force its 1fr track wider than its
   equal share, so later columns overflow the grid instead of everything sharing
   the row evenly. Classic CSS grid + image gotcha. */
.char-picker-tile { cursor: pointer; min-width: 0; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
.char-picker-tile-img-wrap {
  aspect-ratio: 1 / 1; border-radius: var(--radius-lg); overflow: hidden;
  background: var(--off-grey); border: 2px solid transparent;
  transition: border-color var(--transition);
}
.char-picker-tile-img-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (hover: hover) {
  .char-picker-tile:hover .char-picker-tile-img-wrap { border-color: var(--color-hero-tag); }
}
.char-picker-tile:active .char-picker-tile-img-wrap { transform: scale(0.95); }
.char-picker-tile-name {
  font-family: var(--font-sans); font-size: 11px; color: var(--color-text-secondary);
  text-align: center; margin-top: 5px; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 600px) {
  #char-picker-panel { padding: 18px 14px; max-width: none; }
  .char-picker-subgrid { grid-template-columns: repeat(4, 1fr); gap: 8px; }
}

/* TEAM AFFILIATIONS */
#detail-teams {
  text-align: center; padding: 16px 24px 0;
  border-top: 1px solid var(--off-grey);
}
#detail-teams-label {
  font-size: 13px; color: var(--mid-grey);
  margin-bottom: 8px; font-family: system-ui, sans-serif;
}
#detail-teams-list {
  display: flex; flex-wrap: wrap; gap: 8px;
  justify-content: center;
}

/* Play record — functional-first, plain text row (see the log-form styling note in
   this file's header for why: get the data right, style it in a dedicated pass). */
#play-record-section {
  text-align: center; padding: 12px 24px 0;
}
.play-record {
  display: flex; flex-wrap: wrap; gap: 6px 16px; justify-content: center;
  font-family: var(--font-sans); font-size: 13px; color: var(--mid-grey);
}
.play-record-stat strong { color: var(--color-text-primary); }
.team-tag {
  font-family: 'Amboy', sans-serif; font-size: 13px;
  color: var(--color-hero-tag); letter-spacing: var(--tracking-tight);
  padding: 4px 12px; border-radius: 20px;
  border: 1.5px solid var(--color-hero-tag);
  cursor: default;
  transition: background var(--transition), color var(--transition);
  position: relative;
  display: inline-flex; align-items: center; gap: 4px;
}
.team-tag.clickable {
  cursor: pointer;
}
.team-tag.selectable { cursor: pointer; }
.team-tag.selected { background: var(--color-hero-tag); color: var(--color-white); }

/* CARD LOADING */
#cards-loading {
  display: none;
  text-align: center;
  padding: 40px 20px;
}
#cards-loading .loading-spinner {
  width: 32px; height: 32px;
  border-width: 2px;
  margin: 0 auto 10px;
}
#cards-loading-text {
  font-family: 'Amboy', sans-serif;
  font-size: 12px; color: var(--mid-grey);
  letter-spacing: var(--tracking-wide);
}

/* LIGHTBOX */
#lightbox {
  display: none; position: fixed; inset: 0; z-index: 300;
  background: rgba(0,0,0,0.92); align-items: center; justify-content: center;
  user-select: none;
}
#lightbox.open { display: flex; }
#lightbox-close {
  position: absolute; top: 20px; right: 20px;
  color: white; font-size: 0; cursor: pointer;
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  z-index: 1;
}
/* ===== THE X GLYPH — one recipe for every dismiss/remove control =====
   Drawn geometrically from two rotated bars, never the "×" text character: the
   multiplication-sign glyph sits high on the math axis, so as text it renders optically
   off-centre in a round button and its weight drifts with the font. This was already true
   of the close buttons; `.icon-x` promotes it to a shared utility so remove buttons use the
   same glyph instead of falling back to text.

   Add `icon-x` to any new dismiss/remove control and give it no text content — the
   accessible name comes from aria-label. Tune with --x-size (bar length) and --x-bar
   (thickness). The close variants keep `--x-size: 50%` so they scale with their own box,
   which is what they already did. Position is set only on .icon-x: .overlay-close and
   #lightbox-close are already absolutely positioned and must stay that way. */
.icon-x { position: relative; }
.icon-x, #lightbox-close, .overlay-close { font-size: 0; }
.icon-x::before, .icon-x::after,
#lightbox-close::before, #lightbox-close::after,
.overlay-close::before, .overlay-close::after {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: var(--x-size, 11px); height: var(--x-bar, 2px);
  background: currentColor; border-radius: 1px;
}
.icon-x::before,
#lightbox-close::before, .overlay-close::before { transform: translate(-50%,-50%) rotate(45deg); }
.icon-x::after,
#lightbox-close::after, .overlay-close::after { transform: translate(-50%,-50%) rotate(-45deg); }
#lightbox-close, .overlay-close { --x-size: 50%; }
#lightbox-inner {
  position: relative; display: flex; align-items: center; gap: 16px;
}
#lightbox-img-wrap {
  /* Shrink-wrap to the image (capped to the viewport) so wide cards — the villain
     dashboard, horizontal unique cards — aren't boxed into a narrow portrait
     column. Was width:60vw/max 700px, which downscaled wide art to ~700px. */
  max-width: 90vw;
  display: flex; align-items: center; justify-content: center;
  position: relative;
}
#lightbox img {
  /* max-* only (no explicit width/height) so the image renders at its intrinsic
     size, scaled DOWN to fit the viewport but never UP past its actual pixels.
     Touch zoom can still exceed via transform. */
  max-width: 90vw; max-height: 88vh; object-fit: contain; border-radius: 8px;
  display: block; transition: opacity 0.075s ease;
}
#lightbox img.fade { opacity: 0; }
/* Touch: zoom/pan transform animates (double-tap, snap-back); a live gesture
   adds .dragging for 1:1 finger tracking. Swipe offset rides on the wrap.
   Two ids so this out-specifies `#lightbox img` and keeps the transform
   transition (that rule only declares the opacity fade). */
#lightbox #lightbox-img { transition: transform 0.2s ease, opacity 0.075s ease; will-change: transform; }
#lightbox-img-wrap { transition: transform 0.2s ease, opacity 0.2s ease; }
#lightbox.dragging #lightbox-img,
#lightbox.dragging #lightbox-img-wrap { transition: none; }
/* Transient neighbour image for the swipe "peek" carousel: overlays the current
   image, parked one screen-width to the side (inline translateX set in JS) so it
   rides the wrap's translate and slides in from the edge. Hidden when idle.
   Two ids so display:none out-specifies `#lightbox img { display:block }`. */
#lightbox #lightbox-img-2 {
  position: absolute; inset: 0; margin: auto;
  display: none; will-change: transform;
}
.lightbox-nav {
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 1;
  color: rgba(255,255,255,0.6); font-size: 0; cursor: pointer;
  width: 52px; height: 52px;
  display: none; align-items: center; justify-content: center;
  transition: color 0.2s;
}
/* Draw chevrons geometrically so they're centered (the ‹/› glyphs sit high). */
.lightbox-nav::before {
  content: ''; display: block;
  width: 13px; height: 13px;
  border-right: 3px solid currentColor;
  border-bottom: 3px solid currentColor;
}
#lightbox-prev { left: 24px; }
#lightbox-prev::before { transform: rotate(135deg); margin-left: 5px; }
#lightbox-next { right: 24px; }
#lightbox-next::before { transform: rotate(-45deg); margin-right: 5px; }
.lightbox-nav:disabled { opacity: 0.3; cursor: default; }
#lightbox.has-nav .lightbox-nav { display: flex; }
#lightbox-counter {
  position: absolute; bottom: -32px; left: 50%; transform: translateX(-50%);
  color: rgba(255,255,255,0.6); font-family: 'Amboy', sans-serif;
  font-size: 13px; letter-spacing: var(--tracking-wide); white-space: nowrap;
  display: none;
}
#lightbox.has-nav #lightbox-counter { display: block; }

/* "Use this card" + favorite heart — only in picker choose mode. Fixed in the bottom
   thumb zone, OUTSIDE #lightbox-img-wrap so the row never intercepts the swipe gestures
   bound there. The row (not the button) carries the fixed/centering positioning, so
   both children stay centered as a unit — mirrors #detail-action-row's reasoning. */
#lightbox-choose-row { display: none; }
#lightbox.choosing #lightbox-choose-row {
  display: flex; align-items: center; gap: 10px;
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 3;
  bottom: calc(24px + env(safe-area-inset-bottom));
}
#lightbox-choose {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 0 28px;
  background: var(--color-red); color: var(--color-white);
  border: none; border-radius: var(--radius-pill); cursor: pointer;
  font-family: 'Amboy', sans-serif; font-size: 15px; letter-spacing: var(--tracking-wide);
  text-transform: uppercase; box-shadow: 0 6px 20px rgba(0,0,0,0.45);
  transition: transform 0.1s ease, filter 0.2s ease;
}
#lightbox-choose:active { transform: scale(0.97); }
#lightbox-fav-btn {
  flex: 0 0 auto;
  width: 48px; height: 48px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--white); border: none; cursor: pointer;
  color: var(--mid-grey); box-shadow: 0 6px 20px rgba(0,0,0,0.45);
  transition: transform 0.1s ease, color 0.15s ease;
}
#lightbox-fav-btn:active { transform: scale(0.93); }
#lightbox-fav-btn .fav-heart-icon { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 1.75; }
#lightbox-fav-btn.favorited { color: var(--favorite); }
#lightbox-fav-btn.favorited .fav-heart-icon { fill: var(--favorite); }

/* Touch devices: swipe replaces the side chevrons, and the browser must not
   hijack pinch/scroll while we drive the gestures ourselves. The counter stays. */
@media (pointer: coarse) {
  #lightbox.has-nav .lightbox-nav { display: none; }
  #lightbox-img-wrap { touch-action: none; }
}

/* Hover effects only on true hover-capable pointers (desktop mouse), so they
   don't "stick" on touch devices after a tap. */
@media (hover: hover) {
  #team-fab:hover { transform: scale(1.1); box-shadow: 0 6px 28px rgba(0,0,0,0.3); }
  .team-slot.filled:hover { border-color: var(--color-hero-tag); }
  .team-slot.filled:hover .remove-hero { opacity: 1; }
  #clear-team-btn:hover { background: var(--red); color: white; }
  #share-copy-btn:hover { background: var(--purple-dark); }
  #share-download-btn:hover { color: var(--purple); }
  #collection-back-btn:hover { color: var(--purple-dark); }
  .season-toggle-btn:hover { background: var(--purple); color: white; }
  .expansion-char-item:hover { background: var(--off-grey); }
  .equip-flip-hint:hover { background: rgba(0,0,0,0.85); }
  .sh-pick-slot.empty:hover { border-color: var(--color-hero-tag); color: var(--color-hero-tag); }
  .sh-pick-slot.filled:hover { transform: translateY(-6px); }
  .sh-pick-remove:hover { background: var(--color-red); }
  .team-tag.clickable:hover { background: var(--color-hero-tag); color: var(--color-white); }
  #lightbox-close:hover { background: rgba(255,255,255,0.25); }
  .lightbox-nav:hover { color: #fff; }
  #lightbox.choosing #lightbox-choose:hover { filter: brightness(1.08); }
  #lightbox-fav-btn:hover { filter: brightness(0.95); }
}

/* RESPONSIVE */
@media (max-width: 1024px) {
  .card-grid { grid-template-columns: repeat(5, 1fr); }
}
/* The mobile add-to-team footer is inert above 800px: detail.js only relocates the button
   into it at ≤800px, so on desktop it stays empty. Explicit display:none keeps that empty
   div from contributing any box at all. */
#detail-action-bar { display: none; }
/* Its opaque backing strip is likewise mobile-only; the rule inside the 800px block below
   turns it on (and only while a detail overlay is open). */
#detail-action-bar-fill { display: none; }

@media (max-width: 800px) {
  .card-grid { grid-template-columns: repeat(4, 1fr); }

  /* Logo + 3 nav tabs + action don't fit one row below ~790px — stack to two
     rows (logo · action, then full-width nav). Static height, so the logo
     doesn't shrink on scroll here; only the shadow appears. */
  #header-inner {
    grid-template-columns: 1fr auto;
    grid-template-areas: 'logo action' 'nav nav';
    row-gap: 4px;
  }
  #logo-img { height: 60px; }
  #header.condensed #logo-img { height: 60px; }
  #toolbar-wrap { top: 125px; }

  /* Header actions get 44px touch targets. The ≤600px block covers phones; this
     handles the 601–800px stacked-header (tablet) range so the gear/collection
     buttons aren't stuck at their compact desktop size on touch tablets. */
  #settings-btn { width: 44px; height: 44px; justify-content: center; padding: 0; }
  #plays-btn { width: 44px; height: 44px; }
  #collection-btn { min-height: 44px; display: inline-flex; align-items: center; }

  #detail-top { grid-template-columns: 1fr; }
  /* Stacked layout: let the card image set the section height (in-flow, not the
     absolutely-positioned desktop treatment) so there's no wasted vertical band
     above/below it. */
  #detail-art { min-height: 0; padding: 24px 24px 0; }
  #detail-art.no-image { min-height: 280px; padding: 0; }
  #detail-art img {
    position: static; inset: auto;
    width: 100%; height: auto;
  }
  /* Art has 24px side padding here; keep the carousel arrows over the image. */
  .detail-art-nav.prev { left: 16px; }
  .detail-art-nav.next { right: 16px; }
  #detail-art-counter { bottom: 16px; }
  #detail-info { border-left: none; border-top: 1px solid var(--off-grey); max-height: none; }
  #detail-cards-grid, .unique-vertical .unique-grid { grid-template-columns: repeat(3, 1fr); }
  #team-heroes-grid, #team-heroes-extra-grid { grid-template-columns: repeat(4, 1fr); }
  #threat-grid, .unique-horizontal .unique-grid { grid-template-columns: repeat(2, 1fr); }
  #equipment-grid { grid-template-columns: repeat(3, 1fr); }
  #collection-grid { grid-template-columns: repeat(2, 1fr); }

  /* ONE source of truth for the detail modal's bottom gutter. Three things must agree on
     it: the sticky bar's `bottom` offset, #detail-overlay's `padding-bottom`, and the
     filler's height. They were previously three hand-typed `calc(8px + env(...))` values
     kept in sync by comment alone — which is exactly how the bleed-through below slipped
     through. Change it here only. The 8px matches .overlay-backdrop's base padding so the
     panel's side and bottom gutters stay even; the inset keeps things off the home
     indicator. */
  :root { --detail-bar-gutter: calc(8px + env(safe-area-inset-bottom)); }

  /* Sticky footer hosting #add-to-team-btn (moved here by detail.js). Because
     #detail-panel is `overflow: clip` (not a scroll container), this sticks against
     .overlay-backdrop — pinned above the fold while card grids scroll below it, then
     coming to rest ON the panel's bottom edge at full scroll (it inherits the panel's
     rounded bottom corners via the clip). --surface-card matches the panel so docked it
     reads as a seamless footer and the border-top becomes an ordinary divider. */
  #detail-action-bar {
    /* Sharing --detail-bar-gutter with #detail-overlay's padding-bottom is what makes the
       stuck position identical to the docked (in-flow) rest position, so the bar never
       moves at any scroll offset. */
    position: sticky; bottom: var(--detail-bar-gutter); z-index: 5;
    display: flex; padding: 10px 16px;
    background: var(--surface-card);
    border-top: 1px solid var(--off-grey);
  }
  /* Collapse when the button isn't here (nothing to show) or isn't applicable —
     villains/modes hide it via .hidden in renderCharacterDetail / renderModesDetail. */
  #detail-action-bar:empty,
  #detail-action-bar:has(> .hidden) { display: none; }

  #detail-action-bar #add-to-team-btn {
    flex: 1; margin-top: 0; min-height: 48px; font-size: 15px;
  }
  #detail-action-bar #detail-action-row { width: 100%; }
  #detail-action-bar #detail-fav-btn { width: 48px; height: 48px; }

  /* Paired with #detail-action-bar's `bottom` via --detail-bar-gutter — see the note
     there. Scoped to #detail-overlay: the shared .overlay-backdrop class also drives the
     expansion/collection overlays, which have no sticky bar and shouldn't get this. */
  #detail-overlay { padding-bottom: var(--detail-bar-gutter); }

  /* Closes the strip between the bar's bottom edge and the true viewport edge — the two
     gutters above (sticky offset + overlay padding), hence `* 2`.
     Why this exists: #detail-overlay's padding-bottom only becomes empty space once you've
     scrolled to the document's true end. Mid-scroll it's still ordinary in-flow panel
     content, so card art and text showed through beneath the button.
     Why it lives OUTSIDE #detail-overlay (sibling in index.html — load-bearing): the
     backdrop's `backdrop-filter: blur(4px)` establishes a containing block for `fixed`
     descendants, so a nested version scrolls away with the content instead of staying
     pinned. It must be outside the backdrop to behave as fixed at all.
     Opaque by default; detail.js reveals it only at genuine end-of-scroll, which is where
     the panel's rounded bottom edge is supposed to show. */
  #detail-action-bar-fill {
    /* Living outside #detail-overlay means competing with it in the root stacking context,
       so this must out-rank the overlay itself: 200 normally, 215 as .above-team, 217 as
       .above-plays. 218 clears all three. (It can't be a low z-index "just under the bar" —
       that only made sense back when this was going to live inside the panel.) Only ever
       displayed while the detail modal is open (see the :has() rule below), so out-ranking
       the PLAYS overlay costs nothing when detail is closed. Lightbox (300) and toast (400)
       still correctly paint over it.
       The strip must be exactly the PANEL's width — full-bleed left:0/right:0 read as a
       viewport-wide band, visibly wider than the modal it's supposed to extend. left matches
       .overlay-backdrop's 8px padding; the width is set by detail.js (updateBarFill), because
       a classic overlay scrollbar narrows the panel by an amount CSS can't see. right: 8px
       here is only the no-scrollbar fallback until that first JS write.
       No background transition, deliberately: the reveal must be instant. detail.js clears
       it while the panel's white tail is passing under the bar, so a fade would cross-fade
       over moving content; an instant swap is white-to-white and invisible. */
    position: fixed; left: 8px; right: 8px; bottom: 0; z-index: 218;
    height: calc(var(--detail-bar-gutter) * 2);
    background: var(--surface-card);
    pointer-events: none;
  }
  /* Gated on the BAR actually being present, not merely on the overlay being open. This
     strip only makes sense as an extension of the bar: villains and modes hide the button
     (so the bar collapses), and there content running to the viewport edge is just normal
     scrolling with nothing to hide under — showing the strip there paints a white band
     across their card text instead. The selector matches only when the button has been
     relocated into the bar (mobile) AND isn't .hidden, which is exactly when a bar renders;
     on desktop the button lives in #detail-info-body so this never matches.
     #sh-picker-overlay is excluded because it also sits at 215 and opens FROM the detail
     modal (so the detail overlay is still .open behind it) — at 216 this strip would
     otherwise paint a band across the picker's bottom edge.
     The @supports mirrors detail.js's `canStick` gate: without `overflow: clip` the bar was
     never made sticky and detail.js never registers the listener that reveals this strip, so
     showing it would strand an opaque band that never clears. Keep the two gates in step. */
  @supports (overflow: clip) {
    body:has(#detail-overlay.open #detail-action-bar > #detail-action-row:not(.hidden)):not(:has(#sh-picker-overlay.open)) #detail-action-bar-fill {
      display: block;
    }
    #detail-action-bar-fill.at-end { background: transparent; }
  }
}
@media (max-width: 600px) {
  :root { --gutter: 16px; }
  /* Mobile-first shell: header collapses to a single slim row (logo · actions);
     primary nav moves to a fixed bottom tab bar in the thumb zone (see #nav-bar
     below). Search becomes full-width in a two-row toolbar. */
  #header-inner {
    grid-template-columns: 1fr auto;
    grid-template-areas: 'logo action';
    padding: 8px var(--gutter);
  }
  #logo-img { height: 44px; }
  /* Single-row header: already condensed, so it must not change size on scroll.
     Pin both the logo height and the inner padding so the condensed state matches
     the resting state (the desktop rule bumps padding to 10px otherwise).
     Offset = header height (~60px → #toolbar-wrap top). */
  #header.condensed #logo-img { height: 44px; }
  #header.condensed #header-inner { padding: 8px var(--gutter); }

  /* NAV → fixed bottom tab bar. The #nav-bar element is reused (markup-agnostic
     click/history logic in main.js keeps working); only its presentation changes.
     The sliding pill is hidden in favour of an active-colour treatment. */
  #nav-bar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 99;
    justify-self: stretch; width: 100%; max-width: none;
    justify-content: space-around; gap: 0;
    background: var(--surface-raised); border-radius: 0;
    border-top: 1px solid var(--off-grey);
    padding: 0 0 env(safe-area-inset-bottom);
  }
  .nav-pill { display: none; }
  .nav-tab {
    flex: 1; flex-direction: column; justify-content: center;
    /* Symmetric bottom padding: #nav-bar already adds env(safe-area-inset-bottom) for the
       home-indicator clearance, so the old 16px bottom stacked on top of it (~34px on iOS)
       and read as a huge chin. Android has no inset, so it looked fine there. */
    gap: 3px; padding: 12px 0 12px; font-size: 11px;
    letter-spacing: var(--tracking-wide);
    touch-action: manipulation;   /* drop the legacy tap delay / double-tap zoom on tabs */
  }
  .nav-tab-icon { width: 20px; height: 20px; }
  .nav-tab.active { color: var(--purple); }

  /* Header actions: 44px touch targets. */
  #settings-btn, #share-btn {
    width: 44px; height: 44px; justify-content: center; padding: 0;
    border-radius: 12px; background: var(--color-off-white); color: var(--dark-grey);
  }
  /* #plays-btn stays purple/filled at this size too — deliberately NOT folded into the
     rule above, which restyles settings/share into neutral ghost squares. Sizing only. */
  #plays-btn { width: 44px; height: 44px; }
  #collection-btn {
    height: 44px; display: flex; align-items: center;
    font-size: 11px; padding: 0 16px;
  }
  #count-label { font-size: 11px; color: var(--mid-grey); letter-spacing: var(--tracking-wide); }
  #toolbar-wrap { top: calc(60px + env(safe-area-inset-top)); }
  #toolbar {
    padding: 8px var(--gutter);
    flex-direction: column; align-items: stretch; gap: 8px;
  }
  /* Full-width search row; the sort button stays inside #search-wrap so the
     #sort-filter-panel (right:0) keeps anchoring to the search edge. */
  #search-wrap { width: 100%; height: 44px; padding: 0 14px; }
  /* Mobile: the bottom nav bar stays neutral (its selected icon already signals the
     tab via --purple above), so the search bar carries the per-tab tint instead. */
  body:has(#nav-bar .nav-tab[data-tab="heroes"].active) #search-wrap { background: #D3E7F5; }
  body:has(#nav-bar .nav-tab[data-tab="villains"].active) #search-wrap { background: #F1CCCD; }
  body:has(#nav-bar .nav-tab[data-tab="modes"].active) #search-wrap { background: #F8EACD; }
  #search-input { font-size: 16px; }   /* 16px avoids iOS focus auto-zoom */
  #search-clear { width: 32px; height: 44px; justify-content: center; }
  .sort-btn { height: 44px; padding: 0 8px; }
  /* Clear the bottom nav (~66px) plus the flared team peek-tab (~33px above it). */
  #main-content { padding: 6px 0 calc(108px + env(safe-area-inset-bottom)); }
  .card-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; }
  #detail-cards-grid, .unique-vertical .unique-grid { grid-template-columns: repeat(2, 1fr); }
  #team-heroes-grid, #team-heroes-extra-grid { grid-template-columns: repeat(3, 1fr); }
  #threat-grid, .unique-horizontal .unique-grid { grid-template-columns: repeat(1, 1fr); }
  #equipment-grid { grid-template-columns: repeat(2, 1fr); }
  #collection-grid { grid-template-columns: repeat(2, 1fr); }

  /* Filter/sort popover: the desktop `width: 360px` overflows a phone. It already
     anchors to the now-full-width #search-wrap, so let it span that width and cap its
     height with an internal scroll. 100dvh (not 100vh) avoids the iOS URL-bar jump. */
  #sort-filter-panel {
    left: 0; right: 0; width: auto;
    max-height: calc(100dvh - 140px); overflow-y: auto;
    /* A touch that starts in the panel must never scroll the grid behind it —
       neither at the scroll ends nor when the content is short enough to fit. */
    overscroll-behavior: contain;
  }
  /* Settings popover: its anchor (#header-actions) is narrow, so pin it to the viewport
     with gutter insets, dropping just below the header. It stays a DOM child of
     #header-actions, so the existing outside-click / Escape dismissal keeps working. */
  #settings-panel {
    position: fixed;
    left: var(--gutter); right: var(--gutter);
    top: calc(60px + env(safe-area-inset-top) + 6px);
    min-width: 0; width: auto;
    max-height: calc(100dvh - 80px - env(safe-area-inset-top)); overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* Filter popover controls: comfortable touch targets. */
  .sf-pill, .sort-dir-btn { min-height: 40px; }
  .fav-first-btn { width: 40px; height: 40px; }
  /* Reserve exactly the CLEAR FILTERS button's touch-target height so the head
     doesn't grow (and shove the pills down) when a filter goes active. Must track
     .sort-dir-btn's min-height above — keep the two in sync. */
  .sf-section-head { min-height: 40px; }

  /* Modal close buttons: enlarge from 30–34px to a comfortable touch target. */
  .overlay-close { width: 40px; height: 40px; top: 12px; right: 12px; }
  #detail-close { width: 40px; height: 40px; }

  /* Settings panel touch targets. */
  .info-btn { width: 40px; height: 40px; }
  .stepper-btn { width: 44px; height: 44px; }
  .disclosure-head { min-height: 40px; padding: 8px 0; }

  /* Lightbox goes full-bleed on phones (was capped at 60vw, so tapping a card
     opened it smaller than the thumbnail). */
  #lightbox-inner { gap: 0; }
  #lightbox-img-wrap { width: 100vw; max-width: 100vw; }
  #lightbox img { max-width: 100vw; max-height: 92vh; }
  /* TEAM → bottom sheet. The desktop FAB + right-side panel are replaced by a sheet
     anchored to the viewport bottom whose green header peeks above the nav as a swipe
     handle. Collapsed, the sheet sits BELOW the nav (z:90 < nav's 99) so the handle
     tucks behind the nav's rounded top edge; opening raises it above the nav (z:205)
     over a dimming scrim. The .open transform animates via the base transition. */
  #team-fab { display: none; }

  #team-side-panel {
    /* How far #team-sheet-tab pokes ABOVE #team-sheet-handle to form the trapezoid
       grabber — its OWN geometry only (drives #team-sheet-tab's `top` below), not a
       buffer amount. Keep this exactly matching the tab's actual offset. */
    --team-tab-poke: 26px;
    /* How much space to actually RESERVE above the handle inside the scroll
       container: the tab's poke PLUS enough extra to also fit the collapsed-state
       drop-shadow's blur bleed (`filter: drop-shadow(0 -3px 12px ...)` below —
       casts ~15-20px past the tab's own edge). A drop-shadow, like the tab's own
       negative offset, is clipped by an ancestor's overflow:auto if there isn't
       room reserved for it — this is that room. Kept as a SEPARATE variable from
       --team-tab-poke on purpose: widening this buffer must never change the tab's
       actual visual position, only how much invisible space sits above it. Drives
       #team-panel-inner's padding-top AND the collapsed transform below (both must
       move together, same reasoning as before — see memory
       marvel-united-team-sheet-handle for why these have drifted apart historically). */
    --team-handle-reserve: 48px;
    left: 0; right: 0; bottom: 0; top: auto;
    width: 100%; max-width: none; height: auto;
    z-index: 90;
    /* Collapsed: a thin ~11px strip peeks above the ~66px nav; the central tab flares
       up ~29px higher still (so the handle rises ~40px above the nav). The extra
       --team-handle-reserve subtraction cancels #team-panel-inner's padding-top, so
       the green band peeks by exactly the same amount as before that padding
       existed — `100%` is the panel's OWN height, which the padding would
       otherwise inflate. */
    transform: translateY(calc(100% - 77px - var(--team-handle-reserve) - env(safe-area-inset-bottom)));
    pointer-events: none;   /* only the handle is tappable at rest */
  }
  #team-side-panel.open {
    transform: translateY(0);
    /* Overrides the base rule's bouncy open/close easing (cubic-bezier(0.34, 1.56,
       0.64, 1), a "back out" curve that overshoots PAST its target) for opening
       specifically: the overshoot briefly sends translateY negative — the sheet's
       bottom edge (anchored via bottom:0 + this transform) lifts above the true
       viewport bottom for a few frames, and since the sheet's own box ends exactly
       at its content (no reserve below), that gap exposed the page behind it. A
       plain ease-out never overshoots, so the bottom edge never leaves bottom:0.
       Closing keeps the bouncy curve (the base rule, used when .open is removed) —
       an overshoot THERE moves the sheet further off-screen, not away from it, so
       it never exposes this gap. */
    transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
    z-index: 205;
    pointer-events: all;
  }
  /* No .team-empty shorter-open rule anymore — the sheet's height is now adaptive to
     content (see #team-panel-inner below), so an empty team already renders short;
     forcing a separate lower open position on top of that would double-shrink it. */
  /* During a finger drag, kill the transition so the sheet/scrim track the finger with
     no lag (mirrors lightbox.js). Cleared on release so the snap animates. The panel is
     raised above the scrim (and nav) for the whole drag — even opening from the collapsed
     z:90 state — so it's never dimmed by its own scrim. */
  #team-side-panel.dragging, #team-scrim.dragging { transition: none; }
  /* Elevated while dragging AND while the release-snap animates (.settling), so the sheet
     is never briefly covered by its own scrim when settling back toward the collapsed
     z:90 rest state. */
  #team-side-panel.dragging, #team-side-panel.settling { z-index: 205; }

  /* Adaptive height: grows to fit content, capped so the sheet never eclipses the
     whole screen. THIS element is the scroll container (not #team-sheet-body) —
     the handle scrolls away together with the rest of the content as one
     continuous unit, per the user's explicit ask, rather than staying pinned while
     only the body scrolls beneath it.
     padding-top exactly cancels #team-sheet-tab's `top:-26px` escape (see below):
     without it, the tab would render above this element's own top edge and
     overflow-y:auto would clip it clean off. Matching it exactly (rather than
     overshooting) means the tab lands right at the clipping boundary with nothing
     visually different at rest — no extra gap, no lost content. */
  #team-panel-inner {
    height: auto;
    /* Full 100dvh, not `- 80px`: the sheet grows upward as far as the screen allows
       and content then scrolls off the TRUE top edge, instead of disappearing at an
       arbitrary line partway down the screen. */
    max-height: 100dvh;
    overflow-y: auto; overscroll-behavior: contain;
    /* Reserves room for both the tab's poke AND its collapsed-state drop-shadow's
       blur bleed, so this scroll container's overflow:auto doesn't clip either one
       — see --team-handle-reserve above. */
    padding: var(--team-handle-reserve) 0 0;
    display: block;
    border-radius: 0; border: none;
    /* Transparent ONLY through the handle's own reserve+height (48px + 30px, i.e.
       exactly var(--team-handle-reserve) + 30px — where #team-sheet-tab pokes up
       and #team-sheet-handle's rounded top corners cut two small triangles out of
       this otherwise-rectangular box), so that band still reveals the dimmed app
       behind it rather than a white/dark fill. Solid --surface-card for everything
       below that — this is #team-panel-inner's own background (a backgrounds
       doesn't move when its content scrolls), so it's what iOS's elastic
       overscroll bounce paints into the gap when scrolling past the bottom of the
       content, instead of the app page one stacking layer further back. (The
       #team-sheet-body child below the handle already paints --surface-card too,
       at rest — this is what shows through once the bounce scrolls that surface
       out of the way.) */
    background: linear-gradient(to bottom,
      transparent 0, transparent calc(var(--team-handle-reserve) + 30px),
      var(--surface-card) calc(var(--team-handle-reserve) + 30px));
    box-shadow: none;
  }
  #team-close { display: none; }   /* mobile close is the swipe handle, not a button */
  /* Now that #team-title lives inside #team-sheet-body (see index.html), it inherits the
     body's white background and 20px padding for free — no separate white patch needed. */
  #team-title { font-size: var(--text-xl); }

  /* Rebuilt handle: the band and tab are ONE rigid unit with fixed geometry that never
     changes between collapsed and open — the old version resized the band per-state and
     hand-tuned the tab's offset to compensate, which drifted out of sync repeatedly (see
     memory marvel-united-team-sheet-handle). Now the peek amount is governed solely by
     #team-side-panel's translateY, so there is nothing here to counter-tune per state. */
  #team-sheet-handle {
    display: block; position: relative;
    height: 30px; margin-top: 0; background: var(--green);
    border-radius: 18px 18px 0 0;
    pointer-events: auto; cursor: pointer; touch-action: none;
  }
  /* Collapsed only: a pronounced shadow lifts the handle+tab off the busy, heavily-coloured
     hero grid behind it. filter (not box-shadow) so it follows the union silhouette of the
     rectangular band + the trapezoid tab, rather than boxing the tab in a rectangle. Casts
     up/outward since the sheet rises from the bottom. Dropped once open — the scrim already
     separates the sheet from the dimmed app. */
  #team-side-panel:not(.open) #team-sheet-handle {
    filter: drop-shadow(0 -3px 12px rgba(0,0,0,0.38));
  }
  /* Tab pokes a constant amount above the band's top edge in BOTH states — this offset is
     the one tunable that controls how tall the tab reads above the band. */
  #team-sheet-tab {
    position: absolute; left: 50%; top: calc(-1 * var(--team-tab-poke)); transform: translateX(-50%);
    width: 140px; height: 34px;
  }
  /* Trapezoid: rounded top corners, sides flare outward into the band. Drawn on a
     pseudo-element via clip-path so the roster-meter segments (real children) render above
     it. The base sits 3px into the tab's box (bottom edge at y34 with band starting at
     y31-ish once positioned) so it overlaps the band by a few px — no visible seam. */
  #team-sheet-tab::before {
    content: ''; position: absolute; inset: 0; background: var(--green);
    clip-path: path('M0,34 L12.7,10 Q18,0 29.3,0 L110.7,0 Q122,0 127.3,10 L140,34 Z');
  }
  /* Roster meter: a plain grab pill that always stays intact (the drag affordance,
     never mutates), with four status dots stacked below it — solid = a hero on the
     team, ring = an open slot. Team size maxes at 4, so dots map 1:1 to capacity.
     Discrete marks for a discrete count; the pill keeps reading as "drag me" in every
     state. */
  #team-sheet-grab {
    position: absolute; left: 50%; top: 6px; transform: translateX(-50%);
    display: flex; flex-direction: column; align-items: center; gap: 6px;
  }
  .team-grab-pill {
    width: 48px; height: 4px; border-radius: 999px;
    background: rgba(255,255,255,0.85);
  }
  .team-dots { display: flex; gap: 4px; }
  /* Versus modes: two 3-dot groups (one per side) with a wider gap between the
     groups than within one, so "3+3" reads as two sides rather than one run of 6. */
  .team-dots-groups { display: flex; gap: 16px; }
  .team-seg {
    width: 7px; height: 7px; border-radius: 50%; box-sizing: border-box;
    border: 1px solid rgba(255,255,255,0.60);   /* ring = open slot */
    background: transparent;
  }
  .team-seg.is-filled { background: var(--white); border-color: transparent; }

  #team-sheet-body {
    /* No max-height/overflow of its OWN anymore — #team-panel-inner (its parent) is
       the single scroll container now, so the handle and body scroll together as
       one continuous unit instead of the handle staying pinned above an inner
       scrollbox. This element just flows to its natural content height. */
    display: block;
    padding: 20px 20px calc(20px + env(safe-area-inset-bottom));
    background: var(--surface-card);   /* the sheet's surface (below the green header) */
  }

  #team-scrim {
    display: block; position: fixed; inset: 0;
    background: rgba(0,0,0,0.45); opacity: 0; pointer-events: none;
    z-index: 200; transition: opacity 0.3s ease;
  }
  #team-scrim.show { opacity: 1; pointer-events: auto; }

  /* LOG PLAY — the modal's own mobile pass. Everything above in this file's LOG PLAY
     sections was built desktop-first with only a `min-width: 700px` widen-up rule, so
     none of this file's other ≤600px touch-target/font-size work ever reached it. */
  #play-log-panel { padding: 18px; }
  .mode-group-pill, .pl-add-hero-row, #pl-save-btn, .disclosure-head,
  #pl-footer-actions .pl-seat-remove-btn {
    min-height: 44px;
  }
  /* .pl-me-btn and the villain switch are deliberately absent — both keep their compact
     visual size and get their 44px touch target from an ::after overlay instead, so
     neither inflates the row or the band header it sits in. */
  .pl-select, .pl-input, .pl-hero-row-input, textarea.pl-input {
    font-size: 16px; /* 16px avoids iOS focus auto-zoom, same as #search-input above */
  }
  /* Text/date/number inputs and selects sit at ~41px from padding alone — nudge them to
     the same 44px floor the buttons above use. textarea is excluded: its height comes
     from `rows` and it's already well past 44px. */
  .pl-select, input.pl-input { min-height: 44px; }
  .pl-hero-row-remove {
    width: 44px; height: 44px; /* hit area only — the 16px glyph stays put */
    display: flex; align-items: center; justify-content: center;
  }
}
/* Very small phones (~360px and below): trim the header chrome one more notch
   so labels stay legible without crowding. Grid stays at 3 columns. Nav is the
   fixed bottom bar (full-width tabs flex to fit) and search is full-width, so
   neither needs narrowing here. */
@media (max-width: 380px) {
  #collection-btn { font-size: 10px; padding: 0 12px; letter-spacing: 0.5px; }
}
/* 3 columns is the hard minimum — no further column reduction below 600px. */

/* De-inlined from markup (expansion overlay header + light close button) */
.overlay-close--light { color: var(--dark-grey); }
#expansion-header { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
#expansion-title { font-family: 'Amboy', sans-serif; font-size: 18px; color: var(--color-text-primary); }
#expansion-subtitle { font-size: 13px; color: var(--mid-grey); margin-bottom: 16px; }