/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Column Depth
   ═══════════════════════════════════════════════════════════════ */

/* Ambient editor backdrop — a restrained layered gradient over the flat
   surface so the workspace reads with depth and the frosted comparison panel
   has something to refract. Layers (top → bottom): a soft accent glow in the
   top-right, a cool counter-tint in the lower-left, and a faint tonal diagonal.
   All low-alpha and built on theme rgb tokens, so it adapts light/dark and
   stays subtle rather than loud. Sits on top of the element's bg-surface color
   (background-image only — the Tailwind bg-surface color shows through). */
.fr-editor-shell {
  background-image:
    radial-gradient(115% 80% at 100% 0%, rgb(var(--accent-rgb) / .07), transparent 55%),
    radial-gradient(100% 75% at 0% 100%, rgb(var(--surface-3-rgb) / .5), transparent 55%),
    linear-gradient(155deg, rgb(var(--surface-2-rgb) / .45) 0%, transparent 42%);
  background-attachment: local;
}

/* Light mode: subtle lift shadow on column cards for depth */
.fr-editor-column {
  box-shadow: 0 1px 3px rgba(60, 40, 20, 0.06),
              0 1px 2px rgba(60, 40, 20, 0.04);
  /* Containment: column is its own layout/paint/style boundary so a
     reorder in one column can't dirty layout/style in another. Drives
     the drag-time UpdateLayoutTree elementCount way down (was 85k+). */
  contain: layout paint style;
}
html[data-theme="dark"] .fr-editor-column {
  box-shadow: none;
}

/* Skip rendering work for columns scrolled off-screen horizontally.
   Active drag column stays in viewport so SortableJS measurements
   are unaffected. Intrinsic size matches typical column width so
   placeholder doesn't collapse the layout. */
.fr-editor-column {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Row Containment
   ═══════════════════════════════════════════════════════════════ */

/* Each player row is its own layout/paint/style boundary. Without
   this, a class change on body or a parent forces every row's
   subtree through style recalc/layout. With it, invalidation stops
   at the row root. Trace pre-fix showed 34k dirty layout objects
   on a single drag move; containment scopes that to the rows that
   actually changed. Style containment is safe here because rows
   don't use counters/quotes that need to escape. */
.fr-row,
.fr-tier-break-row {
  contain: layout paint style;
}

/* Skip rendering work for rows scrolled off-screen vertically.
   Each column scrolls independently; rows the user can't see don't
   need style/layout/paint. Browser auto-promotes to full rendering
   when a row enters the viewport, so SortableJS sees real rects on
   anything it could practically swap with (drag candidate area is
   always in-viewport). Intrinsic sizes match the rendered heights:
   28 px desktop / 48 px mobile for player rows; 20 px / 28 px for
   tier breaks. Off by a couple px on either side is fine —
   content-visibility's main job is to drop the per-row recalc cost,
   not to preserve scroll position to the pixel. */
/* Per-row content-visibility was deliberately removed: it collapses
   off-screen rows' contribution to the column body's scrollHeight, so the
   .overflow-y-auto body never overflows — overflow-y-auto stays inactive
   and spotlight autoscroll has nothing to scroll. The horizontal
   .fr-editor-column content-visibility (above) still skips off-screen
   columns; per-row containment (contain: layout paint style on .fr-row)
   still scopes style/layout invalidation. The drag perf win from #15
   came mostly from `contain`, not from content-visibility on each row. */

/* SortableJS drag list: paint+layout containment so reorder animations
   don't trigger paint invalidation on neighbors. Children (rows) already
   carry style containment, so it isn't needed at this level. */
[data-controller~="drag"] {
  contain: layout paint;
}

/* (Hover-kill rule deliberately not present here. Rationale: a rule
   like `.fr-editor-shell.fr-dragging .fr-row > *` cascades on every
   class toggle of `.fr-dragging`, forcing the style engine to
   re-evaluate `.fr-row > *` matches across the whole editor (~30k+
   elements). The trace after Step 3 showed a 380 ms style recalc
   spike at pointerdown that traced back to this exact toggle pattern.
   The hover cost it suppressed (~85 ms cumulative dragenter/mouseenter)
   was an order of magnitude smaller than the cascade it caused.
   Any future hover-suppression mechanism MUST avoid descendant
   selectors anchored at the toggled class.) */

/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Drag & Drop States
   ═══════════════════════════════════════════════════════════════ */

/* ── Ghost: the invisible slot left in the list ────────────────
   Content is hidden. The animated gap from neighboring items
   sliding apart is the drop indicator — no decoration needed.
   No pseudo-elements or position: relative to avoid layout
   recalculation jitter as SortableJS moves the ghost in the DOM. */
.fr-sortable-ghost {
  opacity: 0 !important;
}

/* Force-render the dragged row's subtree. Rows carry
   `content-visibility: auto` for offscreen-skip perf, but Chromium's
   native HTML5 drag-image snapshot generator skips content-visibility
   subtrees, producing an empty drag image. Override on the states
   active during drag so the snapshot includes the row's content. */
.fr-sortable-chosen,
.fr-sortable-fallback {
  content-visibility: visible;
}

/* ── Chosen: the instant lift when you grab a row ──────────────
   Provides immediate tactile feedback before dragging begins. */
.fr-sortable-chosen {
  z-index: 10;
}
.fr-sortable-chosen > .fr-row-content {
  background-color: var(--surface-2);
  box-shadow: inset 0 0 0 2px var(--accent),
              0 4px 12px -2px rgba(0, 0, 0, 0.15);
  border-radius: 0.25rem;
}

/* ── Fallback: the card following your cursor ──────────────────
   Elevated card with subtle scale for depth. Clean silhouette
   with hidden expand/tier-break content. */
.fr-sortable-fallback {
  box-shadow: 0 16px 32px -6px rgba(0, 0, 0, 0.30),
              0 8px 16px -4px rgba(0, 0, 0, 0.15),
              0 0 0 1px rgba(255, 255, 255, 0.06);
  border-radius: 0.375rem;
  opacity: 0.95;
  background-color: var(--surface);
}
.fr-sortable-fallback .drag-exclude {
  display: none !important;
}

/* (Expanded-row collapse during drag is now handled in
   drag_controller.js#onChoose by setting inline style directly on the
   handful of `.fr-row-expanded` elements, then restoring on drag-end.
   The previous descendant rule `.fr-editor-shell.fr-dragging .fr-row-expanded`
   triggered a full-tree style invalidation on every `.fr-dragging`
   toggle even though only 0-1 elements actually matched. Same root
   cause as the hover-kill cascade above — descendant selectors
   anchored at a toggled class are paid in full on toggle.) */

/* ── Drop landing: brief highlight when item settles ───────────
   Applied by JS after drop, removed after animation completes. */
.fr-drop-landed > .fr-row-content {
  animation: drop-land 400ms ease-out;
}
@keyframes drop-land {
  0%   { background-color: var(--accent-bg); box-shadow: inset 0 0 0 2px var(--accent); }
  100% { background-color: transparent; box-shadow: none; }
}

/* ── Composite list syncing lock: applied while async propagation is
   in flight. Removed implicitly when the container is replaced by the
   server's turbo_stream response (synchronous path) or by an
   ActionCable broadcast (async path). The watchdog refetch in
   composite_lock_controller is the recovery path for dropped streams.

   Visual treatment: dim the list + lay a slow diagonal stripe shimmer
   over it to communicate "working, don't touch yet". pointer-events
   are blocked on the inner content so accidental clicks/drags can't
   interleave with the in-flight server work. */
.fr-list-syncing {
  position: relative;
  opacity: 0.65;
  transition: opacity 120ms ease-out;
}
.fr-list-syncing > [data-controller~="drag"] {
  pointer-events: none;
}
.fr-list-syncing::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    45deg,
    transparent 0 10px,
    var(--accent-bg) 10px 20px
  );
  background-size: 200% 200%;
  animation: fr-list-syncing-shimmer 1.4s linear infinite;
  border-radius: inherit;
  mix-blend-mode: multiply;
}
html[data-theme="dark"] .fr-list-syncing::after {
  mix-blend-mode: screen;
}
@keyframes fr-list-syncing-shimmer {
  from { background-position: 0 0; }
  to   { background-position: 28px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .fr-list-syncing::after {
    animation: none;
    background: var(--accent-bg);
    opacity: 0.35;
  }
}

/* ── Synced move: flash on composite list nodes that moved as a
   result of a positional list reorder. Applied server-side via
   the Turbo Stream replace so the animation plays on render. */
.fr-synced-move {
  animation: synced-flash 600ms ease-out;
}
@keyframes synced-flash {
  0%   { background-color: var(--accent-bg); box-shadow: inset 3px 0 0 0 var(--accent); }
  100% { background-color: transparent; box-shadow: none; }
}

/* ── Cross-list highlight: same player in other lists ─────────
   When a player row is grabbed, matching rows in other columns
   get an accent tint so the ranker can see where that player
   sits across lists. */
.fr-cross-highlight > .fr-row-content {
  background-color: var(--accent-bg);
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* ── Composite row dividers: stronger contrast against surface-3 ─
   Default --border is near-invisible on composite column backgrounds,
   so bump to --border-hover for legible row separation. */
.bg-surface-3 .border-b {
  border-color: var(--border-hover);
}

/* ── Position row tints: faint background color by position ────
   Uses the same CSS variables as position badges so badge color
   and row tint always match across themes. */
[data-nfl-position="QB"]  > .fr-row-content { background-color: var(--pos-qb-tint); }
[data-nfl-position="RB"]  > .fr-row-content { background-color: var(--pos-rb-tint); }
[data-nfl-position="WR"]  > .fr-row-content { background-color: var(--pos-wr-tint); }
[data-nfl-position="TE"]  > .fr-row-content { background-color: var(--pos-te-tint); }
[data-nfl-position="K"]   > .fr-row-content { background-color: var(--pos-k-tint); }
[data-nfl-position="DEF"] > .fr-row-content { background-color: var(--pos-def-tint); }

/* ── Position number: smooth transition on renumber ────────── */
[data-position-number] {
  transition: opacity 100ms ease;
}
.fr-pos-updating {
  opacity: 0.4;
}

/* ── Prevent text selection during drag ─────────────────────── */
.drag-handle {
  user-select: none;
  -webkit-user-select: none;
}
.fr-sortable-fallback {
  user-select: none;
  -webkit-user-select: none;
}

/* ── Drag handle affordance ──────────────────────────────────── */
.drag-handle {
  transition: color 120ms ease;
}
.drag-handle:hover svg {
  color: var(--accent) !important;
  transform: scale(1.15);
  transition: transform 120ms ease, color 120ms ease;
}
.drag-handle:active svg {
  transform: scale(0.95);
}

/* ── Player spotlight: hover and pinned states ────────────────
   Hover gives ephemeral highlight; pinned is stronger for
   sustained cross-list tracking. Applied to row wrapper, so
   use > .fr-row-content to match existing fr-cross-highlight
   pattern and layer over position tints. */
.fr-spotlight-hover > .fr-row-content {
  background-color: var(--accent-bg);
  box-shadow: inset 3px 0 0 0 var(--accent);
}

.fr-spotlight-pinned > .fr-row-content {
  background-color: var(--accent-bg);
  box-shadow: inset 3px 0 0 0 var(--accent), 0 0 0 1px var(--accent);
}

/* ── ECR delta indicators ────────────────────────────────────
   Hidden by default. Visible when fr-show-ecr is active on an
   ancestor. Content and color are set by JS after toggle/drag. */
.fr-ecr-delta {
  display: none;
}
.fr-show-ecr .fr-ecr-delta {
  display: inline;
}

/* ── Snapshot delta indicators ───────────────────────────────
   Same pattern as ECR. Visible when fr-show-snapshot is active. */
.fr-snapshot-delta {
  display: none;
}
.fr-show-snapshot .fr-snapshot-delta {
  display: inline;
}

/* Board cards are narrow grid cells — let the vs-ECR / vs-snapshot pills size
   to their content instead of the fixed w-8 the toolbar applies for the roomier
   list row (two stacked pills would otherwise overrun the cell). */
.fr-board-card .fr-ecr-delta,
.fr-board-card .fr-snapshot-delta {
  width: auto;
  min-width: 0;
}

/* Tiny single-letter source label inside delta pills (E = ECR, S = Snapshot).
   Same color as delta value via currentColor, dimmed so number stays primary. */
.fr-delta-label {
  font-size: 7px;
  font-weight: 600;
  opacity: 0.55;
  margin-right: 1px;
  letter-spacing: 0.02em;
}

/* ── Scrollbars: thin, translucent, non-intrusive ──────────── */
::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background-color: rgba(0, 0, 0, 0.12);
  border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
  background-color: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-corner {
  background: transparent;
}
/* Firefox */
* {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.12) transparent;
}
html[data-theme="dark"] ::-webkit-scrollbar-thumb {
  background-color: rgba(255, 255, 255, 0.1);
}
html[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
  background-color: rgba(255, 255, 255, 0.2);
}
html[data-theme="dark"] * {
  scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}

/* ── Number input spinners: hide until hover ──────────────── */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  opacity: 0;
  transition: opacity 150ms ease;
}
input[type="number"]:hover::-webkit-inner-spin-button,
input[type="number"]:hover::-webkit-outer-spin-button,
input[type="number"]:focus::-webkit-inner-spin-button,
input[type="number"]:focus::-webkit-outer-spin-button {
  opacity: 1;
}
/* Firefox: hide spinners entirely — value is editable via typing */
input[type="number"] {
  -moz-appearance: textfield;
}

/* ── Reduced motion ────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .fr-sortable-ghost::after,
  .fr-sortable-chosen,
  .fr-sortable-fallback,
  .fr-drop-landed > .fr-row-content,
  .fr-synced-move,
  .fr-cross-highlight > .fr-row-content,
  .fr-spotlight-hover > .fr-row-content,
  .fr-spotlight-pinned > .fr-row-content,
  [data-position-number],
  .drag-handle,
  .drag-handle svg {
    transition: none !important;
    animation: none !important;
    transform: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Tag Dots + Popover
   ═══════════════════════════════════════════════════════════════ */

/* Cursor hint: the cluster carries a native title tooltip listing the tags.
   A styled CSS popover can't be used here — .fr-row sets `contain: layout
   paint style` (load-bearing for drag perf) which clips any absolute/fixed
   descendant painting outside the 28px row. The native title renders in the
   browser's top layer, immune to containment. */
.fr-tag-dots { cursor: help; }

/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Preset Tab Switching
   ═══════════════════════════════════════════════════════════════ */

/* Columns revealed by a preset change get a brief enter animation
   so the swap reads as a deliberate transition, not a reload. */
@keyframes fr-column-enter {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}

.fr-column-enter {
  animation: fr-column-enter 160ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .fr-column-enter {
    animation: none;
  }
}

/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Row Insert Menu
   ═══════════════════════════════════════════════════════════════ */

/* Popover shell — layered shadow gives lift without overpowering the
   surrounding editor. */
.fr-row-insert-menu {
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.25),
    0 8px 24px rgba(0, 0, 0, 0.45);
}

/* Icon column — fixed width so labels line up across items even when
   glyph shapes differ (plus vs dashed-line). Lights up to the accent
   color on parent menuitem hover/focus. */
.fr-row-insert-glyph {
  flex: none;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted, #8a92a0);
  opacity: 0.85;
  transition: color 120ms ease, opacity 120ms ease;
}

.fr-row-insert-item:hover .fr-row-insert-glyph,
.fr-row-insert-item:focus .fr-row-insert-glyph {
  color: var(--color-accent, #4a9eff);
  opacity: 1;
}

/* Keyboard shortcut chip. Top inset highlight + slight bottom shadow +
   thicker bottom border give a faux-3D "physical key" affordance, so P / T
   read as keystrokes, not as labels. */
.fr-row-insert-kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.05), rgba(0, 0, 0, 0.12)),
    var(--color-surface-2, #1f232c);
  border: 1px solid var(--color-border, #353a45);
  border-bottom-width: 2px;
  border-radius: 5px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: 11px;
  font-weight: 600;
  color: var(--color-text-muted, #c0c6d1);
  line-height: 1;
  flex: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    0 1px 0 rgba(0, 0, 0, 0.25);
  transition: color 120ms ease, border-color 120ms ease;
}

.fr-row-insert-item:hover .fr-row-insert-kbd,
.fr-row-insert-item:focus .fr-row-insert-kbd {
  border-color: var(--color-accent, #4a9eff);
  color: var(--color-text-primary, #e6e8eb);
}

/* ── Comparison tray ─────────────────────────────────────────────── */
/* Right-docked side panel at lg+ (a flex-row sibling of the rankings scroller
   in .fr-editor-main, so rankings keep their own width + horizontal scroll).
   Below lg it detaches to a full-screen overlay. Hidden entirely when empty. */
.cmp-tray-wrap {
  flex: 0 0 clamp(360px, 32vw, 520px);
  display: flex;
  min-height: 0;
  border-left: 1px solid var(--border);
  /* Frosted matte slab: a translucent elevated surface lifts the panel off the
     rankings beside it (docked) and frosts the columns behind it (max overlay).
     backdrop-filter only resolves over content that's actually behind the box,
     so it bites in the overlay state; docked still reads as a distinct matte
     tint over the shell. */
  background: rgb(var(--surface-2-rgb) / .62);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  box-shadow: -10px 0 28px rgb(var(--bg-rgb) / .35);
  z-index: 45;
}
[data-compare-state="empty"] .cmp-tray-wrap { display: none; }

/* Popped out to a separate window: hide the in-editor panel and surface a pill
   to bring it back. The frame stays loaded (just hidden) so bring-back is
   instant. */
[data-compare-state="popped"] .cmp-tray-wrap { display: none; }
.cmp-popped-pill { display: none; }
[data-compare-state="popped"] .cmp-popped-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  position: absolute;
  bottom: 16px;
  right: 16px;
  z-index: 46;
  padding: 8px 14px;
  font-size: 12px;
  font-weight: 600;
  color: var(--accent);
  background: rgb(var(--surface-2-rgb) / .92);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: 0 6px 18px rgb(var(--bg-rgb) / .4);
  cursor: pointer;
}
/* Pop triggers are desktop affordances; window/float are impractical on phones.
   The maximize/restore toggle is hidden too: `docked` already fills the screen
   on mobile (identical to `max`), so the button is a visible no-op. Minimize
   STAYS visible — it's the only way to collapse the full-screen panel to the
   slim bottom bar without clearing the comparison. */
@media (max-width: 1023px) {
  .cmp-acts button[data-action="compare-tray#popFloat"],
  .cmp-acts button[data-action="compare-tray#popWindow"],
  .cmp-acts button[data-action="compare-tray#expand"] { display: none; }

  /* The desktop label "slim »" describes the side rail; on mobile minimize drops
     to a bottom bar, so relabel it (font-size:0 hides the text node; ::after
     reinstates a mobile-appropriate label at a real size). */
  .cmp-acts button[data-compare-tray-target="minBtn"] { font-size: 0; }
  .cmp-acts button[data-compare-tray-target="minBtn"]::after {
    content: "minimize ⤓"; font-size: 11px;
  }
}

/* Floating messenger-style box (in-page). Fixed, draggable by its header,
   resizable, collapsible. Default bottom-right; position persisted by
   cmp_float_controller (inline style overrides these defaults once moved). */
[data-compare-state="float"] .cmp-tray-wrap {
  position: fixed;
  bottom: 16px;
  right: 16px;
  top: auto;
  left: auto;
  flex: 0 0 auto;
  width: 380px;
  height: 520px;
  min-width: 300px;
  min-height: 120px;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  resize: both;
  box-shadow: 0 18px 48px rgb(var(--bg-rgb) / .55);
  z-index: 60;
}
[data-compare-state="float"] .cmp-tray-head { cursor: move; }
/* Collapsed: only the header bar shows. */
[data-compare-state="float"].cmp-float-collapsed .cmp-tray-wrap {
  height: auto !important;
  resize: none;
}
[data-compare-state="float"].cmp-float-collapsed .cmp-tray-scroll,
[data-compare-state="float"].cmp-float-collapsed .cmp-take { display: none; }
/* The collapse + dock buttons only make sense in the float box. */
.cmp-acts button[data-action="cmp-float#toggleCollapse"],
.cmp-acts button[data-action="compare-tray#dock"] { display: none; }
[data-compare-state="float"] .cmp-acts button[data-action="cmp-float#toggleCollapse"],
[data-compare-state="float"] .cmp-acts button[data-action="compare-tray#dock"] { display: inline; }

/* In the popped-out window the panel is a focused compare surface — the editor
   shell affordances (dock/float/window/clear/maximize) don't apply, so hide the
   whole header action cluster there. Per-player ✕, season/scoring pills, and
   click-to-sort remain (they live in the table body, not .cmp-acts). */
.cmp-popout-body .cmp-acts { display: none; }
/* The popped-out window had no bounded height, so .cmp-tray-scroll never became
   a scroll container and the whole document scrolled instead — which also made
   the sticky identity strip a no-op there. Bounding the root hands scrolling
   back to the panel, matching the dock. Height goes on the root rather than
   turning <body> into a flex column, which would relayout the toast container. */
.cmp-popout-body { margin: 0; }
.cmp-popout-root { display: flex; flex-direction: column; min-height: 0;
  height: 100vh; height: 100dvh; }

/* Minimized: slim the docked panel to a thin right-edge rail that stays out of
   the way; the rail click restores it (dock()). A desktop docked affordance —
   hidden < lg where the panel is already a full-screen overlay (nothing to
   slim). The wrap keeps its border + frosted bg, so the rail reads as a
   deliberate strip rather than a stub. */
[data-compare-state="min"] .cmp-tray-wrap { flex: 0 0 40px; }
[data-compare-state="min"] .cmp-tray-head,
[data-compare-state="min"] .cmp-tray-scroll,
[data-compare-state="min"] .cmp-take { display: none; }

.cmp-rail { display: none; }
[data-compare-state="min"] .cmp-rail {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.cmp-rail-ico { font-size: 16px; line-height: 1; color: var(--accent); }
.cmp-rail-ttl {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color 120ms ease;
}
.cmp-rail:hover .cmp-rail-ttl,
.cmp-rail:focus-visible .cmp-rail-ttl { color: var(--accent); }

/* The slim/minimize trigger only applies to the docked side panel — hide it in
   the float box, full-screen overlay, and popped-out state. In `min` the whole
   head is hidden anyway; in `empty` the whole wrap is. */
[data-compare-state="float"] .cmp-acts button[data-action="compare-tray#minimize"],
[data-compare-state="max"] .cmp-acts button[data-action="compare-tray#minimize"],
[data-compare-state="popped"] .cmp-acts button[data-action="compare-tray#minimize"] { display: none; }

/* Full-screen overlay deep-dive (desktop "maximize", and the only open mode on
   mobile). Covers the editor shell; sits above app chrome inside the shell. */
[data-compare-state="max"] .cmp-tray-wrap {
  position: absolute;
  inset: 0;
  flex-basis: auto;
  border-left: 0;
  z-index: 60;
  box-shadow: 0 0 0 100vmax rgba(0,0,0,.25);
}

/* The dock wrapper nests its content under a <turbo-frame id="comparison-tray">.
   A turbo-frame is an inline custom element with no layout box, so without this
   it can't carry the wrapper's bounded height down to .cmp-tray — .cmp-tray then
   grows to content height and .cmp-tray-scroll never scrolls (content spills
   past the viewport). Make the frame a height-filling flex column so the bounded
   height propagates and the inner scroll region works. */
turbo-frame#comparison-tray { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; min-width: 0; }
.cmp-tray { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; width: 100%; }
.cmp-tray-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
.cmp-tray-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--border); flex: none; }
.cmp-ttl { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--accent); font-weight: 700; }
.cmp-acts button { background: none; border: 0; color: var(--text-dim); font-size: 11px; cursor: pointer; margin-left: 10px; }

/* Below lg there's no room for a side dock. `docked` fills the editor (and
   `max` already does at all widths). `min` is the exception: it must NOT cover
   the editor — it collapses to a slim full-width bottom bar so the ranking list
   stays visible and tappable above it. `float`/`popped` keep their own
   positioning. Scoping by state matters: a blanket `.cmp-tray-wrap { inset:0 }`
   also caught `min`, leaving the minimized rail floating over a blanked editor. */
@media (max-width: 1023px) {
  [data-compare-state="docked"] .cmp-tray-wrap {
    position: absolute;
    inset: 0;
    flex-basis: auto;
    border-left: 0;
    z-index: 60;
  }

  /* Minimized → slim bottom bar, not a full-screen overlay. */
  [data-compare-state="min"] .cmp-tray-wrap {
    position: absolute;
    inset: auto 0 0 0;
    height: auto;
    flex: none;
    border-left: 0;
    border-top: 1px solid var(--border);
    z-index: 60;
  }
  [data-compare-state="min"] .cmp-rail {
    flex-direction: row;
    justify-content: center;
    gap: 8px;
    width: 100%;
    padding: 12px 16px;
  }
  [data-compare-state="min"] .cmp-rail-ttl {
    writing-mode: horizontal-tb;
    transform: none;
  }
  [data-compare-state="min"] .cmp-rail-ico {
    transform: rotate(90deg); /* ‹ → up chevron: tap to expand upward */
  }
}
/* Full-width, fixed layout so every stat/rank row's dot-plot track spans the
   whole tray. --cmp-w is no longer set (identity moved out of these tables
   into the .cmp-idstrip above), so max-width falls back to 100%; the var stays
   as a hook in case a future width constraint is reintroduced. */
table.cmp-t { width: 100%; max-width: var(--cmp-w, 100%); margin-inline: auto; table-layout: fixed; border-collapse: collapse; font-size: 11px; }
/* The lazy stats frame spans the full scroll width so its inner table centres
   on the same axis as the ranks sibling table below it. It must keep a layout
   BOX (display:block, NOT contents): a display:contents frame has no box, so
   Turbo's loading="lazy" IntersectionObserver never fires and the stats
   never load. The ranks frame needs display:block for a different reason:
   <turbo-frame> is an inline custom element by default, so without it the
   ranks table renders inline and escapes the fixed-layout grid. */
turbo-frame#comparison-stats,
turbo-frame#comparison-ranks { display: block; width: 100%; }
/* While a frame is fetching its update, Turbo sets [busy]; dim it briefly so a
   targeted reload reads as "updating" instead of a hard content pop. */
turbo-frame#comparison-stats,
turbo-frame#comparison-ranks { transition: opacity 120ms ease-out; }
turbo-frame#comparison-stats[busy],
turbo-frame#comparison-ranks[busy] { opacity: .55; }
@media (prefers-reduced-motion: reduce) {
  turbo-frame#comparison-stats,
  turbo-frame#comparison-ranks { transition: none; }
}
/* Tile look: a vertical rule off the label gutter so the matrix reads as a
   side-by-side control panel rather than an airy table. The label gutter
   (first col) stays flat. */
.cmp-t td:not(.cmp-rowlbl):not([colspan]) { border-left: 1px solid var(--border); }
.cmp-t th, .cmp-t td { padding: 4px 6px; text-align: center; border-bottom: 1px solid var(--border); white-space: nowrap; }
.cmp-t thead th { text-align: center; background: var(--surface-2); position: sticky; top: 0; }
/* Recedes until the row is hovered/focused — on a compact single line the ✕ was
   competing with the name it sits beside. */
.cmp-pl-x { background: none; border: 0; color: var(--text-dim); cursor: pointer;
  font-size: 9px; line-height: 1; padding: 2px; opacity: .45; transition: opacity .12s ease; }
.cmp-id:hover .cmp-pl-x, .cmp-pl-x:focus-visible { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .cmp-pl-x { transition: none; } }
.cmp-grp td { background: var(--surface-2); color: var(--accent); font-size: 10px; letter-spacing: .07em; text-transform: uppercase; font-weight: 700; text-align: left; padding-top: 7px; padding-bottom: 7px; }
/* Cross-column tracking: tint the whole row on hover so the eye can follow a
   single stat/rank across all player columns. Group-header rows are excluded. */
.cmp-t tbody tr:not(.cmp-grp):hover { background: rgb(var(--text-rgb) / .045); }
.cmp-lazy { padding: 0; }
.cmp-spinner { padding: 10px; text-align: center; color: var(--text-dim); font-size: 11px; }
.cmp-row-btn.is-comparing { opacity: 1; color: var(--accent); }
.cmp-row-btn:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }

/* ── Comparison tray v2 — typed cells ── */
.cmp-legend-row td { padding: 0; background: transparent; }
.cmp-legend { display: flex; align-items: center; gap: 7px; padding: 5px 12px;
  color: var(--text-dim, #6f6889); font-size: 10px; line-height: 1.3; }
.cmp-legend-tick { flex: none; }
.cmp-legend-tick { width: 26px; height: 7px; border-radius: 4px; background: var(--surface-2, #352e4d);
  position: relative; }
.cmp-legend-tick::after { content: ""; position: absolute; left: 50%; top: -1px; bottom: -1px;
  width: 2px; background: var(--cmp-tick, #cfc7e6); opacity: .7; }
.cmp-delta { font-size: 9px; font-weight: 700; padding: 1px 6px; border-radius: 9px; background: var(--surface-2); color: var(--text-dim); }
.cmp-delta--up { background: rgba(43,163,69,.14); color: #8fc7a3; }
.cmp-delta--down { background: rgba(239,83,80,.14); color: #d89a98; }
.cmp-pips { display: inline-flex; gap: 2px; }
.cmp-pips i { width: 7px; height: 10px; border-radius: 1px; background: var(--surface-2); display: inline-block; }
.cmp-pips i.on { background: rgb(var(--text-muted-rgb) / .45); }
.cmp-pips.is-best i.on { background: rgb(var(--green-rgb) / .6); }
.cmp-tier { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; background: var(--surface-2); color: var(--text-muted); }
.cmp-tier--0 { background: rgba(167,139,250,.18); color: #c4b5fd; }
.cmp-tier--1 { background: rgba(96,165,250,.16); color: #93c5fd; }
.cmp-num { font-variant-numeric: tabular-nums; }
.cmp-num.is-best { color: #8fc7a3; font-weight: 700; }
/* Tag chips in the compact strip. The td is nowrap + centered, so the chip row
   is its own flex container to let a multi-tag cell wrap instead of running
   under its neighbour. Colour lives on the dot only; the label stays neutral. */
.cmp-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 3px; }
.cmp-chip { display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
  background: var(--surface-2); border: 1px solid rgb(var(--text-rgb) / .12); border-radius: 4px;
  padding: 1px 6px; font-size: 9px; font-weight: 600; color: var(--text-muted); }
.cmp-chip-dot { flex: none; width: 5px; height: 5px; border-radius: 50%; }
.cmp-chip-dot--red { background: rgb(var(--red-rgb)); }
.cmp-chip-dot--orange { background: rgb(var(--orange-rgb)); }
.cmp-chip-dot--yellow { background: rgb(var(--yellow-rgb)); }
.cmp-chip-dot--green { background: rgb(var(--green-rgb)); }
.cmp-chip-dot--blue { background: rgb(var(--blue-rgb)); }
.cmp-chip-dot--purple { background: rgb(var(--purple-rgb)); }
.cmp-chip-dot--pink { background: rgb(var(--pink-rgb)); }
.cmp-chip-dot--gray { background: rgb(var(--text-rgb) / .35); }
.cmp-take { padding: 14px 16px 18px; border-top: 1px solid var(--border); }
.cmp-take-hd { font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 12px;
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 6px; }
.cmp-take-hd::-webkit-details-marker { display: none; }
.cmp-take-hd::before { content: "\25B8"; font-size: 9px; color: var(--text-dim); transition: transform .12s ease; }
details.cmp-take[open] > .cmp-take-hd::before { transform: rotate(90deg); }
details.cmp-take:not([open]) > .cmp-take-hd { margin-bottom: 0; }
.cmp-take-cols { display: grid; gap: 18px; justify-content: center; }
.cmp-take-gutter { /* aligns blurb columns under the table's player columns */ }
.cmp-take-col { border-left: 2px solid var(--border); padding-left: 12px; }
.cmp-take-col.is-leader { border-left-color: rgb(var(--green-rgb) / .65); }
.cmp-take-col h4 { margin: 0 0 1px; font-size: 13px; color: var(--text); font-weight: 700; }
.cmp-take-meta { font-size: 9px; color: var(--text-dim); margin-bottom: 8px; }

.cmp-rail, .cmp-rail.is-empty { flex: none; }
.cmp-take-col p { margin: 0; font-family: Georgia, 'Times New Roman', serif; font-size: 13.5px; line-height: 1.62; color: var(--text-muted); }

/* ── Comparison tray — season/scoring controls header ── */
.cmp-controls { display: inline-flex; gap: 8px; flex-wrap: nowrap; justify-content: flex-end; }
/* Segmented radio-pill group — replaces the native <select>s. Each set is a
   bordered track; the checked option fills with the accent. The native radio
   is visually hidden but stays the focus/keyboard target. */
.cmp-pillset { display: inline-flex; gap: 2px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 2px; }
.cmp-pill { position: relative; display: inline-flex; margin: 0; }
.cmp-pill input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
.cmp-pill span { display: block; padding: 3px 7px; border-radius: 4px; font-size: 10px; line-height: 1; color: var(--text-dim); white-space: nowrap; cursor: pointer; transition: color 100ms ease, background 100ms ease; }
.cmp-pill:hover span { color: var(--text-muted); }
.cmp-pill input:checked + span { background: var(--accent); color: #fff; font-weight: 600; }
.cmp-pill input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: 1px; }

.cmp-undo-btn { margin-left: 10px; background: none; border: 0; color: var(--accent); font-weight: 700; cursor: pointer; }

/* ── Comparison tray — mobile layout ── */


/* ── Comparison tray — identity strip ── */
.cmp-controls-bar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:11px 16px; border-bottom:1px solid var(--border); }
/* Who is being compared has to stay answerable at any scroll depth — the rows
   below are unlabelled by design (identity lives on the marks), so scrolling
   past this strip left the panel anonymous. Sticks to the top of
   .cmp-tray-scroll, which is the scroll container; needs an opaque ground of its
   own or the rows read through it. */
.cmp-idstrip { position:sticky; top:0; z-index:4;
  display:flex; gap:4px 12px; flex-wrap:wrap; padding:6px 12px;
  background:var(--surface); border-bottom:1px solid var(--border);
  /* Rows pass underneath, so the strip needs to read as the nearer layer. */
  box-shadow:0 6px 10px -8px rgb(0 0 0 / .6); }
.cmp-id { display:flex; align-items:baseline; gap:6px; border-radius:6px;
  padding:2px 6px; margin:0 -2px; transition:background .12s ease; }
/* The name is the spotlight toggle. Styled as the surrounding text rather than
   as a control — it only needs to look pressable on hover/focus. */
.cmp-id-pin { font:inherit; font-size:12px; font-weight:700; color:var(--text);
  background:none; border:0; padding:0; cursor:pointer; border-radius:4px;
  white-space:nowrap; }
.cmp-id:hover { background:rgb(var(--text-rgb) / .05); }
/* The strip is a paint target too (it carries data-cmp-player), so it has to
   answer the spotlight. Held well above the marks' .28 — this is the reference
   for who is in the comparison, and it stays readable at any dim. */
.cmp-id.is-dim { opacity:.5; }
.cmp-id-pin:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
/* Pinned reads as held-down, so it's clear the spotlight will survive the
   pointer leaving — otherwise a pinned panel looks like a stuck hover. */
.cmp-id:has(.cmp-id-pin[aria-pressed="true"]) { background:rgb(var(--text-rgb) / .10); }
.cmp-id-pin[aria-pressed="true"] { color:var(--accent); }
@media (prefers-reduced-motion: reduce) { .cmp-id { transition:none; } }
.cmp-id-sub { font-size:9.5px; color:var(--text-dim); white-space:nowrap; }



/* ═══════════════════════════════════════════════════════════════
   FlexRank Editor — Draft Board View
   ═══════════════════════════════════════════════════════════════ */

/* The board's vertical scroller. overflow-y:auto makes it the scroll container
   board_drag's edge-autoscroll targets; overflow-x:clip keeps a gutter divider
   nudged past the grid's left/right edge from spawning a horizontal scrollbar
   (clip, unlike hidden, doesn't create a scroll box or force overflow-y off
   auto). */
.fr-board-wrap {
  overflow-y: auto;
  overflow-x: clip;
}

/* Serpentine flow spine. board_drag traces the pick order through card centres
   into the <path>; the SVG is the grid's first child with no z-index, so tree
   order paints it beneath the position:relative cards. The line is hidden under
   each card and only surfaces in the gutter slivers between them, so it reads as
   subtle lanes threading the board — the flow cue that replaced the per-card
   direction glyphs. overflow:visible lets the rounded U-turn caps bleed into the
   gap without clipping. */
.fr-board-lanes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
/* Quiet "#" prefix on the corner rank tag. Rendered via ::before so the
   [data-position-number] span text stays a bare integer — board_drag and the
   vs-ECR/snapshot pass both parseInt that textContent, and a "#" in it would
   NaN the commit + delta math. Dimmer than the number so the digit anchors. */
.fr-board-rank::before {
  content: "#";
  opacity: 0.5;
  font-weight: 500;
  margin-right: 0.5px;
}

/* Draft (round.pick) labels are meaningful only as a draft board — show them
   in Snake, hide in Linear (a plain grid). Snake state lives on the shell. */
.fr-editor-shell:not([data-board-snake="true"]) [data-round-pick] { display: none; }

.fr-board-lanes path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Subtle: a faint colored thread, not a highlight. Tuned to read in the
     gutters without competing with card content in either theme. */
  opacity: 0.14;
}

/* Configurable-width draft grid (1-16 cols, default 12 — see --fr-board-cols,
   set on the shell and stepped live by editor-view-mode#stepCols). Server
   emits cards + dividers in flat rank order; board_drag_controller assigns
   each child a grid-row / grid-column-start (snake vs straight, and now
   width, are pure client concerns). position:relative anchors the
   absolutely-positioned drop caret. */
.fr-board-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--fr-board-cols, 12), minmax(var(--fr-board-min-card, 120px), 1fr));
  /* Uniform rows: every card the same height regardless of how many lines its
     name wraps to. A physical draft board is a grid of identical stickers —
     ragged row heights broke that read (and made the meta row land at a
     different height on every card). 84px seats a 2-line name comfortably;
     the rare 3-line name bleeds a few px into the card's own padding rather
     than truncating (names never truncate). Uniform rows also make the
     content-visibility placeholder height exact, so lazily-revealed rows can
     no longer shift layout under a long autoscroll drag. */
  grid-auto-rows: var(--fr-board-row, 84px);
  column-gap: var(--fr-board-gap, 10px);
  row-gap: var(--fr-board-gap, 10px);
  align-items: stretch;
}

/* A card = the list row expanded into a compact vertical tile: rank+round.pick
   row, wrapping name, team/pos/bye row. content-visibility skips offscreen
   cards so a 300-card board stays cheap; contain-intrinsic-size reserves the
   collapsed height so scroll geometry holds. Deliberately NO `contain: paint`
   — a card's kebab menu portals to <body>, but an explicit paint box would
   also clip the fixed-position picker popover if it ever anchored here. The
   hover ring is an INSET shadow so it can't be clipped by content-visibility's
   implicit containment. */
.fr-board-card {
  position: relative;
  height: 100%;
  border-radius: 8px;
  border: 1px solid var(--border);
  background-color: var(--surface);
  content-visibility: auto;
  contain-intrinsic-size: auto var(--fr-board-row, 84px);
  /* The whole card is grabbable — say so before the user has to guess. */
  cursor: grab;
  /* `transform` here animates the make-room preview shifts board_drag writes
     inline (cards sliding along the winding path), plus the hover lift. */
  /* Make-room slide: snappy, near-instant start then a quick settle so cards
     feel like they're getting out of the way in real time rather than easing
     lazily behind the pointer. 130ms reads as responsive without teleporting. */
  transition:
    border-color 120ms ease,
    box-shadow 120ms ease,
    transform 130ms cubic-bezier(0.22, 1, 0.36, 1);
  /* Pointer-driven drag (board_drag_controller) needs the browser to hand us
     pointermove instead of scroll-panning when a touch/pen starts on a card —
     tablet drag support. Phones never render the board (server-gated < lg). */
  touch-action: none;
  /* Card text is labeling, not prose — and a mousedown-to-drag would otherwise
     anchor a text selection that then sweeps the page as the pointer moves. */
  user-select: none;
  -webkit-user-select: none;
}
.fr-board-card:active {
  cursor: grabbing;
}
.fr-board-card > .fr-row-content {
  border-radius: 7px;
}
/* Hover = the card rises slightly to meet the hand: physical "pick me up" cue.
   Suppressed while any drag is live so parked cards sit still as the ghost
   passes over them. Outer drop shadow (not just the inset ring) sells the lift. */
.fr-board-card:hover,
.fr-board-card:focus-within {
  border-color: var(--accent);
  box-shadow:
    inset 0 0 0 1px var(--accent),
    0 3px 10px rgba(0, 0, 0, 0.25);
  z-index: 1;
}
/* Drag-live styling hooks anchor at .fr-drag-live on the GRID (a few hundred
   descendants), never at the shell's .fr-dragging (tens of thousands —
   toggling broad descendant rules there costs a ~200ms style recalc; see the
   hover-kill cascade note at the top of this file). The shell class remains
   purely a JS state contract (turbo_sync_guard deferral, toggleSnake guard);
   the page-wide grabbing cursor is an inline style on <body> set by the
   controller — inline on one element invalidates nothing. */
.fr-board-grid:not(.fr-drag-live) .fr-board-card:hover {
  transform: translateY(-1px);
}
.fr-drag-live .fr-board-card:hover {
  border-color: var(--border);
  box-shadow: none;
}
.fr-drag-live .fr-board-card {
  cursor: grabbing;
  user-select: none !important;
  -webkit-user-select: none !important;
  /* NOTE: no blanket will-change here. A board typically fits its scroller
     (~all cards on-screen), so content-visibility does NOT bound the set —
     promoting every drag-live card spawned ~70 compositor layers and made the
     drag WORSE. Promotion is now applied per-card, only to the handful actually
     sliding, in board_drag_controller._paintPreview (cleared on drag end). */
}

/* Hover-intent action cluster: reveal only after a deliberate pause so a
   grab-and-drag (press + move well under the delay) never collides with the
   buttons. Focus-within reveals instantly (keyboard). Hidden outright while a
   drag is live — parked cards shouldn't sprout toolbars as the ghost passes.
   The grip glyph mirrors the cluster inversely (same delay), yielding its
   corner exactly when the cluster arrives. */
.fr-board-actions {
  opacity: 0;
  pointer-events: none;
  transition: opacity 100ms ease;
}
.fr-board-card:hover .fr-board-actions {
  opacity: 1;
  pointer-events: auto;
  transition-delay: 350ms;
}
.fr-board-card:focus-within .fr-board-actions {
  opacity: 1;
  pointer-events: auto;
  transition-delay: 0ms;
}
.fr-board-grip {
  transition: opacity 100ms ease;
}
.fr-board-card:hover .fr-board-grip {
  opacity: 0;
  transition-delay: 350ms;
}
.fr-board-card:focus-within .fr-board-grip {
  opacity: 0;
  transition-delay: 0ms;
}
.fr-drag-live .fr-board-actions {
  opacity: 0 !important;
  pointer-events: none !important;
}
.fr-drag-live .fr-board-grip {
  opacity: 1 !important;
}

/* One-frame transition suppression while board_drag restores preview
   transforms in the same paint as a grid re-place (drop/cancel teardown). */
.fr-board-no-transition .fr-board-card {
  transition: none !important;
}

/* Composite (surface-3) boards: bump the card border so tiles stay legible on
   the darker composite backdrop, mirroring the list's composite treatment. */
.bg-surface-3 .fr-board-card {
  border-color: var(--border-hover);
}

/* Tier Shelves layout — one grid section ("shelf") per tier. Header/chip/
   add-tier chrome is styled by Tailwind utilities in the component; these
   rules own the inter-shelf spacing and the per-shelf grid. The grid mirrors
   .fr-board-grid exactly (same shared --fr-board-cols width, uniform 84px
   rows, gaps) so cards read identically to the flat board and the layout is
   correct even before shelf_drag hydrates (graceful degradation); shelf_drag
   sets each card's explicit grid-row/column-start on top of this for snake. */
/* Positioning context for the hover insert widget (parked absolutely by
   shelf_drag against this root, spanning across the section grids). */
.fr-shelves {
  position: relative;
  /* Single source for the shelves subtree: the grid track floor, the .fr-shelf
     definite-width calc, and shelf_drag's inline gap all read these off here
     (custom props cascade down), so the floor and the tracks can never drift.
     Keep --fr-board-min-card in sync with MIN_CARD_WIDTH in compare_tray_controller.js. */
  --fr-board-min-card: 120px;
  --fr-board-gap: 10px;
}
.fr-shelf {
  margin-bottom: 1.25rem;
}
.fr-shelf:last-child {
  margin-bottom: 0;
}
.fr-shelf-header {
  margin-bottom: 0.5rem;
}
.fr-shelf-grid {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--fr-board-cols, 12), minmax(var(--fr-board-min-card, 120px), 1fr));
  grid-auto-rows: var(--fr-board-row, 84px);
  column-gap: var(--fr-board-gap, 10px);
  row-gap: var(--fr-board-gap, 10px);
  align-items: stretch;
}

/* Give each section a DEFINITE floor width = cols * min-card + gaps, so when the
   available width is below it the section (header + grid) overflows uniformly to
   the right and the shared container (board.html.erb) scrolls — headers span the
   scrolled width, every shelf's grid stays column-aligned. Deliberately NOT
   `max-content`: that would put the minmax(120px,1fr) grid into intrinsic sizing,
   where 1fr tracks size to the longest (never-truncated) player name instead of
   flooring at 120px — inflating the surface and scrolling even undocked. A
   definite calc floors at exactly the same width the grid tracks do, so when the
   container is wider the section fills it (1fr distributes the slack, no scroll). */
.fr-shelf {
  min-width: calc(
    var(--fr-board-cols, 12) * var(--fr-board-min-card, 120px) +
    (var(--fr-board-cols, 12) - 1) * var(--fr-board-gap, 10px)
  );
}

/* Hover "insert tier break here" widget — one per shelves surface, parked by
   shelf_drag in the gutter between two cards of the hovered shelf (strictly
   BETWEEN players; the shelf-end is the component's "+ Add tier below"). Mirror
   of .fr-board-insert; hidden during an active drag. */
.fr-shelf-insert {
  position: absolute;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 100ms ease;
}
.fr-shelf-insert--visible {
  opacity: 1;
  pointer-events: auto;
}
.fr-shelf-insert-btn {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background-color: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  transition:
    color 100ms ease,
    border-color 100ms ease,
    background-color 100ms ease,
    transform 100ms ease;
}
.fr-shelf-insert-btn:hover,
.fr-shelf-insert-btn:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  transform: scale(1.08);
}
.fr-drag-live .fr-shelf-insert {
  display: none;
}

/* Drop-target placeholder — a dashed outline shelf_drag parks in the grid cell
   the dragged card would land in (the gap the make-room preview opens). Placed
   as a grid item at the insertion cell during a drag, hidden otherwise. Mirror
   of .fr-board-slot. */
.fr-shelf-slot {
  display: none;
  border: 1.5px dashed color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 8px;
  background-color: color-mix(in srgb, var(--accent) 6%, transparent);
  pointer-events: none;
  z-index: 0;
}

/* Gutter divider — a thin colored rail carrying a narrow vertical tier label.
   align-self: stretch lets it span the height of the row board_drag places it
   in. The label reads vertically so the rail can stay slim in the gutter. */
.fr-board-divider {
  align-self: stretch;
  justify-self: start;
  min-width: 14px;
  touch-action: none;
  /* Sits above the cards it overlaps so the tier rail always reads. */
  z-index: 2;
}
.fr-board-divider-label {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
}
/* board_drag stacks a divider into the SAME grid cell as an adjacent card and
   tags which gutter to nudge toward (data-board-gutter). The card sizes the
   cell; these transforms slide the thin rail into the between-columns gutter
   (half the item + half the gap centers it on the gap), or hug the row's inner
   edge when the divider lands at a row's reading start/end (no gutter there).
   justify-self:start keeps the item from stretching to fill the 1fr column. */
.fr-board-divider[data-board-gutter="left"] {
  transform: translateX(calc(-50% - var(--fr-board-gap, 10px) / 2));
}
.fr-board-divider[data-board-gutter="right"] {
  justify-self: end;
  transform: translateX(calc(50% + var(--fr-board-gap, 10px) / 2));
}
.fr-board-divider[data-board-gutter="edge-start"] {
  justify-self: start;
  transform: none;
}
.fr-board-divider[data-board-gutter="edge-end"] {
  justify-self: end;
  transform: none;
}

/* Edge tier rails (edge-start / edge-end) have no gutter to sit in, so they
   stack ON their neighbour card. board_drag tags that neighbour and we inset
   its content so the vertical tier label never overlaps the rank/name — the
   fix for the crammed "TIER 1" on the board's first card. Only the tier-
   adjacent card pays the ~rail-width of space. */
.fr-board-card--tier-lead .fr-row-content {
  padding-left: 20px;
}
.fr-board-card--tier-trail .fr-row-content {
  padding-right: 20px;
}

/* Implied Tier 1 now renders as a real .fr-board-divider rail (data-implied-tier)
   that board_drag places edge-start before card 0 — see board_component.html.erb.
   It reuses all the divider rail/gutter/tier-lead CSS above, so no dedicated
   styles are needed here anymore. */

/* Slim gutter caret — DIVIDER drags only (a tier rail moves between gutters,
   so a gutter marker is the honest preview). Hidden until board_drag positions
   + reveals it. */
.fr-board-caret {
  display: none;
  position: absolute;
  width: 3px;
  border-radius: 2px;
  background-color: var(--accent);
  pointer-events: none;
  z-index: 5;
}

/* Open drop slot — CARD drags. board_drag grid-places this into the cell the
   surrounding cards just vacated (the make-room preview), so the empty
   outlined cell literally is where the card will land. */
.fr-board-slot {
  display: none;
  border: 1.5px dashed color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 8px;
  background-color: color-mix(in srgb, var(--accent) 6%, transparent);
  pointer-events: none;
  z-index: 0;
}

/* Single gutter insert widget (Task 7) — replaces the old per-card kebab
   insert menu (~300 fewer nodes/controllers: one widget for the whole board,
   not one per card). Absolutely positioned like the caret/slot above;
   board_drag's idle-hover placement (_placeInsertWidget) sets left/top/height
   to park it in the gutter nearest the pointer and toggles --visible to
   reveal it. Two stacked minimal icon buttons — add player / add tier — so a
   single hover-and-click inserts at that gutter's rank. Opacity + pointer-
   events (not display:none) so the reveal can fade rather than pop. */
.fr-board-insert {
  position: absolute;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 20px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 100ms ease;
}
.fr-board-insert--visible {
  opacity: 1;
  pointer-events: auto;
}
.fr-board-insert-btn {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background-color: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
  transition:
    color 100ms ease,
    border-color 100ms ease,
    background-color 100ms ease,
    transform 100ms ease;
}
.fr-board-insert-btn:hover,
.fr-board-insert-btn:focus-visible {
  color: var(--accent);
  border-color: var(--accent);
  transform: scale(1.08);
}
/* Tier-insert is forbidden at rank 1 (the implied Tier 1 boundary — Task 5's
   min-2 validation rejects start_position 1) — board_drag hides this button
   in the gutter above card 1 rather than rendering a control that would
   always fail. */
.fr-board-insert-btn--hidden {
  display: none;
}
/* Mid-drag: the widget is an idle-hover affordance only. Hide it outright the
   instant a real drag begins (same .fr-drag-live hook the actions cluster and
   grip use — see above) so it never overlaps the ghost, caret, or slot. */
.fr-drag-live .fr-board-insert {
  display: none;
}

/* Drag states. Source = the real card, hidden for the drag (display:none, but
   kept in the DOM for a clean Escape/cancel revert). Its grid cell stays
   RESERVED (empty) the whole drag — the survivors do NOT reflow closed at
   pickup, so nothing shifts when a drag begins; board_drag's homeIndex bridges
   the resulting open-hole off-by-one for the hit-test + make-room preview.
   Ghost = a fixed-position clone appended to <body> that tracks the pointer;
   the controller sets its position/size inline, this only styles the lift. */
.fr-board-drag-source {
  display: none;
}
/* Ghost = a fixed wrapper the controller moves with a compositor-only
   translate3d (left/top tracking forced layout on every pointermove), wrapping
   the visual clone whose lift/settle transitions animate scale/rotate/shadow
   independently of the tracking transform. */
.fr-board-ghost-wrap {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 60;
  pointer-events: none;
  will-change: transform;
}
/* Settle: the wrapper glides (transform transition) into the open slot. */
.fr-board-ghost-wrap.fr-board-ghost-settling {
  transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fr-board-drag-ghost {
  opacity: 0.95;
  cursor: grabbing;
  /* Two-stage physicality: the clone starts flat exactly over the source card,
     then .fr-board-ghost-lifted (added one frame later) scales/tilts it up —
     the card visibly leaves the board. The settle removes the lift mid-glide. */
  transform: scale(1) rotate(0deg);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
  transition:
    transform 150ms cubic-bezier(0.2, 0.8, 0.2, 1),
    box-shadow 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.fr-board-drag-ghost.fr-board-ghost-lifted {
  transform: scale(1.04) rotate(1.5deg);
  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.5);
}

@media (prefers-reduced-motion: reduce) {
  .fr-board-card,
  .fr-board-drag-ghost,
  .fr-board-ghost-settling {
    transition: none;
  }
}

/* Task 10: minimal mobile grid. Coarse pointers (touch, no hover — phones/
   tablets) never trigger :hover, so a card's action cluster reveals off a
   `--selected` class board_drag toggles on tap (pointerup that never crossed
   the drag slop — see _toggleTapSelect) instead of hover.
   board_drag mirrors this same `(hover: none)` query in JS (_coarsePointer)
   to clamp the effective width to <=4 and force snake off.

   Round.pick draft labels are hidden device-wide here (not just when Snake
   is off): _snake() forces linear math on coarse pointers, but the base
   label-visibility rule above is keyed on the raw data-board-snake
   attribute, which defaults to true — without this, a default user on a
   phone would see labels computed for a grid the JS never actually rendered
   (stale server math, since _refreshDraftLabels no-ops when forced-false).
   !important beats the snake-keyed `display: none` toggle above when Snake
   is on. */
@media (hover: none) {
  [data-round-pick] {
    display: none !important;
  }

  .fr-board-card--selected .fr-board-actions {
    opacity: 1;
    pointer-events: auto;
    transition-delay: 0ms;
  }

  /* The `+` insert widget stays hidden on coarse pointers (default non-hover
     state, no reveal rule here). Its on-screen position is only recomputed
     from idle `pointermove`, which a plain tap doesn't fire, so revealing it
     would show a tappable but permanently mispositioned/no-op affordance.
     Deferred: needs tap-based widget positioning before it can be surfaced
     on touch. */
}

/* ── Comparison dot-plot ─────────────────────────────────────────────── */
:root { --cmp-gold:#e6be4a; --cmp-silver:#c2c6d2; --cmp-bronze:#cd8a52; --cmp-tick:#cfc7e6;
  /* Vertical step per card tier — one card's height plus breathing room. */
  --mk-tier-step:30px; }

.cmp-dp-lbl { width:104px; color:var(--text-muted); font-weight:600; font-size:12px; padding-left:16px; }
.cmp-dp-lbl small { display:block; color:var(--text-dim); font-weight:500; font-size:9.5px; }
.cmp-dp-absent { font-style:italic; }
.cmp-dp-span { font-variant-numeric:tabular-nums; letter-spacing:.01em; }
/* The row is 56px with the axis low, not centred: every label sits above the
   line in one card, and the band under the axis carries the neighbour-gap
   annotation. Still shorter than the 60px this layout replaced, and the 8px it
   spends over the label-only version buys stated differences rather than
   decoration.

   Everything inside the plot is anchored to the BOTTOM. When cards can't fit
   side by side (narrow panel, phone) spreadMarks tiers them and raises --mk-tiers,
   and the row grows upward from a fixed axis — anchoring from the top would drag
   the axis down the row instead. */
.cmp-dp-plot { position:relative; margin-right:30px;
  height:calc(56px + var(--mk-tiers, 0) * var(--mk-tier-step)); }
.cmp-dp-track { position:absolute; left:30px; right:24px; top:0; bottom:0; }
.cmp-dp-base { position:absolute; left:0; right:0; bottom:15px; height:2px; border-radius:2px;
  background:linear-gradient(90deg, rgb(var(--text-rgb) / .06), rgb(var(--text-rgb) / .15)); }
.cmp-dp-gap { position:absolute; bottom:14.5px; height:3px; border-radius:3px; background:rgb(var(--text-rgb) / .08); }
.cmp-dp-bound { position:absolute; bottom:12px; height:9px; width:1px; background:rgb(var(--text-rgb) / .28); border-radius:1px; }
.cmp-dp-bound--lo { left:0; }
.cmp-dp-bound--hi { right:0; }
.cmp-dp-tick { position:absolute; bottom:11px; height:12px; width:10px; margin-left:-5px; display:flex; justify-content:center; }
.cmp-dp-tick::before { content:""; width:2px; height:100%; background:var(--cmp-tick); opacity:.5; border-radius:2px; }
.cmp-dp-tick:hover::before, .cmp-dp-tick:focus::before { opacity:.9; }
.cmp-dp-tick-pop { position:absolute; bottom:16px; left:50%; transform:translateX(-50%); display:none; white-space:nowrap;
  font-size:9px; padding:2px 6px; border-radius:4px; background:var(--surface-2); border:1px solid var(--border); color:var(--text-muted); z-index:3; }
.cmp-dp-tick:hover .cmp-dp-tick-pop, .cmp-dp-tick:focus .cmp-dp-tick-pop { display:block; }
.cmp-dp-band { position:absolute; bottom:11px; height:12px; border-radius:3px; background:rgb(var(--green-rgb) / .10); border:1px solid rgb(var(--green-rgb) / .22); }
.cmp-dp-cap { position:absolute; bottom:13px; right:-20px; font-size:9px; color:var(--text-dim); font-variant-numeric:tabular-nums; }
/* Neighbour-gap annotation, drawn as a dimension bracket: a dotted line drops
   from each of the two ticks, turns, and runs inward to the distance between
   them. Anchoring it to the ticks ties the number to the marks it measures
   instead of leaving a rule floating under the axis.
   The rules are faint solids rather than dashes: a dash pattern is texture, and
   texture is what the eye catches first, so the annotation kept announcing
   itself. Solid at roughly half the alpha carries the same visual weight with
   nothing to catch on, sitting in the same ink range as the axis line it hangs
   from (.06–.15) so it reads as chart furniture until looked for. */
.cmp-dp-gapmark { position:absolute; bottom:1px; height:12px;
  --gap-ink:rgb(var(--text-rgb) / .10); --gap-ink-drop:rgb(var(--text-rgb) / .14); }
/* The drops carry slightly more ink than the long horizontal run: they are only
   7px, and they are the part that ties the number to its two ticks, so they have
   to survive where the horizontal can stay fainter. */
.cmp-dp-gapmark::before, .cmp-dp-gapmark::after { content:""; position:absolute; top:0; width:1px; height:7px;
  background:var(--gap-ink-drop); }
.cmp-dp-gapmark::before { left:0; }
.cmp-dp-gapmark::after { right:0; }
.cmp-dp-gaprow { position:absolute; left:0; right:0; top:2px; height:10px;
  display:flex; align-items:center; gap:4px; }
.cmp-dp-gaprow i { flex:1; height:1px; background:var(--gap-ink); }
/* Held a step below the values it derives from, so the annotation is there to
   tune into and easy to look past when reading the marks themselves. */
.cmp-dp-gaplabel { font-size:8px; font-weight:600; font-style:normal; line-height:10px;
  color:rgb(var(--text-rgb) / .34); font-variant-numeric:tabular-nums; }
/* Too narrow to seat its own label — spreadMarks measures and flags these. A
   stub of dotted rule with no number is noise, so the whole mark goes. */
.cmp-dp-gapmark.is-tight { display:none; }

/* Mark layout inside the bottom 56px of the row: card 2–29 · stem 29–39 ·
   tick 39. The box is bottom-anchored so a tiered row grows above it. */
.cmp-mk { position:absolute; bottom:0; height:56px; width:0; }
/* One stem, anchored to the dot and running up to the card. When the card is
   nudged aside, JS leans it (--stem-h/--stem-rot) so it runs from the true dot
   x out to the displaced card and the two still read as one object. */
/* --tier lifts a card clear of its neighbours when they can't sit side by side;
   the stem grows to keep reaching its tick, so the pairing survives the lift. */
.cmp-mk-stem { position:absolute; left:0; bottom:17px; width:1px;
  height:calc(10px + var(--tier, 0) * var(--mk-tier-step));
  transform-origin:bottom center; background:rgb(var(--text-rgb) / .15); }
.cmp-mk.is-nudged .cmp-mk-stem { height:var(--stem-h,10px); transform:rotate(var(--stem-rot,0)); }
/* Name over value in one container. Two containers straddling the axis made
   each player two objects to track and put their identity and their number on
   opposite sides of the line; stacked, the pair reads as one thing. */
/* Rows stretch rather than centre, so the card's contents land on its edges: the
   name flush left, the value and its rank pushed to opposite corners. Centring
   every element gave three ragged axes to track per card; edge-aligned, the eye
   reads a fixed column position for each part. */
.cmp-mk-card { position:absolute; top:calc(2px - var(--tier, 0) * var(--mk-tier-step)); left:0; transform:translateX(-50%); white-space:nowrap;
  display:flex; flex-direction:column; align-items:stretch; gap:1px;
  padding:2px 6px; border-radius:6px;
  background:var(--surface-2); border:1px solid rgb(var(--text-rgb) / .16); }
/* The name is identity, not the quantity under comparison, so it sits a step
   back in muted ink and lets the value carry the emphasis. Medal-tinting it was
   the other option, but the medal is already on the card border, the dot and the
   value — a fourth statement, and two shouting elements instead of a hierarchy. */
.cmp-mk-nm { font-size:9px; font-weight:600; line-height:10px; color:var(--text-muted); text-align:left; }
/* --tick-shift staggers ticks that share an x so both stay legible; x stays
   exact, so the tick still encodes the true value. */
/* A tick across the axis, not a disc. Only the mark's *position* is the datum,
   and a 2px-wide tick states that position with an exact edge where a circle
   smeared it over its diameter. (Distinct from .cmp-dp-tick, which is the
   positional-median marker on the axis itself.) The medal is the tick's colour:
   a ring around a mark this small would be mostly ring, and it restated what
   x-position already says — see the medal block below. */
.cmp-mk-tick { position:absolute; bottom:13px; left:0; transform:translate(-50%,calc(-50% + var(--tick-shift,0px)));
  width:2px; height:8px; border-radius:1px; background:rgb(var(--text-rgb) / .5); border:0; }
/* Value tracks the name's 9px — a larger number read as the louder of the two,
   which inverted the intended hierarchy (the name should anchor the mark). */
/* Bottom row is two panels: the value on the left edge, its rank/delta on the
   right, the divider riding with the right panel however wide the card gets. */
.cmp-mk-val { display:flex; align-items:center; justify-content:space-between; gap:8px;
  font-size:9px; font-weight:700; line-height:10px;
  font-variant-numeric:tabular-nums; color:var(--text); }
.cmp-mk-meta { display:inline-flex; align-items:center; gap:3px; line-height:10px;
  padding-left:5px; border-left:1px solid rgb(var(--text-rgb) / .18); }
.cmp-mk-rk { font-size:8px; font-weight:600; color:var(--text-dim); }
.cmp-mk-delta { font-size:8px; font-weight:600; }
.cmp-mk-delta--up { color:rgb(var(--green-rgb)); } .cmp-mk-delta--down { color:var(--cmp-bronze); } .cmp-mk-delta--zero { color:var(--text-dim); }

/* Medal = a thin flat ring on the dot + a matching thin pill border. Kept
   minimal so dense rows stay legible — the per-row placement medal is the
   only medal in the tray now (no header coin disc). */
.cmp-mk.is-gold   .cmp-mk-card { border-color:var(--cmp-gold); }
.cmp-mk.is-silver .cmp-mk-card { border-color:var(--cmp-silver); }
.cmp-mk.is-bronze .cmp-mk-card { border-color:var(--cmp-bronze); }
.cmp-mk.is-gold   .cmp-mk-val { color:var(--cmp-gold-text); }
.cmp-mk.is-bronze .cmp-mk-val { color:var(--cmp-bronze-text); }
/* The medal is the tick's own colour rather than a ring around it. A ring drew a
   second contour to say what the fill already says, and at this size there is no
   room for both — the mark stays a mark and still carries the medal. */
.cmp-mk.is-gold   .cmp-mk-tick { background:var(--cmp-gold); }
.cmp-mk.is-silver .cmp-mk-tick { background:var(--cmp-silver); }
.cmp-mk.is-bronze .cmp-mk-tick { background:var(--cmp-bronze); }

/* Crowded-row de-clutter — spreadMarks (lib/compare_sync) adds .is-nudged and a
   --nudge x-offset that fans the card off the true dot x. */
.cmp-mk.is-nudged .cmp-mk-card { transform:translateX(calc(-50% + var(--nudge, 0px))); }

/* Cross-row spotlight (bindSpotlight in lib/compare_sync). Hovering one player's
   mark drops the others back so a single player can be read straight down the
   table. The dimming does the work — the hovered mark keeps its own colours
   rather than gaining a highlight, so nothing shifts or reflows on hover. */
.cmp-mk, .cmp-cp-cell { transition:opacity .12s ease; }
.cmp-mk.is-dim, .cmp-cp-cell.is-dim { opacity:.28; }
.cmp-mk.is-lit { z-index:2; }
.cmp-mk.is-lit .cmp-mk-card { border-color:rgb(var(--text-rgb) / .45); }
.cmp-mk.is-lit.is-gold .cmp-mk-card { border-color:var(--cmp-gold); }
.cmp-mk.is-lit.is-silver .cmp-mk-card { border-color:var(--cmp-silver); }
.cmp-mk.is-lit.is-bronze .cmp-mk-card { border-color:var(--cmp-bronze); }
/* The gap brackets measure pairs, not players, so they recede as a group rather
   than trying to half-belong to the lifted mark. */
.cmp-dp-track:has(.cmp-mk.is-dim) .cmp-dp-gapmark { opacity:.4; }
@media (prefers-reduced-motion: reduce) {
  .cmp-mk, .cmp-cp-cell { transition:none; }
}

/* ── Comparison compact rows (Games/progress, tier, rating, number) ──── */
.cmp-cp-strip { display:flex; gap:22px; padding:8px 24px 8px 0; }
.cmp-cp-cell { flex:1; max-width:230px; }
/* Games as countable units on one shared axis. The name column is fixed and the
   cells are a fixed-track grid, so game N sits at the same x for every player
   and a missed stretch reads down the column. Unplayed cells stay drawn rather
   than blank, so the denominator is legible off the graphic itself. */
.cmp-mx { display:flex; flex-direction:column; gap:3px; padding:8px 24px 8px 0; }
/* max-content + justify-content:start keeps the count beside the cells; an auto
   track stretches to the panel width and strands it at the far edge. */
.cmp-mx-row { display:grid; grid-template-columns:88px max-content max-content; justify-content:start;
  align-items:center; gap:10px; transition:opacity .12s ease; }
.cmp-mx-nm { font-size:9px; font-weight:600; color:var(--text-muted); text-align:right;
  overflow:hidden; text-overflow:ellipsis; }
.cmp-mx-cells { display:grid; grid-template-columns:repeat(var(--mx-units,17), 6px); gap:2px; }
.cmp-mx-cells i { height:6px; border-radius:1.5px; background:rgb(var(--text-rgb) / .09);
  box-shadow:inset 0 0 0 1px rgb(var(--text-rgb) / .10); }
.cmp-mx-cells i.on { background:rgb(var(--text-muted-rgb) / .55); box-shadow:none; }
.cmp-mx-val { font-size:10px; font-weight:700; font-variant-numeric:tabular-nums; color:var(--text); }
.cmp-mx-row.is-gold   .cmp-mx-cells i.on { background:var(--cmp-gold); }
.cmp-mx-row.is-silver .cmp-mx-cells i.on { background:var(--cmp-silver); }
.cmp-mx-row.is-bronze .cmp-mx-cells i.on { background:var(--cmp-bronze); }
.cmp-mx-row.is-gold   .cmp-mx-val { color:var(--cmp-gold-text); }
.cmp-mx-row.is-bronze .cmp-mx-val { color:var(--cmp-bronze-text); }
.cmp-mx-row.is-dim { opacity:.28; }
@media (prefers-reduced-motion: reduce) { .cmp-mx-row { transition:none; } }
.cmp-cp-nm { display:block; font-size:9.5px; color:var(--text-dim); margin-top:3px; }

/* ── Comparison tray — phone layout ──────────────────────────────────
   Placed after the dot-plot rules on purpose: these override properties the
   base rules also set (--mk-tier-step, .cmp-mk-card flex-direction), and at
   equal specificity the later declaration wins. Sitting above them, half of
   this block silently lost. */
@media (max-width: 640px) {
  .cmp-t { max-width: none; }
  /* No horizontal scrolling: the marks tier instead of running off the track,
     so the row fits the phone and reading a comparison never means dragging
     the table sideways to find the fourth player. */
  .cmp-tray-scroll { overflow-x: hidden; }

  /* The card collapses to a single line — name beside value rather than over
     it. Wider, but less than half the height, which is the better trade once
     rows are tiering: each tier now costs 18px instead of 30px. */
  .cmp-mk-card { flex-direction: row; align-items: baseline; gap: 5px; padding: 1px 5px; }
  .cmp-mk-val { justify-content: flex-start; gap: 5px; }
  :root { --mk-tier-step: 18px; }

  /* Give the plot the width back: the label gutter is the only fixed cost in
     the row, and at this size the axis needs every pixel. */
  .cmp-dp-lbl { width: 58px; padding-left: 8px; font-size: 11px; }
  .cmp-dp-plot { margin-right: 14px; }
  .cmp-dp-track { left: 10px; right: 12px; }
  .cmp-dp-cap { display: none; } /* the ceiling label has nowhere to sit */

  /* Identity strip: two per line, meta dropped. It is sticky, so every line it
     costs is a line permanently off the rows below. */
  .cmp-idstrip { gap: 2px 8px; padding: 6px 10px; }
  .cmp-id { flex: 1 1 42%; }
  .cmp-id-sub { display: none; }

  .cmp-controls-bar { padding: 8px 10px; gap: 6px; }
  .cmp-mx-row { grid-template-columns: 64px max-content max-content; gap: 7px; }
  .cmp-mx { padding: 8px 10px 8px 0; }
}
